Completed
Push — 1.0 ( 8819c3...b01434 )
by David
03:23
created

YamlDefinitionLoader::parseImports()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 34
rs 6.7273
cc 7
eloc 18
nc 7
nop 1
1
<?php
2
namespace TheCodingMachine\Definition;
3
4
use Assembly\AliasDefinition;
5
use Assembly\FactoryDefinition;
6
use Assembly\InstanceDefinition;
7
use Assembly\MethodCall;
8
use Assembly\ParameterDefinition;
9
use Assembly\PropertyAssignment;
10
use Assembly\Reference;
11
use Interop\Container\Definition\DefinitionInterface;
12
use Interop\Container\Definition\DefinitionProviderInterface;
13
use Symfony\Component\Yaml\Exception\ParseException;
14
use Symfony\Component\Yaml\Parser;
15
use TheCodingMachine\Definition\Exception\FileNotFoundException;
16
use TheCodingMachine\Definition\Exception\InvalidArgumentException;
17
use TheCodingMachine\Definition\Exception\RuntimeException;
18
19
class YamlDefinitionLoader implements DefinitionProviderInterface
20
{
21
    /**
22
     * The name of the YAML file to be loaded.
23
     *
24
     * @var string
25
     */
26
    private $fileName;
27
28
    public function __construct($fileName)
29
    {
30
        $this->fileName = $fileName;
31
    }
32
33
    /**
34
     * Returns the definition to register in the container.
35
     *
36
     * @return DefinitionInterface[]
37
     */
38
    public function getDefinitions()
39
    {
40
        $content = $this->loadFile($this->fileName);
41
42
        // empty file
43
        if (null === $content) {
44
            return [];
45
        }
46
47
        // imports
48
        $definitions = $this->parseImports($content);
49
50
        // parameters
51
        if (isset($content['parameters'])) {
52
            if (!is_array($content['parameters'])) {
53
                throw new InvalidArgumentException(sprintf('The "parameters" key should contain an array in %s. Check your YAML syntax.', $this->fileName));
54
            }
55
56
            foreach ($content['parameters'] as $key => $value) {
57
                $definitions[$key] = new ParameterDefinition($key, $value);
58
                //$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...
59
            }
60
        }
61
62
        // services
63
        $serviceDefinitions = $this->parseDefinitions($content);
64
        $definitions = $definitions + $serviceDefinitions;
65
66
        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 declared by the interface Interop\Container\Defini...terface::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...
67
    }
68
69
    /**
70
     * Parses all imports.
71
     *
72
     * @param array  $content
73
     * @param $content
74
     * @return DefinitionInterface[]
75
     */
76
    private function parseImports($content)
77
    {
78
        if (!isset($content['imports'])) {
79
            return [];
80
        }
81
82
        if (!is_array($content['imports'])) {
83
            throw new InvalidArgumentException(sprintf('The "imports" key should contain an array in %s. Check your YAML syntax.', $this->fileName));
84
        }
85
86
        $additionalDefinitions = [];
87
88
        foreach ($content['imports'] as $import) {
89
            if (!is_array($import)) {
90
                throw new InvalidArgumentException(sprintf('The values in the "imports" key should be arrays in %s. Check your YAML syntax.', $this->fileName));
91
            }
92
93
            if (isset($import['ignore_errors'])) {
94
                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));
95
            }
96
97
            $importFileName = $import['resource'];
98
99
            if (strpos($importFileName, '/') !== 0) {
100
                $importFileName = dirname($this->fileName).'/'.$importFileName;
101
            }
102
103
            $yamlDefinitionLoader = new self($importFileName);
104
            $newDefinitions = $yamlDefinitionLoader->getDefinitions();
105
106
            $additionalDefinitions = $newDefinitions + $additionalDefinitions;
107
        }
108
        return $additionalDefinitions;
109
    }
110
111
    /**
112
     * Parses definitions.
113
     *
114
     * @param array  $content
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
     * @throws InvalidArgumentException When tags are invalid
144
     */
145
    private function parseDefinition($id, $service)
146
    {
147
        if (is_string($service) && 0 === strpos($service, '@')) {
148
            return new AliasDefinition($id, substr($service, 1));
149
        }
150
151 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...
152
            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));
153
        }
154
155
        if (isset($service['alias'])) {
156
            if (isset($service['public'])) {
157
                throw new InvalidArgumentException('The "public" key is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
158
            }
159
            return new AliasDefinition($id, $service['alias']);
160
        }
161
162
        if (isset($service['parent'])) {
163
            throw new InvalidArgumentException('Definition decorators via the "parent" key are not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
164
        }
165
166
        $definition = null;
167
168
        if (isset($service['class'])) {
169
            $definition = new InstanceDefinition($id, $service['class']);
170
171
            if (isset($service['arguments'])) {
172
                $arguments = $this->resolveServices($service['arguments']);
173
                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...
174
                    $definition->addConstructorArgument($argument);
175
                }
176
            }
177
178
            if (isset($service['properties'])) {
179
                $properties = $this->resolveServices($service['properties']);
180
                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...
181
                    $definition->addPropertyAssignment($name, $property);
182
                }
183
            }
184
185
            if (isset($service['calls'])) {
186 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...
187
                    throw new InvalidArgumentException(sprintf('Parameter "calls" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $this->fileName));
188
                }
189
190
                foreach ($service['calls'] as $call) {
191
                    if (isset($call['method'])) {
192
                        $method = $call['method'];
193
                        $args = isset($call['arguments']) ? $this->resolveServices($call['arguments']) : array();
194
                    } else {
195
                        $method = $call[0];
196
                        $args = isset($call[1]) ? $this->resolveServices($call[1]) : array();
197
                    }
198
199
                    array_unshift($args, $method);
200
                    call_user_func_array([$definition, 'addMethodCall'], $args);
201
                }
202
            }
203
204
        }
205
206
        if (isset($service['factory'])) {
207
            if (is_string($service['factory'])) {
208
                if (strpos($service['factory'], ':') !== false && strpos($service['factory'], '::') === false) {
209
                    $parts = explode(':', $service['factory']);
210
                    $definition = new FactoryDefinition($id, $this->resolveServices('@'.$parts[0]), $parts[1]);
211
                } elseif (strpos($service['factory'], ':') !== false && strpos($service['factory'], '::') !== false) {
212
                    $parts = explode('::', $service['factory']);
213
                    $definition = new FactoryDefinition($id, $parts[0], $parts[1]);
214
                } else {
215
                    throw new InvalidArgumentException('A "factory" must be in the format "service_name:method_name" or "class_name::method_name".Got "'.$service['factory'].'"');
216
                }
217
            } else {
218
                $definition = new FactoryDefinition($id, $this->resolveServices($service['factory'][0]), $service['factory'][1]);
219
            }
220
221
            if (isset($service['arguments'])) {
222
                $arguments = $this->resolveServices($service['arguments']);
223
                call_user_func_array([$definition, 'setArguments'], $arguments);
224
            }
225
        }
226
227
        if (isset($service['shared'])) {
228
            throw new InvalidArgumentException('The "shared" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
229
        }
230
231
        if (isset($service['synthetic'])) {
232
            throw new InvalidArgumentException('The "synthetic" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
233
        }
234
235
        if (isset($service['lazy'])) {
236
            throw new InvalidArgumentException('The "lazy" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
237
        }
238
239
        if (isset($service['public'])) {
240
            throw new InvalidArgumentException('The "public" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
241
        }
242
243
        if (isset($service['abstract'])) {
244
            throw new InvalidArgumentException('The "abstract" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
245
        }
246
247
        if (array_key_exists('deprecated', $service)) {
248
            throw new InvalidArgumentException('The "deprecated" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
249
        }
250
251
        if (isset($service['file'])) {
252
            throw new InvalidArgumentException('The "file" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
253
        }
254
255
256
        if (isset($service['configurator'])) {
257
            throw new InvalidArgumentException('The "configurator" key in instance definitions is not supported by YamlDefinitionLoader. This is a Symfony specific feature.');
258
        }
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.');            } 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...
371
                }
372
                return new Reference($value);
373
            }
374
        } else {
375
            return $value;
376
        }
377
    }
378
}
379