Компонент OptionsResolver (Разрешитель опций)

Компонент OptionsResolver - это array_replace на стероидах. Он позволяет вам создавать систему опций с обязательными опциями, значениями по умолчанию, валидацией (типа, значения), нормализаицей и больше.

Установка

1
$ composer require symfony/options-resolver

Также вы можете клонировать репозиторий https://github.com/symfony/options-resolver.

Note

If you install this component outside of a Symfony application, you must require the vendor/autoload.php file in your code to enable the class autoloading mechanism provided by Composer. Read this article for more details.

Использование

Представьте, что у вас есть класс Mailer, который имеет четыре опции: host, username, password и port:

1
2
3
4
5
6
7
8
9
class Mailer
{
    protected $options;

    public function __construct(array $options = array())
    {
        $this->options = $options;
    }
}

При получении доступа к``$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, чтобы исправить это:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Mailer
{
    // ...

    public function __construct(array $options = array())
    {
        $this->options = array_replace(array(
            'host'     => 'smtp.example.org',
            'username' => 'user',
            'password' => 'pa$$word',
            'port'     => 25,
        ), $options);
    }
}

Теперь все четыре опции точно будут установлены. Но что, если пользователь класса Mailer сделает ошибку?

1
2
3
$mailer = new Mailer(array(
    'usernme' => 'johndoe',  // usernme misspelled (instead of username)
));

Ошибка не будет показана. В лучшем случае, баг проявится во время тестирования, но разработчик потратит время на поиск проблемы. В худшем случае- баг не появится, пока не бует развёрнут в живой системе.

К счастью, класс 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: Опция "usernme" не существует.
// Известные опции: "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: Отсутствует обязательная опция "host".

Метод 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
13
14
15
16
17
18
19
20
// ...
class Mailer
{
    // ...

    public function configureOptions(OptionsResolver $resolver)
    {
        // ...

        // укажите один разрешённый тип
        $resolver->setAllowedTypes('host', 'string');

        // укажите несколько разрешённых типов
        $resolver->setAllowedTypes('port', array('null', 'int'));

        // рекурсивно проверьте все объекты в массиве на тип
        $resolver->setAllowedTypes('dates', 'DateTime[]');
        $resolver->setAllowedTypes('ports', 'int[]');
    }
}

Вы можете передать любой тип, для которого функция is_<type>() определена в PHP. Вы можете также передать полное имя класса или интерфейса (который проверяется, используя instanceof). Кроме того, вы можете валидировать все объекты в массиве рекурсивно, добавив к типу суффикс [].

Если вы сейчас передадите невалидную опцию, будет вызван InvalidOptionsException:

1
2
3
4
5
6
$mailer = new Mailer(array(
    'host' => 25,
));

// InvalidOptionsException: Опция "host" со значение "25" должна
// иметь тип "string"

В подклассах вы можете использовать addAllowedTypes(), чтобы добавить дополнительные разрешённые типы, не стирая те, что уже установлены.

Валидация значения

Некоторые опции могут использовать только один из списков предопределённых значений. Например, представьте, чтоб класс Mailer имеет опцию transport, которая может быть одним из sendmail, mail и smtp. Используйте метод setAllowedValues(), чтобы убедиться, что переданная опция содержит одно из этих значений:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ...
class Mailer
{
    // ...

    public function configureOptions(OptionsResolver $resolver)
    {
        // ...
        $resolver->setDefault('transport', 'sendmail');
        $resolver->setAllowedValues('transport', array('sendmail', 'mail', 'smtp'));
    }
}

Если вы передадите невалидный транспорт, будет вызван InvalidOptionsException:

1
2
3
4
5
6
$mailer = new Mailer(array(
    'transport' => 'send-mail',
));

// InvalidOptionsException: Опция "transport" имеет значение
// "send-mail", но должна быть одним из "sendmail", "mail", "smtp"

Для опций с более сложными схемами валидации, передайте завершитель, который возвращает true для приемлемых значений, и false - для невалидных:

1
2
3
4
// ...
$resolver->setAllowedValues('transport', function ($value) {
    // вернуть true или false
});

В подклассах вы можете использовать addAllowedValues(), чтобы добавить дополнительные разрешённые значения, не стирая уже установленные.

Нормализация опций

Иногда, значения опций нужно нормализовать перед тем, как использовать. Например, представьте, что host должен всегда начинаться с http://. Чтобы сделать это, вы можете написать нормализаторы. Нормализаторы выполняются после валидации опции. Вы можете сконфигурировать нормализатор, вызвав setNormalizer():

 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;
        });
    }
}

Нормализатор получает настоящее $value и возвращает нормализованную форму. Вы видите, что завершитель также использует параметр $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. Точнее, вы хотите установить порт 465, если используется SSL, и 25 - в других случаях.

Вы можете реализовать эту функцию, передав завершитель в качестве значения по умолчанию опции port. Завершитель получает опцию в качестве аргумента. Основываясь на этих опциях, вы можете вернуть желанное значение по умолчанию:

 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->setDefault('encryption', null);

        $resolver->setDefault('port', function (Options $options) {
            if ('ssl' === $options['encryption']) {
                return 465;
            }

            return 25;
        });
    }
}

Caution

Аргумент вызываемоего должен быть типизирован как Options. Иначе, само вызываемое рассматривается, как значение опции по умолчанию.

Note

Завершитель выполняется только, если опция 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
25
26
27
28
29
30
// ...
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'
            }

            // Взять значение по умолчанию, сконфигурированное в базовом классе
            return $previousValue;
        });
    }
}

Как видно в примере, эта функция наиболее полезна, если вы хотите повторно использовать значения по умолчанию, установленные в родительских классах, в подклассах.

Опции без значений по умолчанию

В некоторых случаях, полезно определять опцию, не устанавливая значения по умолчанию. Это полезно, если вам нужно знать, действительно ли пользователь установил опцию. Например, если вы установите значене по умолчанию для опции, невозможно узнать, передал ли пользователь значение, или оно просто является значением по умолчанию:

 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->setDefault('port', 25);
    }

    // ...
    public function sendMail($from, $to)
    {
        // Это значение по умолчанию, или инициатор вызова класса действительно
        // установил порт 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);
// => Не установлено!

$mailer = new Mailer(array(
    'port' => 25,
));
$mailer->sendMail($from, $to);
// => Установлено!

Вы также можете передать массив имён опций, если вы хотите определять несколько опций за раз:

 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')) {
            // Было вызвано одно из следующих:

            // $resolver->setDefault('host', ...);
            // $resolver->setRequired('host');
            // $resolver->setDefined('host');
        }

        $definedOptions = $resolver->getDefinedOptions();
    }
}

Устаревание настройки

New in version 4.2: Метод setDeprecated() появился в Symfony 4.2.

Как только настройка устарела или вы решили больше её не поддерживать, вы можете пометить её устаревшей используя метод setDeprecated():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$resolver
    ->setDefined(array('hostname', 'host'))
    // this outputs the following generic deprecation message:
    // это выведет следующее общее сообщение об устаревании:
    // The option "hostname" is deprecated.
    ->setDeprecated('hostname')

    // вы можете также передать своё сообщение об устаревании
    ->setDeprecated('hostname', 'The option "hostname" is deprecated, use "host" instead.')
;

Вместо передачи сообщения вы можете также передать замыкание, которе вернёт строку (сообщение об устаревании) или пустую строку для игнорирования устаревания. Это замыкание удобно для устаревания только некоторых типов или значений настройки:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$resolver
    ->setDefault('port', null)
    ->setAllowedTypes('port', array('null', 'int'))
    ->setDeprecated('port', function ($value) {
        if (null === $value) {
            return 'Passing "null" to option "port" is deprecated, pass an integer instead.';
        }

        return '';
    })
;

Это замыкание получает аргументом значение настройки после валидирования и нормализирования, когда настройка получена.

Настройки производительности

С текущей реализацией, методй 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
// ...
class Mailer
{
    private static $resolversByClass = array();

    protected $options;

    public function __construct(array $options = array())
    {
        // Какой это тип Mailer: Mailer, GoogleMailer, ... ?
        $class = get_class($this);

        // Была ли выполнена 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 будет создан один раз для одного класса и далее использован повторно. Имейте в виду, что это может привести к пробелам в памяти в долгосрочных приложениях, если опции по умолчанию содержат ссылки на объекты или графики объектов. Если это такой случай, реализуйте метод clearOptionsConfig() и периодически вызывайте его:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ...
class Mailer
{
    private static $resolversByClass = array();

    public static function clearOptionsConfig()
    {
        self::$resolversByClass = array();
    }

    // ...
}

Вот и всё! Теперь у вас есть все инструменты и знания, необходимые для лёгкой обработки опций в вашем коде.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.