PageRenderTime 42ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php

https://github.com/symfony/symfony
PHP | 261 lines | 217 code | 17 blank | 27 comment | 12 complexity | 8eadf33cbb13b2432d6e475ad85609ee MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\DependencyInjection\Loader;
  11. use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
  12. use Symfony\Component\Config\Exception\LoaderLoadException;
  13. use Symfony\Component\Config\FileLocatorInterface;
  14. use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader;
  15. use Symfony\Component\Config\Loader\Loader;
  16. use Symfony\Component\Config\Resource\GlobResource;
  17. use Symfony\Component\DependencyInjection\Attribute\When;
  18. use Symfony\Component\DependencyInjection\ChildDefinition;
  19. use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
  20. use Symfony\Component\DependencyInjection\ContainerBuilder;
  21. use Symfony\Component\DependencyInjection\Definition;
  22. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
  23. /**
  24. * FileLoader is the abstract class used by all built-in loaders that are file based.
  25. *
  26. * @author Fabien Potencier <fabien@symfony.com>
  27. */
  28. abstract class FileLoader extends BaseFileLoader
  29. {
  30. public const ANONYMOUS_ID_REGEXP = '/^\.\d+_[^~]*+~[._a-zA-Z\d]{7}$/';
  31. protected $container;
  32. protected $isLoadingInstanceof = false;
  33. protected $instanceof = [];
  34. protected $interfaces = [];
  35. protected $singlyImplemented = [];
  36. protected $autoRegisterAliasesForSinglyImplementedInterfaces = true;
  37. public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, string $env = null)
  38. {
  39. $this->container = $container;
  40. parent::__construct($locator, $env);
  41. }
  42. /**
  43. * {@inheritdoc}
  44. *
  45. * @param bool|string $ignoreErrors Whether errors should be ignored; pass "not_found" to ignore only when the loaded resource is not found
  46. */
  47. public function import(mixed $resource, string $type = null, bool|string $ignoreErrors = false, string $sourceResource = null, $exclude = null): mixed
  48. {
  49. $args = \func_get_args();
  50. if ($ignoreNotFound = 'not_found' === $ignoreErrors) {
  51. $args[2] = false;
  52. } elseif (!\is_bool($ignoreErrors)) {
  53. throw new \TypeError(sprintf('Invalid argument $ignoreErrors provided to "%s::import()": boolean or "not_found" expected, "%s" given.', static::class, get_debug_type($ignoreErrors)));
  54. }
  55. try {
  56. return parent::import(...$args);
  57. } catch (LoaderLoadException $e) {
  58. if (!$ignoreNotFound || !($prev = $e->getPrevious()) instanceof FileLocatorFileNotFoundException) {
  59. throw $e;
  60. }
  61. foreach ($prev->getTrace() as $frame) {
  62. if ('import' === ($frame['function'] ?? null) && is_a($frame['class'] ?? '', Loader::class, true)) {
  63. break;
  64. }
  65. }
  66. if (__FILE__ !== $frame['file']) {
  67. throw $e;
  68. }
  69. }
  70. return null;
  71. }
  72. /**
  73. * Registers a set of classes as services using PSR-4 for discovery.
  74. *
  75. * @param Definition $prototype A definition to use as template
  76. * @param string $namespace The namespace prefix of classes in the scanned directory
  77. * @param string $resource The directory to look for classes, glob-patterns allowed
  78. * @param string|string[]|null $exclude A globbed path of files to exclude or an array of globbed paths of files to exclude
  79. * @param string|null $source The path to the file that defines the auto-discovery rule
  80. */
  81. public function registerClasses(Definition $prototype, string $namespace, string $resource, string|array $exclude = null/*, string $source = null*/)
  82. {
  83. if (!str_ends_with($namespace, '\\')) {
  84. throw new InvalidArgumentException(sprintf('Namespace prefix must end with a "\\": "%s".', $namespace));
  85. }
  86. if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++$/', $namespace)) {
  87. throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: "%s".', $namespace));
  88. }
  89. $source = \func_num_args() > 4 ? func_get_arg(4) : null;
  90. $autoconfigureAttributes = new RegisterAutoconfigureAttributesPass();
  91. $autoconfigureAttributes = $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null;
  92. $classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes, $source);
  93. // prepare for deep cloning
  94. $serializedPrototype = serialize($prototype);
  95. foreach ($classes as $class => $errorMessage) {
  96. if (null === $errorMessage && $autoconfigureAttributes && $this->env) {
  97. $r = $this->container->getReflectionClass($class);
  98. $attribute = null;
  99. foreach ($r->getAttributes(When::class) as $attribute) {
  100. if ($this->env === $attribute->newInstance()->env) {
  101. $attribute = null;
  102. break;
  103. }
  104. }
  105. if (null !== $attribute) {
  106. continue;
  107. }
  108. }
  109. if (interface_exists($class, false)) {
  110. $this->interfaces[] = $class;
  111. } else {
  112. $this->setDefinition($class, $definition = unserialize($serializedPrototype));
  113. if (null !== $errorMessage) {
  114. $definition->addError($errorMessage);
  115. continue;
  116. }
  117. foreach (class_implements($class, false) as $interface) {
  118. $this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class;
  119. }
  120. }
  121. }
  122. if ($this->autoRegisterAliasesForSinglyImplementedInterfaces) {
  123. $this->registerAliasesForSinglyImplementedInterfaces();
  124. }
  125. }
  126. public function registerAliasesForSinglyImplementedInterfaces()
  127. {
  128. foreach ($this->interfaces as $interface) {
  129. if (!empty($this->singlyImplemented[$interface]) && !$this->container->has($interface)) {
  130. $this->container->setAlias($interface, $this->singlyImplemented[$interface]);
  131. }
  132. }
  133. $this->interfaces = $this->singlyImplemented = [];
  134. }
  135. /**
  136. * Registers a definition in the container with its instanceof-conditionals.
  137. */
  138. protected function setDefinition(string $id, Definition $definition)
  139. {
  140. $this->container->removeBindings($id);
  141. if ($this->isLoadingInstanceof) {
  142. if (!$definition instanceof ChildDefinition) {
  143. throw new InvalidArgumentException(sprintf('Invalid type definition "%s": ChildDefinition expected, "%s" given.', $id, get_debug_type($definition)));
  144. }
  145. $this->instanceof[$id] = $definition;
  146. } else {
  147. $this->container->setDefinition($id, $definition->setInstanceofConditionals($this->instanceof));
  148. }
  149. }
  150. private function findClasses(string $namespace, string $pattern, array $excludePatterns, ?RegisterAutoconfigureAttributesPass $autoconfigureAttributes, ?string $source): array
  151. {
  152. $parameterBag = $this->container->getParameterBag();
  153. $excludePaths = [];
  154. $excludePrefix = null;
  155. $excludePatterns = $parameterBag->unescapeValue($parameterBag->resolveValue($excludePatterns));
  156. foreach ($excludePatterns as $excludePattern) {
  157. foreach ($this->glob($excludePattern, true, $resource, true, true) as $path => $info) {
  158. if (null === $excludePrefix) {
  159. $excludePrefix = $resource->getPrefix();
  160. }
  161. // normalize Windows slashes and remove trailing slashes
  162. $excludePaths[rtrim(str_replace('\\', '/', $path), '/')] = true;
  163. }
  164. }
  165. $pattern = $parameterBag->unescapeValue($parameterBag->resolveValue($pattern));
  166. $classes = [];
  167. $prefixLen = null;
  168. foreach ($this->glob($pattern, true, $resource, false, false, $excludePaths) as $path => $info) {
  169. if (null === $prefixLen) {
  170. $prefixLen = \strlen($resource->getPrefix());
  171. if ($excludePrefix && !str_starts_with($excludePrefix, $resource->getPrefix())) {
  172. throw new InvalidArgumentException(sprintf('Invalid "exclude" pattern when importing classes for "%s": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s).', $namespace, $excludePattern, $pattern));
  173. }
  174. }
  175. if (isset($excludePaths[str_replace('\\', '/', $path)])) {
  176. continue;
  177. }
  178. if (!str_ends_with($path, '.php') || !$info->isReadable()) {
  179. continue;
  180. }
  181. $class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, -4)), '\\');
  182. if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class)) {
  183. continue;
  184. }
  185. try {
  186. $r = $this->container->getReflectionClass($class);
  187. } catch (\ReflectionException $e) {
  188. $classes[$class] = $e->getMessage();
  189. continue;
  190. }
  191. // check to make sure the expected class exists
  192. if (!$r) {
  193. throw new InvalidArgumentException(sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern));
  194. }
  195. if ($r->isInstantiable() || $r->isInterface()) {
  196. $classes[$class] = null;
  197. }
  198. if ($autoconfigureAttributes && !$r->isInstantiable()) {
  199. $autoconfigureAttributes->processClass($this->container, $r);
  200. }
  201. }
  202. // track only for new & removed files
  203. if ($resource instanceof GlobResource) {
  204. $this->container->addResource($resource);
  205. } else {
  206. foreach ($resource as $path) {
  207. $this->container->fileExists($path, false);
  208. }
  209. }
  210. if (null !== $prefixLen) {
  211. $attributes = null !== $source ? ['source' => sprintf('in "%s/%s"', basename(\dirname($source)), basename($source))] : [];
  212. foreach ($excludePaths as $path => $_) {
  213. $class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, str_ends_with($path, '.php') ? -4 : null)), '\\');
  214. if (!$this->container->has($class)) {
  215. $this->container->register($class)
  216. ->setAbstract(true)
  217. ->addTag('container.excluded', $attributes);
  218. }
  219. }
  220. }
  221. return $classes;
  222. }
  223. }