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

Commit dc9926a

Browse files
committed
Merge branch 'feature/lazy-service-controller-factory'
Close #165
2 parents 3845c2d + 6417097 commit dc9926a

11 files changed

+556
-2
lines changed

CHANGELOG.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,32 @@
22

33
All notable changes to this project will be documented in this file, in reverse chronological order by release.
44

5-
## 3.0.1 - TBD
5+
## 3.0.1 - 2016-06-23
66

77
### Added
88

9-
- Nothing.
9+
- [#165](https://github.com/zendframework/zend-mvc/pull/165) adds a new
10+
controller factory, `LazyControllerAbstractFactory`, that provides a
11+
Reflection-based approach to instantiating controllers. You may register it
12+
either as an abstract factory or as a named factory in your controller
13+
configuration:
14+
15+
```php
16+
'controllers' => [
17+
'abstract_factories' => [
18+
'Zend\Mvc\Controller\LazyControllerAbstractFactory`,
19+
],
20+
'factories' => [
21+
'MyModule\Controller\FooController' => 'Zend\Mvc\Controller\LazyControllerAbstractFactory`,
22+
],
23+
],
24+
```
25+
26+
The factory uses the typehints to lookup services in the container, using
27+
aliases for well-known services such as the `FilterManager`,
28+
`ValidatorManager`, etc. If an `array` typehint is used with a `$config`
29+
parameter, the `config` service is injected; otherwise, an empty array is
30+
provided. For all other types, a null value is injected.
1031

1132
### Deprecated
1233

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Automating Controller Factories
2+
3+
Writing a factory class for each and every controller that has dependencies
4+
can be tedious, particularly in early development as you are still sorting
5+
out dependencies.
6+
7+
As of version 3.0.1, zend-mvc ships with `Zend\Mvc\Controller\LazyControllerAbstractFactory`,
8+
which provides a reflection-based approach to controller instantiation,
9+
resolving constructor dependencies to the relevant services. The factory may be
10+
used as either an abstract factory, or mapped to specific controller names as a
11+
factory:
12+
13+
```php
14+
use Zend\Mvc\Controller\LazyControllerAbstractFactory;
15+
16+
return [
17+
/* ... */
18+
'controllers' => [
19+
'abstract_factories' => [
20+
LazyControllerAbstractFactory::class,
21+
],
22+
'factories' => [
23+
'MyModule\Controller\FooController' => LazyControllerAbstractFactory::class,
24+
],
25+
],
26+
/* ... */
27+
];
28+
```
29+
30+
Mapping controllers to the factory is more explicit and performant.
31+
32+
The factory operates with the following constraints/features:
33+
34+
- A parameter named `$config` typehinted as an array will receive the
35+
application "config" service (i.e., the merged configuration).
36+
- Parameters typehinted against array, but not named `$config`, will
37+
be injected with an empty array.
38+
- Scalar parameters will be resolved as null values.
39+
- If a service cannot be found for a given typehint, the factory will
40+
raise an exception detailing this.
41+
- Some services provided by Zend Framework components do not have
42+
entries based on their class name (for historical reasons); the
43+
factory contains a map of these class/interface names to the
44+
corresponding service name to allow them to resolve. These include:
45+
- `Zend\Console\Adapter\AdapterInterface` maps to `ConsoleAdapter`,
46+
- `Zend\Filter\FilterPluginManager` maps to `FilterManager`,
47+
- `Zend\Hydrator\HydratorPluginManager` maps to `HydratorManager`,
48+
- `Zend\InputFilter\InputFilterPluginManager` maps to `InputFilterManager`,
49+
- `Zend\Log\FilterPluginManager` maps to `LogFilterManager`,
50+
- `Zend\Log\FormatterPluginManager` maps to `LogFormatterManager`,
51+
- `Zend\Log\ProcessorPluginManager` maps to `LogProcessorManager`,
52+
- `Zend\Log\WriterPluginManager` maps to `LogWriterManager`,
53+
- `Zend\Serializer\AdapterPluginManager` maps to `SerializerAdapterManager`,
54+
- `Zend\Validator\ValidatorPluginManager` maps to `ValidatorManager`,
55+
56+
`$options` passed to the factory are ignored in all cases, as we cannot
57+
make assumptions about which argument(s) they might replace.
58+
59+
Once your dependencies have stabilized, we recommend writing a dedicated
60+
factory, as reflection can introduce performance overhead.
61+
62+
## References
63+
64+
This feature was inspired by [a blog post by Alexandre Lemaire](http://circlical.com/blog/2016/3/9/preparing-for-zend-f).

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pages:
1818
- 'v2.X to v2.7': migration/to-v2-7.md
1919
- 'v2.X to v3.0': migration/to-v3-0.md
2020
- Cookbook:
21+
- 'Automating controller factories': cookbook/automating-controller-factories.md
2122
- 'Using middleware within event listeners': cookbook/middleware-in-listeners.md
2223
site_name: zend-mvc
2324
site_description: 'zend-mvc: MVC application provider'
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
/**
3+
* @link http://github.com/zendframework/zend-mvc for the canonical source repository
4+
* @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
5+
* @license http://framework.zend.com/license/new-bsd New BSD License
6+
*/
7+
8+
namespace Zend\Mvc\Controller;
9+
10+
use Interop\Container\ContainerInterface;
11+
use ReflectionClass;
12+
use ReflectionParameter;
13+
use Zend\Console\Adapter\AdapterInterface as ConsoleAdapterInterface;
14+
use Zend\Filter\FilterPluginManager;
15+
use Zend\Hydrator\HydratorPluginManager;
16+
use Zend\InputFilter\InputFilterPluginManager;
17+
use Zend\Log\FilterPluginManager as LogFilterManager;
18+
use Zend\Log\FormatterPluginManager as LogFormatterManager;
19+
use Zend\Log\ProcessorPluginManager as LogProcessorManager;
20+
use Zend\Log\WriterPluginManager as LogWriterManager;
21+
use Zend\Serializer\AdapterPluginManager as SerializerAdapterManager;
22+
use Zend\ServiceManager\Exception\ServiceNotFoundException;
23+
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
24+
use Zend\Stdlib\DispatchableInterface;
25+
use Zend\Validator\ValidatorPluginManager;
26+
27+
/**
28+
* Reflection-based factory for controllers.
29+
*
30+
* To ease development, this factory may be used for controllers with
31+
* type-hinted arguments that resolve to services in the application
32+
* container; this allows omitting the step of writing a factory for
33+
* each controller.
34+
*
35+
* You may use it as either an abstract factory:
36+
*
37+
* <code>
38+
* 'controllers' => [
39+
* 'abstract_factories' => [
40+
* LazyControllerAbstractFactory::class,
41+
* ],
42+
* ],
43+
* </code>
44+
*
45+
* Or as a factory, mapping a controller class name to it:
46+
*
47+
* <code>
48+
* 'controllers' => [
49+
* 'factories' => [
50+
* MyControllerWithDependencies::class => LazyControllerAbstractFactory::class,
51+
* ],
52+
* ],
53+
* </code>
54+
*
55+
* The latter approach is more explicit, and also more performant.
56+
*
57+
* The factory has the following constraints/features:
58+
*
59+
* - A parameter named `$config` typehinted as an array will receive the
60+
* application "config" service (i.e., the merged configuration).
61+
* - Parameters type-hinted against array, but not named `$config` will
62+
* be injected with an empty array.
63+
* - Scalar parameters will be resolved as null values.
64+
* - If a service cannot be found for a given typehint, the factory will
65+
* raise an exception detailing this.
66+
* - Some services provided by Zend Framework components do not have
67+
* entries based on their class name (for historical reasons); the
68+
* factory contains a map of these class/interface names to the
69+
* corresponding service name to allow them to resolve.
70+
*
71+
* `$options` passed to the factory are ignored in all cases, as we cannot
72+
* make assumptions about which argument(s) they might replace.
73+
*/
74+
class LazyControllerAbstractFactory implements AbstractFactoryInterface
75+
{
76+
/**
77+
* Maps known classes/interfaces to the service that provides them; only
78+
* required for those services with no entry based on the class/interface
79+
* name.
80+
*
81+
* Extend the class if you wish to add to the list.
82+
*
83+
* @var string[]
84+
*/
85+
protected $aliases = [
86+
ConsoleAdapterInterface::class => 'ConsoleAdapter',
87+
FilterPluginManager::class => 'FilterManager',
88+
HydratorPluginManager::class => 'HydratorManager',
89+
InputFilterPluginManager::class => 'InputFilterManager',
90+
LogFilterManager::class => 'LogFilterManager',
91+
LogFormatterManager::class => 'LogFormatterManager',
92+
LogProcessorManager::class => 'LogProcessorManager',
93+
LogWriterManager::class => 'LogWriterManager',
94+
SerializerAdapterManager::class => 'SerializerAdapterManager',
95+
ValidatorPluginManager::class => 'ValidatorManager',
96+
];
97+
98+
/**
99+
* {@inheritDoc}
100+
*
101+
* @return DispatchableInterface
102+
*/
103+
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
104+
{
105+
$reflectionClass = new ReflectionClass($requestedName);
106+
107+
if (null === ($constructor = $reflectionClass->getConstructor())) {
108+
return new $requestedName();
109+
}
110+
111+
$reflectionParameters = $constructor->getParameters();
112+
113+
if (empty($reflectionParameters)) {
114+
return new $requestedName();
115+
}
116+
117+
$parameters = array_map(
118+
$this->resolveParameter($container, $requestedName),
119+
$reflectionParameters
120+
);
121+
122+
return new $requestedName(...$parameters);
123+
}
124+
125+
/**
126+
* {@inheritDoc}
127+
*/
128+
public function canCreate(ContainerInterface $container, $requestedName)
129+
{
130+
if (! class_exists($requestedName)) {
131+
return false;
132+
}
133+
134+
return in_array(DispatchableInterface::class, class_implements($requestedName), true);
135+
}
136+
137+
/**
138+
* Resolve a parameter to a value.
139+
*
140+
* Returns a callback for resolving a parameter to a value.
141+
*
142+
* @param ContainerInterface $container
143+
* @param string $requestedName
144+
* @return callable
145+
*/
146+
private function resolveParameter(ContainerInterface $container, $requestedName)
147+
{
148+
/**
149+
* @param ReflectionClass $parameter
150+
* @return mixed
151+
* @throws ServiceNotFoundException If type-hinted parameter cannot be
152+
* resolved to a service in the container.
153+
*/
154+
return function (ReflectionParameter $parameter) use ($container, $requestedName) {
155+
if ($parameter->isArray()
156+
&& $parameter->getName() === 'config'
157+
&& $container->has('config')
158+
) {
159+
return $container->get('config');
160+
}
161+
162+
if ($parameter->isArray()) {
163+
return [];
164+
}
165+
166+
if (! $parameter->getClass()) {
167+
return;
168+
}
169+
170+
$type = $parameter->getClass()->getName();
171+
$type = isset($this->aliases[$type]) ? $this->aliases[$type] : $type;
172+
173+
if (! $container->has($type)) {
174+
throw new ServiceNotFoundException(sprintf(
175+
'Unable to create controller "%s"; unable to resolve parameter "%s" using type hint "%s"',
176+
$requestedName,
177+
$parameter->getName(),
178+
$type
179+
));
180+
}
181+
182+
return $container->get($type);
183+
};
184+
}
185+
}

0 commit comments

Comments
 (0)