Aura.Dispatcher
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., (*1)
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., (*2)
It is installable and autoloadable via Composer as aura/dispatcher., (*3)
Alternatively, download a release or clone this repository, then require or include its autoload.php file., (*4)
Quality
, (*5)
To run the unit tests at the command line, issue phpunit
at the package root. (This requires PHPUnit to be available as phpunit
.), (*6)
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., (*7)
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., (*8)
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)., (*9)
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., (*10)
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., (*11)
When a non-dispatchable result is returned, the Dispatcher stops recursion
and returns the non-dispatchable result., (*12)
Closures and Invokable Objects
First, we tell the Dispatcher to examine the controller
parameter to find
the name of the object to dispatch to:, (*13)
setObjectParam('controller');
?>
Next, we set a closure object into the Dispatcher using setObject()
:, (*14)
<?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:, (*15)
'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:, (*16)
<?php
class InvokableBlog
{
public function __invoke($id)
{
return "Read blog entry $id";
}
}
?>
Next, set an instance of the object into the Dispatcher:, (*17)
<?php
$dispatcher->setObject('blog', new InvokableBlog);
?>
Finally, dispatch to the invokable object (the parameters and logic are
the same as above):, (*18)
'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., (*19)
First, tell the Dispatcher to examine the value of the action
param to
find the name of the method it should invoke., (*20)
<?php
$dispatcher->setMethodParam('action');
?>
Next, define the object we will dispatch to; note that the method is read()
instead of __invoke()
., (*21)
<?php
class Blog
{
public function read($id)
{
return "Read blog entry $id";
}
}
?>
Then, we set the object into the Dispatcher ..., (*22)
<?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:, (*23)
'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., (*24)
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 ..., (*25)
new InvokableBlog,
'id' => 88,
];
$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>
... and for object-methods:, (*26)
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., (*27)
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., (*28)
<?php
$dispatcher->setObject('blog', function () {
return new Blog;
});
?>
Then we invoke the dispatcher with the same params as before., (*29)
'blog',
'action' => 'read',
'id' => 88,
];
$result = $dispatcher($params); // or call __invoke() directly
echo $result; // "Read blog entry 88"
?>
What happens is this:, (*30)
-
The Dispatcher finds the 'blog' dispatchable object, sees that it
is a closure, and invokes it with the params., (*31)
-
The Dispatcher examines the result, sees the result is a dispatchable
object, and invokes it with the params., (*32)
-
The Dispatcher examines that result, sees that it is not a callable
object, and returns the result., (*33)
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., (*34)
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., (*35)
At first, the developer uses closures embedded in the params:, (*36)
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:, (*37)
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:, (*38)
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., (*39)
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., (*40)
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.), (*41)
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., (*42)
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"
?>