kuria / options
Resolve structured arrays according to the specified set of options
Installs: 3 729
Dependents: 2
Suggesters: 0
Security: 0
Stars: 2
Watchers: 2
Forks: 0
Open Issues: 0
Requires
- php: >=7.1
- kuria/debug: ^4.0
Requires (Dev)
- kuria/dev-meta: ^0.6
This package is auto-updated.
Last update: 2024-10-22 16:50:33 UTC
README
Resolve structured arrays (e.g. configuration) according to the specified set of options.
Contents
- Features
- Requirements
- Usage
Features
- type validation
- typed lists
- nullable options
- choices
- default values
- lazy defaults (that may depend on other options)
- custom validators and normalizers
- nested options (multi-dimensional arrays)
- custom resolver context
Requirements
- PHP 7.1+
Usage
Resolving options
Use Resolver
to resolve arrays according to the specified options.
The resolve()
method returns an instance of Node
, which can be
accessed as an array. See Working with Node instances.
If the passed value is invalid, ResolverException
will be thrown.
See Handling validation errors.
<?php use Kuria\Options\Resolver; use Kuria\Options\Option; // create a resolver $resolver = new Resolver(); // define options $resolver->addOption( Option::string('path'), Option::int('interval')->default(null) ); // resolve an array $node = $resolver->resolve([ 'path' => 'file.txt', ]); var_dump($node['path'], $node['interval']);
Output:
string(8) "file.txt" NULL
Working with Node
instances
By default, Resolver->resolve()
returns a Node
instance with the resolved options.
Node
implementsArrayAccess
, so the individual options can be acessed using array syntax:$node['option']
lazy default values are resolved once that option is read (or when
toArray()
is called)nested node options are also returned as
Node
instances(if you need to work exclusively with arrays, use
$node->toArray()
)
Resolver context
Resolver->resolve()
accepts a second argument, which may be an array of additional arguments
to pass to all validators, normalizers and lazy default closures. The values may be of any type.
use Kuria\Options\Node; use Kuria\Options\Option; use Kuria\Options\Resolver; $resolver = new Resolver(); $resolver->addOption( Option::string('option') ->normalize(function (string $value, $foo, $bar) { echo 'NORMALIZE: ', $foo, ', ', $bar, "\n"; return $value; }) ->validate(function (string $value, $foo, $bar) { echo 'VALIDATE: ', $foo, ', ', $bar, "\n"; }), Option::string('optionWithLazyDefault') ->default(function (Node $node, $foo, $bar) { echo 'DEFAULT: ', $foo, ', ', $bar, "\n"; return 'value'; }) ); $options = $resolver->resolve( ['option' => 'value'], ['context argument 1', 'context argument 2'] )->toArray();
Output:
NORMALIZE: context argument 1, context argument 2 VALIDATE: context argument 1, context argument 2 DEFAULT: context argument 1, context argument 2
Defining options
Terminology
- leaf option
- An option in the option tree that does not contain children.
- node option
- An option defined via
Option::node()
orOption::nodeList()
. They are branches in the option tree. - child option
- Any option nested inside a node option. It can be either leaf or a node option.
Option factories
The Option
class provides a number of static factories to create option instances.
Option configuration
Option instances can be configured further by using the following methods.
All methods implement a fluent interface, for example:
<?php use Kuria\Options\Option; Option::string('name') ->default('foo') ->nullable();
required()
Makes the option required (and removes any previously set default value).
- a leaf option is required by default
- a node option is not required by default, but having
a required child option will make it required
(unless the node option itself defaults to
NULL
).
default($default)
Makes the option optional and specifies a default value.
- specifying
NULL
as the default value also makes the option nullable - default value of a leaf option is not subject to validation or normalization and is used as-is
- default value of a node option must be an array or
NULL
and is validated and normalized according to the specified child options
Lazy default values (leaf-only)
To specify a lazy default value, pass a closure with the following signature:
<?php use Kuria\Options\Node; use Kuria\Options\Option; Option::string('foo')->default(function (Node $node) { // return value can also depend on other options return 'default'; });
Once the default value is needed, the closure will be called and its return value stored for later use (so it will not be called more than once).
Note
The typehinted Node
parameter is required. A closure with incompatible
signature will be considered a default value itself and returned as-is.
Note
Node options do not support lazy default values.
Tip
It is possible to pass additional arguments to all lazy default closures. See Resolver context.
nullable()
Make the option nullable, accepting NULL
in addition to the specified type.
notNullable()
Make the option non-nullable, not accepting NULL
.
Note
Options are non-nullable by default.
allowEmpty()
Allow empty values to be passed to this option.
Note
Options accept empty values by default.
notEmpty()
Make the option reject empty values.
A value is considered empty if PHP's empty()
returns TRUE
.
normalize(callable $normalizer)
Append a normalizer to the option. The normalizer should accept a value
and return the normalized value or throw Kuria\Options\Exception\NormalizerException
on failure.
See Normalizer and validator value types.
- normalizers are called before validators defined by
validate()
- normalizers are called in the order they were appended
- normalizers are not called if the type of the value is not valid
- the order in which options are normalized is undefined (but node options are normalized in child-first order)
<?php use Kuria\Options\Resolver; use Kuria\Options\Option; $resolver = new Resolver(); $resolver->addOption( Option::string('name')->normalize('trim') ); var_dump($resolver->resolve(['name' => ' foo bar ']));
Output:
object(Kuria\Options\Node)#7 (1) { ["name"]=> string(7) "foo bar" }
Note
To normalize all options at the root level, define one or more normalizers
using $resolver->addNormalizer()
.
Tip
It is possible to use normalizers to convert nodes into custom objects,
so you don't have to work with anonymous Node
objects.
Tip
It is possible to pass additional arguments to all normalizers. See Resolver context.
validate(callable $validator)
Append a validator to the option. The validator should accept and validate a value.
- validators are called after normalizers defined by
normalize()
- validators are called in the order they were appended
- validators are not called if the type of the value is not valid or its normalization has failed
- if a validator returns one or more errors, no other validators of that option will be called
- the order in which options are validated is undefined (but node options are validated in child-first order)
The validator should return one of the following:
NULL
or an empty array if there no errors- errors as a
string
, an array of strings or Error instances
<?php use Kuria\Options\Exception\ResolverException; use Kuria\Options\Resolver; use Kuria\Options\Option; $resolver = new Resolver(); $resolver->addOption( Option::string('email')->validate(function (string $email) { if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { return 'must be a valid email address'; } }) ); try { var_dump($resolver->resolve(['email' => 'this is not an email'])); } catch (ResolverException $e) { echo $e->getMessage(), "\n"; }
Output:
Failed to resolve options due to following errors: 1) email: must be a valid email address
Note
To validate all options at the root level, define one or more validators
using $resolver->addValidator()
.
Tip
It is possible to pass additional arguments to all validators. See Resolver context.
Supported types
NULL
- any type"bool"
"int"
"float"
"number"
- integer or float"numeric"
- integer, float or a numeric string"string"
"array"
"iterable"
- array or an instance ofTraversable
"object"
"resource"
"scalar"
- integer, float, string or a boolean"callable"
Any other type is considered to be a class name, accepting instances of the given class or interface (or their descendants).
An option defined as nullable will also accept a NULL
value. See nullable().
Normalizer and validator value types
The type of the value passed to normalizers and validators depend on the type of the option.
Option::list()
,Option::choiceList()
- an array of valuesOption::node()
- aNode
instanceOption::nodeList()
- an array ofNode
instances- other - depends on the type of the option (
string
,int
, etc.)
Note
A normalizer may modify or replace the value (including its type) before it is passed to subsequent normalizers and validators.
Node options
Node options accept an array of the specified options. With them it is possible to resolve more complex structures.
- node options are resolved iteratively (without recursion)
- certain configuration behaves differently with node options, see Option configuration
<?php use Kuria\Options\Option; use Kuria\Options\Resolver; $resolver = new Resolver(); $resolver->addOption( Option::string('username'), Option::node( 'personalInformation', Option::int('birthYear'), Option::int('height')->default(null), Option::float('weight')->default(null) ), Option::nodeList( 'securityLog', Option::string('action'), Option::int('timestamp'), Option::node( 'client', Option::string('ip'), Option::string('userAgent') ) ) );
Handling validation errors
The Resolver->resolve()
method throws Kuria\Options\Exception\ResolverException
on failure.
The specific errors can be retrieved by calling getErrors()
on the exception object.
<?php use Kuria\Options\Resolver; use Kuria\Options\Exception\ResolverException; use Kuria\Options\Option; $resolver = new Resolver(); $resolver->addOption( Option::string('name'), Option::int('level'), Option::int('score') ); try { $resolver->resolve([ 'name' => null, 'level' => 'not_a_string', 'foo' => 'bar', ]); } catch (ResolverException $e) { foreach ($e->getErrors() as $error) { echo $error->getFormattedPath(), "\t", $error->getMessage(), "\n"; } }
Output:
name string expected, but got NULL instead level int expected, but got "not_a_string" instead score this option is required foo unknown option
Ignoring unknown keys
The Resolver
can be configured to ignore unknown keys by calling
$resolver->setIgnoreUnknown(true)
.
UnknownOptionError
will no longer be raised for unknown keys- this applies to nested options as well
- the unknown keys will be present among the resolved options
Integrating the options resolver
The StaticOptionsTrait
can be used to easily add static option support
to a class.
It has the added benefit of caching and reusing the resolver in multiple
instances of the class. If needed, the cache can be cleared by calling
Foo::clearOptionsResolverCache()
.
<?php use Kuria\Options\Integration\StaticOptionsTrait; use Kuria\Options\Node; use Kuria\Options\Option; use Kuria\Options\Resolver; class Foo { use StaticOptionsTrait; /** @var Node */ private $config; function __construct(array $options) { $this->config = static::resolveOptions($options); } protected static function defineOptions(Resolver $resolver): void { $resolver->addOption( Option::string('path'), Option::bool('enableCache')->default(false) ); } function dumpConfig(): void { var_dump($this->config); } }
Instantiation example:
<?php $foo = new Foo(['path' => 'file.txt']); $foo->dumpConfig();
Output:
object(Kuria\Options\Node)#8 (2) { ["path"]=> string(8) "file.txt" ["enableCache"]=> bool(false) }