YamlFileLoader   C
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 221
Duplicated Lines 5.43 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 59
lcom 1
cbo 6
dl 12
loc 221
ccs 112
cts 112
cp 1
rs 6.1904
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
C load() 0 47 9
A supports() 0 6 3
F parseRoute() 0 28 11
F parseImport() 0 54 21
C validate() 12 33 13
A generateLocaleRoutes() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like YamlFileLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use YamlFileLoader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace FrankDeJonge\SymfonyI18nRouting\Routing\Loader;
4
5
use Generator;
6
use InvalidArgumentException;
7
use Symfony\Component\Config\Loader\FileLoader;
8
use Symfony\Component\Config\Resource\FileResource;
9
use Symfony\Component\Routing\Route;
10
use Symfony\Component\Routing\RouteCollection;
11
use Symfony\Component\Yaml\Exception\ParseException;
12
use Symfony\Component\Yaml\Parser as YamlParser;
13
use function is_array;
14
15
/**
16
 * YamlFileLoader loads Yaml routing files.
17
 *
18
 * @author Fabien Potencier <[email protected]>
19
 * @author Tobias Schultze <http://tobion.de>
20
 */
21
class YamlFileLoader extends FileLoader
22
{
23
    private $yamlParser;
24
25
    private static $availableKeys = [
26
        'locales', 'resource', 'type', 'prefix', 'path',
27
        'host', 'schemes', 'methods', 'defaults', 'requirements',
28
        'options', 'condition', 'controller',
29
    ];
30
31
    /**
32
     * Loads a Yaml file.
33
     *
34
     * @param string      $file A Yaml file path
35
     * @param string|null $type The resource type
36
     *
37
     * @return RouteCollection A RouteCollection instance
38
     *
39
     * @throws InvalidArgumentException When a route can't be parsed because YAML is invalid
40
     */
41 36
    public function load($file, $type = null)
42
    {
43 36
        $path = $this->locator->locate($file, null, true);
44
45 36
        if ( ! stream_is_local($path)) {
46 2
            throw new InvalidArgumentException(sprintf('This is not a local file "%s".', $path));
47
        }
48
49 34
        if ( ! file_exists($path)) {
50 2
            throw new InvalidArgumentException(sprintf('File "%s" not found.', $path));
51
        }
52
53 32
        if (null === $this->yamlParser) {
54 32
            $this->yamlParser = new YamlParser();
55
        }
56
57
        try {
58 32
            $parsedConfig = $this->yamlParser->parseFile($path);
59 2
        } catch (ParseException $e) {
60 2
            throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e);
61
        }
62
63 30
        $collection = new RouteCollection();
64 30
        $collection->addResource(new FileResource($path));
0 ignored issues
show
Bug introduced by
It seems like $path defined by $this->locator->locate($file, null, true) on line 43 can also be of type array; however, Symfony\Component\Config...Resource::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
65
66
        // empty file
67 30
        if (null === $parsedConfig) {
68 2
            return $collection;
69
        }
70
71
        // not an array
72 28
        if ( ! is_array($parsedConfig)) {
73 2
            throw new InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path));
74
        }
75
76 26
        foreach ($parsedConfig as $name => $config) {
77 26
            $this->validate($config, $name, $path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by $this->locator->locate($file, null, true) on line 43 can also be of type array; however, FrankDeJonge\SymfonyI18n...lFileLoader::validate() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
78
79 14
            if (isset($config['resource'])) {
80 12
                $this->parseImport($collection, $config, $path, $file);
81
            } else {
82 14
                $this->parseRoute($collection, $name, $config, $path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by $this->locator->locate($file, null, true) on line 43 can also be of type array; however, FrankDeJonge\SymfonyI18n...ileLoader::parseRoute() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
83
            }
84
        }
85
86 14
        return $collection;
87
    }
88
89 14
    public function supports($resource, $type = null)
90
    {
91 14
        return $type === 'i18n_routes'
92 14
            && is_string($resource)
93 14
            && in_array(pathinfo($resource, PATHINFO_EXTENSION), ['yml', 'yaml'], true);
94
    }
95
96
    /**
97
     * Parses a route and adds it to the RouteCollection.
98
     *
99
     * @param RouteCollection $collection A RouteCollection instance
100
     * @param string          $name       Route name
101
     * @param array           $config     Route definition
102
     * @param string          $path       Full path of the YAML file being processed
103
     */
104 14
    protected function parseRoute(RouteCollection $collection, $name, array $config, $path)
0 ignored issues
show
Unused Code introduced by
The parameter $path is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
105
    {
106 14
        $defaults = isset($config['defaults']) ? $config['defaults'] : [];
107 14
        $requirements = isset($config['requirements']) ? $config['requirements'] : [];
108 14
        $options = isset($config['options']) ? $config['options'] : [];
109 14
        $host = isset($config['host']) ? $config['host'] : '';
110 14
        $schemes = isset($config['schemes']) ? $config['schemes'] : [];
111 14
        $methods = isset($config['methods']) ? $config['methods'] : [];
112 14
        $condition = isset($config['condition']) ? $config['condition'] : null;
113
114 14
        if (isset($config['controller'])) {
115 14
            $defaults['_controller'] = $config['controller'];
116
        }
117
118 14
        if (isset($config['locales'])) {
119 12
            $routes = $this->generateLocaleRoutes(
120 12
                $name,
121 12
                $config['locales'],
122 12
                new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition)
123
            );
124 12
            foreach ($routes as $routeName => $route) {
125 12
                $collection->add($routeName, $route);
126
            }
127
        } else {
128 12
            $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
129 12
            $collection->add($name, $route);
130
        }
131 14
    }
132
133 12
    protected function parseImport(RouteCollection $collection, array $config, $path, $file)
134
    {
135 12
        $type = isset($config['type']) ? $config['type'] : 'i18n_routes';
136 12
        $prefix = isset($config['prefix']) ? $config['prefix'] : '';
137 12
        $defaults = isset($config['defaults']) ? $config['defaults'] : [];
138 12
        $requirements = isset($config['requirements']) ? $config['requirements'] : [];
139 12
        $options = isset($config['options']) ? $config['options'] : [];
140 12
        $host = isset($config['host']) ? $config['host'] : null;
141 12
        $condition = isset($config['condition']) ? $config['condition'] : null;
142 12
        $schemes = isset($config['schemes']) ? $config['schemes'] : null;
143 12
        $methods = isset($config['methods']) ? $config['methods'] : null;
144
145 12
        if (isset($config['controller'])) {
146 2
            $defaults['_controller'] = $config['controller'];
147
        }
148
149 12
        $this->setCurrentDir(dirname($path));
150
151
        /* @var $subCollection RouteCollection */
152 12
        $subCollection = $this->import($config['resource'], $type, false, $file);
153 12
        $prefixIsLocalized = is_array($prefix);
154
155
156 12
        foreach ($subCollection->all() as $routeName => $route) {
157 12
            $routeLocale = $route->getDefault('_locale');
158 12
            if ($prefixIsLocalized && null === $routeLocale) {
159 2
                throw new InvalidArgumentException("Route {$routeName} doesn't have a locale.");
160
            }
161 10
            if ($prefixIsLocalized && false === isset($prefix[$routeLocale])) {
162 2
                throw new InvalidArgumentException("Route {$routeName} with locale {$routeLocale} does not have a prefix defined in {$file}");
163
            }
164
165 10
            $localePrefix = is_array($prefix) ? $prefix[$routeLocale] : $prefix;
166 10
            $route->setPath($localePrefix . $route->getPath());
167
        }
168
169 8
        if (null !== $host) {
170 2
            $subCollection->setHost($host);
171
        }
172 8
        if (null !== $condition) {
173 2
            $subCollection->setCondition($condition);
174
        }
175 8
        if (null !== $schemes) {
176 2
            $subCollection->setSchemes($schemes);
177
        }
178 8
        if (null !== $methods) {
179 2
            $subCollection->setMethods($methods);
180
        }
181 8
        $subCollection->addDefaults($defaults);
182 8
        $subCollection->addRequirements($requirements);
183 8
        $subCollection->addOptions($options);
184
185 8
        $collection->addCollection($subCollection);
186 8
    }
187
188
    /**
189
     * Validates the route configuration.
190
     *
191
     * @param array  $config A resource config
192
     * @param string $name   The config key
193
     * @param string $path   The loaded file path
194
     *
195
     * @throws InvalidArgumentException If one of the provided config keys is not supported,
196
     *                                   something is missing or the combination is nonsense
197
     */
198 26
    protected function validate($config, $name, $path)
199
    {
200 26
        if ( ! is_array($config)) {
201 2
            throw new InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));
202
        }
203 24
        if ($extraKeys = array_diff(array_keys($config), self::$availableKeys)) {
204 2
            throw new InvalidArgumentException(sprintf(
205 2
                'The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".',
206 2
                $path, $name, implode('", "', $extraKeys), implode('", "', self::$availableKeys)
207
            ));
208
        }
209 22
        if (isset($config['resource']) && (isset($config['path']) || isset($config['locales']))) {
210 2
            throw new InvalidArgumentException(sprintf(
211 2
                'The routing file "%s" must not specify both the "resource" key and the "path" or "locales" key for "%s". Choose between an import and a route definition.',
212 2
                $path, $name
213
            ));
214
        }
215 20 View Code Duplication
        if ( ! isset($config['resource']) && isset($config['type'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
216 2
            throw new InvalidArgumentException(sprintf(
217 2
                'The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.',
218 2
                $name, $path
219
            ));
220
        }
221 18 View Code Duplication
        if ( ! isset($config['resource']) && ! isset($config['path']) && ! isset($config['locales'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
222 2
            throw new InvalidArgumentException(sprintf(
223 2
                'You must define a "path" or "locales" for the route "%s" in file "%s".',
224 2
                $name, $path
225
            ));
226
        }
227 16
        if (isset($config['controller']) && isset($config['defaults']['_controller'])) {
228 2
            throw new InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" key and the defaults key "_controller" for "%s".', $path, $name));
229
        }
230 14
    }
231
232 12
    private function generateLocaleRoutes(string $name, array $locales, Route $route): Generator
233
    {
234 12
        foreach ($locales as $locale => $path) {
235 12
            $localeRoute = clone $route;
236 12
            $localeRoute->setPath($path);
237 12
            $localeRoute->setDefault('_locale', $locale);
238 12
            yield "{$name}.{$locale}" => $localeRoute;
239
        }
240
    }
241
}