| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 | <?php/* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */namespace Symfony\Component\Routing\Loader;use Symfony\Component\Config\Loader\FileLoader;use Symfony\Component\Config\Resource\FileResource;use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;use Symfony\Component\Routing\RouteCollection;use Symfony\Component\Yaml\Exception\ParseException;use Symfony\Component\Yaml\Parser as YamlParser;use Symfony\Component\Yaml\Yaml;/** * YamlFileLoader loads Yaml routing files. * * @author Fabien Potencier <fabien@symfony.com> * @author Tobias Schultze <http://tobion.de> */class YamlFileLoader extends FileLoader{    use HostTrait;    use LocalizedRouteTrait;    use PrefixTrait;    private const AVAILABLE_KEYS = [        'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8', 'exclude', 'stateless',    ];    private $yamlParser;    /**     * Loads a Yaml file.     *     * @param string      $file A Yaml file path     * @param string|null $type The resource type     *     * @return RouteCollection     *     * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid     */    public function load($file, ?string $type = null)    {        $path = $this->locator->locate($file);        if (!stream_is_local($path)) {            throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $path));        }        if (!file_exists($path)) {            throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path));        }        if (null === $this->yamlParser) {            $this->yamlParser = new YamlParser();        }        try {            $parsedConfig = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT);        } catch (ParseException $e) {            throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML: ', $path).$e->getMessage(), 0, $e);        }        $collection = new RouteCollection();        $collection->addResource(new FileResource($path));        // empty file        if (null === $parsedConfig) {            return $collection;        }        // not an array        if (!\is_array($parsedConfig)) {            throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path));        }        foreach ($parsedConfig as $name => $config) {            if (0 === strpos($name, 'when@')) {                if (!$this->env || 'when@'.$this->env !== $name) {                    continue;                }                foreach ($config as $name => $config) {                    $this->validate($config, $name.'" when "@'.$this->env, $path);                    if (isset($config['resource'])) {                        $this->parseImport($collection, $config, $path, $file);                    } else {                        $this->parseRoute($collection, $name, $config, $path);                    }                }                continue;            }            $this->validate($config, $name, $path);            if (isset($config['resource'])) {                $this->parseImport($collection, $config, $path, $file);            } else {                $this->parseRoute($collection, $name, $config, $path);            }        }        return $collection;    }    /**     * {@inheritdoc}     */    public function supports($resource, ?string $type = null)    {        return \is_string($resource) && \in_array(pathinfo($resource, \PATHINFO_EXTENSION), ['yml', 'yaml'], true) && (!$type || 'yaml' === $type);    }    /**     * Parses a route and adds it to the RouteCollection.     */    protected function parseRoute(RouteCollection $collection, string $name, array $config, string $path)    {        if (isset($config['alias'])) {            $alias = $collection->addAlias($name, $config['alias']);            $deprecation = $config['deprecated'] ?? null;            if (null !== $deprecation) {                $alias->setDeprecated(                    $deprecation['package'],                    $deprecation['version'],                    $deprecation['message'] ?? ''                );            }            return;        }        $defaults = $config['defaults'] ?? [];        $requirements = $config['requirements'] ?? [];        $options = $config['options'] ?? [];        foreach ($requirements as $placeholder => $requirement) {            if (\is_int($placeholder)) {                throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s"?', $placeholder, $requirement, $name, $path));            }        }        if (isset($config['controller'])) {            $defaults['_controller'] = $config['controller'];        }        if (isset($config['locale'])) {            $defaults['_locale'] = $config['locale'];        }        if (isset($config['format'])) {            $defaults['_format'] = $config['format'];        }        if (isset($config['utf8'])) {            $options['utf8'] = $config['utf8'];        }        if (isset($config['stateless'])) {            $defaults['_stateless'] = $config['stateless'];        }        $routes = $this->createLocalizedRoute($collection, $name, $config['path']);        $routes->addDefaults($defaults);        $routes->addRequirements($requirements);        $routes->addOptions($options);        $routes->setSchemes($config['schemes'] ?? []);        $routes->setMethods($config['methods'] ?? []);        $routes->setCondition($config['condition'] ?? null);        if (isset($config['host'])) {            $this->addHost($routes, $config['host']);        }    }    /**     * Parses an import and adds the routes in the resource to the RouteCollection.     */    protected function parseImport(RouteCollection $collection, array $config, string $path, string $file)    {        $type = $config['type'] ?? null;        $prefix = $config['prefix'] ?? '';        $defaults = $config['defaults'] ?? [];        $requirements = $config['requirements'] ?? [];        $options = $config['options'] ?? [];        $host = $config['host'] ?? null;        $condition = $config['condition'] ?? null;        $schemes = $config['schemes'] ?? null;        $methods = $config['methods'] ?? null;        $trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true;        $namePrefix = $config['name_prefix'] ?? null;        $exclude = $config['exclude'] ?? null;        if (isset($config['controller'])) {            $defaults['_controller'] = $config['controller'];        }        if (isset($config['locale'])) {            $defaults['_locale'] = $config['locale'];        }        if (isset($config['format'])) {            $defaults['_format'] = $config['format'];        }        if (isset($config['utf8'])) {            $options['utf8'] = $config['utf8'];        }        if (isset($config['stateless'])) {            $defaults['_stateless'] = $config['stateless'];        }        $this->setCurrentDir(\dirname($path));        /** @var RouteCollection[] $imported */        $imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: [];        if (!\is_array($imported)) {            $imported = [$imported];        }        foreach ($imported as $subCollection) {            $this->addPrefix($subCollection, $prefix, $trailingSlashOnRoot);            if (null !== $host) {                $this->addHost($subCollection, $host);            }            if (null !== $condition) {                $subCollection->setCondition($condition);            }            if (null !== $schemes) {                $subCollection->setSchemes($schemes);            }            if (null !== $methods) {                $subCollection->setMethods($methods);            }            if (null !== $namePrefix) {                $subCollection->addNamePrefix($namePrefix);            }            $subCollection->addDefaults($defaults);            $subCollection->addRequirements($requirements);            $subCollection->addOptions($options);            $collection->addCollection($subCollection);        }    }    /**     * Validates the route configuration.     *     * @param array  $config A resource config     * @param string $name   The config key     * @param string $path   The loaded file path     *     * @throws \InvalidArgumentException If one of the provided config keys is not supported,     *                                   something is missing or the combination is nonsense     */    protected function validate($config, string $name, string $path)    {        if (!\is_array($config)) {            throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));        }        if (isset($config['alias'])) {            $this->validateAlias($config, $name, $path);            return;        }        if ($extraKeys = array_diff(array_keys($config), self::AVAILABLE_KEYS)) {            throw new \InvalidArgumentException(sprintf('The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', $path, $name, implode('", "', $extraKeys), implode('", "', self::AVAILABLE_KEYS)));        }        if (isset($config['resource']) && isset($config['path'])) {            throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "resource" key and the "path" key for "%s". Choose between an import and a route definition.', $path, $name));        }        if (!isset($config['resource']) && isset($config['type'])) {            throw new \InvalidArgumentException(sprintf('The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.', $name, $path));        }        if (!isset($config['resource']) && !isset($config['path'])) {            throw new \InvalidArgumentException(sprintf('You must define a "path" for the route "%s" in file "%s".', $name, $path));        }        if (isset($config['controller']) && isset($config['defaults']['_controller'])) {            throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" key and the defaults key "_controller" for "%s".', $path, $name));        }        if (isset($config['stateless']) && isset($config['defaults']['_stateless'])) {            throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" key and the defaults key "_stateless" for "%s".', $path, $name));        }    }    /**     * @throws \InvalidArgumentException If one of the provided config keys is not supported,     *                                   something is missing or the combination is nonsense     */    private function validateAlias(array $config, string $name, string $path): void    {        foreach ($config as $key => $value) {            if (!\in_array($key, ['alias', 'deprecated'], true)) {                throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify other keys than "alias" and "deprecated" for "%s".', $path, $name));            }            if ('deprecated' === $key) {                if (!isset($value['package'])) {                    throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "package" of the "deprecated" option for "%s".', $path, $name));                }                if (!isset($value['version'])) {                    throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "version" of the "deprecated" option for "%s".', $path, $name));                }            }        }    }}
 |