This repository was archived by the owner on Jan 29, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 91
New LazyControllerFactory #165
Merged
weierophinney
merged 9 commits into
zendframework:master
from
weierophinney:feature/lazy-service-controller-factory
Jun 23, 2016
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8e2e226
New LazyControllerFactory
weierophinney df3000d
Added documentation for LazyControllerFactory
weierophinney 4ba6e25
Use `::class` for aliased classes in LazyControllerFactory
weierophinney 33c8cc9
Renamed factory to incorporate verbiage "Abstract"
weierophinney e4a813c
Remove test for non-string $requestedName
weierophinney dd34bad
Made $aliases protected (not private)
weierophinney bc75c3d
Use splat operator
weierophinney 7d30314
Refactor: extract method
weierophinney 00ba958
Refactor array_map
weierophinney File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
* | ||
* @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); | ||
}; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. 😄