1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\DependencyInjection; 13 14use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; 15use Symfony\Component\DependencyInjection\Argument\ServiceLocator as ArgumentServiceLocator; 16use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; 17use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; 18use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException; 19use Symfony\Component\DependencyInjection\Exception\RuntimeException; 20use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; 21use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; 22use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; 23use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; 24use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; 25use Symfony\Contracts\Service\ResetInterface; 26 27// Help opcache.preload discover always-needed symbols 28class_exists(RewindableGenerator::class); 29class_exists(ArgumentServiceLocator::class); 30 31/** 32 * Container is a dependency injection container. 33 * 34 * It gives access to object instances (services). 35 * Services and parameters are simple key/pair stores. 36 * The container can have four possible behaviors when a service 37 * does not exist (or is not initialized for the last case): 38 * 39 * * EXCEPTION_ON_INVALID_REFERENCE: Throws an exception (the default) 40 * * NULL_ON_INVALID_REFERENCE: Returns null 41 * * IGNORE_ON_INVALID_REFERENCE: Ignores the wrapping command asking for the reference 42 * (for instance, ignore a setter if the service does not exist) 43 * * IGNORE_ON_UNINITIALIZED_REFERENCE: Ignores/returns null for uninitialized services or invalid references 44 * 45 * @author Fabien Potencier <fabien@symfony.com> 46 * @author Johannes M. Schmitt <schmittjoh@gmail.com> 47 */ 48class Container implements ResettableContainerInterface 49{ 50 protected $parameterBag; 51 protected $services = []; 52 protected $privates = []; 53 protected $fileMap = []; 54 protected $methodMap = []; 55 protected $factories = []; 56 protected $aliases = []; 57 protected $loading = []; 58 protected $resolving = []; 59 protected $syntheticIds = []; 60 61 private $envCache = []; 62 private $compiled = false; 63 private $getEnv; 64 65 public function __construct(ParameterBagInterface $parameterBag = null) 66 { 67 $this->parameterBag = $parameterBag ?: new EnvPlaceholderParameterBag(); 68 } 69 70 /** 71 * Compiles the container. 72 * 73 * This method does two things: 74 * 75 * * Parameter values are resolved; 76 * * The parameter bag is frozen. 77 */ 78 public function compile() 79 { 80 $this->parameterBag->resolve(); 81 82 $this->parameterBag = new FrozenParameterBag($this->parameterBag->all()); 83 84 $this->compiled = true; 85 } 86 87 /** 88 * Returns true if the container is compiled. 89 * 90 * @return bool 91 */ 92 public function isCompiled() 93 { 94 return $this->compiled; 95 } 96 97 /** 98 * Gets the service container parameter bag. 99 * 100 * @return ParameterBagInterface A ParameterBagInterface instance 101 */ 102 public function getParameterBag() 103 { 104 return $this->parameterBag; 105 } 106 107 /** 108 * Gets a parameter. 109 * 110 * @param string $name The parameter name 111 * 112 * @return mixed The parameter value 113 * 114 * @throws InvalidArgumentException if the parameter is not defined 115 */ 116 public function getParameter($name) 117 { 118 return $this->parameterBag->get($name); 119 } 120 121 /** 122 * Checks if a parameter exists. 123 * 124 * @param string $name The parameter name 125 * 126 * @return bool The presence of parameter in container 127 */ 128 public function hasParameter($name) 129 { 130 return $this->parameterBag->has($name); 131 } 132 133 /** 134 * Sets a parameter. 135 * 136 * @param string $name The parameter name 137 * @param mixed $value The parameter value 138 */ 139 public function setParameter($name, $value) 140 { 141 $this->parameterBag->set($name, $value); 142 } 143 144 /** 145 * Sets a service. 146 * 147 * Setting a synthetic service to null resets it: has() returns false and get() 148 * behaves in the same way as if the service was never created. 149 * 150 * @param string $id The service identifier 151 * @param object|null $service The service instance 152 */ 153 public function set($id, $service) 154 { 155 // Runs the internal initializer; used by the dumped container to include always-needed files 156 if (isset($this->privates['service_container']) && $this->privates['service_container'] instanceof \Closure) { 157 $initialize = $this->privates['service_container']; 158 unset($this->privates['service_container']); 159 $initialize(); 160 } 161 162 if ('service_container' === $id) { 163 throw new InvalidArgumentException('You cannot set service "service_container".'); 164 } 165 166 if (!(isset($this->fileMap[$id]) || isset($this->methodMap[$id]))) { 167 if (isset($this->syntheticIds[$id]) || !isset($this->getRemovedIds()[$id])) { 168 // no-op 169 } elseif (null === $service) { 170 throw new InvalidArgumentException(sprintf('The "%s" service is private, you cannot unset it.', $id)); 171 } else { 172 throw new InvalidArgumentException(sprintf('The "%s" service is private, you cannot replace it.', $id)); 173 } 174 } elseif (isset($this->services[$id])) { 175 throw new InvalidArgumentException(sprintf('The "%s" service is already initialized, you cannot replace it.', $id)); 176 } 177 178 if (isset($this->aliases[$id])) { 179 unset($this->aliases[$id]); 180 } 181 182 if (null === $service) { 183 unset($this->services[$id]); 184 185 return; 186 } 187 188 $this->services[$id] = $service; 189 } 190 191 /** 192 * Returns true if the given service is defined. 193 * 194 * @param string $id The service identifier 195 * 196 * @return bool true if the service is defined, false otherwise 197 */ 198 public function has($id) 199 { 200 if (isset($this->aliases[$id])) { 201 $id = $this->aliases[$id]; 202 } 203 if (isset($this->services[$id])) { 204 return true; 205 } 206 if ('service_container' === $id) { 207 return true; 208 } 209 210 return isset($this->fileMap[$id]) || isset($this->methodMap[$id]); 211 } 212 213 /** 214 * Gets a service. 215 * 216 * @param string $id The service identifier 217 * @param int $invalidBehavior The behavior when the service does not exist 218 * 219 * @return object|null The associated service 220 * 221 * @throws ServiceCircularReferenceException When a circular reference is detected 222 * @throws ServiceNotFoundException When the service is not defined 223 * @throws \Exception if an exception has been thrown when the service has been resolved 224 * 225 * @see Reference 226 */ 227 public function get($id, $invalidBehavior = /* self::EXCEPTION_ON_INVALID_REFERENCE */ 1) 228 { 229 $service = $this->services[$id] 230 ?? $this->services[$id = $this->aliases[$id] ?? $id] 231 ?? ('service_container' === $id ? $this : ($this->factories[$id] ?? [$this, 'make'])($id, $invalidBehavior)); 232 233 if (!\is_object($service) && null !== $service) { 234 @trigger_error(sprintf('Non-object services are deprecated since Symfony 4.4, please fix the "%s" service which is of type "%s" right now.', $id, \gettype($service)), E_USER_DEPRECATED); 235 } 236 237 return $service; 238 } 239 240 /** 241 * Creates a service. 242 * 243 * As a separate method to allow "get()" to use the really fast `??` operator. 244 */ 245 private function make(string $id, int $invalidBehavior) 246 { 247 if (isset($this->loading[$id])) { 248 throw new ServiceCircularReferenceException($id, array_merge(array_keys($this->loading), [$id])); 249 } 250 251 $this->loading[$id] = true; 252 253 try { 254 if (isset($this->fileMap[$id])) { 255 return /* self::IGNORE_ON_UNINITIALIZED_REFERENCE */ 4 === $invalidBehavior ? null : $this->load($this->fileMap[$id]); 256 } elseif (isset($this->methodMap[$id])) { 257 return /* self::IGNORE_ON_UNINITIALIZED_REFERENCE */ 4 === $invalidBehavior ? null : $this->{$this->methodMap[$id]}(); 258 } 259 } catch (\Exception $e) { 260 unset($this->services[$id]); 261 262 throw $e; 263 } finally { 264 unset($this->loading[$id]); 265 } 266 267 if (/* self::EXCEPTION_ON_INVALID_REFERENCE */ 1 === $invalidBehavior) { 268 if (!$id) { 269 throw new ServiceNotFoundException($id); 270 } 271 if (isset($this->syntheticIds[$id])) { 272 throw new ServiceNotFoundException($id, null, null, [], sprintf('The "%s" service is synthetic, it needs to be set at boot time before it can be used.', $id)); 273 } 274 if (isset($this->getRemovedIds()[$id])) { 275 throw new ServiceNotFoundException($id, null, null, [], sprintf('The "%s" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.', $id)); 276 } 277 278 $alternatives = []; 279 foreach ($this->getServiceIds() as $knownId) { 280 if ('' === $knownId || '.' === $knownId[0]) { 281 continue; 282 } 283 $lev = levenshtein($id, $knownId); 284 if ($lev <= \strlen($id) / 3 || false !== strpos($knownId, $id)) { 285 $alternatives[] = $knownId; 286 } 287 } 288 289 throw new ServiceNotFoundException($id, null, null, $alternatives); 290 } 291 292 return null; 293 } 294 295 /** 296 * Returns true if the given service has actually been initialized. 297 * 298 * @param string $id The service identifier 299 * 300 * @return bool true if service has already been initialized, false otherwise 301 */ 302 public function initialized($id) 303 { 304 if (isset($this->aliases[$id])) { 305 $id = $this->aliases[$id]; 306 } 307 308 if ('service_container' === $id) { 309 return false; 310 } 311 312 return isset($this->services[$id]); 313 } 314 315 /** 316 * {@inheritdoc} 317 */ 318 public function reset() 319 { 320 $services = $this->services + $this->privates; 321 $this->services = $this->factories = $this->privates = []; 322 323 foreach ($services as $service) { 324 try { 325 if ($service instanceof ResetInterface) { 326 $service->reset(); 327 } 328 } catch (\Throwable $e) { 329 continue; 330 } 331 } 332 } 333 334 /** 335 * Gets all service ids. 336 * 337 * @return string[] An array of all defined service ids 338 */ 339 public function getServiceIds() 340 { 341 return array_map('strval', array_unique(array_merge(['service_container'], array_keys($this->fileMap), array_keys($this->methodMap), array_keys($this->aliases), array_keys($this->services)))); 342 } 343 344 /** 345 * Gets service ids that existed at compile time. 346 * 347 * @return array 348 */ 349 public function getRemovedIds() 350 { 351 return []; 352 } 353 354 /** 355 * Camelizes a string. 356 * 357 * @param string $id A string to camelize 358 * 359 * @return string The camelized string 360 */ 361 public static function camelize($id) 362 { 363 return strtr(ucwords(strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']); 364 } 365 366 /** 367 * A string to underscore. 368 * 369 * @param string $id The string to underscore 370 * 371 * @return string The underscored string 372 */ 373 public static function underscore($id) 374 { 375 return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], str_replace('_', '.', $id))); 376 } 377 378 /** 379 * Creates a service by requiring its factory file. 380 */ 381 protected function load($file) 382 { 383 return require $file; 384 } 385 386 /** 387 * Fetches a variable from the environment. 388 * 389 * @param string $name The name of the environment variable 390 * 391 * @return mixed The value to use for the provided environment variable name 392 * 393 * @throws EnvNotFoundException When the environment variable is not found and has no default value 394 */ 395 protected function getEnv($name) 396 { 397 if (isset($this->resolving[$envName = "env($name)"])) { 398 throw new ParameterCircularReferenceException(array_keys($this->resolving)); 399 } 400 if (isset($this->envCache[$name]) || \array_key_exists($name, $this->envCache)) { 401 return $this->envCache[$name]; 402 } 403 if (!$this->has($id = 'container.env_var_processors_locator')) { 404 $this->set($id, new ServiceLocator([])); 405 } 406 if (!$this->getEnv) { 407 $this->getEnv = new \ReflectionMethod($this, __FUNCTION__); 408 $this->getEnv->setAccessible(true); 409 $this->getEnv = $this->getEnv->getClosure($this); 410 } 411 $processors = $this->get($id); 412 413 if (false !== $i = strpos($name, ':')) { 414 $prefix = substr($name, 0, $i); 415 $localName = substr($name, 1 + $i); 416 } else { 417 $prefix = 'string'; 418 $localName = $name; 419 } 420 $processor = $processors->has($prefix) ? $processors->get($prefix) : new EnvVarProcessor($this); 421 422 $this->resolving[$envName] = true; 423 try { 424 return $this->envCache[$name] = $processor->getEnv($prefix, $localName, $this->getEnv); 425 } finally { 426 unset($this->resolving[$envName]); 427 } 428 } 429 430 /** 431 * @param string|false $registry 432 * @param string|bool $load 433 * 434 * @return mixed 435 * 436 * @internal 437 */ 438 final protected function getService($registry, string $id, ?string $method, $load) 439 { 440 if ('service_container' === $id) { 441 return $this; 442 } 443 if (\is_string($load)) { 444 throw new RuntimeException($load); 445 } 446 if (null === $method) { 447 return false !== $registry ? $this->{$registry}[$id] ?? null : null; 448 } 449 if (false !== $registry) { 450 return $this->{$registry}[$id] ?? $this->{$registry}[$id] = $load ? $this->load($method) : $this->{$method}(); 451 } 452 if (!$load) { 453 return $this->{$method}(); 454 } 455 456 return ($factory = $this->factories[$id] ?? $this->factories['service_container'][$id] ?? null) ? $factory() : $this->load($method); 457 } 458 459 private function __clone() 460 { 461 } 462} 463