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