abivia/configurable

Trait to support creating complex nested class structures from JSON/YAML objects.

0.5.3 2019-10-30 21:55 UTC

README

This trait facilitates the conversion of a set of data structures, typically generated by decoding JSON or YAML configuration files, into PHP classes. Configurable can convert arrays of objects into associative arrays using a property of the object. It can also validate inputs as well as guard and remap property names.

Configurable supports property mapping, gated properties (via allow, block, and ignore methods), data validation, and data-driven class instantiation. It will map arrays of objects to associative arrays of PHP classes that are indexed by any unique scalar property in the object.

Loading can be either fault-tolerant or strict. Strict validation can either fail with a false result or by throwing the Exception class of your choice.

Installation

Old School: Configurable has no dependencies, so you can just include it and use it.

Composer: require abivia/configurable

Basic Usage

  • 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:

class ConfigurableObject {
    use \Abivia\Configurable\Configurable;

    protected $userName;
    protected $password;

}
$json = '{"userName": "admin"; "password": "insecure"}';
$obj = new ConfigurableObject();
$obj->configure(json_decode($json));
echo $obj->userName . ', ' . $obj->password;

Output: admin, insecure

Advanced Usage

  • 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

{
    "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:

    class SomeClass {
        use \Abivia\Configurable;

        protected $depth;
        protected $length;
        protected $width;

    }

    $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:

{"name": "foo"}

Your application can support a shorthand expression:

"somevalue"

With this code in the initialization:

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.

protected function configurePropertyMap($property) {
    if ($property[0] == '$') {
        $property = 'dollar_' . substr($property, 1);
    }
    return $property;
}

Contained Classes

configureClassMap() can be used to cause the instantiation and configuration of classes that are contained within the top level class. These contained classes must provide the configure() method, either of their own making or by also adopting the Configurable trait.

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 object that has the className property and any of the optional properties.

className (string|callable)

In the simplest case, className is the name of a class that will be instantiated and configured. However, className may also be a callable that takes the current property value as an argument. This allows the creation of data-specific object classes.

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.

Examples

The unit tests contain a number of examples that should be illustrative. More detailed examples with sample output can be found at https://gitlab.com/abivia/configurable-examples