One Abstract Action Factory For All
- Reading time:
- 3 minutes
- Published:
- Modified:
- Tags:
- zend-expressive
- dependency-injection
- zend-servicemanager
Yesterday I wrote about using one ActionFactory for all your PSR-7 actions. I used zend-servicemanager for it, together with some voodoo to detect the dependencies and inject it. I was pretty happy with the solution and then I got this:
@xtreamwayz question: why not use abstract_factories? while it may take slower, it will reduce repetitive reg with same factory
— Abdul Malik Ikhsan (@samsonasik) December 29, 2015
After some more research I decided to try it out and it’s actually pretty brilliant. Zend ServiceManger 3 is needed for this.
<?php
namespace App\Action;
use Interop\Container\ContainerInterface;
use ReflectionClass;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
class AbstractActionFactory implements AbstractFactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
// Construct a new ReflectionClass object for the requested action
$reflection = new ReflectionClass($requestedName);
// Get the constructor
$constructor = $reflection->getConstructor();
if (is_null($constructor)) {
// There is no constructor, just return a new class
return new $requestedName;
}
// Get the parameters
$parameters = $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
// Get the parameter class
$class = $parameter->getClass();
// Get the class from the container
$dependencies[] = $container->get($class->getName());
}
// Return the requested class and inject its dependencies
return $reflection->newInstanceArgs($dependencies);
}
public function canCreate(ContainerInterface $container, $requestedName)
{
// Only accept Action classes
if (substr($requestedName, -6) == 'Action') {
return true;
}
return false;
}
}
As you can see, the code is almost the same as what I did before with the ActionFactory. However it now extends an
AbstractFactoryInterface
and it has this canCreate
method.
To register the factory you need to add this line:
'dependencies' => [
'invokables' => [
],
'factories' => [
],
'abstract_factories' => [
App\Action\AbstractActionFactory::class,
],
],
And now the most brilliant part… Remove all action factories from dependencies -> factories / invokables. Yes you read it correctly, you can remove them. The abstract factory will automatically handle all actions from now on.
In case you used the expressive-skeleton, remove these two lines from routes.global.php
:
'dependencies' => [
'invokables' => [
App\Action\PingAction::class => App\Action\PingAction::class,
],
'factories' => [
App\Action\HomePageAction::class => App\Action\HomePageFactory::class,
],
],
While trying to get homepage, under the hood the container (still zend-servicemanager) is looking for the
HomePageAction
class at the usual locations. But since it’s not registered with the container, it falls back to
this abstract factory. In its canCreate
method it tells the container it can handle all classes
ending with Action
. After that the abstract factory returns the Action class with the right dependencies.
There might be a downside though. Since the servicemanager checks if unregistered classes can be handled by a specific abstract factory, it causes some overhead. As long as you limit the number of abstract factories you still have a good performance.
At the time of writing this solution is used for this site and it’s open source.