支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
OptionsResolver组件是打了激素的
array_replace
。它允许你创建一个选项系统,包括必填项、默认选项、验证(类型、值)、标准化(normalization),乃至更多。
你可以通过下述两种方式安装:
通过Composer安装(Packagist上的symfony/options-resolver
)
通过官方Git宝库(https://github.com/symfony/options-resolver)
然后,包容vendor/autoload.php
文件,以开启Composer提供的自动加载机制。否则,你的程序将无法找到这个Symfony组件的类。
设想,你有一个 Mailer
类,它有四个选项:host
,
username
, password
和 port
:
在访问 $options
时,你需要添加大量模板代码以检查“哪些选项被设置”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class Mailer
{
// ...
public function sendMail($from, $to)
{
$mail = ...;
$mail->setHost(isset($this->options['host'])
? $this->options['host']
: 'smtp.example.org');
$mail->setUsername(isset($this->options['username'])
? $this->options['username']
: 'user');
$mail->setPassword(isset($this->options['password'])
? $this->options['password']
: 'pa$$word');
$mail->setPort(isset($this->options['port'])
? $this->options['port']
: 25);
// ...
}
} |
这样的模板难于阅读和复用。同时,选项的默认值被你的逻辑代码所掩盖。使用 array_replace
来替代:
现在,可以确保四个选项被设置。但是,如果 Mailer
类的用户名发生错误了该怎么办?
1 2 3 | $mailer = new Mailer(array(
'usernme' => 'johndoe', // usernAme misspelled / 用户名误拼
)); |
不会有错误信息显示出来。最好的情况是,这个bug会在测试中出现,但开发者会花费大量时间寻找问题。最坏的情况则是,在部署到生产环境之前这个bug根本不出现。
幸运的是 OptionsResolver
类可以帮你修复这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | use Symfony\Component\OptionsResolver\OptionsResolver;
class Mailer
{
// ...
public function __construct(array $options = array())
{
$resolver = new OptionsResolver();
$resolver->setDefaults(array(
'host' => 'smtp.example.org',
'username' => 'user',
'password' => 'pa$$word',
'port' => 25,
));
$this->options = $resolver->resolve($options);
}
} |
和以前一样,所有的选项都确保被进行了设置。另外,如果传入未知选项,一个 UndefinedOptionsException
异常会被抛出:
1 2 3 4 5 6 | $mailer = new Mailer(array(
'usernme' => 'johndoe',
));
// UndefinedOptionsException: The option "usernme" does not exist.
// Known options are: "host", "password", "port", "username" |
你在剩下的代码中可以访问选项的值,而不再需要模板代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // ...
class Mailer
{
// ...
public function sendMail($from, $to)
{
$mail = ...;
$mail->setHost($this->options['host']);
$mail->setUsername($this->options['username']);
$mail->setPassword($this->options['password']);
$mail->setPort($this->options['port']);
// ...
}
} |
一个最佳实践,是把选项配置信息独立到一个方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // ...
class Mailer
{
// ...
public function __construct(array $options = array())
{
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$this->options = $resolver->resolve($options);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'host' => 'smtp.example.org',
'username' => 'user',
'password' => 'pa$$word',
'port' => 25,
'encryption' => null,
));
}
} |
首先,你的代码更易阅读,特别是当构造器要处理选项之外的更多事时。其次,子类现在可以覆写 configureOptions()
方法来调整选项的配置信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // ...
class GoogleMailer extends Mailer
{
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults(array(
'host' => 'smtp.google.com',
'encryption' => 'ssl',
));
}
} |
如果一个选项必须被调用者设置,传递此选项到 setRequired()
。例如,要让 host
选项成为必须,你可以:
1 2 3 4 5 6 7 8 9 10 11 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setRequired('host');
}
} |
如果你忽略了必填的选项,一个 MissingOptionsException
异常会被抛出:
1 2 3 | $mailer = new Mailer();
// MissingOptionsException: The required option "host" is missing. |
setRequired()
方法接收一个单独的选项名称,或是一个选项名称数组,如果你有一个以上的必填项的话:
1 2 3 4 5 6 7 8 9 10 11 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setRequired(array('host', 'username', 'password'));
}
} |
使用 isRequired()
来确认一个选项是否为必填。你可以使用 getRequiredOptions()
来取出所有必填选项的名称:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // ...
class GoogleMailer extends Mailer
{
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
if ($resolver->isRequired('host')) {
// ...
}
$requiredOptions = $resolver->getRequiredOptions();
}
} |
如果你希望检查一个必填项“是否仍然从默认选项中丢失”,你可以使用 isMissing()
。它和 isRequired()
的区别在于,如果一个必填项已经被设置,本方法将返回false:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setRequired('host');
}
}
// ...
class GoogleMailer extends Mailer
{
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->isRequired('host');
// => true
$resolver->isMissing('host');
// => true
$resolver->setDefault('host', 'smtp.google.com');
$resolver->isRequired('host');
// => true
$resolver->isMissing('host');
// => false
}
} |
getMissingOptions()
可让你访问所有遗失选项的名称。
你可以对选项执行附加检查,确保它们被正确传入。对类型进行验证是可选的,调用 setAllowedTypes()
:
1 2 3 4 5 6 7 8 9 10 11 12 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setAllowedTypes('host', 'string');
$resolver->setAllowedTypes('port', array('null', 'int'));
}
} |
对于每一个选项,你既可以定义某一个类型,也可以定义可接受类型的数组。你可以传入任何 is_<type>()
函数在PHP中所定义的类型,可以传入FQCN类名(fully qualified class name)或接口名:
如果现在你传入一个有效选项,一个 InvalidOptionsException
异常会抛出:
1 2 3 4 5 6 | $mailer = new Mailer(array(
'host' => 25,
));
// InvalidOptionsException: The option "host" with value "25" is
// expected to be of type "string" |
在子类中,你可以使用 addAllowedTypes()
来添加附加的“许可类型”而毋须抹掉之前已经设置好的选项类型。
某些选项只接受预定义值列表中的某一个。例如,假设 Mailer
类有一个 transport
选项,它可以是 sendmail
, mail
和 smtp
中的一个。使用 setAllowedValues()
方法来验证传入的选项包含这些值中的一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setDefaults(array(
'encryption' => null,
'host' => 'example.org',
));
}
} |
如果你传入了无效的transport,InvalidOptionsException
异常会抛出:
1 2 3 4 5 6 | $mailer = new Mailer(array(
'transport' => 'send-mail',
));
// InvalidOptionsException: The option "transport" has the value
// "send-mail", but is expected to be one of "sendmail", "mail", "smtp" |
对于更复杂验证场景下的选项,传入一个closure,它对于接受的值返回true
,而对无效值返回false
:
1 2 3 4 | // ...
$resolver->setAllowedValues('transport', function ($value) {
// return true or false
}); |
在子类中,你可以使用 addAllowedTypes()
来添加附加的“许可类型”而毋须抹掉之前已经设置好的选项类型。
有时,在你使用选项值之前,需要对它们进行标准化(normalize)。例如,假设 host
选项始终以 http://
开头。要实现这个,你可以编写normalizers。Normalizers在选项被验证之后执行。你可以通过调用 setNormalizer()
来配置一个normalizer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | use Symfony\Component\OptionsResolver\Options;
// ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setNormalizer('host', function (Options $options, $value) {
if ('http://' !== substr($value, 0, 7)) {
$value = 'http://'.$value;
}
return $value;
});
}
} |
normalizer接收真正的 $value
并返回标准化之后的形式。你注意到closure也接收一个 $options
参数。这在你标准化过程中需要其他选项时很有用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setNormalizer('host', function (Options $options, $value) {
if ('http://' !== substr($value, 0, 7) && 'https://' !== substr($value, 0, 8)) {
if ('ssl' === $options['encryption']) {
$value = 'https://'.$value;
} else {
$value = 'http://'.$value;
}
}
return $value;
});
}
} |
假设你希望设置 port
选项的默认值,这个值取决于 Mailer
类的用户所选择的加密方式。更为巧思的是,你希望在使用SSL时设置端口号为 465
否则就是 25
。
通过传入一个closure作为 port
选项的默认值,你就可以实现这个功能。closure接收选项作为参数。根据这些选项,你可以返回需要的默认值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setDefault('port', 25);
}
// ...
public function sendMail($from, $to)
{
// Is this the default value or did the caller of the class really
// set the port to 25?
// 这是默认值吗?或者,这个类的调用者是否真的把port设置成了25?
if (25 === $this->options['port']) {
// ...
}
}
} |
回调中的参数,必须使用 options
这个类型提示(type hint)。否则,回调自身将被认为是选项的默认值。
闭包仅在 port
选项没有被用户设置时,或是在子类中被覆写时才执行。
前面设置过的默认值可以通过closure中的第二个参数来访问到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setDefaults(array(
'encryption' => null,
'host' => 'example.org',
));
}
}
class GoogleMailer extends Mailer
{
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('host', function (Options $options, $previousValue) {
if ('ssl' === $options['encryption']) {
return 'secure.example.org'
}
// Take default value configured in the base class
// 取得了基类中配置的默认值
return $previousValue;
});
}
} |
如上例所见,这个功能非常有用,如果你希望在子类中复用在父类中设置的默认值的话。
在一些场景下,定义一个没有默认值的选项是有必要的。如果你需要知道“用户是否真正地 设置过一个选项”,此时会有用。例如,若你对一个选项设置了默认值,便不可能知道用户是否传入的就是此值,也不知道它是否直接来自于默认值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setDefault('port', 25);
}
// ...
public function sendMail($from, $to)
{
// Is this the default value or did the caller of the class really
// set the port to 25?
// 这个25是来自类的调用者自行设置,还是来自默认值?
if (25 === $this->options['port']) {
// ...
}
}
} |
你可以使用 setDefined()
来定义一个没有默认值的选项。然后这个选项将仅在它真正传入 resolve()
方法中时,才被包容到解析过的选项中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setDefined('port');
}
// ...
public function sendMail($from, $to)
{
if (array_key_exists('port', $this->options)) {
echo 'Set!';
} else {
echo 'Not Set!';
}
}
}
$mailer = new Mailer();
$mailer->sendMail($from, $to);
// => Not Set!
$mailer = new Mailer(array(
'port' => 25,
));
$mailer->sendMail($from, $to);
// => Set! |
如果你希望一步定义多个选项,可以传入一个选项名称的数组:
1 2 3 4 5 6 7 8 9 10 | // ...
class Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setDefined(array('port', 'encryption'));
}
} |
isDefined()
和 getDefinedOptions()
方法允许你找出哪些选项被定义过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // ...
class GoogleMailer extends Mailer
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
if ($resolver->isDefined('host')) {
// One of the following was called: / 下列之一会被调用:
// $resolver->setDefault('host', ...);
// $resolver->setRequired('host');
// $resolver->setDefined('host');
}
$definedOptions = $resolver->getDefinedOptions();
}
} |
目前的实现过程,configureOptions()
将被每一个 Mailer
实例所调用。根据配置选项的数量和创建的实例数量,这可能会导致你的程序增加可观的开销。如果这些开销成为一个问题的话,你可以改变代码以令每个类在配置时只执行一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // ...
class Mailer
{
private static $resolversByClass = array();
protected $options;
public function __construct(array $options = array())
{
// What type of Mailer is this, a Mailer, a GoogleMailer, ... ?
// 这个Mailer是何种类型的?是Mailer还是GoogleMailer, ...?
$class = get_class($this);
// Was configureOptions() executed before for this class?
// 之前configureOptions()为这个类执行过吗?
if (!isset(self::$resolversByClass[$class])) {
self::$resolversByClass[$class] = new OptionsResolver();
$this->configureOptions(self::$resolversByClass[$class]);
}
$this->options = self::$resolversByClass[$class]->resolve($options);
}
public function configureOptions(OptionsResolver $resolver)
{
// ...
}
} |
现在,每个类的 OptionsResolver
实例只创建一次,并从此复用。注意,这在程序长时间运行后可能导致内存泄露,如果默认选项包含了对“对象和对象图表(object graphs)”的引用的话。如果这正是你面临的情况,实现 clearOptionsConfig()
方法,并且阶段性调用它:
就是这样!现在你已经具备“在代码中轻松处理选项”的全部工具和知识。
本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。