Completed
Push — master ( 33bfe2...c04ba9 )
by Martin
02:12
created

NeonFileLoader::parseDefinitions()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 18
rs 8.8571
cc 5
eloc 10
nc 5
nop 2
1
<?php
2
3
/*
4
 * This is part of the symfonette/neon-integration package.
5
 *
6
 * (c) Martin Hasoň <[email protected]>
7
 * (c) Webuni s.r.o. <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace Symfonette\NeonIntegration\DependencyInjection;
14
15
use Nette\Neon\Entity;
16
use Nette\Neon\Neon;
17
use Symfony\Component\Config\Resource\FileResource;
18
use Symfony\Component\DependencyInjection\Alias;
19
use Symfony\Component\DependencyInjection\ContainerInterface;
20
use Symfony\Component\DependencyInjection\Definition;
21
use Symfony\Component\DependencyInjection\DefinitionDecorator;
22
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
23
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
24
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
25
use Symfony\Component\DependencyInjection\Loader\FileLoader;
26
use Symfony\Component\DependencyInjection\Reference;
27
use Symfony\Component\ExpressionLanguage\Expression;
28
29
class NeonFileLoader extends FileLoader
30
{
31
    private static $keywords = [
32
        'alias' => 'alias',
33
        'parent' => 'parent',
34
        'class' => 'class',
35
        'shared' => 'shared',
36
        'synthetic' => 'synthetic',
37
        'lazy' => 'lazy',
38
        'public' => 'public',
39
        'abstract' => 'abstract',
40
        'deprecated' => 'deprecated',
41
        'factory' => 'factory',
42
        'file' => 'file',
43
        'arguments' => 'arguments',
44
        'properties' => 'properties',
45
        'configurator' => 'configurator',
46
        'calls' => 'calls',
47
        'tags' => 'tags',
48
        'decorates' => 'decorates',
49
        'decoration_inner_name' => 'decoration_inner_name',
50
        'decoration_priority' => 'decoration_priority',
51
        'autowire' => 'autowire',
52
        'autowiring_types' => 'autowiring_types',
53
        'setup' => 'setup', // nette
54
    ];
55
56
    private $anonymousServicesCount;
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function supports($resource, $type = null)
62
    {
63
        return is_string($resource) && 'neon' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'neon' === $type);
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function load($resource, $type = null)
70
    {
71
        $path = $this->locator->locate($resource);
72
73
        $content = $this->loadFile($path);
74
75
        $this->container->addResource(new FileResource($path));
1 ignored issue
show
Bug introduced by
It seems like $path defined by $this->locator->locate($resource) on line 71 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...
76
77
        if (null === $content) {
78
            return;
79
        }
80
81
        $this->parseImports($content, $path);
82
83
        if (isset($content['parameters'])) {
84
            if (!is_array($content['parameters'])) {
85
                throw new InvalidArgumentException(sprintf('The "parameters" key should contain an array in %s. Check your NEON syntax.', $resource));
86
            }
87
88
            foreach ($content['parameters'] as $key => $value) {
89
                $this->container->setParameter($key, $this->resolveServices($value, $path));
90
            }
91
        }
92
93
        $this->loadFromExtensions($content);
94
95
        $this->parseDefinitions($content, $resource);
96
    }
97
98
    private function loadFile($file)
99
    {
100
        if (!class_exists('Nette\Neon\Neon')) {
101
            throw new RuntimeException('Unable to load NEON config files as the Nette Neon Component is not installed.');
102
        }
103
104
        if (!stream_is_local($file)) {
105
            throw new InvalidArgumentException(sprintf('This is not a local file "%s".', $file));
106
        }
107
108
        if (!file_exists($file)) {
109
            throw new InvalidArgumentException(sprintf('The service file "%s" is not valid.', $file));
110
        }
111
112
        try {
113
            $configuration = Neon::decode(file_get_contents($file));
114
        } catch (\Exception $e) {
115
            throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid NEON.', $file), 0, $e);
116
        }
117
118
        return $this->validate($configuration, $file);
119
    }
120
121
    private function validate($content, $file)
122
    {
123
        if (null === $content) {
124
            return $content;
125
        }
126
127
        if (!is_array($content)) {
128
            throw new InvalidArgumentException(sprintf('The service file "%s" is not valid. It should contain an array. Check your NEON syntax.', $file));
129
        }
130
131
        foreach ($content as $namespace => $data) {
132
            if (in_array($namespace, ['imports', 'parameters', 'services'])) {
133
                continue;
134
            }
135
136
            if (!$this->container->hasExtension($namespace)) {
137
                $extensionNamespaces = array_filter(array_map(
138
                    function (ExtensionInterface $ext) {
139
                        return $ext->getAlias();
140
                    },
141
                    $this->container->getExtensions()
142
                ));
143
144
                throw new InvalidArgumentException(sprintf(
145
                    'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s',
146
                    $namespace,
147
                    $file,
148
                    $namespace,
149
                    $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'
150
                ));
151
            }
152
        }
153
154
        return $content;
155
    }
156
157
    private function parseImports($content, $file)
158
    {
159
        // nette
160
        if (!isset($content['imports']) && !isset($content['includes'])) {
161
            return;
162
        }
163
164
        // nette
165
        if (isset($content['imports']) && isset($content['includes'])) {
166
            throw new InvalidArgumentException('The "imports" and "includes" keys cannot be used together. Checkyour NEON syntax.', $file);
167
        }
168
169 View Code Duplication
        if (isset($content['imports']) && !is_array($content['imports'])) {
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...
170
            throw new InvalidArgumentException(sprintf('The "imports" key should contain an array in %s. Check your NEON syntax.', $file));
171
        }
172
173
        // nette
174 View Code Duplication
        if (isset($content['includes']) && !is_array($content['includes'])) {
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...
175
            throw new InvalidArgumentException(sprintf('The "includes" key should contain an array in %s. Check your NEON syntax.', $file));
176
        }
177
178
        // nette
179
        $content = array_merge(['imports' => [], 'includes' => []], $content);
180
181
        foreach ($content['imports'] as $import) {
182
            if (!is_array($import)) {
183
                throw new InvalidArgumentException(sprintf('The values in the "imports" key should be arrays in %s. Check your NEON syntax.', $file));
184
            }
185
186
            $this->setCurrentDir(dirname($file));
187
            $this->import($import['resource'], null, isset($import['ignore_errors']) ? (bool) $import['ignore_errors'] : false, $file);
188
        }
189
190
        // nette
191
        foreach ($content['includes'] as $include) {
192
            $this->setCurrentDir(dirname($file));
193
            $this->import($include, null, false, $file);
194
        }
195
    }
196
197
    private function parseDefinitions($content, $file)
198
    {
199
        if (!isset($content['services'])) {
200
            return;
201
        }
202
203
        if (!is_array($content['services'])) {
204
            throw new InvalidArgumentException(sprintf('The "services" key should contain an array in %s. Check your YAML syntax.', $file));
205
        }
206
207
        $this->anonymousServicesCount = 0;
208
        foreach ($content['services'] as $id => $service) {
209
            if (is_int($id)) {
210
                $id = $this->generateAnonymousServiceId($file);
211
            }
212
            $this->parseDefinition($id, $service, $file);
213
        }
214
    }
215
216
    private function parseDefinition($id, $service, $file)
217
    {
218
        // nette
219
        if ($service instanceof Entity) {
220
            $value = $service->value;
221
            $service = ['arguments' => $service->attributes];
222
            if (false === strpos($value, ':')) {
223
                $service['class'] = $value;
224
            } else {
225
                $service['factory'] = $value;
226
            }
227
        }
228
229
        // nette
230
        if (preg_match('#^(\S+)\s+<\s+(\S+)\z#', $id, $matches)) {
231 View Code Duplication
            if (isset($service['parent']) && $matches[2] !== $service['parent']) {
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...
232
                throw new InvalidArgumentException(sprintf('Two parent services "%s" and "%s" are defined for service "%s" in "%s". Check your NEON syntax.', $service['parent'], $matches[2], $matches[1], $file));
233
            }
234
235
            $id = $matches[1];
236
            $parent = $matches[2];
237
        }
238
239
        // nette
240
        if (is_string($service) && false !== strpos($service, ':')) {
241
            $service = ['factory' => $this->parseFactory($service, $file)];
242
        } elseif (is_string($service) && 0 === strpos($service, '@')) {
243
            $this->container->setAlias($id, substr($service, 1));
244
245
            return;
246
        } elseif (is_string($service)) {
247
            $service = ['class' => $service];
248
        }
249
250
        if (!is_array($service)) {
251
            throw new InvalidArgumentException(sprintf('A service definition must be an array or a string starting with "@" or a NEON entity but %s found for service "%s" in %s. Check your NEON syntax.', gettype($service), $id, $file));
252
        }
253
254
        self::checkDefinition($id, $service, $file);
255
256
        if (isset($service['alias'])) {
257
            $public = !array_key_exists('public', $service) || (bool) $service['public'];
258
            $this->container->setAlias($id, new Alias($service['alias'], $public));
259
260
            foreach ($service as $key => $value) {
261
                if (!in_array($key, ['alias', 'public'])) {
262
                    throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported for alias definition "%s" in "%s". Allowed configuration keys are "alias" and "public".', $key, $id, $file));
263
                }
264
            }
265
266
            return;
267
        }
268
269
        // nette
270
        if (isset($parent)) {
271
            $service['parent'] = $parent;
272
        }
273
274
        if (isset($service['parent'])) {
275
            $definition = new DefinitionDecorator($service['parent']);
276
        } else {
277
            $definition = new Definition();
278
        }
279
280 View Code Duplication
        if (isset($service['class'])) {
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...
281
            $class = $service['class'];
282
283
            // nette
284
            if ($class instanceof Entity) {
285
                if (isset($service['arguments']) && !empty($class->attributes)) {
286
                    throw new InvalidArgumentException(sprintf('Duplicated definition of arguments for service "%s" in "%s". Check you NEON syntax.', $id, $file));
287
                }
288
289
                $service['arguments'] = $class->attributes;
290
                $class = $class->value;
291
            }
292
293
            $definition->setClass($class);
294
        }
295
296
        if (isset($service['shared'])) {
297
            $definition->setShared($service['shared']);
298
        }
299
300
        if (isset($service['synthetic'])) {
301
            $definition->setSynthetic($service['synthetic']);
302
        }
303
304
        if (isset($service['lazy'])) {
305
            $definition->setLazy($service['lazy']);
306
        }
307
308
        if (isset($service['public'])) {
309
            $definition->setPublic($service['public']);
310
        }
311
312
        if (isset($service['abstract'])) {
313
            $definition->setAbstract($service['abstract']);
314
        }
315
316
        if (array_key_exists('deprecated', $service)) {
317
            $definition->setDeprecated(true, $service['deprecated']);
318
        }
319
320 View Code Duplication
        if (isset($service['factory'])) {
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...
321
            $factory = $service['factory'];
322
323
            //nette
324
            if ($factory instanceof Entity) {
325
                if (isset($service['arguments']) && !empty($factory->attributes)) {
326
                    throw new InvalidArgumentException(sprintf('Duplicated definition of arguments for service "%s" in "%s". Check you NEON syntax.', $id, $file));
327
                }
328
329
                $service['arguments'] = $factory->attributes;
330
                $factory = $factory->value;
331
            }
332
333
            $definition->setFactory($this->parseFactory($factory, $file));
334
        }
335
336
        if (isset($service['file'])) {
337
            $definition->setFile($service['file']);
338
        }
339
340
        if (isset($service['arguments'])) {
341
            $autowired = false;
342
            array_walk($service['arguments'], function (&$value) use (&$autowired) {
343
                if ('...' === $value) {
344
                    $value = '';
345
                    $autowired = true;
346
                }
347
348
                return $value;
349
            });
350
351
            $definition->setAutowired($autowired);
352
            $definition->setArguments($this->resolveServices($service['arguments'], $file));
353
        }
354
355
        // nette
356
        if (isset($service['setup'])) {
357
            foreach ($service['setup'] as $setup) {
358
                if ($setup instanceof Entity) {
359
                    $name = $setup->value;
360
                    $args = $setup->attributes;
361
                } elseif (is_array($setup)) {
362
                    $name = $setup[0];
363
                    $args = isset($setup[1]) ? $setup[1] : [];
364
                } else {
365
                    $name = $setup;
366
                    $args = [];
367
                }
368
369
                if ('$' === $name[0]) {
370
                    $service['properties'][substr($name, 1)] = $args;
371
                } else {
372
                    $service['calls'][] = [$name, $args];
373
                }
374
            }
375
        }
376
377
        if (isset($service['properties'])) {
378
            $definition->setProperties($this->resolveServices($service['properties'], $file));
379
        }
380
381
        if (isset($service['configurator'])) {
382
            if (is_string($service['configurator'])) {
383
                $definition->setConfigurator($service['configurator']);
384
            } else {
385
                $definition->setConfigurator([$this->resolveServices($service['configurator'][0], $file), $service['configurator'][1]]);
386
            }
387
        }
388
389
        if (isset($service['calls'])) {
390
            if (!is_array($service['calls'])) {
391
                throw new InvalidArgumentException(sprintf('Parameter "calls" must be an array for service "%s" in %s. Check your NEON syntax.', $id, $file));
392
            }
393
394
            foreach ($service['calls'] as $call) {
395
                if ($call instanceof Entity) { // nette
396
                    $method = $call->value;
397
                    $args = $this->resolveServices($call->attributes, $file);
398
                } elseif (isset($call['method'])) {
399
                    $method = $call['method'];
400
                    $args = isset($call['arguments']) ? $this->resolveServices($call['arguments'], $file) : [];
401
                } elseif (is_array($call)) {
402
                    $method = $call[0];
403
                    $args = isset($call[1]) ? $this->resolveServices($call[1], $file) : [];
404
                } else { // nette
405
                    $method = $call;
406
                    $args = [];
407
                }
408
409
                $definition->addMethodCall($method, $args);
410
            }
411
        }
412
413
        if (isset($service['tags'])) {
414
            if (!is_array($service['tags'])) {
415
                throw new InvalidArgumentException(sprintf('Parameter "tags" must be an array for service "%s" in %s. Check your NEON syntax.', $id, $file));
416
            }
417
418
            foreach ($service['tags'] as $tag) {
419
                if ($tag instanceof Entity) {
420
                    $tag = ['name' => $tag->value] + $tag->attributes;
421
                } elseif (is_string($tag)) {
422
                    $tag = ['name' => $tag];
423
                }
424
425
                if (!is_array($tag)) {
426
                    throw new InvalidArgumentException(sprintf('A "tags" entry must be an array for service "%s" in %s. Check your NEON syntax.', $id, $file));
427
                }
428
429
                if (!isset($tag['name'])) {
430
                    throw new InvalidArgumentException(sprintf('A "tags" entry is missing a "name" key for service "%s" in %s.', $id, $file));
431
                }
432
433
                if (!is_string($tag['name']) || '' === $tag['name']) {
434
                    throw new InvalidArgumentException(sprintf('The tag name for service "%s" in %s must be a non-empty string.', $id, $file));
435
                }
436
437
                $name = $tag['name'];
438
                unset($tag['name']);
439
440
                foreach ($tag as $attribute => $value) {
441
                    if (!is_scalar($value) && null !== $value) {
442
                        throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in %s. Check your NEON syntax.', $id, $name, $attribute, $file));
443
                    }
444
                }
445
446
                $definition->addTag($name, $tag);
447
            }
448
        }
449
450
        if (isset($service['decorates'])) {
451
            $renameId = isset($service['decoration_inner_name']) ? $service['decoration_inner_name'] : null;
452
            $priority = isset($service['decoration_priority']) ? $service['decoration_priority'] : 0;
453
            $definition->setDecoratedService($service['decorates'], $renameId, $priority);
454
        }
455
456
        // nette
457 View Code Duplication
        if (isset($service['autowired'])) {
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...
458
            if (isset($service['autowire']) && $service['autowire'] !== $service['autowired']) {
459
                throw new InvalidArgumentException(sprintf('Contradictory definition of autowiring for service "%s" in "%s". Check you NEON syntax.', $id, $file));
460
            }
461
462
            $service['autowire'] = $service['autowired'];
463
        }
464
465
        if (isset($service['autowire'])) {
466
            // nette
467
            if ($definition->isAutowired() && !$service['autowire']) {
468
                throw new InvalidArgumentException(sprintf('Contradictory definition of autowiring for service "%s" in "%s". Check you NEON syntax.', $id, $file));
469
            }
470
471
            $definition->setAutowired($service['autowire']);
472
        }
473
474
        if (isset($service['autowiring_types'])) {
475
            if (is_string($service['autowiring_types'])) {
476
                $definition->addAutowiringType($service['autowiring_types']);
477
            } else {
478
                if (!is_array($service['autowiring_types'])) {
479
                    throw new InvalidArgumentException(sprintf('Parameter "autowiring_types" must be a string or an array for service "%s" in %s. Check your NEON syntax.', $id, $file));
480
                }
481
482
                foreach ($service['autowiring_types'] as $autowiringType) {
483
                    if (!is_string($autowiringType)) {
484
                        throw new InvalidArgumentException(sprintf('A "autowiring_types" attribute must be of type string for service "%s" in %s. Check your NEON syntax.', $id, $file));
485
                    }
486
487
                    $definition->addAutowiringType($autowiringType);
488
                }
489
            }
490
        }
491
492
        $this->container->setDefinition($id, $definition);
493
    }
494
495
    private function parseFactory($factory, $file)
496
    {
497
        if (is_string($factory)) {
498
            if (strpos($factory, '::') !== false) {
499
                $parts = explode('::', $factory, 2);
500
501
                return ['@' === $parts[0][0] ? $this->resolveServices($parts[0], $file) : $parts[0], $parts[1]];
502
            } elseif (strpos($factory, ':') !== false) {
503
                $parts = explode(':', $factory, 2);
504
505
                return [$this->resolveServices(('@' === $parts[0][0] ?: '@').$parts[0], $file), $parts[1]];
506
            } else {
507
                return $factory;
508
            }
509
        } else {
510
            return [$this->resolveServices($factory[0], $file), $factory[1]];
511
        }
512
    }
513
514
    private function resolveServices($value, $file)
515
    {
516
        // nette
517
        if ($value instanceof Entity) {
518
            if ('expression' === $value->value || 'expr' === $value->value) {
519
                return new Expression(reset($value->attributes));
520
            } elseif (0 === strpos($value->value, '@')) {
521
                $value = $value->value;
522
            } else {
523
                $id = $this->generateAnonymousServiceId($file);
524
                $this->parseDefinition($id, $value, $file);
525
                $value = new Reference($id);
526
            }
527
        }
528
529
        if (is_array($value)) {
530
            $value = array_map(function ($value) use ($file) {
531
                return $this->resolveServices($value, $file);
532
            }, $value);
533
        } elseif (is_string($value) &&  0 === strpos($value, '@=')) {
534
            return new Expression(substr($value, 2));
535
        } elseif (is_string($value) &&  0 === strpos($value, '@')) {
536
            if (0 === strpos($value, '@@')) {
537
                $value = substr($value, 1);
538
                $invalidBehavior = null;
539
            } elseif (0 === strpos($value, '@?')) {
540
                $value = substr($value, 2);
541
                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
542
            } else {
543
                $value = substr($value, 1);
544
                $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
545
            }
546
547
            if ('=' === substr($value, -1)) {
548
                $value = substr($value, 0, -1);
549
            }
550
551
            if (null !== $invalidBehavior) {
552
                $value = new Reference($value, $invalidBehavior);
553
            }
554
        }
555
556
        return $value;
557
    }
558
559
    private function loadFromExtensions($content)
560
    {
561
        foreach ($content as $namespace => $values) {
562
            // nette
563
            if (in_array($namespace, ['imports', 'includes', 'parameters', 'services'])) {
564
                continue;
565
            }
566
567
            if (!is_array($values)) {
568
                $values = [];
569
            }
570
571
            $this->container->loadFromExtension($namespace, $values);
572
        }
573
    }
574
575
    private function generateAnonymousServiceId($file)
576
    {
577
        return sprintf('%s_%d', hash('sha256', $file), ++$this->anonymousServicesCount);
578
    }
579
580
    private static function checkDefinition($id, array $definition, $file)
581
    {
582
        foreach ($definition as $key => $value) {
583
            if (!isset(self::$keywords[$key])) {
584
                throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported for service definition "%s" in "%s". Allowed configuration keys are "%s".', $key, $id, $file, implode('", "', self::$keywords)));
585
            }
586
        }
587
    }
588
}
589