abivia / configurable
特性:支持从JSON/YAML对象创建复杂嵌套类结构。
2.0.0
2021-07-28 00:02 UTC
Requires
- php: >=7.3
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^9.0-stable
- symfony/yaml: ^5.1@dev
README
对于更健壮、更易用、文档更好的解决方案,请参阅abivia\hydration。Configurable将会被维护,但不再进行新的开发工作。
Configurable旨在使从可编辑的JSON或YAML源轻松填充(填充)复杂数据结构变得简单。
- 如果你的应用程序
- 具有多层嵌套的配置,
- 未在配置文件中验证用户可编辑的数据,
- 花费大量精力从由json_decode()或yaml_parse()创建的stdClass对象中读取,以将它们转换为应用程序的类结构,或者
只是使用类型差、IDE不友好的stdClass对象进行配置
Configurable正是为了解决这个问题而存在的。
- Configurable可以将解码JSON或YAML配置文件返回的一组无类型数据结构轻松转换为PHP类。Configurable可以
- 创建按类属性索引的命名类关联数组,
- 有选择地强制类型转换属性为数组,
- 验证源数据,
- 防止覆盖受保护的属性,并将属性从用户友好的名称映射到应用程序有意义的标识符。
示例
Configurable
可以轻松地将如下数据转换为类结构
{
"application-name": "MyApp",
"database": [
{
"label": "crm",
"driver": "mysql",
"host": "localhost",
"name": "crm",
"pass": "insecure",
"port": 3306,
"user": "admin"
},
{
"label": "geocoder",
"driver": "mysql",
"host": "localhost",
"name": "geo",
"pass": "insecure",
"port": 3306,
"user": "admin"
}
]
}
并转换为如下结构
Environment Object
(
[appName] => MyApp
[database] => Array
(
[crm] => DatabaseConfiguration Object
(
[driver:protected] => mysql
[host:protected] => localhost
[key] => crm
[label:protected] => crm
[name:protected] => crm
[pass:DatabaseConfiguration:private] => insecure
[port:protected] => 3306
[user:protected] => admin
)
[geocoder] => DatabaseConfiguration Object
(
[driver:protected] => mysql
[host:protected] => localhost
[key] => geocoder
[label:protected] => geocoder
[name:protected] => geo
[pass:DatabaseConfiguration:private] => insecure
[port:protected] => 3306
[user:protected] => admin
)
)
)
特性
Configurable
支持属性映射、门控属性(通过允许、阻止和忽略方法)、数据验证和数据驱动的类实例化。它将对象数组映射到按对象中任何唯一标量属性索引的PHP类关联数组。
加载可以是容错的或严格的。严格验证可以失败并返回一个false
结果,或者抛出你指定的异常。
安装
Configurable uses the Symphony YAML parser.
Basic Use
----
- Implement `configureClassMap()` for the top level class and any properties that map to your classes.
- Add the configurable trait to your classes.
- Instantiate the top level class and pass your decoded JSON or YAML to the `configure()` method.
Basic example:
```php
class ConfigurableObject {
use \Abivia\Configurable\Configurable;
public $userName;
public $password;
}
$json = '{"userName": "admin"; "password": "insecure"}';
$obj = new ConfigurableObject();
$obj->configure(json_decode($json));
echo $obj->userName . ', ' . $obj->password;
```
Output:
admin, insecure
Selectively Convert Objects to Arrays
---
The `json_decode()` method has an option to force conversion of objects
to arrays, but there is no way to get selective conversion. Configurable can
do this via a class map to 'array'. See `CastArrayTest.php`
for a working example.
```php
class ConfigCastArray
{
use \Abivia\Configurable\Configurable;
/**
* @var array
*/
public $simple = [];
protected function configureClassMap($property, $value)
{
if ($property === 'simple') {
return ['className' => 'array'];
}
return false;
}
}
$json = '{"simple": { "a": "this is a", "*": "this is *"}}';
$hydrate = new ConfigCastArray();
$hydrate->configure(json_decode($json));
print_r($hydrate);
```
Will give this result
```
(
[simple] => Array
(
[a] => this is a
[*] => this is *
)
)```
Associative arrays using a property as the key
---
Have an array of objects with a property that you'd like to extract for use as
an array key? No problem.
```php
class SocialMedia
{
public array $list;
protected function configureClassMap($property, $value)
{
if ($property === 'list') {
return ['className' => 'stdClass', 'key' => 'code'];
}
return false;
}
}
$json = '{"list":[
{"code": "fb", "name": "Facebook"},
{"code": "t", "name": "Twitter"},
{"code": "ig", "name": "Instagram"}
]}
';
$hydrate = new SocialMedia();
$hydrate->configure(json_decode($json));
echo implode(', ', array_keys($hydrate->list) "\n";
echo $hydrate->list['ig']->name;
```
Will output
```
fb, t, ig
Instagram
```
Advanced Use
---
- If you need to map properties, implement `configurePropertyMap()` where needed.
- Add property validation by implementing `configureValidate()`.
- Gate access to properties by implementing any of `configurePropertyAllow()`,
`configurePropertyBlock()` or `configurePropertyIgnore()`.
- You can also initialize the class instance at run time with `configureInitialize()`.
- Semantic validation of the result can be performed at the end of the loading process by
implementing `configureComplete()`.
Options
---
The options parameter can contain these elements:
- 'newLog' If missing or set true, Configurable's error log is cleared before any
processing.
- 'parent' when instantiating a subclass, this is a reference to the parent class.
- 'strict' Controls error handling. If strict is false, Configurable will ignore minor
issues such as additional properties. If strict is true, Configurable will return false
if any errors are encountered. If strict is a string, this will be taken as the name of
a Throwable class, and an instance of that class will be thrown.
Applications can also pass in their own context via options. The current options are
available via the `$configureOptions` property. Option names starting with an underscore
are guaranteed to not conflict with future options used by Configurable.
Note that a copy of the options array is passed to subclass configuration, no data can
be returned to the parent via this array.
Filtering
-----
The properties of the configured object can be explicitly permitted by overriding the
`configurePropertyAllow()` method, blocked by overriding the `configurePropertyBlock()`
method, or ignored via the `configurePropertyIgnore()` method. Ignore takes precedence, then
blocking, then allow. By default, attempts to set guarded properties
are ignored, but if the $strict parameter is either true or the name of a `Throwable`
class, then the configuration will terminate when the invalid parameter is encountered,
unless it has been explicitly ignored.
For a JSON input like this
```json
{
"depth": 15,
"length": 22,
"primary": "Red",
"width": 3
}
```
with a class that does not have the `primary` property, the result depends on the
`strict` option:
```php
class SomeClass {
use \Abivia\Configurable;
protected $depth;
protected $length;
protected $width;
}
$jsonDecoded = json_decode('some valid json string');
$obj = new SomeClass();
// Returns true
$obj->configure($jsonDecoded);
// Lazy validation: Returns true
$obj->configure($jsonDecoded, ['strict' => false]);
// Strict validation: Returns false
$obj->configure($jsonDecoded, ['strict' => true]);
// Strict validation: throws MyException
$obj->configure($jsonDecoded, ['strict' => 'MyException']);
```
Initialization and Completion
---
In many cases it is required that the object be in a known state before configuration,
and that the configured object has acceptable values. `Configurable` supplies
`configureInitialize()` and `configureComplete()` for this purpose. `configureInitialize()`
can be used to return a previously instantiated object to a known state.
`configureInitialize()` gets passed references to the configuration data and the options
array, and is thus able to pre-process the inputs if required.
One use case for pre-processing during initialization is to allow shorthand expressions.
For example, if you have an object with one property:
```json
{"name": "foo"}
```
Your application can support a shorthand expression:
```json
"somevalue"
```
With this code in the initialization:
```php
class MyClass
{
protected function configureInitialize(&$config) {
if (is_string($config)) {
$obj = new stdClass;
$obj->name = $config;
$config = $obj;
}
}
}
```
Validation
---
Scalar properties can be validated with `configureValidate()`. This method takes the property name and the input value
as arguments. The value is passed by reference so that the validation can enforce specific formats required by the
object (for example by forcing case or cleaning out unwanted characters).
The `configureComplete()` method provides a mechanism for object level validation. For example, this method could be
used to validate a set of access credentials, logging an error or aborting the configuration process entirely if they
are not valid.
Property Name Mapping
---
Since configuration files allow properties that are not valid PHP property names,
`configurePropertyMap()` can be used to convert illegal input properties to valid PHP identifiers.
```php
class MyClass
{
protected function configurePropertyMap($property) {
if ($property[0] == '$') {
$property = 'dollar_' . substr($property, 1);
}
return $property;
}
}
```
If the property does not reference another configurable class then
the method can also return an array containing a property name and array index.
For example this json:
```json
{
"prop.one": "element one",
"prop.six": "element six"
}
```
Can be used to create an array:
````php
$configured->prop = ['one' => 'element one', 'six' => 'element six'];
````
Contained Classes
---
The real power of Configurable is through `configureClassMap()` which can be
used to instantiate and configure classes that are contained in the current
class. Contained classes must either be `stdClass` or provide the
`configure()` method, either via the `Configurable` trait or by providing
their own method.
`configureClassMap()` takes the name and value of a property as arguments and returns:
- the name of a class to be instantiated and configured, or
- an array or object that has the `className` property and any of the optional properties.
### className (string|callable)
`className` can be the name of a class that will be instantiated and configured,
or it can be a callable that takes the current property value as an argument.
This allows the creation of data-specific object classes.
If a property `foo` returns an array of `['className' => 'MyClass']` or just
the string `MyClass` then Configurable will instantiate a new `MyClass` and
pass the value to the `configure()` method.
### construct (bool)
`className` is the name of a class that will be instantiated by passing the
value to the class constructor.
If a property `foo` returns an array of `['className' => 'DateInterval', 'construct' => true]`
then Configurable will create a `new DateInterval($value)` and assign it to `foo`.
### constructUnpack (bool)
`className` is the name of a class that will be instantiated by passing the
unpacked value (which must be an array) to the class constructor.
If a property `foo` returns an array of `['className' => 'MyClass', 'constructUnpack' => true]`
then Configurable will create a `new MyClass(...$value)` and assign it to `foo`.
### key (string|callable)
The `key` property is optional and tells Configurable to populate an array.
- if `key` is absent or blank, the constructed object is appended to the array,
- if `key` is a string, then it is taken as the name of a property or method (if
`keyIsMethod` is true) in the constructed object, and this value is used as the
key for an associative array, and
- if `key` is a callable array, then it is called with the object under construction
as an argument.
### keyIsMethod (bool)
The `keyIsMethod` property is only used when `key` is present and not a callable. When
set, `key` is treated as a method of the constructed object. Typically this is a getter.
### allowDups (bool)
If Configurable is creating an associative array, the normal response to a duplicate
key is to generate an error message. if the `allowDups` flag is present and set,
no error is generated.
Error Logging
-------------
Any problems encountered during configuration are logged. An array of errors can be
retrieved by calling the `configureGetErrors()` method. The error log is cleared by an
application call to `configure()` unless the newLog option is set to false.
Unit Tests and Examples
========
Unit tests are organized by PHP support level. Tests that use features of PHP
that are not available in PHP 7.2 are maintained in separate directories.
PHPUnit will automatically run tests up to your current PHP version but
not above.
The unit tests also contain a number of examples that should be helpful in
understanding how Configurable works. More detailed
examples with sample output can be found at
https://gitlab.com/abivia/configurable-examples