1<?php 2 3declare(strict_types=1); 4 5namespace DI; 6 7use DI\Definition\Definition; 8use DI\Definition\Exception\InvalidDefinition; 9use DI\Definition\FactoryDefinition; 10use DI\Definition\Helper\DefinitionHelper; 11use DI\Definition\InstanceDefinition; 12use DI\Definition\ObjectDefinition; 13use DI\Definition\Resolver\DefinitionResolver; 14use DI\Definition\Resolver\ResolverDispatcher; 15use DI\Definition\Source\DefinitionArray; 16use DI\Definition\Source\MutableDefinitionSource; 17use DI\Definition\Source\ReflectionBasedAutowiring; 18use DI\Definition\Source\SourceChain; 19use DI\Definition\ValueDefinition; 20use DI\Invoker\DefinitionParameterResolver; 21use DI\Proxy\ProxyFactory; 22use InvalidArgumentException; 23use Invoker\Invoker; 24use Invoker\InvokerInterface; 25use Invoker\ParameterResolver\AssociativeArrayResolver; 26use Invoker\ParameterResolver\Container\TypeHintContainerResolver; 27use Invoker\ParameterResolver\DefaultValueResolver; 28use Invoker\ParameterResolver\NumericArrayResolver; 29use Invoker\ParameterResolver\ResolverChain; 30use Psr\Container\ContainerInterface; 31 32/** 33 * Dependency Injection Container. 34 * 35 * @api 36 * 37 * @author Matthieu Napoli <matthieu@mnapoli.fr> 38 */ 39class Container implements ContainerInterface, FactoryInterface, InvokerInterface 40{ 41 /** 42 * Map of entries that are already resolved. 43 * @var array 44 */ 45 protected $resolvedEntries = []; 46 47 /** 48 * @var MutableDefinitionSource 49 */ 50 private $definitionSource; 51 52 /** 53 * @var DefinitionResolver 54 */ 55 private $definitionResolver; 56 57 /** 58 * Map of definitions that are already fetched (local cache). 59 * 60 * @var (Definition|null)[] 61 */ 62 private $fetchedDefinitions = []; 63 64 /** 65 * Array of entries being resolved. Used to avoid circular dependencies and infinite loops. 66 * @var array 67 */ 68 protected $entriesBeingResolved = []; 69 70 /** 71 * @var InvokerInterface|null 72 */ 73 private $invoker; 74 75 /** 76 * Container that wraps this container. If none, points to $this. 77 * 78 * @var ContainerInterface 79 */ 80 protected $delegateContainer; 81 82 /** 83 * @var ProxyFactory 84 */ 85 protected $proxyFactory; 86 87 /** 88 * Use `$container = new Container()` if you want a container with the default configuration. 89 * 90 * If you want to customize the container's behavior, you are discouraged to create and pass the 91 * dependencies yourself, the ContainerBuilder class is here to help you instead. 92 * 93 * @see ContainerBuilder 94 * 95 * @param ContainerInterface $wrapperContainer If the container is wrapped by another container. 96 */ 97 public function __construct( 98 MutableDefinitionSource $definitionSource = null, 99 ProxyFactory $proxyFactory = null, 100 ContainerInterface $wrapperContainer = null 101 ) { 102 $this->delegateContainer = $wrapperContainer ?: $this; 103 104 $this->definitionSource = $definitionSource ?: $this->createDefaultDefinitionSource(); 105 $this->proxyFactory = $proxyFactory ?: new ProxyFactory(false); 106 $this->definitionResolver = new ResolverDispatcher($this->delegateContainer, $this->proxyFactory); 107 108 // Auto-register the container 109 $this->resolvedEntries = [ 110 self::class => $this, 111 ContainerInterface::class => $this->delegateContainer, 112 FactoryInterface::class => $this, 113 InvokerInterface::class => $this, 114 ]; 115 } 116 117 /** 118 * Returns an entry of the container by its name. 119 * 120 * @param string $name Entry name or a class name. 121 * 122 * @throws DependencyException Error while resolving the entry. 123 * @throws NotFoundException No entry found for the given name. 124 * @return mixed 125 */ 126 public function get($name) 127 { 128 // If the entry is already resolved we return it 129 if (isset($this->resolvedEntries[$name]) || array_key_exists($name, $this->resolvedEntries)) { 130 return $this->resolvedEntries[$name]; 131 } 132 133 $definition = $this->getDefinition($name); 134 if (! $definition) { 135 throw new NotFoundException("No entry or class found for '$name'"); 136 } 137 138 $value = $this->resolveDefinition($definition); 139 140 $this->resolvedEntries[$name] = $value; 141 142 return $value; 143 } 144 145 /** 146 * @param string $name 147 * 148 * @return Definition|null 149 */ 150 private function getDefinition($name) 151 { 152 // Local cache that avoids fetching the same definition twice 153 if (!array_key_exists($name, $this->fetchedDefinitions)) { 154 $this->fetchedDefinitions[$name] = $this->definitionSource->getDefinition($name); 155 } 156 157 return $this->fetchedDefinitions[$name]; 158 } 159 160 /** 161 * Build an entry of the container by its name. 162 * 163 * This method behave like get() except resolves the entry again every time. 164 * For example if the entry is a class then a new instance will be created each time. 165 * 166 * This method makes the container behave like a factory. 167 * 168 * @param string $name Entry name or a class name. 169 * @param array $parameters Optional parameters to use to build the entry. Use this to force specific parameters 170 * to specific values. Parameters not defined in this array will be resolved using 171 * the container. 172 * 173 * @throws InvalidArgumentException The name parameter must be of type string. 174 * @throws DependencyException Error while resolving the entry. 175 * @throws NotFoundException No entry found for the given name. 176 * @return mixed 177 */ 178 public function make($name, array $parameters = []) 179 { 180 if (! is_string($name)) { 181 throw new InvalidArgumentException(sprintf( 182 'The name parameter must be of type string, %s given', 183 is_object($name) ? get_class($name) : gettype($name) 184 )); 185 } 186 187 $definition = $this->getDefinition($name); 188 if (! $definition) { 189 // If the entry is already resolved we return it 190 if (array_key_exists($name, $this->resolvedEntries)) { 191 return $this->resolvedEntries[$name]; 192 } 193 194 throw new NotFoundException("No entry or class found for '$name'"); 195 } 196 197 return $this->resolveDefinition($definition, $parameters); 198 } 199 200 /** 201 * Test if the container can provide something for the given name. 202 * 203 * @param string $name Entry name or a class name. 204 * 205 * @throws InvalidArgumentException The name parameter must be of type string. 206 * @return bool 207 */ 208 public function has($name) 209 { 210 if (! is_string($name)) { 211 throw new InvalidArgumentException(sprintf( 212 'The name parameter must be of type string, %s given', 213 is_object($name) ? get_class($name) : gettype($name) 214 )); 215 } 216 217 if (array_key_exists($name, $this->resolvedEntries)) { 218 return true; 219 } 220 221 $definition = $this->getDefinition($name); 222 if ($definition === null) { 223 return false; 224 } 225 226 return $this->definitionResolver->isResolvable($definition); 227 } 228 229 /** 230 * Inject all dependencies on an existing instance. 231 * 232 * @param object $instance Object to perform injection upon 233 * @throws InvalidArgumentException 234 * @throws DependencyException Error while injecting dependencies 235 * @return object $instance Returns the same instance 236 */ 237 public function injectOn($instance) 238 { 239 if (!$instance) { 240 return $instance; 241 } 242 243 $className = get_class($instance); 244 245 // If the class is anonymous, don't cache its definition 246 // Checking for anonymous classes is cleaner via Reflection, but also slower 247 $objectDefinition = false !== strpos($className, '@anonymous') 248 ? $this->definitionSource->getDefinition($className) 249 : $this->getDefinition($className); 250 251 if (! $objectDefinition instanceof ObjectDefinition) { 252 return $instance; 253 } 254 255 $definition = new InstanceDefinition($instance, $objectDefinition); 256 257 $this->definitionResolver->resolve($definition); 258 259 return $instance; 260 } 261 262 /** 263 * Call the given function using the given parameters. 264 * 265 * Missing parameters will be resolved from the container. 266 * 267 * @param callable $callable Function to call. 268 * @param array $parameters Parameters to use. Can be indexed by the parameter names 269 * or not indexed (same order as the parameters). 270 * The array can also contain DI definitions, e.g. DI\get(). 271 * 272 * @return mixed Result of the function. 273 */ 274 public function call($callable, array $parameters = []) 275 { 276 return $this->getInvoker()->call($callable, $parameters); 277 } 278 279 /** 280 * Define an object or a value in the container. 281 * 282 * @param string $name Entry name 283 * @param mixed|DefinitionHelper $value Value, use definition helpers to define objects 284 */ 285 public function set(string $name, $value) 286 { 287 if ($value instanceof DefinitionHelper) { 288 $value = $value->getDefinition($name); 289 } elseif ($value instanceof \Closure) { 290 $value = new FactoryDefinition($name, $value); 291 } 292 293 if ($value instanceof ValueDefinition) { 294 $this->resolvedEntries[$name] = $value->getValue(); 295 } elseif ($value instanceof Definition) { 296 $value->setName($name); 297 $this->setDefinition($name, $value); 298 } else { 299 $this->resolvedEntries[$name] = $value; 300 } 301 } 302 303 /** 304 * Get defined container entries. 305 * 306 * @return string[] 307 */ 308 public function getKnownEntryNames() : array 309 { 310 $entries = array_unique(array_merge( 311 array_keys($this->definitionSource->getDefinitions()), 312 array_keys($this->resolvedEntries) 313 )); 314 sort($entries); 315 316 return $entries; 317 } 318 319 /** 320 * Get entry debug information. 321 * 322 * @param string $name Entry name 323 * 324 * @throws InvalidDefinition 325 * @throws NotFoundException 326 */ 327 public function debugEntry(string $name) : string 328 { 329 $definition = $this->definitionSource->getDefinition($name); 330 if ($definition instanceof Definition) { 331 return (string) $definition; 332 } 333 334 if (array_key_exists($name, $this->resolvedEntries)) { 335 return $this->getEntryType($this->resolvedEntries[$name]); 336 } 337 338 throw new NotFoundException("No entry or class found for '$name'"); 339 } 340 341 /** 342 * Get formatted entry type. 343 * 344 * @param mixed $entry 345 */ 346 private function getEntryType($entry) : string 347 { 348 if (is_object($entry)) { 349 return sprintf("Object (\n class = %s\n)", get_class($entry)); 350 } 351 352 if (is_array($entry)) { 353 return preg_replace(['/^array \(/', '/\)$/'], ['[', ']'], var_export($entry, true)); 354 } 355 356 if (is_string($entry)) { 357 return sprintf('Value (\'%s\')', $entry); 358 } 359 360 if (is_bool($entry)) { 361 return sprintf('Value (%s)', $entry === true ? 'true' : 'false'); 362 } 363 364 return sprintf('Value (%s)', is_scalar($entry) ? $entry : ucfirst(gettype($entry))); 365 } 366 367 /** 368 * Resolves a definition. 369 * 370 * Checks for circular dependencies while resolving the definition. 371 * 372 * @throws DependencyException Error while resolving the entry. 373 * @return mixed 374 */ 375 private function resolveDefinition(Definition $definition, array $parameters = []) 376 { 377 $entryName = $definition->getName(); 378 379 // Check if we are already getting this entry -> circular dependency 380 if (isset($this->entriesBeingResolved[$entryName])) { 381 throw new DependencyException("Circular dependency detected while trying to resolve entry '$entryName'"); 382 } 383 $this->entriesBeingResolved[$entryName] = true; 384 385 // Resolve the definition 386 try { 387 $value = $this->definitionResolver->resolve($definition, $parameters); 388 } finally { 389 unset($this->entriesBeingResolved[$entryName]); 390 } 391 392 return $value; 393 } 394 395 protected function setDefinition(string $name, Definition $definition) 396 { 397 // Clear existing entry if it exists 398 if (array_key_exists($name, $this->resolvedEntries)) { 399 unset($this->resolvedEntries[$name]); 400 } 401 $this->fetchedDefinitions = []; // Completely clear this local cache 402 403 $this->definitionSource->addDefinition($definition); 404 } 405 406 private function getInvoker() : InvokerInterface 407 { 408 if (! $this->invoker) { 409 $parameterResolver = new ResolverChain([ 410 new DefinitionParameterResolver($this->definitionResolver), 411 new NumericArrayResolver, 412 new AssociativeArrayResolver, 413 new DefaultValueResolver, 414 new TypeHintContainerResolver($this->delegateContainer), 415 ]); 416 417 $this->invoker = new Invoker($parameterResolver, $this); 418 } 419 420 return $this->invoker; 421 } 422 423 private function createDefaultDefinitionSource() : SourceChain 424 { 425 $source = new SourceChain([new ReflectionBasedAutowiring]); 426 $source->setMutableDefinitionSource(new DefinitionArray([], new ReflectionBasedAutowiring)); 427 428 return $source; 429 } 430} 431