YamlDefinitionLoader   C
last analyzed

Complexity

Total Complexity 73

Size/Duplication

Total Lines 365
Duplicated Lines 1.64 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 73
c 5
b 0
f 0
lcom 1
cbo 7
dl 6
loc 365
rs 5.5448

8 Methods

Rating   Name   Duplication   Size   Complexity  
A parseDefinitions() 0 18 4
A loadFile() 0 20 4
B validate() 0 24 5
C resolveServices() 0 24 9
A __construct() 0 4 1
B getDefinitions() 0 30 5
C parseImports() 0 35 7
F parseDefinition() 6 137 38

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 YamlDefinitionLoader 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 YamlDefinitionLoader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace TheCodingMachine\Definition;
4
5
use Assembly\FactoryCallDefinition;
6
use Assembly\ObjectDefinition;
7
use Assembly\ParameterDefinition;
8
use Assembly\Reference;
9
use Interop\Container\Definition\DefinitionInterface;
10
use Interop\Container\Definition\DefinitionProviderInterface;
11
use Symfony\Component\Yaml\Exception\ParseException;
12
use Symfony\Component\Yaml\Parser;
13
use TheCodingMachine\Definition\Exception\FileNotFoundException;
14
use TheCodingMachine\Definition\Exception\InvalidArgumentException;
15
16
class YamlDefinitionLoader implements DefinitionProviderInterface
17
{
18
    /**
19
     * The name of the YAML file to be loaded.
20
     *
21
     * @var string
22
     */
23
    private $fileName;
24
25
    public function __construct($fileName)
26
    {
27
        $this->fileName = $fileName;
28
    }
29
30
    /**
31
     * Returns the definition to register in the container.
32
     *
33
     * @return DefinitionInterface[]
34
     */
35
    public function getDefinitions()
36
    {
37
        $content = $this->loadFile($this->fileName);
38
39
        // empty file
40
        if (null === $content) {
41
            return [];
42
        }
43
44
        // imports
45
        $definitions = $this->parseImports($content);
46
47
        // parameters
48
        if (isset($content['parameters'])) {
49
            if (!is_array($content['parameters'])) {
50
                throw new InvalidArgumentException(sprintf('The "parameters" key should contain an array in %s. Check your YAML syntax.', $this->fileName));
51
            }
52
53
            foreach ($content['parameters'] as $key => $value) {
54
                $definitions[$key] = new ParameterDefinition($value);
55
                //$this->container->setParameter($key, $this->resolveServices($value));
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
56
            }
57
        }
58
59
        // services
60
        $serviceDefinitions = $this->parseDefinitions($content);
61
        $definitions = $definitions + $serviceDefinitions;
62
63
        return $definitions;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $definitions; (array<*,Interop\Containe...on\DefinitionInterface>) is incompatible with the return type documented by TheCodingMachine\Definit...nLoader::getDefinitions of type Interop\Container\Definition\DefinitionInterface[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
64
    }
65
66
    /**
67
     * Parses all imports.
68
     *
69
     * @param array $content
70
     * @param $content
71
     *
72
     * @return DefinitionInterface[]
73
     */
74
    private function parseImports($content)
75
    {
76
        if (!isset($content['imports'])) {
77
            return [];
78
        }
79
80
        if (!is_array($content['imports'])) {
81
            throw new InvalidArgumentException(sprintf('The "imports" key should contain an array in %s. Check your YAML syntax.', $this->fileName));
82
        }
83
84
        $additionalDefinitions = [];
85
86
        foreach ($content['imports'] as $import) {
87
            if (!is_array($import)) {
88
                throw new InvalidArgumentException(sprintf('The values in the "imports" key should be arrays in %s. Check your YAML syntax.', $this->fileName));
89
            }
90
91
            if (isset($import['ignore_errors'])) {
92
                throw new InvalidArgumentException(sprintf('The "ignore_errors" key is not supported in YamlDefinitionLoader. This is a Symfony specific syntax. Check your YAML syntax.', $this->fileName));
93
            }
94
95
            $importFileName = $import['resource'];
96
97
            if (strpos($importFileName, '/') !== 0) {
98
                $importFileName = dirname($this->fileName).'/'.$importFileName;
99
            }
100
101
            $yamlDefinitionLoader = new self($importFileName);
102
            $newDefinitions = $yamlDefinitionLoader->getDefinitions();
103
104
            $additionalDefinitions = $newDefinitions + $additionalDefinitions;
105
        }
106
107
        return $additionalDefinitions;
108
    }
109
110
    /**
111
     * Parses definitions.
112
     *
113
     * @param array $content
114
     *
115
     * @return DefinitionInterface[]
116
     */
117
    private function parseDefinitions($content)
118
    {
119
        if (!isset($content['services'])) {
120
            return [];
121
        }
122
123
        if (!is_array($content['services'])) {
124
            throw new InvalidArgumentException(sprintf('The "services" key should contain an array in %s. Check your YAML syntax.', $this->fileName));
125
        }
126
127
        $definitions = [];
128
129
        foreach ($content['services'] as $id => $service) {
130
            $definitions[$id] = $this->parseDefinition($id, $service);
131
        }
132
133
        return $definitions;
134
    }
135
136
    /**
137
     * Parses a definition.
138
     *
139
     * @param string $id
140
     * @param array  $service
141
     *
142
     * @return DefinitionInterface
143
     *
144
     * @throws InvalidArgumentException When tags are invalid
145
     */
146
    private function parseDefinition($id, $service)
147
    {
148
        if (is_string($service) && 0 === strpos($service, '@')) {
149
            return new Reference(substr($service, 1));
150
        }
151
152 View Code Duplication
        if (!is_array($service)) {
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...
153
            throw new InvalidArgumentException(sprintf('A service definition must be an array or a string starting with "@" but %s found for service "%s" in %s. Check your YAML syntax.', gettype($service), $id, $this->fileName));
154
        }
155
156
        if (isset($service['alias'])) {
157
            if (isset($service['public'])) {
158
                throw new InvalidArgumentException('The "public" key is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
159
            }
160
161
            return new Reference($service['alias']);
162
        }
163
164
        if (isset($service['parent'])) {
165
            throw new InvalidArgumentException('Definition decorators via the "parent" key are not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
166
        }
167
168
        $definition = null;
169
170
        if (isset($service['class'])) {
171
            $definition = new ObjectDefinition($service['class']);
172
173
            if (isset($service['arguments'])) {
174
                $arguments = $this->resolveServices($service['arguments']);
175
                foreach ($arguments as $argument) {
0 ignored issues
show
Bug introduced by
The expression $arguments of type array|string|object<Assembly\Reference> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
176
                    $definition->addConstructorArgument($argument);
177
                }
178
            }
179
180
            if (isset($service['properties'])) {
181
                $properties = $this->resolveServices($service['properties']);
182
                foreach ($properties as $name => $property) {
0 ignored issues
show
Bug introduced by
The expression $properties of type array|string|object<Assembly\Reference> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
183
                    $definition->addPropertyAssignment($name, $property);
184
                }
185
            }
186
187
            if (isset($service['calls'])) {
188 View Code Duplication
                if (!is_array($service['calls'])) {
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...
189
                    throw new InvalidArgumentException(sprintf('Parameter "calls" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $this->fileName));
190
                }
191
192
                foreach ($service['calls'] as $call) {
193
                    if (isset($call['method'])) {
194
                        $method = $call['method'];
195
                        $args = isset($call['arguments']) ? $this->resolveServices($call['arguments']) : array();
196
                    } else {
197
                        $method = $call[0];
198
                        $args = isset($call[1]) ? $this->resolveServices($call[1]) : array();
199
                    }
200
201
                    array_unshift($args, $method);
202
                    call_user_func_array([$definition, 'addMethodCall'], $args);
203
                }
204
            }
205
        }
206
207
        if (isset($service['factory'])) {
208
            if (is_string($service['factory'])) {
209
                if (strpos($service['factory'], ':') !== false && strpos($service['factory'], '::') === false) {
210
                    $parts = explode(':', $service['factory']);
211
                    $definition = new FactoryCallDefinition($this->resolveServices('@'.$parts[0]), $parts[1]);
212
                } elseif (strpos($service['factory'], ':') !== false && strpos($service['factory'], '::') !== false) {
213
                    $parts = explode('::', $service['factory']);
214
                    $definition = new FactoryCallDefinition($parts[0], $parts[1]);
215
                } else {
216
                    throw new InvalidArgumentException('A "factory" must be in the format "service_name:method_name" or "class_name::method_name".Got "'.$service['factory'].'"');
217
                }
218
            } else {
219
                $definition = new FactoryCallDefinition($this->resolveServices($service['factory'][0]), $service['factory'][1]);
220
            }
221
222
            if (isset($service['arguments'])) {
223
                $arguments = $this->resolveServices($service['arguments']);
224
                call_user_func_array([$definition, 'setArguments'], $arguments);
225
            }
226
        }
227
228
        if (isset($service['shared'])) {
229
            throw new InvalidArgumentException('The "shared" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
230
        }
231
232
        if (isset($service['synthetic'])) {
233
            throw new InvalidArgumentException('The "synthetic" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
234
        }
235
236
        if (isset($service['lazy'])) {
237
            throw new InvalidArgumentException('The "lazy" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
238
        }
239
240
        if (isset($service['public'])) {
241
            // TODO: add support for private services => mapping to nested definitions
242
            throw new InvalidArgumentException('The "public" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
243
        }
244
245
        if (isset($service['abstract'])) {
246
            throw new InvalidArgumentException('The "abstract" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
247
        }
248
249
        if (array_key_exists('deprecated', $service)) {
250
            throw new InvalidArgumentException('The "deprecated" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
251
        }
252
253
        if (isset($service['file'])) {
254
            throw new InvalidArgumentException('The "file" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
255
        }
256
257
        if (isset($service['configurator'])) {
258
            throw new InvalidArgumentException('The "configurator" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
259
        }
260
261
        if (isset($service['tags'])) {
262
            throw new InvalidArgumentException('The "tags" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
263
        }
264
265
        if (isset($service['decorates'])) {
266
            throw new InvalidArgumentException('The "decorates" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
267
        }
268
269
        if (isset($service['autowire'])) {
270
            throw new InvalidArgumentException('The "autowire" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
271
        }
272
273
        if (isset($service['autowiring_types'])) {
274
            throw new InvalidArgumentException('The "autowiring_types" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
275
        }
276
277
        if ($definition === null) {
278
            throw new InvalidArgumentException(sprintf('Invalid service declaration for "%s" (in %s). You should specify at least a "class", "alias" or "factory" key.', $id, $this->fileName));
279
        }
280
281
        return $definition;
282
    }
283
284
    /**
285
     * Loads a YAML file.
286
     *
287
     * @param string $file
288
     *
289
     * @return array The file content
290
     *
291
     * @throws InvalidArgumentException when the given file is not a local file or when it does not exist
292
     */
293
    protected function loadFile($file)
294
    {
295
        if (!stream_is_local($file)) {
296
            throw new InvalidArgumentException(sprintf('This is not a local file "%s".', $file));
297
        }
298
299
        if (!is_readable($file)) {
300
            throw new FileNotFoundException(sprintf('The file "%s" does not exist or is not readable.', $file));
301
        }
302
303
        $yamlParser = new Parser();
304
305
        try {
306
            $configuration = $yamlParser->parse(file_get_contents($file));
307
        } catch (ParseException $e) {
308
            throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $file), 0, $e);
309
        }
310
311
        return $this->validate($configuration, $file);
312
    }
313
314
    /**
315
     * Validates a YAML file.
316
     *
317
     * @param mixed  $content
318
     * @param string $file
319
     *
320
     * @return array
321
     *
322
     * @throws InvalidArgumentException When service file is not valid
323
     */
324
    private function validate($content, $file)
325
    {
326
        if (null === $content) {
327
            return $content;
328
        }
329
330
        if (!is_array($content)) {
331
            throw new InvalidArgumentException(sprintf('The service file "%s" is not valid. It should contain an array. Check your YAML syntax.', $file));
332
        }
333
334
        foreach ($content as $namespace => $data) {
335
            if (in_array($namespace, array('imports', 'parameters', 'services'))) {
336
                continue;
337
            }
338
339
            throw new InvalidArgumentException(sprintf(
340
                'Cannot load the configuration for file "%s". Unexpected "%s" key. Expecting one of "imports", "parameters", "services".',
341
                $file,
342
                $namespace
343
            ));
344
        }
345
346
        return $content;
347
    }
348
349
    /**
350
     * Resolves services.
351
     *
352
     * @param string|array $value
353
     *
354
     * @return array|string|Reference
355
     */
356
    private function resolveServices($value)
357
    {
358
        if (is_array($value)) {
359
            return array_map(array($this, 'resolveServices'), $value);
360
        } elseif (is_string($value) &&  0 === strpos($value, '@=')) {
361
            throw new InvalidArgumentException('Expressions (starting by "@=") are not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
362
        } elseif (is_string($value) &&  0 === strpos($value, '@')) {
363
            if (0 === strpos($value, '@@')) {
364
                return substr($value, 1);
365
            } elseif (0 === strpos($value, '@?')) {
366
                throw new InvalidArgumentException('Optional services (starting by "@?") are not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
367
            } else {
368
                $value = substr($value, 1);
369
                if ('=' === substr($value, -1)) {
370
                    throw new InvalidArgumentException('Non-strict services (ending with "=") are not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
371
                } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
372
                }
373
374
                return new Reference($value);
375
            }
376
        } else {
377
            return $value;
378
        }
379
    }
380
}
381