aura/dispatcher

Creates objects from a factory and invokes methods using named parameters; also provides a trait for invoking closures and object methods with named parameters.

2.0.4 2016-10-03 19:45 UTC

This package is auto-updated.

Last update: 2024-03-11 03:28:27 UTC


README

Provides tools to map arbitrary names to dispatchable objects, then to dispatch to those objects using named parameters. This is useful for invoking controller and command objects based on path-info parameters or command line arguments, for dispatching to closure-based controllers, and for building dispatchable objects from factories.

Foreword

Installation

This library requires PHP 5.4 or later; we recommend using the latest available version of PHP as a matter of principle. It has no userland dependencies.

It is installable and autoloadable via Composer as aura/dispatcher.

Alternatively, download a release or clone this repository, then require or include its autoload.php file.

Quality

Scrutinizer Code Quality Code Coverage Build Status

To run the unit tests at the command line, issue phpunit at the package root. (This requires PHPUnit to be available as phpunit.)

This library attempts to comply with PSR-1, PSR-2, and PSR-4. If you notice compliance oversights, please send a patch via pull request.

Community

To ask questions, provide feedback, or otherwise communicate with the Aura community, please join our Google Group, follow @auraphp on Twitter, or chat with us on #auraphp on Freenode.

Getting Started

Overview

First, an external routing mechanism such as Aura.Router or a micro-framework router creates an array of parameters. (Alternatively, the parameters may be an object that implements ArrayAccess).

The parameters are then passed to the Dispatcher. It examines them and picks an object to invoke with those parameters, optionally with a method determined by the parameters.

The Dispatcher then examines the returned result from that first invocation. If the result is itself a dispatchable object, the Dispatcher will recursively dispatch the result until something other than a dispatchable object is returned.

When a non-dispatchable result is returned, the Dispatcher stops recursion and returns the non-dispatchable result.

Closures and Invokable Objects

First, we tell the Dispatcher to examine the controller parameter to find the name of the object to dispatch to:

<?php
use Aura\Dispatcher\Dispatcher;

$dispatcher = new Dispatcher;
$dispatcher->setObjectParam('controller');
?>

Next, we set a closure object into the Dispatcher using setObject():

<?php
$dispatcher->setObject('blog', function ($id) {
    return "Read blog entry $id";
});
?>

We can now dispatch to that closure by using the name as the value for the controller parameter:

<?php
$params = [
    'controller' => 'blog',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

The same goes for invokable objects. First, define a class with an __invoke() method:

<?php
class InvokableBlog
{
    public function __invoke($id)
    {
        return "Read blog entry $id";
    }
}
?>

Next, set an instance of the object into the Dispatcher:

<?php
$dispatcher->setObject('blog', new InvokableBlog);
?>

Finally, dispatch to the invokable object (the parameters and logic are the same as above):

<?php
$params = [
    'controller' => 'blog',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

Object Method

We can tell the Dispatcher to examine the params for a method to call on the object. This method will take precedence over the __invoke() method on an object, if such a method exists.

First, tell the Dispatcher to examine the value of the action param to find the name of the method it should invoke.

<?php
$dispatcher->setMethodParam('action');
?>

Next, define the object we will dispatch to; note that the method is read() instead of __invoke().

<?php
class Blog
{
    public function read($id)
    {
        return "Read blog entry $id";
    }
}
?>

Then, we set the object into the Dispatcher ...

<?php
$dispatcher->setObject('blog', new Blog);
?>

... and finally, we invoke the Dispatcher; we have added an action parameter with the name of the method to invoke:

<?php
$params = [
    'controller' => 'blog',
    'action' => 'read',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

Embedding Objects in Parameters

If you like, you can place dispatchable objects directly in the parameters. (This is often how micro-framework routers work.) For example, let's put a closure into the controller parameter; when we invoke the Dispatcher, it will invoke that closure.

<?php
$params = [
    'controller' => function ($id) {
        return "Read blog entry $id";
    },
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

The same is true for invokable objects ...

<?php
$params = [
    'controller' => new InvokableBlog,
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

... and for object-methods:

<?php
$params = [
    'controller' => new Blog,
    'action' => 'read',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

Recursion and Lazy Loading

The Dispatcher is recursive. After dispatching to the first object, if that object returns a dispatchable object, the Dispatcher will re-dispatch to that object. It will continue doing this until the returned result is not a dispatchable object.

Let's turn the above example of an invokable object in the Dispatcher into a lazy-loaded instantiation. All we have to do is wrap the instantiation in another dispatchable object (in this example, a closure). The benefit of this is that we can fill the Dispatcher with as many objects as we like, and they won't get instantiated until the Dispatcher calls on them.

<?php
$dispatcher->setObject('blog', function () {
    return new Blog;
});
?>

Then we invoke the dispatcher with the same params as before.

<?php
$params = [
    'controller' => 'blog',
    'action' => 'read',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

What happens is this:

  • The Dispatcher finds the 'blog' dispatchable object, sees that it is a closure, and invokes it with the params.

  • The Dispatcher examines the result, sees the result is a dispatchable object, and invokes it with the params.

  • The Dispatcher examines that result, sees that it is not a callable object, and returns the result.

Sending The Array Of Params Directly

Sometimes you will want to send the entire array of parameters directly to the object method or closure, as opposed to matching parameter keys with function argument names. To do so, name a key in the parameters array for the argument name that will receive them, and then set the parameters array into itself using that name. If may be easier to do this by reference, or by copy, depending on your needs.

<?php
// a dispatchable closure that takes an array of params directly,
// not the individual params by keys matching argument names
$dispatcher->setObject('blog', function ($params) {
    return "Read blog entry {$params['id']}"
});

// the initial params
$params = [
     'controller' => 'blog',
     'action' => 'read',
     'id' => 88,
];

// set a params reference into itself; this corresponds with the
// 'params' closure argument
$params['params'] =& $params;

// dispatch
$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

Refactoring To Architecture Changes

The Dispatcher is built with the idea that some developers may begin with a micro-framework architecture, and evolve over time toward a full-stack architecture.

At first, the developer uses closures embedded in the params:

<?php
$dispatcher->setObjectParam('controller');

$params = [
    'controller' => function ($id) {
        return "Read blog entry $id";
    },
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

After adding several controllers, the developer is likely to want to keep the routing configurations separate from the controller actions. At this point the developer may start putting the controller actions in the Dispatcher:

<?php
$dispatcher->setObject('blog', function ($id) {
    return "Read blog entry $id!";
});

$params = [
    'controller' => 'blog',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

As the number and complexity of controllers continues to grow, the developer may wish to put the controllers into their own classes, lazy-loading along the way:

<?php
class Blog
{
    public function __invoke($id)
    {
        return "Read blog entry $id";
    }
}

$dispatcher->setObject('blog', function () {
    return new Blog;
});

$params = [
    'controller' => 'blog',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

Finally, the developer may collect several actions into a single controller, keeping related functionality in the same class. At this point the developer should call setMethodParam() to tell the Dispatcher where to find the method to invoke on the dispatchable object.

<?php
class Blog
{
    public function browse()
    {
        // ...
    }

    public function read($id)
    {
        return "Read blog entry $id";
    }

    public function edit($id)
    {
        // ...
    }

    public function add()
    {
        // ...
    }

    public function delete($id)
    {
        // ...
    }
}

$dispatcher->setMethodParam('action');

$dispatcher->setObject('blog', function () {
    return new Blog;
});

$params = [
    'controller' => 'blog',
    'action' => 'read',
    'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>

Construction-Based Configuration

You can set all dispatchable objects, along with the object parameter name and the method parameter name, at construction time. This makes it easier to configure the Dispatcher object in a single call.

<?php
$object_param = 'controller';
$method_param = 'action';
$objects = [
    'blog' => function () {
        return new BlogController;
    },
    'wiki' => function () {
        return new WikiController;
    },
    'forum' => function () {
        return new ForumController;
    },
];

$dispatcher = new Dispatcher($objects, $object_param, $method_param);
?>

Intercessory Dispatch Methods

Sometimes your classes will have an intercessory method that picks an action to run, either on itself or on another object. This package provides an InvokeMethodTrait to invoke a method on an object using named parameters. (The InvokeMethodTrait honors protected and private scopes.)

<?php
use Aura\Dispatcher\InvokeMethodTrait;

class Blog
{
    use InvokeMethodTrait;

    public function __invoke(array $params)
    {
        $action = isset($params['action']) ? $params['action'] : 'index';
        $method = 'action' . ucfirst($action);
        return $this->invokeMethod($this, $method, $params);
    }

    protected function actionRead($id = null)
    {
        return "Read blog entry $id";
    }
}
?>

You can then dispatch to the object as normal, and it will determine its own logical flow.

<?php
$dispatcher->setObject('blog', function () {
    return new Blog;
});

$params = [
     'controller' => 'blog',
     'action' => 'read',
     'id' => 88,
];

$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>