Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

New LazyControllerFactory #165

64 changes: 64 additions & 0 deletions doc/book/cookbook/automating-controller-factories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Automating Controller Factories

Writing a factory class for each and every controller that has dependencies
can be tedious, particularly in early development as you are still sorting
out dependencies.

As of version 3.0.1, zend-mvc ships with `Zend\Mvc\Controller\LazyControllerAbstractFactory`,
which provides a reflection-based approach to controller instantiation,
resolving constructor dependencies to the relevant services. The factory may be
used as either an abstract factory, or mapped to specific controller names as a
factory:

```php
use Zend\Mvc\Controller\LazyControllerAbstractFactory;

return [
/* ... */
'controllers' => [
'abstract_factories' => [
LazyControllerAbstractFactory::class,
],
'factories' => [
'MyModule\Controller\FooController' => LazyControllerAbstractFactory::class,
],
],
/* ... */
];
```

Mapping controllers to the factory is more explicit and performant.

The factory operates with the following constraints/features:

- A parameter named `$config` typehinted as an array will receive the
application "config" service (i.e., the merged configuration).
- Parameters typehinted against array, but not named `$config`, will
be injected with an empty array.
- Scalar parameters will be resolved as null values.
- If a service cannot be found for a given typehint, the factory will
raise an exception detailing this.
- Some services provided by Zend Framework components do not have
entries based on their class name (for historical reasons); the
factory contains a map of these class/interface names to the
corresponding service name to allow them to resolve. These include:
- `Zend\Console\Adapter\AdapterInterface` maps to `ConsoleAdapter`,
- `Zend\Filter\FilterPluginManager` maps to `FilterManager`,
- `Zend\Hydrator\HydratorPluginManager` maps to `HydratorManager`,
- `Zend\InputFilter\InputFilterPluginManager` maps to `InputFilterManager`,
- `Zend\Log\FilterPluginManager` maps to `LogFilterManager`,
- `Zend\Log\FormatterPluginManager` maps to `LogFormatterManager`,
- `Zend\Log\ProcessorPluginManager` maps to `LogProcessorManager`,
- `Zend\Log\WriterPluginManager` maps to `LogWriterManager`,
- `Zend\Serializer\AdapterPluginManager` maps to `SerializerAdapterManager`,
- `Zend\Validator\ValidatorPluginManager` maps to `ValidatorManager`,

`$options` passed to the factory are ignored in all cases, as we cannot
make assumptions about which argument(s) they might replace.

Once your dependencies have stabilized, we recommend writing a dedicated
factory, as reflection can introduce performance overhead.

## References

This feature was inspired by [a blog post by Alexandre Lemaire](http://circlical.com/blog/2016/3/9/preparing-for-zend-f).
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pages:
- 'v2.X to v2.7': migration/to-v2-7.md
- 'v2.X to v3.0': migration/to-v3-0.md
- Cookbook:
- 'Automating controller factories': cookbook/automating-controller-factories.md
- 'Using middleware within event listeners': cookbook/middleware-in-listeners.md
site_name: zend-mvc
site_description: 'zend-mvc: MVC application provider'
Expand Down
185 changes: 185 additions & 0 deletions src/Controller/LazyControllerAbstractFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php
/**
* @link http://github.com/zendframework/zend-mvc for the canonical source repository
* @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace Zend\Mvc\Controller;

use Interop\Container\ContainerInterface;
use ReflectionClass;
use ReflectionParameter;
use Zend\Console\Adapter\AdapterInterface as ConsoleAdapterInterface;
use Zend\Filter\FilterPluginManager;
use Zend\Hydrator\HydratorPluginManager;
use Zend\InputFilter\InputFilterPluginManager;
use Zend\Log\FilterPluginManager as LogFilterManager;
use Zend\Log\FormatterPluginManager as LogFormatterManager;
use Zend\Log\ProcessorPluginManager as LogProcessorManager;
use Zend\Log\WriterPluginManager as LogWriterManager;
use Zend\Serializer\AdapterPluginManager as SerializerAdapterManager;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
use Zend\Stdlib\DispatchableInterface;
use Zend\Validator\ValidatorPluginManager;

/**
* Reflection-based factory for controllers.
*
* To ease development, this factory may be used for controllers with
* type-hinted arguments that resolve to services in the application
* container; this allows omitting the step of writing a factory for
* each controller.
*
* You may use it as either an abstract factory:
*
* <code>
* 'controllers' => [
* 'abstract_factories' => [
* LazyControllerAbstractFactory::class,
* ],
* ],
* </code>
*
* Or as a factory, mapping a controller class name to it:
*
* <code>
* 'controllers' => [
* 'factories' => [
* MyControllerWithDependencies::class => LazyControllerAbstractFactory::class,
* ],
* ],
* </code>
*
* The latter approach is more explicit, and also more performant.
*
* The factory has the following constraints/features:
*
* - A parameter named `$config` typehinted as an array will receive the
* application "config" service (i.e., the merged configuration).
* - Parameters type-hinted against array, but not named `$config` will
* be injected with an empty array.
* - Scalar parameters will be resolved as null values.
* - If a service cannot be found for a given typehint, the factory will
* raise an exception detailing this.
* - Some services provided by Zend Framework components do not have
* entries based on their class name (for historical reasons); the
* factory contains a map of these class/interface names to the
* corresponding service name to allow them to resolve.
*
* `$options` passed to the factory are ignored in all cases, as we cannot
* make assumptions about which argument(s) they might replace.
*/
class LazyControllerAbstractFactory implements AbstractFactoryInterface
{
/**
* Maps known classes/interfaces to the service that provides them; only
* required for those services with no entry based on the class/interface
* name.
*
* Extend the class if you wish to add to the list.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we get this list from configuration?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not add a configuration point for it. This currently lists all "special case" service names, and we recommend that users utilize fully qualified interface and/or class names for their service names.

If a user really wants to add more, they should have to think twice before doing so.

Copy link
Contributor

@snapshotpl snapshotpl Jun 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I see that https://github.com/zendframework/zend-mvc/pull/165/files#diff-898a3a94db9fcef93eeeb26a23397ecaR139 will resolve many cases. Anyway this feature it's for really lazy developers, and never will be cover all cases

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisely. If you need something more robust, or with more features, you should likely create a custom factory at that point, not extend this one. 😄

*
* @var string[]
*/
protected $aliases = [
ConsoleAdapterInterface::class => 'ConsoleAdapter',
FilterPluginManager::class => 'FilterManager',
HydratorPluginManager::class => 'HydratorManager',
InputFilterPluginManager::class => 'InputFilterManager',
LogFilterManager::class => 'LogFilterManager',
LogFormatterManager::class => 'LogFormatterManager',
LogProcessorManager::class => 'LogProcessorManager',
LogWriterManager::class => 'LogWriterManager',
SerializerAdapterManager::class => 'SerializerAdapterManager',
ValidatorPluginManager::class => 'ValidatorManager',
];

/**
* {@inheritDoc}
*
* @return DispatchableInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$reflectionClass = new ReflectionClass($requestedName);

if (null === ($constructor = $reflectionClass->getConstructor())) {
return new $requestedName();
}

$reflectionParameters = $constructor->getParameters();

if (empty($reflectionParameters)) {
return new $requestedName();
}

$parameters = array_map(
$this->resolveParameter($container, $requestedName),
$reflectionParameters
);

return new $requestedName(...$parameters);
}

/**
* {@inheritDoc}
*/
public function canCreate(ContainerInterface $container, $requestedName)
{
if (! class_exists($requestedName)) {
return false;
}

return in_array(DispatchableInterface::class, class_implements($requestedName), true);
}

/**
* Resolve a parameter to a value.
*
* Returns a callback for resolving a parameter to a value.
*
* @param ContainerInterface $container
* @param string $requestedName
* @return callable
*/
private function resolveParameter(ContainerInterface $container, $requestedName)
{
/**
* @param ReflectionClass $parameter
* @return mixed
* @throws ServiceNotFoundException If type-hinted parameter cannot be
* resolved to a service in the container.
*/
return function (ReflectionParameter $parameter) use ($container, $requestedName) {
if ($parameter->isArray()
&& $parameter->getName() === 'config'
&& $container->has('config')
) {
return $container->get('config');
}

if ($parameter->isArray()) {
return [];
}

if (! $parameter->getClass()) {
return;
}

$type = $parameter->getClass()->getName();
$type = isset($this->aliases[$type]) ? $this->aliases[$type] : $type;

if (! $container->has($type)) {
throw new ServiceNotFoundException(sprintf(
'Unable to create controller "%s"; unable to resolve parameter "%s" using type hint "%s"',
$requestedName,
$parameter->getName(),
$type
));
}

return $container->get($type);
};
}
}
Loading