jclaveau/php-deferred-callchain

Register a chain of calls that will be applied on an instance later

1.4.2 2019-11-19 11:23 UTC

This package is auto-updated.

Last update: 2022-05-19 17:18:51 UTC


README

This class simply provides a way to define fluent chain of methods, functions, array access calls before having the target (object or native value) you wan't to apply it to. Once the expected targets are available, simply call the chain on them as if it was a function.

Latest Stable Version License Total Downloads contributions welcome

Quality

Build Status codecov Scrutinizer Code Quality

Overview

// having
class MyClass
{
    protected $name = 'unnamed';
    
    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }
    
    public function nameEntries()
    {
        return [
            'my_entry_1' => $this->name . " 1",
            'my_entry_2' => $this->name . " 2",
        ];
    }
}

// We can define some chained calls (none is executed)
$doDummyCallsLater = DeferredCallChain::new_( MyClass::class )  // Targets must be MyClass instances
    ->nameEntries()['my_entry_2']                               // access array entry
    ->strtoupper()                                              // apply strtoupper() to it
    ;

// do whatever we want
// ...

// Get your targets
$myInstance1 = (new MyClass)->setName('foo');
$myInstance2 = (new MyClass)->setName('bar');

// Execute the callchain
echo $doDummyCallsLater( $myInstance1 ); // => FOO 2
echo $doDummyCallsLater( $myInstance2 ); // => BAR 2

Installation

php-deferred-callchain is installable via Composer

composer require jclaveau/php-deferred-callchain

Testing

Tests are located here and runnable by calling

./phpunit

Usage

Functionnal and chained construction

DeferredCallChain can be instanciated classically

$nameRobert = (new DeferredCallChain(Human::class))->...

Statically

$nameRobert = DeferredCallChain::new_(Human::class)->...

Or functionnaly

$nameRobert = later(Human::class)->...

Fluent call chain

$nameRobert = DeferredCallChain::new_()
    ->setName('Muda')
    ->setFirstName('Robert')
    ;

$mySubjectIMissedBefore = new Human;
$robert = $nameRobert( $mySubjectIMissedBefore );

echo $robert->getFullName(); // => "Robert Muda"
echo (string) $nameRobert;   // => "(new JClaveau\Async\DeferredCallChain)->setName('Muda')->setFirstName('Robert')"

Working with arrays

$getSubColumnValue = DeferredCallChain::new_()
    ['column_1']
    ['sub_column_3']
    ;

$sub_column_3_value = $getSubColumnValue( [
    'column_1' => [
        'sub_column_1' => 'lalala',
        'sub_column_2' => 'lololo',
        'sub_column_3' => 'lilili',
    ],
    'column_2' => [
        'sub_column_1' => 'lululu',
        'sub_column_2' => 'lelele',
        'sub_column_3' => 'lylyly',
    ],
] );

echo $sub_column_3_value;           // => "lilili"
echo (string) $getSubColumnValue;   // => "(new JClaveau\Async\DeferredCallChain)['column_1']['sub_column_3']"

Working with native types and functions

The features above make calls to objects methods easy and async but when their result is not an object, the fluent syntax has to stop, and the async behavior also.

Based on Daniel S Deboer work https://github.com/danielsdeboer/pipe, support of chained function calls has been added.

class MyClass
{
    public function getString()
    {
        return 'string';
    }
}

$upperifyMyClassString = DeferredCallChain::new_( MyClass::class )
    ->getString()
    ->strtoupper();

echo $upperifyMyClassString( new MyClass ); // prints "STRING"

Some functions do not use the subject of the fluent syntax as first argument. In this case, giving '$$' as the parameter you want to be replaced by the subject.

class MyClass
{
    public function getSentence()
    {
        return 'such a funny lib to implement';
    }
}

$explodeMyClassSentence = DeferredCallChain::new_( MyClass::class )
    ->getSentence()
    ->explode(' ', '$$');

$explodeMyClassSentence( new MyClass ); // returns ['such', 'a', 'funny', 'lib', 'to', 'implement']

Specifying on which class, interface, type or instance, the chain is callable

You can force the target of your call chain to:

  • be an instance of a specific class
$nameRobert = DeferredCallChain::new_(Alien::class)
    ->setName('Muda')
    ->setFirstName('Robert')
    ;

$mySubjectIMissedBefore = new Human;
$robert = $nameRobert( $mySubjectIMissedBefore );

// throws BadTargetClassException
  • implement a specific interface
$getCount = DeferredCallChain::new_("\Traversable")
    ->count()
    ;

$myCountableIMissedBefore = new CountableClass; // class implementing Countable

// throws BadTargetInterfaceException
  • be of a specific native type
$nameRobert = DeferredCallChain::new_("array")
    ->setName('Muda')
    ->setFirstName('Robert')
    ;

$mySubjectIMissedBefore = new Human;
$robert = $nameRobert( $mySubjectIMissedBefore );

// throws BadTargetTypeException
  • be a specific instance given at construction
$myTarget = new Human;
$nameRobert = DeferredCallChain::new_($myTarget)
    ->setName('Muda')
    ->setFirstName('Robert')
    ;

$robert = $nameRobert( new Human );

// throws TargetAlreadyDefinedException

Calls provoking exceptions

As a call can be made far before it's effectivelly applied, exceptions need more debug information for a smooth workflow. To achieve that, a line is added to every exception message thrown during a DeferredCallChain execution, pointing to the buggy call and where it is coded.

For example, an exception having as message An exception has been thrown by some user code will print

An exception has been thrown by some user code
When applying (new JClaveau\Async\DeferredCallChain( <instance id> ))->previousSuccessfullCall()->buggyCall('Robert') defined at <file>:<line>

Static calls

Static calls can be useful, especially for singletons. For some technical reasons explained here, the only way to support it is to call them as normal methods (e.g. with -> ) and look for it as a static method once we know it doesn't exist as a regular one.

later(MyModel::class)->getInstance()->myNormalGetter();
// or
later($myModel)->getInstance()->myNormalGetter(); // like $myModel::getInstance()