1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-servicemanager for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-servicemanager/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-servicemanager/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\ServiceManager\AbstractFactory;
10
11use Interop\Container\ContainerInterface;
12use Laminas\ServiceManager\Exception\ServiceNotFoundException;
13use Laminas\ServiceManager\Factory\AbstractFactoryInterface;
14use ReflectionClass;
15use ReflectionParameter;
16
17/**
18 * Reflection-based factory.
19 *
20 * To ease development, this factory may be used for classes with
21 * type-hinted arguments that resolve to services in the application
22 * container; this allows omitting the step of writing a factory for
23 * each controller.
24 *
25 * You may use it as either an abstract factory:
26 *
27 * <code>
28 * 'service_manager' => [
29 *     'abstract_factories' => [
30 *         ReflectionBasedAbstractFactory::class,
31 *     ],
32 * ],
33 * </code>
34 *
35 * Or as a factory, mapping a class name to it:
36 *
37 * <code>
38 * 'service_manager' => [
39 *     'factories' => [
40 *         MyClassWithDependencies::class => ReflectionBasedAbstractFactory::class,
41 *     ],
42 * ],
43 * </code>
44 *
45 * The latter approach is more explicit, and also more performant.
46 *
47 * The factory has the following constraints/features:
48 *
49 * - A parameter named `$config` typehinted as an array will receive the
50 *   application "config" service (i.e., the merged configuration).
51 * - Parameters type-hinted against array, but not named `$config` will
52 *   be injected with an empty array.
53 * - Scalar parameters will result in an exception being thrown, unless
54 *   a default value is present; if the default is present, that will be used.
55 * - If a service cannot be found for a given typehint, the factory will
56 *   raise an exception detailing this.
57 * - Some services provided by Laminas components do not have
58 *   entries based on their class name (for historical reasons); the
59 *   factory allows defining a map of these class/interface names to the
60 *   corresponding service name to allow them to resolve.
61 *
62 * `$options` passed to the factory are ignored in all cases, as we cannot
63 * make assumptions about which argument(s) they might replace.
64 *
65 * Based on the LazyControllerAbstractFactory from laminas-mvc.
66 */
67class ReflectionBasedAbstractFactory implements AbstractFactoryInterface
68{
69    /**
70     * Maps known classes/interfaces to the service that provides them; only
71     * required for those services with no entry based on the class/interface
72     * name.
73     *
74     * Extend the class if you wish to add to the list.
75     *
76     * Example:
77     *
78     * <code>
79     * [
80     *     \Laminas\Filter\FilterPluginManager::class       => 'FilterManager',
81     *     \Laminas\Validator\ValidatorPluginManager::class => 'ValidatorManager',
82     * ]
83     * </code>
84     *
85     * @var string[]
86     */
87    protected $aliases = [];
88
89    /**
90     * Constructor.
91     *
92     * Allows overriding the internal list of aliases. These should be of the
93     * form `class name => well-known service name`; see the documentation for
94     * the `$aliases` property for details on what is accepted.
95     *
96     * @param string[] $aliases
97     */
98    public function __construct(array $aliases = [])
99    {
100        if (! empty($aliases)) {
101            $this->aliases = $aliases;
102        }
103    }
104
105    /**
106     * {@inheritDoc}
107     *
108     * @return DispatchableInterface
109     */
110    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
111    {
112        $reflectionClass = new ReflectionClass($requestedName);
113
114        if (null === ($constructor = $reflectionClass->getConstructor())) {
115            return new $requestedName();
116        }
117
118        $reflectionParameters = $constructor->getParameters();
119
120        if (empty($reflectionParameters)) {
121            return new $requestedName();
122        }
123
124        $resolver = $container->has('config')
125            ? $this->resolveParameterWithConfigService($container, $requestedName)
126            : $this->resolveParameterWithoutConfigService($container, $requestedName);
127
128        $parameters = array_map($resolver, $reflectionParameters);
129
130        return new $requestedName(...$parameters);
131    }
132
133    /**
134     * {@inheritDoc}
135     */
136    public function canCreate(ContainerInterface $container, $requestedName)
137    {
138        return class_exists($requestedName) && $this->canCallConstructor($requestedName);
139    }
140
141    private function canCallConstructor($requestedName)
142    {
143        $constructor = (new ReflectionClass($requestedName))->getConstructor();
144
145        return $constructor === null || $constructor->isPublic();
146    }
147
148    /**
149     * Resolve a parameter to a value.
150     *
151     * Returns a callback for resolving a parameter to a value, but without
152     * allowing mapping array `$config` arguments to the `config` service.
153     *
154     * @param ContainerInterface $container
155     * @param string $requestedName
156     * @return callable
157     */
158    private function resolveParameterWithoutConfigService(ContainerInterface $container, $requestedName)
159    {
160        /**
161         * @param ReflectionParameter $parameter
162         * @return mixed
163         * @throws ServiceNotFoundException If type-hinted parameter cannot be
164         *   resolved to a service in the container.
165         */
166        return function (ReflectionParameter $parameter) use ($container, $requestedName) {
167            return $this->resolveParameter($parameter, $container, $requestedName);
168        };
169    }
170
171    /**
172     * Returns a callback for resolving a parameter to a value, including mapping 'config' arguments.
173     *
174     * Unlike resolveParameter(), this version will detect `$config` array
175     * arguments and have them return the 'config' service.
176     *
177     * @param ContainerInterface $container
178     * @param string $requestedName
179     * @return callable
180     */
181    private function resolveParameterWithConfigService(ContainerInterface $container, $requestedName)
182    {
183        /**
184         * @param ReflectionParameter $parameter
185         * @return mixed
186         * @throws ServiceNotFoundException If type-hinted parameter cannot be
187         *   resolved to a service in the container.
188         */
189        return function (ReflectionParameter $parameter) use ($container, $requestedName) {
190            if ($parameter->isArray() && $parameter->getName() === 'config') {
191                return $container->get('config');
192            }
193            return $this->resolveParameter($parameter, $container, $requestedName);
194        };
195    }
196
197    /**
198     * Logic common to all parameter resolution.
199     *
200     * @param ReflectionParameter $parameter
201     * @param ContainerInterface $container
202     * @param string $requestedName
203     * @return mixed
204     * @throws ServiceNotFoundException If type-hinted parameter cannot be
205     *   resolved to a service in the container.
206     */
207    private function resolveParameter(ReflectionParameter $parameter, ContainerInterface $container, $requestedName)
208    {
209        if ($parameter->isArray()) {
210            return [];
211        }
212
213        if (! $parameter->getClass()) {
214            if (! $parameter->isDefaultValueAvailable()) {
215                throw new ServiceNotFoundException(sprintf(
216                    'Unable to create service "%s"; unable to resolve parameter "%s" '
217                    . 'to a class, interface, or array type',
218                    $requestedName,
219                    $parameter->getName()
220                ));
221            }
222
223            return $parameter->getDefaultValue();
224        }
225
226        $type = $parameter->getClass()->getName();
227        $type = isset($this->aliases[$type]) ? $this->aliases[$type] : $type;
228
229        if ($container->has($type)) {
230            return $container->get($type);
231        }
232
233        if (! $parameter->isOptional()) {
234            throw new ServiceNotFoundException(sprintf(
235                'Unable to create service "%s"; unable to resolve parameter "%s" using type hint "%s"',
236                $requestedName,
237                $parameter->getName(),
238                $type
239            ));
240        }
241
242        // Type not available in container, but the value is optional and has a
243        // default defined.
244        return $parameter->getDefaultValue();
245    }
246}
247