Standard Core Libraries

SPL extensions, array utilities, error handlers, and more.

Introduction

Standard Core Libraries is sets of useful basic libraries and helpers. these libraries are used as part of core functionalities of Poirot Framework. The component are distributed under the "Poirot/Std" namespace and has a git repository, and, for the php components, a light autoloader and composer support. All Poirot components following the PSR-4 convention for namespace related to directory structure and so, can be loaded using any modern framework autoloader. 100% Test Coverage and fully testable by PHPUnit Test.

In Case You need any support or question use the links below:

Slack: getpoirot.slack.com https://join.slack.com/t/getpoirot/shared_invite/zt-dvfhkoes-h45XoKlyxamfhaHm6G4uSA

Arguments Resolver

It helps to automatically call a callable (function, method or closure) or create an instance of a class with providing necessary arguments from list of available options.

Resolver can resolve arguments by argument name, or type hint provided to the argument. Argument name has the most priority, if name is not match within provided options then try to match based on the type hint of argument by searching from the first element to last options and find the match.

function foo(array $array, stdClass $object, callable $callable, array $secondArray) {}

$ar = new ArgumentsResolver(new CallableResolver('foo'));
$args = $ar->withOptions([
    function () {},
    ['second' => ''],
    new stdClass(),
    'array' => [],
])->getResolvedArguments();

/*
[
  'array' => [],
  'object' => object(stdClass)
  'callable' => object(Closure)
  'secondArray' => ['second' => '']
]
*/

Class Instantiator

Create an instance of class and resolve the arguments needed to constructing object by available given options.

class SimpleClass
{
    public $internalValue;

    function __construct($value)
    {
        $this->internalValue = $x;
    }
}

// Resolving class construct arguments from available options
// 
$ar = new ArgumentsResolver(new InstantiatorResolver(
    SimpleClass::class
));

$simple = $ar->withOptions(['value' => 5, 'not_used' => 'any'])
    ->resolve()
    
echo $simple->internalValue; // 5

or simply use the global function available to create instance of class:

use function Poirot\Std\Invokable\resolveInstantClass;

$simple = resolveInstantClass(SimpleClass::class, ['value' => 5]);
echo $simple->internalValue; // 5

Callable Resolver

Allows to determine the arguments to pass to a function or method and call it. The input to the resolver can be any callable.

new CallableResolver([new MyClass(), 'myMethod']);
new CallableResolver(['MyClass', 'myStaticMethod']);
new CallableResolver('MyClass::myStaticMethod');
new CallableResolver(new MyInvokableClass());
new CallableResolver(function ($foo) {});
new CallableResolver('MyNamespace\my_function');

Usage:

Resolve to callable arguments as an array:

function foo($foo, $bar = 'baz') {
    return $foo . $bar;
}

// Resolve Arguments
//
$ar = new ArgumentsResolver(
    new ArgumentsResolver\CallableResolver('foo')
);

$arguments = $ar->withOptions(['foo' => 'foo'])
   ->getResolvedArguments(); // Array ( [foo] => foo [bar] => baz )
   
call_user_func_array('foo', $arguments);

Call callable dynamically by list of available arguments: this will resolve the arguments list needed to execute callable and wrap it to the \Closure that can be invoked directly. The codes below is equivalent as the code block that you can find above.

function foo($foo, $bar = 'baz') {
    return $foo . $bar;
}

// Resolve Arguments
//
$ar = new ArgumentsResolver(
    new ArgumentsResolver\CallableResolver('foo')
);

$ar->withOptions(['foo' => 'foo'])
   ->resolve()->__invoke();

or simply use the available global function helper:

resolveCallable('foo', ['foo' => 'foo'])();

Reflection Resolver

$ar = new ArgumentsResolver(new ArgumentsResolver\ReflectionResolver(
    new \ReflectionFunction(function ($foo, $bar = 'baz') {
        return $foo . $bar;
    })
));

$ar->withOptions(['foo' => 'foo'])
   ->resolve()();

There is also an utility class which helps in creating a reflection instance: The input can be any callable.

$reflection = \Poirot\Std\Invokable\makeReflectFromCallable($callable);

Configurable

Configurable objects are classes which implemented ipConfigurable pact interface, which allow object to parse configuration properties from a resource to an iterator and build the object itself with this given properties.

abstract class aConfigurable
    implements ipConfigurable
{
    use tConfigurable;


    function __construct($options = null, array $skipExceptions = [])
    {
        if ($options !== null)
            $this->build(static::parse($options), $skipExceptions);
    }

    /**
     * Build Object With Provided Options
     *
     * @param array $options        Usually associated array
     * @param array $skipExceptions List of exception classes to skip on error
     *
     * @return $this
     * @throws ConfigurationError Invalid Option(s) value provided
     */
    abstract function build(array $options, array $skipExceptions = []);
}

Register custom config parser

Parser can be registered globally for classes which extends aConfigurable each registered parser should implement iConfigParser interface. here is an example of registering json parser for imaginary Application Configurable class.

// My Json Parser

class JsonParser implement iConfigParser
{
    /**
     * @inheritDoc
     */
    function canParse($optionsResource): bool
    {
       // simple check, given string resources considered as json
       return is_string($optionsResource);
    }

    /**
     * @inheritDoc
     */
    function parse($optionsResource): array
    {
       return json_decode($optionsResource, true);
    }
}
class SimpleConfigurable
    extends aConfigurable
{
    function setConfigs(array $options, array $skipExceptions = [])
    {
        // ... do configuration
    }
};

SimpleConfigurable::registerParser(new JsonParser);

$configurableClass = new SimpleConfigurable;
$configurableClass->setConfig(SimpleConfigurable::parse('{
  "a": 1,
  "c": 5
}'));

Configurable setter

Built-in configurable which map whole properties to a setter method defined inside the class and feed them by calling setter method and associated value of given property.

This is the simple demonstration of what is expected of a configurable object:

class SimpleConfigurable
    extends aConfigurableSetter
{
    protected $a;
    protected $c = 3; // cant be null

    function setA(?int $val) {
        $this->a = $val;
    }

    function setC(int $val) {
        $this->c = $val;
    }

    function __get($key) {
        return $this->{$key};
    }

    static function parse($optionsResource)
    {
        if (is_string($optionsResource)) {
            $optionsResource = json_decode($optionsResource, true);
        }

        return parent::parse($optionsResource);
    }
};

$configurableClass = new SimpleConfigurable;
$configurableClass->build(SimpleConfigurable::parse('{
  "a": 1,
  "c": 5
}'));

echo $configurableClass->a;
echo $configurableClass->c;
// 15

Multi Parameter Setter Methods

If the setter method needs to have more than one value as a parameter the easiest way is to pass required parameters as an iterable or array to a method. with configurableSetter it's possible to define parameters needed for a setting as a separate arguments to the setter method.

class SimpleConfigurable
    extends aConfigurableSetter
{
    function setCredentials($username, $password)
    {
       // ...
    }
};

$configurableClass = new SimpleConfigurable;
$configurableClass->setConfigs([
  'username' => 'us1000',
  'password' => 'secret',
]);

Avoid configuration exceptions

Exceptions of type ConfigurationError expected to thrown when something is not correct with given configuration properties. for example when given property is unknown (UnknownConfigurationPropertyError) for configurable object or the given value has not correct type (ConfigurationPropertyTypeError).

During building object throwing these known exceptions can be eliminated and skipped.

$configurableClass = new simpleConfigurable('{
  "a": 1,
  "d": 5
}');

// Fatal error: Uncaught UnknownConfigurationPropertyError: 
// Configuration Setter option "d" not found.
$configurableClass = new simpleConfigurable('{
  "a": 1,
  "d": 5
}', [UnknownConfigurationPropertyError::class]);

echo $configurableClass->a;
echo $configurableClass->c;
// 13

Environment configuration

To ease apply different PHP runtime configurations at once the iEnvironmentContext interface is defined which can hold various possible runtime configuration values. This contexts can be considered as different environments with different setup for a specific purpose. such as Development, Test or Production setup.

Simple usage

EnvRegistry::apply(EnvRegistry::Development);
echo EnvRegistry::currentEnvironment(); // development

Available configurations

Here is the list of available configuration provisioning attributes and their corresponding built-in php command:

Attribute

Value

Equivalent PHP Command

define_const

array(['const_name' => 'value'])

define((string) $const, $value);

display_errors

(int) 0, 1

ini_set('display_errors', $value);

display_startup_errors

(int) 0, 1

ini_set('display_startup_errors', $value);

env_global

array(['env_name' => 'value'])

$_ENV[$name] = $value; //simplified

error_reporting

(string) 'E_ALL' bitwise can't be used

(int) E_ALL & ~E_NOTICE

predefined constants

error_reporting(E_ALL);

html_errors

(int) 0, 1

ini_set('html_errors', $value);

time_zone

date_default_timezone_set('UCT')

max_execution_time

(int) 30

set_time_limit($seconds)

PreDefined Contexts

Development, Will enable all error reporting level to show to end-user.

class DevelopmentContext
    extends aEnvironmentContext
{
    protected $displayErrors  = 1;
    /** PHP 5.3 or later, the default value is E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED */
    protected $errorReporting = E_ALL;
    protected $displayStartupErrors = 1;
}

Production, to mitigate all error messages to display to end user.

class ProductionContext
    extends aEnvironmentContext
{
    protected $displayErrors  = 0;
    protected $errorReporting = 0;
    protected $displayStartupErrors = 0;
}

PhpServer, will give the current values from php server configuration.

class PhpServerContext
    extends aEnvironmentContext
{
    function getDisplayErrors()
    {
        if($this->errorReporting === null)
            $this->setDisplayErrors( (int) ini_get('display_errors'));

        return $this->displayErrors;
    }

    // Doing the same for all attributes
    // ...
}

Environment Context Registry

Environment context values can be override while registry provision configurations.

.env.development.php
return [
   'max_executuin_time' => 0,
   'env_global' => [
      'PDO_USER' => 'root',
      'PDO_PASS' => 'secret',
      'PDO_SERVER' => 'mariadb',
    ],
];
index.php
$envName = $_SERVER['APP_ENV'];
$envFile = __DIR__ . '/.env.' . $envName . '.php';
EnvRegistry::apply($envName, file_exists($envFile) ? include $envFile : []);

after applying environment context it's accessible from registry anytime.

somewhere_during_dispatching.php
printf('Environment %s were applied and active.',
  (string) EnvRegistry::getCurrentEnvironment()
);

printf('Pdo User is:%s and executuion time limited to %d.',
  EnvRegistry::currentEnvironment()->getContext()->getEnvGlobal()['PDO_USER'],
  EnvRegistry::currentEnvironment()->getContext()->getMaxExecutionTime()
);

Custom environment context

It's possible to register an actual context with different aliases naming or create a new context which implementing iEnvironmentContext interface. Alias naming of existing contexts:

if (! EnvRegistry::getContextNameByAlias('local')) {
    EnvRegistry::setAliases(EnvRegistry::Development, 'local', 'localhost');
}

EnvRegistry::apply('localhost');
echo EnvRegistry::currentEnvironment(); // development

Custom context implementation:

if ($_SERVER['APP_DEBUG']) {
    EnvRegistry::register(MyCustomDebugModeContext::class, 'debug_mode');
    EnvRegistry::apply('debug_mode');
}

Error Handler

It's a wrapper around default php set_error_handler and set_exception_handler functions. it can be used to create a custom error handler that lets you control how PHP behave when some runtime errors happens. With different error types custom error handling can handle only errors were expecting and take the appropriate action based on that.

Simple usage

ErrorHandler::handle();

10/0; // Warning
echo $foo; // Notice
trigger_error('User error', E_USER_WARNING);
@trigger_error('User error', E_USER_WARNING); // ignored by @ error control operator

if ($errors = ErrorHandler::handleDone()) {
    foreach ($errors as $err) {
        echo get_class($err) . ' - ' . $err->getMessage() . PHP_EOL;
    }
}

/*
ErrorException - User error
ErrorException - Undefined variable: foo
ErrorException - Division by zero
*/

Custom error handler

ErrorHandler::handle takes the callable as it's second parameter, and it servers to notify PHP that if there are any php runtime errors. Callable expected to get \ErrorException object as all errors will converted to by error handler.

ErrorHandler::handle(E_ALL, function (\ErrorException $e) {
    print sprintf(
        "Encountered error %s in %s, line %s: %s\n",
        $e->getSeverity(), $e->getFile(), $e->getLine(), $e->getMessage()
    );
    
    // if you wish let error pop up and handle with
    // php default error handler.
    #return false;
});

10/0;
// Encountered error 2 in index.php, line 23: Division by zero
echo $foo; // Notice Error
// Encountered error 8 in index.php, line 24: Undefined variable: foo

ErrorHandler::handleDone();

Error level handling

First argument of ErrorHandler lets you choose what errors should handled, and it works like the error_reporting directive in php.ini. However, it's important to remember that you can only have one active error handler at any time, not one for each level of error.

ErrorHandler::handle(E_NOTICE, function ($e) {
    echo get_class($e) . ' - ' . $e->getMessage() . PHP_EOL;
});

    // Indentation added for more readability 
    ErrorHandler::handle(E_WARNING | E_ERROR, function ($e) {
        print "This will not triggered.";
    });
    echo $foo; # Notice Error triggered and show to user by php default
    ErrorHandler::handleDone();


    echo $_SERVER[1];
    # ErrorException - Undefined index: NOT_EXISTS 
    
// ...

By default error handler will not catch errors that is silenced by error_reporting level otherwise we use a specific bit control flag ErrorHandler::E_ScreamAll

error_reporting(E_ALL & ~E_WARNING);

ErrorHandler::handle(E_ALL, function ($e) {
   print "This will not triggered.";
});

10/0; // Warning error will stay silence because of error_reporting level

ErrorHandler::handle(E_ALL|ErrorHandler::E_ScreamAll, function ($e) {
   print "This will triggered.";
});

10/0;
// This will triggered.

There is a special bit mask flag ErrorHandler::E_ScreamAll that can be added to error level handling to catch all errors if accrued regardless of what php error reporting level is.

error_reporting(E_ALL & ~E_WARNING);

ErrorHandler::handle(E_ALL | ErrorHandler::E_ScreamAll, function ($e) {
    echo get_class($e) . ' - ' . $e->getMessage() . PHP_EOL;
});

10/0;
// ErrorException - Division by zero

Handle Exceptions

As exceptions terminate the execution flow there is no much control over Exception handling the only thing is triggering callable registered in chain.

ErrorHandler::handle(ErrorHandler::E_HandleAll, function(\Throwable $e) {
    // This will triggered second. 
    error_log($e->getMessage());
});

ErrorHandler::handle(ErrorHandler::E_HandleAll, function(\Throwable $e) {
    // This will triggered first.
    echo get_class($e) . ' - ' . $e->getMessage() . PHP_EOL;
    // Throw an error exception that is
    // caught by error handler to catch by other handlers
    throw $e;
});

throw new \RuntimeException('error happen.');
// RuntimeException - error happen.
// execution terminated.

Hydrator

Hydrator provide a mechanisms both for mostly extracting data sets from objects, as well as manipulating objects data. A simple example of when hydrators came handy is when user send form data from website these data can be in different naming as we expect in domain logic and probably doing some filter over data as they are not trusted usually.

Entity Hydrator

Entity Hydrator abstract basically separate manipulation and extraction to two different job, with defined setter methods hydrator object can be manipulated with given data set then based on the given data we have getter methods which is expected to filter and extract prepared data for next stage to receiver of data.

class ProfileHydrator
    extends aHydrateEntity
{
    protected $fullname;
    protected $email;

    // Hydrate Setter

    function setFullname($fullname)
    {
        $this->fullname = trim( (string) $fullname );
    }

    function setEmail($email)
    {
        $this->email = (string) $email;
    }

    // Hydrate Getters

    function getFullname()
    {
        return $this->fullname;
    }

    function getEmailAddress()
    {
        return $this->email;
    }
}

$hydrator = new ProfileHydrator(ProfileHydrator::parse($_POST));
echo $hydrator->getFullname();
print_r(iterator_to_array($hydrator));

Register global input data parser: with data parsers it's possible to parse different data types to an iterator to feed into entity hydrators to related setter method.

HydrateEntityFixture::registerParser(new FunctorHydrateParser(
  function($resource) {
    $unserialized = unserialize($resource);
    if (is_array($unserialized))
       return $unserialized;
}));

$hydrator = new HydrateEntityFixture(
   serialize(['fullname' => 'Full Name'])
);

Getters hydrator

extract data from an object with getter methods, generally getter hydrator will make an Reflection object of class find getter methods and iterate them by calling them method and return the sanitaized property name associated with the value returned from method call.

class SimpleGetterClass
{
    function getClassname()
    {
        return static::class;
    }

    function getSnakeCaseProperty()
    {
        return 'snake_case_property';
    }

    function getNullable()
    {
        return null;
    }
}

$hydGetter = new HydrateGetters(new SimpleGetterClass);
print_r(iterator_to_array($hydGetter));
/*
[
    [classname] => SimpleGetterClass
    [snake_case_property] => snake_case_property
    [nullable] => 
]
 */

Some property methods can be excluded to not considered as data:

$hydGetter = new HydrateGetters(new SimpleGetterClass);
$hydGetter->excludePropertyMethod('getClassname', 'getNullable');

print_r(iterator_to_array($hydGetter));
/*
[
    [snake_case_property] => snake_case_property
]
*/

Excluding properties can be defined as well internally into class by notations:

/**
 * @excludeByNotation enabled
 */
class SimpleGetterClass
{
    /**
     * @ignore considered as property method
     */
    function getClassname()
    {
        return static::class;
    }

    function getSnakeCaseProperty()
    {
        return 'snake_case_property';
    }

    /**
     * @ignore considered as property method
     */
    function getNullable()
    {
        return null;
    }
}

print_r(iterator_to_array($hydGetter));
/*
[
    [snake_case_property] => snake_case_property
]
*/

IteratorWrapper

This iterator wrapper allows the conversion of anything that is Traversable into an Iterator. This class provide a wrapper around any php Traversable and adding extra functionalities to that.

$iterator = new IteratorWrapper([0, 1, 2, 3, 4], function ($value) {
   return $value;
});

while ($iterator->valid()) {
   $key = $iterator->key();
   $value = $iterator->current();
    
   $iterator->next();
}

Manipulating key and values

On iterator callback the returned value will considered as a current value of iterator item:

// Multiply all values by 2 in given array set 
$iterator = new IteratorWrapper([0, 1, 2], function ($value) {
   return $value * 2;
});

we also can change the key if we need to do this:

$iterator = new IteratorWrapper([0, 1, 2], function ($value, &$index) {
   $index = chr(97 + $index); // 97 is ascii code char for "a"
   return $value * 2;
});

$result = iterator_to_array($iterator); // ['a' => 0, 'b' => 1, 'c' => 2]

Filter items

With iterator wrapper we can skip unwanted items from the iterator with provide Closure control how we iterate over object.

$iterator = new IteratorWrapper([0, 1, 2, 3, 4], function ($value, $index) {
    if ($index % 2 == 0)
        /** @var IteratorWrapper $this */
        $this->skipCurrentIteration();

    return $value;
});

$result = iterator_to_array($iterator); // [1 => 1, 3 => 3]

Cutout items

Sometimes it's needed to stop iterating over items when specific conditions met before we get to the end of iterator items. like when we have limit on database result items.

$iterator = new IteratorWrapper([0, 1, 2, 3, 4], function ($value, $index) {
    if ($index >= 3)
        /** @var IteratorWrapper $this */
        $this->stopAfterThisIteration();

    return $value;
});

$result = iterator_to_array($iterator); // [0, 1, 2, 3]
$iterator = new IteratorWrapper([0, 1, 2, 3, 4], function ($value, $index) {
    if ($index >= 2) {
        /** @var IteratorWrapper $this */
        $this->skipCurrentIteration()
        $this->stopAfterThisIteration();
    }
    
    return $value;
});

$result = iterator_to_array($iterator); // [0, 1, 2]

Caching Iterator

As we have built-in support for CachingIterator but IteratorWrapper itself comes with the internal caching support to the iteration. Some times you have an iterator which is not iterable multiple times for instance it could be the case when you fetch result from databases like MangoDB which you cant rewind the iteration. here is the example can illustrating this better:

one_time_traverable_problem.php
$oneTimeTraversable = (function() {
    static $isIterated;
    if (null === $isIterated) {
        foreach ([1, 2, 3, 4] as $i => $v)
            yield $i => $v;
    }

    $isIterated = true;
})();

foreach ($oneTimeTraversable as $i) {
    // Do something ...
}

// Will throw:
// Exception: Cannot traverse an already closed generator
foreach ($oneTimeTraversable as $i) {
    // Do Something ...
}
$oneTimeTraversable = (function() {
    static $isIterated;
    if (null === $isIterated) {
        foreach ([1, 2, 3, 4] as $i => $v)
            yield $i => $v;
    }

    $isIterated = true;
})();

$wrapIterator = new IteratorWrapper($oneTimeTraversable, function ($value) {
    return $value;
});

foreach ($wrapIterator as $i) {
    // Do something ...
}

foreach ($wrapIterator as $i) {
    // Continue Execution ...
}

// Reach here without throwing Exception

MutexLock

The Mutex allows mutual execution of concurrent processes in order to prevent "race conditions". This is achieved by using a "lock" mechanism. Each possibly concurrent thread cooperates by acquiring a lock before accessing the corresponding data.

Usage example:

if ($mutexLock->acquire()) {
    // resource is free and lock acquired successfully.
    // business logic execution
} else {
    // when resource is allready locked, blocked!
}

How to instantiate lock for specific resource:

$realm = 'your_lock_realm';
$lockDir = sys_get_temp_dir();
$mutexLock = new MutexLock($realm)
   ->giveLockDirPath($lockDir);

here we created an instantiate of lock object which is try to make lock file on given $lockDir path, the $realm is an indicator and unique name to our lock resource. MutexLock::giveLockDirPath is an Immutable method so when we gave the path we couldn't change the value later on.

Object can acquire lock multiple times

When object is already acquire lock on resource all further tries to lock from same object will be success:

if ($mutexLock->acquire()) {
    // do something ...
    if ($mutexLock->acquire()) {
       // somewhere else on code base it will acquired if 
       // we try to get lock on already locked resource.
    }
    
    // we done just realease the lock
   $mutexLock->release();
}

Different lock object instances can't acquire lock on same resource

If we create different lock objects with same setup lock will not acquired on second calls if resource is already got locked.

$mutexLock = new MutexLock('your_lock_realm')
   ->giveLockDirPath($lockDir);

$otherMutexLock = new MutexLock('your_lock_realm')
   ->giveLockDirPath($lockDir);

if ($mutexLock->acquire()) {
   if ($otherMutexLock->acquire()) {
      throw new \Exception('It will not reach here.');
   }
}

// rest of execution

Releasing lock, cleanup and destructing object

As the crash could happen to PHP while we acquired lock the resource might kept as locked as execution didn't reach the __destruct method of lock object. we always allow MutexLock to release on resources. We need a system that cleans the garbage in case of crash, just like PHP does for everything else. register_shutdown_function() could cover the cases of exit(), die() and other exceptions in the code code, but it wouldn’t be enough for a crash or an interruption.

lock_file.php
$mutexLock = new MutexLock('your_lock_realm')
   ->giveLockDirPath($lockDir);

while($mutexLock->acquire()) {
  // php died while doing concurrent process incidintally.
  // ...
  
  // job is done.
  break; 
}

// by destructing object the lock will release.
unset($mutexLock);
cleanup_garabage_lock.php
$otherMutexLock = new MutexLock('your_lock_realm')
   ->giveLockDirPath($lockDir);

if ($otherMutexLock->isLocked()) {
   // Note:
   // We cant use unset() or __destruct to release the lock.
   // just the release() method should be called directly if the same
   // object didnt create lock on resource.
   $otherMutexLock->release();
}

Expiration TTL on lock

The TTL expiration on seconds amount of time can be defined to keep lock on resource.

$mutexLock->acquire(3);
while ($mutexLock->isLocked()) {
   // deny other concurences to access script for 3 seconds.
   
   // do something ...
   sleep(1);
}

// continue executuin of script

ResponderChain

The ResponderChain allows to run multiple callable one after other and pass the result from each callable to others to attain to the final result.

Default Chaining Result Behaviour

Merging values

By default if value returned from callable is array or StdArray object will get merged to previous result regardless if the previous result is array, StdArray or not.

$result = ResponderChain::new()
    ->then(function () {
        return 'first';
    })
    ->then(function () {
        return StdArray::of(['second' => 2]);
    })
    ->then(function () {
        return ['third' => 3];
    })
    ->handle();

/*
[
    'first',
    'second' => 2,
    'third' => 3,
]
*/

Replacing values

Any value other than array or StdArray returned by callable will replace the result from previous callable regardless if the previous result is arrayor not.

$result = ResponderChain::new()
    ->then(function () {
        return 'first';
    })->then(function() {
        return 'second';
    })->then(function() {
        return 'third';
    })->handle();
    
// 'third'

Another example to demonstrate merge and replace together:

$result = ResponderChain::new()
    ->then(function () {
        return ['first' => 1];
    })
    ->then(function () {
        return 'second will replaced first';
    })
    ->then(function () {
        return ['third' => 3]; // get merged, because it's array
    })
    ->handle();

/*
[
    'second will replaced first',
    'third' => 3,
]
*/

Key Value Pair Result

The KeyValueResult object can be returned by the callable to indicate that value is an key, value pair result which a key holding the value associated with that. The KeyValueResult will not get merged by the previous value and always replace the last one and will cast to a single key associative array at the end.

$result = ResponderChain::new()
    ->then(function () {
        return new KeyValueResult('name', 'PHP');
    })
    ->handle();
    
// ['name' => 'PHP']

Instead regular array this result won't get merged by last result from chain:

$result = ResponderChain::new()
    ->then(function () {
        return 'first';
    })
    ->then(function () {
        return new KeyValueResult('name', 'Python');
    })
    ->then(function () {
        return new KeyValueResult('name', 'PHP');
    })
    ->handle();
    
// ['name' => 'PHP']

Aggregate Result

The AggregateResult will accept a variadic arguments of any data result implementation or any PHP default data type and merge them together, the final result is a multi key / value pair array.

The AggregateResult will not get merged by the previous value and always replace the last result.

$result = ResponderChain::new()
    ->then(function () {
        return new AggregateResult(
            new AggregateResult('PHP', 'Dynamic Language', ['name' => 'PHP']),
            new KeyValueResult('designed_by', 'Rasmus Lerdorf'),
            ['first_release' => '1995']
        );
    })
    ->handle();
    
/*
[
    'PHP',
    'Dynamic Language',
    'name' => 'PHP',
    'designed_by' => 'Rasmus Lerdorf',
    'first_release' => '1995',
]
*/

The AggregateResult respond result won't get merged by last result from chain:

$result = ResponderChain::new()
    ->then(function() {
        return ['first_release' => '1995'];
    })
    ->then(function () {
        return new AggregateResult([
           'name' => 'PHP',
           'type' => 'Dynamic Language'
       ]);
    })
    ->handle();

/*
[
    'name' => 'PHP',
    'type' => 'Dynamic Language',
];
*/

Merge Result

When result from callable is MergeResult will merge the data from last result, it's similar to simple array type behaviour when it's returned by callable.

$result = ResponderChain::new()
    ->then(function () {
        return 'first';
    })->then(function() {
        return new MergeResult(['second']);
    })->then(function() {
        return new MergeResult(['third']);
    })
    ->handle();

// ['first', 'second', 'third']


$result = ResponderChain::new()
    ->then(function() {
        return ['first_release' => '1995'];
    })
    ->then(function () {
        return new MergeResult(['name' => 'PHP', 'type' => 'Dynamic Language']);
    })
    ->handle();

/*
[
    'first_release' => '1995',
    'name' => 'PHP',
    'type' => 'Dynamic Language',
]
*/

Passing Parameters Through Call Chain

After each callable execution returned result get merged to params and will get resolved by type and name of argument to next callable.

Define Default Parameters

default parameters can be set on creation time, these parameters can be resolved to callable which required the parameter by defining an argument with same type or name to callable. to read more about resolving arguments see ArgumentsResolver.

$defaultParams = [
   'view_renderer' => new ViewModelRenderer,
   'layout_name'   => 'main', 
];

$result = ResponderChain::new($defaultParams)
    ->then(function(ViewModelRenderer $view) {
        return $view->capture('welcome_page');
    })
    // lastResult is result from previous call
    // layoutName resolved to callable from defaultParams
    ->then(function($lastResult, $layoutName) {
        return $view->capture($layoutName, ['content' => $lastResult])
    })
    ->handle();

Parameters get updated based on each result from callable

Every result returned by each callable will get merged to the default parameters and will replace if the current parameters with same name exists then these parameters can be resolved to next callable.

$result = ResponderChain::new()
    ->then(function() {
       return ['designed_by' => 'Rasmus Lerdorf'];
    })
    ->then(function() {
        return new KeyValueResult('name', 'php');
    })
    ->then(function() {
        return new AggregateResult(['first_release' => '1995']);
    })
    ->then(function($name, $firstRelease, $designedBy) {
        return strtoupper($name) . ' ' . $firstRelease . ' ' . $designedBy;
    })
    ->handle();

// PHP 1995 Rasmus Lerdorf

Exception Handling

With onFailure method the callable can be assigned to a callable chain which will be executed, the \Exception which is thrown and other parameters will be resolved to given callable.

$result = ResponderChain::new()
    ->then(function() {
        return ['designed_by' => 'Rasmus Lerdorf'];
    })
    ->then(function() {
        return new KeyValueResult('name', 'PHP');
    })
    ->then(function() {
        throw new \RuntimeException('Stop Executing Because Of Something');
    })
    ->then(function() {
        return 'Code wont reach here.';
    })
    ->onFailure(function(\Exception $error, $name, $designedBy) {
        return $error->getMessage() . ': ' . $name  . ' ' . $designedBy;
    })
    ->handle();
    
// Stop Executing Because Of Something: PHP Rasmus Lerdorf

Validator abstraction

Validation is a very common task while we dealing with Data, Data can come from forms submitted by users or data before it is written into a database or passed to other web service. Any object can implement Validator abstraction interface and trait which makes validation task transparent by providing an abstraction layer to generalize the validation. any Validation libraries can still be use out of the box to ease validation task or just simple php code block.

Usage example

The only task of objects implementing validation abstract is to add ValidationError object as error(s) which found during validation.

class Author
    implements ipValidator
{
    use tValidator;

    public $name;

    protected function doAssertValidate(&$exceptions)
    {
        if (empty($this->name)) {
            $exceptions[] = $this->createError(
                'Author name is required.',
                'required',
                'name',
                $this->name,
            );
        } elseif (strlen($this->name) > 70) {
            $exceptions[] = $this->createError(
                'Name length exceed maximum allowed characters.',
                'length',
                'name',
                $this->name
            );
        }
    }
}


$author = new Author;
$author->name = '';

try {
    $author->assertValidate();
} catch (ValidationError $e) {
    echo json_encode([
        'errors' => $e->asErrorsArray()
    ]);
}

/*
{
   "errors":{
      "name":{
         "type":"required",
         "message":"Author name is required.",
         "value":""
      }
   }
}
*/

Error chain

Each error caught by validator will chained together as an exception which is traversable by getting the previous exception chain.

class SimpleObject
    implements ipValidator
{
    use tValidator;

    function doAssertValidate(&$exceptions) {
        $exceptions[] = $this->createError('Username is already reserved.');
        $exceptions[] = $this->createError('Password should me more than 8 characters.');
        $exceptions[] = $this->createError('Registration Failed.');
    }
};


$simpleObject = new SimpleObject;

try {
    $simpleObject->assertValidate();
} catch (ValidationError $e) {
    do {
        print $e->getMessage() . PHP_EOL;
    } while($e = $e->getPrevious());
}

/*
Registration Failed.
Password should me more than 8 characters.
Username is already reserved.
*/

Error message processor

Validation error messages are usually shows to users directly, depends of different scenarios we might wanted to show different messages to end user like translating error messages. There is some samples of error messages processors which give you the whole idea of how to use them. Default registered message processor will replace %param% and %value% with ValidatorError into error message.

class SimpleObject
    implements ipValidator
{
    use tValidator;

    function doAssertValidate(&$exceptions) {
        $exceptions[] = $this->createError(
            'Parameter (%param%) has wrong value (%value%) as input.',
            ValidationError::NotInRangeError,
            'namedParam',
            'invalid-value');
    }
};


$simpleObject = new SimpleObject;

try {
    $simpleObject->assertValidate();
} catch (ValidationError $e) {
    print $e->getMessage();
}

// Parameter (namedParam) has wrong value ('invalid-value') as input.

Any custom message processor can be attached statically to the ValidationError class.

// Register this to truncate error message to specified size with minimum priority
ValidationError::addMessageProcessor(new TruncateLength(50), PHP_INT_MIN);

Last updated

Was this helpful?