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

Commit 8e2e226

Browse files
committed
New LazyControllerFactory
Inspired by http://circlical.com/blog/2016/3/9/preparing-for-zend-f, this abstract factory/factory can be used to create controller instances for controllers defining constructor dependencies, using the following rules: - 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. As it implements zend-servicemanager's v3 AbstractFactoryInterface, it may be used as either an abstract factory, or by mapping controller class names to the factory.
1 parent 3845c2d commit 8e2e226

8 files changed

+446
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 Zend\ServiceManager\Exception\ServiceNotFoundException;
13+
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
14+
use Zend\Stdlib\DispatchableInterface;
15+
16+
/**
17+
* Reflection-based factory for controllers.
18+
*
19+
* To ease development, this factory may be used for controllers with
20+
* type-hinted arguments that resolve to services in the application
21+
* container; this allows omitting the step of writing a factory for
22+
* each controller.
23+
*
24+
* You may use it as either an abstract factory:
25+
*
26+
* <code>
27+
* 'controllers' => [
28+
* 'abstract_factories' => [
29+
* LazyControllerFactory::class,
30+
* ],
31+
* ],
32+
* </code>
33+
*
34+
* Or as a factory, mapping a controller class name to it:
35+
*
36+
* <code>
37+
* 'controllers' => [
38+
* 'factories' => [
39+
* MyControllerWithDependencies::class => LazyControllerFactory::class,
40+
* ],
41+
* ],
42+
* </code>
43+
*
44+
* The latter approach is more explicit, and also more performant.
45+
*
46+
* The factory has the following constraints/features:
47+
*
48+
* - A parameter named `$config` typehinted as an array will receive the
49+
* application "config" service (i.e., the merged configuration).
50+
* - Parameters type-hinted against array, but not named `$config` will
51+
* be injected with an empty array.
52+
* - Scalar parameters will be resolved as null values.
53+
* - If a service cannot be found for a given typehint, the factory will
54+
* raise an exception detailing this.
55+
* - Some services provided by Zend Framework components do not have
56+
* entries based on their class name (for historical reasons); the
57+
* factory contains a map of these class/interface names to the
58+
* corresponding service name to allow them to resolve.
59+
*
60+
* `$options` passed to the factory are ignored in all cases, as we cannot
61+
* make assumptions about which argument(s) they might replace.
62+
*/
63+
class LazyControllerFactory implements AbstractFactoryInterface
64+
{
65+
/**
66+
* Maps known classes/interfaces to the service that provides them; only
67+
* required for those services with no entry based on the class/interface
68+
* name.
69+
*
70+
* @var string[]
71+
*/
72+
private $aliases = [
73+
'Zend\Console\Adapter\AdapterInterface' => 'ConsoleAdapter',
74+
'Zend\Filter\FilterPluginManager' => 'FilterManager',
75+
'Zend\Hydrator\HydratorPluginManager' => 'HydratorManager',
76+
'Zend\InputFilter\InputFilterPluginManager' => 'InputFilterManager',
77+
'Zend\Log\FilterPluginManager' => 'LogFilterManager',
78+
'Zend\Log\FormatterPluginManager' => 'LogFormatterManager',
79+
'Zend\Log\ProcessorPluginManager' => 'LogProcessorManager',
80+
'Zend\Log\WriterPluginManager' => 'LogWriterManager',
81+
'Zend\Serializer\AdapterPluginManager' => 'SerializerAdapterManager',
82+
'Zend\Validator\ValidatorPluginManager' => 'ValidatorManager',
83+
];
84+
85+
/**
86+
* {@inheritDoc}
87+
*
88+
* @return DispatchableInterface
89+
*/
90+
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
91+
{
92+
$reflectionClass = new ReflectionClass($requestedName);
93+
94+
if (null === ($constructor = $reflectionClass->getConstructor())) {
95+
return new $requestedName();
96+
}
97+
98+
$reflectionParameters = $constructor->getParameters();
99+
100+
if (empty($reflectionParameters)) {
101+
return new $requestedName();
102+
}
103+
104+
$parameters = [];
105+
foreach ($reflectionParameters as $parameter) {
106+
if ($parameter->isArray()
107+
&& $parameter->getName() === 'config'
108+
&& $container->has('config')
109+
) {
110+
$parameters[] = $container->get('config');
111+
continue;
112+
}
113+
114+
if ($parameter->isArray()) {
115+
$parameters[] = [];
116+
continue;
117+
}
118+
119+
if (! $parameter->getClass()) {
120+
$parameters[] = null;
121+
continue;
122+
}
123+
124+
$type = $parameter->getClass()->getName();
125+
$type = array_key_exists($type, $this->aliases) ? $this->aliases[$type] : $type;
126+
127+
if (! $container->has($type)) {
128+
throw new ServiceNotFoundException(sprintf(
129+
'Unable to create controller "%s"; unable to resolve parameter "%s" using type hint "%s"',
130+
$requestedName,
131+
$parameter->getName(),
132+
$type
133+
));
134+
}
135+
136+
$parameters[] = $container->get($type);
137+
}
138+
139+
return $reflectionClass->newInstanceArgs($parameters);
140+
}
141+
142+
/**
143+
* {@inheritDoc}
144+
*/
145+
public function canCreate(ContainerInterface $container, $requestedName)
146+
{
147+
if (! is_string($requestedName) || ! class_exists($requestedName)) {
148+
return false;
149+
}
150+
151+
$implements = class_implements($requestedName);
152+
return in_array(DispatchableInterface::class, $implements, true);
153+
}
154+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 ZendTest\Mvc\Controller;
9+
10+
use Interop\Container\ContainerInterface;
11+
use PHPUnit_Framework_TestCase as TestCase;
12+
use Zend\Mvc\Controller\LazyControllerFactory;
13+
use Zend\ServiceManager\Exception\ServiceNotFoundException;
14+
use Zend\Validator\ValidatorPluginManager;
15+
16+
class LazyControllerFactoryTest extends TestCase
17+
{
18+
public function setUp()
19+
{
20+
$this->container = $this->prophesize(ContainerInterface::class);
21+
}
22+
23+
public function nonClassRequestedNames()
24+
{
25+
return [
26+
'null' => [null],
27+
'true' => [true],
28+
'false' => [false],
29+
'zero' => [0],
30+
'int' => [1],
31+
'zero-float' => [0.0],
32+
'float' => [1.1],
33+
'non-class-string' => ['non-class-string'],
34+
'array' => [['non-class-string']],
35+
'object' => [(object) ['class' => 'non-class-string']],
36+
];
37+
}
38+
39+
/**
40+
* @dataProvider nonClassRequestedNames
41+
*/
42+
public function testCanCreateReturnsFalseForNonClassRequestedNames($requestedName)
43+
{
44+
$factory = new LazyControllerFactory();
45+
$this->assertFalse($factory->canCreate($this->container->reveal(), $requestedName));
46+
}
47+
48+
public function testCanCreateReturnsFalseForClassesThatDoNotImplementDispatchableInterface()
49+
{
50+
$factory = new LazyControllerFactory();
51+
$this->assertFalse($factory->canCreate($this->container->reveal(), __CLASS__));
52+
}
53+
54+
public function testFactoryInstantiatesClassDirectlyIfItHasNoConstructor()
55+
{
56+
$factory = new LazyControllerFactory();
57+
$controller = $factory($this->container->reveal(), TestAsset\SampleController::class);
58+
$this->assertInstanceOf(TestAsset\SampleController::class, $controller);
59+
}
60+
61+
public function testFactoryInstantiatesClassDirectlyIfConstructorHasNoArguments()
62+
{
63+
$factory = new LazyControllerFactory();
64+
$controller = $factory($this->container->reveal(), TestAsset\ControllerWithEmptyConstructor::class);
65+
$this->assertInstanceOf(TestAsset\ControllerWithEmptyConstructor::class, $controller);
66+
}
67+
68+
public function testFactoryRaisesExceptionWhenUnableToResolveATypeHintedService()
69+
{
70+
$this->container->has(TestAsset\SampleInterface::class)->willReturn(false);
71+
$factory = new LazyControllerFactory();
72+
$this->setExpectedException(
73+
ServiceNotFoundException::class,
74+
sprintf(
75+
'Unable to create controller "%s"; unable to resolve parameter "sample" using type hint "%s"',
76+
TestAsset\ControllerWithTypeHintedConstructorParameter::class,
77+
TestAsset\SampleInterface::class
78+
)
79+
);
80+
$factory($this->container->reveal(), TestAsset\ControllerWithTypeHintedConstructorParameter::class);
81+
}
82+
83+
public function testFactoryPassesNullForScalarParameters()
84+
{
85+
$factory = new LazyControllerFactory();
86+
$controller = $factory($this->container->reveal(), TestAsset\ControllerWithScalarParameters::class);
87+
$this->assertInstanceOf(TestAsset\ControllerWithScalarParameters::class, $controller);
88+
$this->assertNull($controller->foo);
89+
$this->assertNull($controller->bar);
90+
}
91+
92+
public function testFactoryInjectsConfigServiceForConfigArgumentsTypeHintedAsArray()
93+
{
94+
$config = ['foo' => 'bar'];
95+
$this->container->has('config')->willReturn(true);
96+
$this->container->get('config')->willReturn($config);
97+
98+
$factory = new LazyControllerFactory();
99+
$controller = $factory($this->container->reveal(), TestAsset\ControllerAcceptingConfigToConstructor::class);
100+
$this->assertInstanceOf(TestAsset\ControllerAcceptingConfigToConstructor::class, $controller);
101+
$this->assertEquals($config, $controller->config);
102+
}
103+
104+
public function testFactoryCanInjectKnownTypeHintedServices()
105+
{
106+
$sample = $this->prophesize(TestAsset\SampleInterface::class)->reveal();
107+
$this->container->has(TestAsset\SampleInterface::class)->willReturn(true);
108+
$this->container->get(TestAsset\SampleInterface::class)->willReturn($sample);
109+
110+
$factory = new LazyControllerFactory();
111+
$controller = $factory($this->container->reveal(), TestAsset\ControllerWithTypeHintedConstructorParameter::class);
112+
$this->assertInstanceOf(TestAsset\ControllerWithTypeHintedConstructorParameter::class, $controller);
113+
$this->assertSame($sample, $controller->sample);
114+
}
115+
116+
public function testFactoryResolvesTypeHintsForServicesToWellKnownServiceNames()
117+
{
118+
$validators = $this->prophesize(ValidatorPluginManager::class)->reveal();
119+
$this->container->has('ValidatorManager')->willReturn(true);
120+
$this->container->get('ValidatorManager')->willReturn($validators);
121+
122+
$factory = new LazyControllerFactory();
123+
$controller = $factory(
124+
$this->container->reveal(),
125+
TestAsset\ControllerAcceptingWellKnownServicesAsConstructorParameters::class
126+
);
127+
$this->assertInstanceOf(
128+
TestAsset\ControllerAcceptingWellKnownServicesAsConstructorParameters::class,
129+
$controller
130+
);
131+
$this->assertSame($validators, $controller->validators);
132+
}
133+
134+
public function testFactoryCanSupplyAMixOfParameterTypes()
135+
{
136+
$validators = $this->prophesize(ValidatorPluginManager::class)->reveal();
137+
$this->container->has('ValidatorManager')->willReturn(true);
138+
$this->container->get('ValidatorManager')->willReturn($validators);
139+
140+
$sample = $this->prophesize(TestAsset\SampleInterface::class)->reveal();
141+
$this->container->has(TestAsset\SampleInterface::class)->willReturn(true);
142+
$this->container->get(TestAsset\SampleInterface::class)->willReturn($sample);
143+
144+
$config = ['foo' => 'bar'];
145+
$this->container->has('config')->willReturn(true);
146+
$this->container->get('config')->willReturn($config);
147+
148+
$factory = new LazyControllerFactory();
149+
$controller = $factory($this->container->reveal(), TestAsset\ControllerWithMixedConstructorParameters::class);
150+
$this->assertInstanceOf(TestAsset\ControllerWithMixedConstructorParameters::class, $controller);
151+
152+
$this->assertEquals($config, $controller->config);
153+
$this->assertNull($controller->foo);
154+
$this->assertEquals([], $controller->options);
155+
$this->assertSame($sample, $controller->sample);
156+
$this->assertSame($validators, $controller->validators);
157+
}
158+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 ZendTest\Mvc\Controller\TestAsset;
9+
10+
use Zend\Mvc\Controller\AbstractActionController;
11+
12+
class ControllerAcceptingConfigToConstructor extends AbstractActionController
13+
{
14+
public $config;
15+
16+
public function __construct(array $config)
17+
{
18+
$this->config = $config;
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 ZendTest\Mvc\Controller\TestAsset;
9+
10+
use Zend\Mvc\Controller\AbstractActionController;
11+
use Zend\Validator\ValidatorPluginManager;
12+
13+
class ControllerAcceptingWellKnownServicesAsConstructorParameters extends AbstractActionController
14+
{
15+
public $validators;
16+
17+
public function __construct(ValidatorPluginManager $validators)
18+
{
19+
$this->validators = $validators;
20+
}
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 ZendTest\Mvc\Controller\TestAsset;
9+
10+
use Zend\Mvc\Controller\AbstractActionController;
11+
12+
class ControllerWithEmptyConstructor extends AbstractActionController
13+
{
14+
public function __construct()
15+
{
16+
}
17+
}

0 commit comments

Comments
 (0)