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\Config\Loader; 13 14use Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException; 15use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; 16use Symfony\Component\Config\Exception\LoaderLoadException; 17use Symfony\Component\Config\FileLocatorInterface; 18use Symfony\Component\Config\Resource\FileExistenceResource; 19use Symfony\Component\Config\Resource\GlobResource; 20 21/** 22 * FileLoader is the abstract class used by all built-in loaders that are file based. 23 * 24 * @author Fabien Potencier <fabien@symfony.com> 25 */ 26abstract class FileLoader extends Loader 27{ 28 protected static $loading = []; 29 30 protected $locator; 31 32 private $currentDir; 33 34 public function __construct(FileLocatorInterface $locator) 35 { 36 $this->locator = $locator; 37 } 38 39 /** 40 * Sets the current directory. 41 * 42 * @param string $dir 43 */ 44 public function setCurrentDir($dir) 45 { 46 $this->currentDir = $dir; 47 } 48 49 /** 50 * Returns the file locator used by this loader. 51 * 52 * @return FileLocatorInterface 53 */ 54 public function getLocator() 55 { 56 return $this->locator; 57 } 58 59 /** 60 * Imports a resource. 61 * 62 * @param mixed $resource A Resource 63 * @param string|null $type The resource type or null if unknown 64 * @param bool $ignoreErrors Whether to ignore import errors or not 65 * @param string|null $sourceResource The original resource importing the new resource 66 * @param string|string[]|null $exclude Glob patterns to exclude from the import 67 * 68 * @return mixed 69 * 70 * @throws LoaderLoadException 71 * @throws FileLoaderImportCircularReferenceException 72 * @throws FileLocatorFileNotFoundException 73 */ 74 public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null/*, $exclude = null*/) 75 { 76 if (\func_num_args() < 5 && __CLASS__ !== static::class && !str_starts_with(static::class, 'Symfony\Component\\') && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { 77 @trigger_error(sprintf('The "%s()" method will have a new "$exclude = null" argument in version 5.0, not defining it is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED); 78 } 79 $exclude = \func_num_args() >= 5 ? func_get_arg(4) : null; 80 81 if (\is_string($resource) && \strlen($resource) !== ($i = strcspn($resource, '*?{[')) && !str_contains($resource, "\n")) { 82 $excluded = []; 83 foreach ((array) $exclude as $pattern) { 84 foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) { 85 // normalize Windows slashes and remove trailing slashes 86 $excluded[rtrim(str_replace('\\', '/', $path), '/')] = true; 87 } 88 } 89 90 $ret = []; 91 $isSubpath = 0 !== $i && str_contains(substr($resource, 0, $i), '/'); 92 foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath, false, $excluded) as $path => $info) { 93 if (null !== $res = $this->doImport($path, 'glob' === $type ? null : $type, $ignoreErrors, $sourceResource)) { 94 $ret[] = $res; 95 } 96 $isSubpath = true; 97 } 98 99 if ($isSubpath) { 100 return isset($ret[1]) ? $ret : ($ret[0] ?? null); 101 } 102 } 103 104 return $this->doImport($resource, $type, $ignoreErrors, $sourceResource); 105 } 106 107 /** 108 * @internal 109 */ 110 protected function glob(string $pattern, bool $recursive, &$resource = null, bool $ignoreErrors = false, bool $forExclusion = false, array $excluded = []) 111 { 112 if (\strlen($pattern) === $i = strcspn($pattern, '*?{[')) { 113 $prefix = $pattern; 114 $pattern = ''; 115 } elseif (0 === $i || !str_contains(substr($pattern, 0, $i), '/')) { 116 $prefix = '.'; 117 $pattern = '/'.$pattern; 118 } else { 119 $prefix = \dirname(substr($pattern, 0, 1 + $i)); 120 $pattern = substr($pattern, \strlen($prefix)); 121 } 122 123 try { 124 $prefix = $this->locator->locate($prefix, $this->currentDir, true); 125 } catch (FileLocatorFileNotFoundException $e) { 126 if (!$ignoreErrors) { 127 throw $e; 128 } 129 130 $resource = []; 131 foreach ($e->getPaths() as $path) { 132 $resource[] = new FileExistenceResource($path); 133 } 134 135 return; 136 } 137 $resource = new GlobResource($prefix, $pattern, $recursive, $forExclusion, $excluded); 138 139 yield from $resource; 140 } 141 142 private function doImport($resource, string $type = null, bool $ignoreErrors = false, $sourceResource = null) 143 { 144 try { 145 $loader = $this->resolve($resource, $type); 146 147 if ($loader instanceof self && null !== $this->currentDir) { 148 $resource = $loader->getLocator()->locate($resource, $this->currentDir, false); 149 } 150 151 $resources = \is_array($resource) ? $resource : [$resource]; 152 for ($i = 0; $i < $resourcesCount = \count($resources); ++$i) { 153 if (isset(self::$loading[$resources[$i]])) { 154 if ($i == $resourcesCount - 1) { 155 throw new FileLoaderImportCircularReferenceException(array_keys(self::$loading)); 156 } 157 } else { 158 $resource = $resources[$i]; 159 break; 160 } 161 } 162 self::$loading[$resource] = true; 163 164 try { 165 $ret = $loader->load($resource, $type); 166 } finally { 167 unset(self::$loading[$resource]); 168 } 169 170 return $ret; 171 } catch (FileLoaderImportCircularReferenceException $e) { 172 throw $e; 173 } catch (\Exception $e) { 174 if (!$ignoreErrors) { 175 // prevent embedded imports from nesting multiple exceptions 176 if ($e instanceof LoaderLoadException) { 177 throw $e; 178 } 179 180 throw new LoaderLoadException($resource, $sourceResource, 0, $e, $type); 181 } 182 } 183 184 return null; 185 } 186} 187