Completed
Push — master ( aaddac...6fa914 )
by Martin
02:07
created

NeonFileLoader::parseImports()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 26
rs 4.909
cc 9
eloc 14
nc 8
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) && (null === $type || 'neon' === $type);
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));
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->validateFile($configuration, $file);
119
    }
120
121
    private function validateFile($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', 'includes', '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
        foreach (['imports', 'includes'] as $key) {
165
            if (isset($content[$key]) && !is_array($content[$key])) {
166
                throw new InvalidArgumentException(sprintf('The "%s" key should contain an array in %s. Check your NEON syntax.', $key, $file));
167
            }
168
        }
169
170
        // nette
171
        $content = array_merge(['imports' => [], 'includes' => []], $content);
172
        $imports = array_merge($content['imports'], $content['includes']);
173
174
        foreach ($imports as $import) {
175
            $this->setCurrentDir(dirname($file));
176
            if (is_array($import)) {
177
                $this->import($import['resource'], null, isset($import['ignore_errors']) ? (bool) $import['ignore_errors'] : false, $file);
178
            } else {
179
                $this->import($import, null, false, $file); // nette
180
            }
181
        }
182
    }
183
184
    private function parseDefinitions($content, $file)
185
    {
186
        if (!isset($content['services'])) {
187
            return;
188
        }
189
190
        if (!is_array($content['services'])) {
191
            throw new InvalidArgumentException(sprintf('The "services" key should contain an array in %s. Check your YAML syntax.', $file));
192
        }
193
194
        $this->anonymousServicesCount = 0;
195
        foreach ($content['services'] as $id => $service) {
196
            if (is_int($id)) {
197
                $id = $this->generateAnonymousServiceId($file);
198
            }
199
            $this->parseDefinition($id, $service, $file);
200
        }
201
    }
202
203
    private function parseDefinition($id, $service, $file)
204
    {
205
        if ($this->processService($id, $service, $file)) {
206
            return;
207
        }
208
209
        $this->checkDefinition($id, $service, $file);
210
211
        if ($this->processAlias($id, $service, $file)) {
212
            return;
213
        }
214
215
        $definition = isset($service['parent']) ? new DefinitionDecorator($service['parent']) : new Definition();
216
217
        $this->processClass($id, $service, $definition, $file);
218
219
        if (isset($service['shared'])) {
220
            $definition->setShared($service['shared']);
221
        }
222
223
        if (isset($service['synthetic'])) {
224
            $definition->setSynthetic($service['synthetic']);
225
        }
226
227
        if (isset($service['lazy'])) {
228
            $definition->setLazy($service['lazy']);
229
        }
230
231
        if (isset($service['public'])) {
232
            $definition->setPublic($service['public']);
233
        }
234
235
        if (isset($service['abstract'])) {
236
            $definition->setAbstract($service['abstract']);
237
        }
238
239
        if (array_key_exists('deprecated', $service)) {
240
            $definition->setDeprecated(true, $service['deprecated']);
241
        }
242
243
        $this->processFactory($id, $service, $definition, $file);
244
245
        if (isset($service['file'])) {
246
            $definition->setFile($service['file']);
247
        }
248
249
        $this->processArguments($service, $definition, $file);
250
251
        // nette
252
        $this->processSetup($service);
253
254
        if (isset($service['properties'])) {
255
            $definition->setProperties($this->resolveServices($service['properties'], $file));
256
        }
257
258
        if (isset($service['configurator'])) {
259
            if (is_string($service['configurator'])) {
260
                $definition->setConfigurator($service['configurator']);
261
            } else {
262
                $definition->setConfigurator([$this->resolveServices($service['configurator'][0], $file), $service['configurator'][1]]);
263
            }
264
        }
265
266
        $this->processCalls($id, $service, $definition, $file);
267
        $this->processTags($id, $service, $definition, $file);
268
269
        if (isset($service['decorates'])) {
270
            $renameId = isset($service['decoration_inner_name']) ? $service['decoration_inner_name'] : null;
271
            $priority = isset($service['decoration_priority']) ? $service['decoration_priority'] : 0;
272
            $definition->setDecoratedService($service['decorates'], $renameId, $priority);
273
        }
274
275
        $this->processAutowire($id, $service, $definition, $file);
276
        $this->processAutowiringTypes($id, $service, $definition, $file);
277
278
        $this->container->setDefinition($id, $definition);
279
    }
280
281
    private function processService(&$id, &$service, $file)
282
    {
283
        // nette
284
        if ($service instanceof Entity) {
285
            $value = $service->value;
286
            $service = ['arguments' => $service->attributes];
287
            if (false === strpos($value, ':')) {
288
                $service['class'] = $value;
289
            } else {
290
                $service['factory'] = $value;
291
            }
292
        }
293
294
        // nette
295
        if (preg_match('#^(\S+)\s+<\s+(\S+)\z#', $id, $matches)) {
296 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...
297
                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));
298
            }
299
300
            $id = $matches[1];
301
            $parent = $matches[2];
302
        }
303
304
        // nette
305
        if (is_string($service) && false !== strpos($service, ':')) {
306
            $service = ['factory' => $this->parseFactory($service, $file)];
307
        } elseif (is_string($service) && 0 === strpos($service, '@')) {
308
            $this->container->setAlias($id, substr($service, 1));
309
310
            return true;
311
        } elseif (is_string($service)) {
312
            $service = ['class' => $service];
313
        }
314
315
        // nette
316
        if (isset($parent)) {
317
            $service['parent'] = $parent;
318
        }
319
    }
320
321
    private function processAlias($id, array &$service, $file)
322
    {
323
        if (!isset($service['alias'])) {
324
            return;
325
        }
326
327
        $public = !array_key_exists('public', $service) || (bool) $service['public'];
328
        $this->container->setAlias($id, new Alias($service['alias'], $public));
329
330
        foreach ($service as $key => $value) {
331
            if (!in_array($key, ['alias', 'public'])) {
332
                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));
333
            }
334
        }
335
336
        return true;
337
    }
338
339 View Code Duplication
    private function processClass($id, &$service, Definition $definition, $file)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
340
    {
341
        if (!isset($service['class'])) {
342
            return;
343
        }
344
345
        $class = $service['class'];
346
347
        // nette
348
        if ($class instanceof Entity) {
349
            if (isset($service['arguments']) && !empty($class->attributes)) {
350
                throw new InvalidArgumentException(sprintf('Duplicated definition of arguments for service "%s" in "%s". Check you NEON syntax.', $id, $file));
351
            }
352
353
            $service['arguments'] = $class->attributes;
354
            $class = $class->value;
355
        }
356
357
        $definition->setClass($class);
358
    }
359
360 View Code Duplication
    private function processFactory($id, array &$service, Definition $definition, $file)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
361
    {
362
        if (!isset($service['factory'])) {
363
            return;
364
        }
365
366
        $factory = $service['factory'];
367
368
        //nette
369
        if ($factory instanceof Entity) {
370
            if (isset($service['arguments']) && !empty($factory->attributes)) {
371
                throw new InvalidArgumentException(sprintf('Duplicated definition of arguments for service "%s" in "%s". Check you NEON syntax.', $id, $file));
372
            }
373
374
            $service['arguments'] = $factory->attributes;
375
            $factory = $factory->value;
376
        }
377
378
        $definition->setFactory($this->parseFactory($factory, $file));
379
    }
380
381
    private function processArguments(array &$service, Definition $definition, $file)
382
    {
383
        if (!isset($service['arguments'])) {
384
            return;
385
        }
386
387
        $autowired = false;
388
        array_walk($service['arguments'], function (&$value) use (&$autowired) {
389
            if ('...' === $value) {
390
                $value = '';
391
                $autowired = true;
392
            }
393
394
            return $value;
395
        });
396
397
        $definition->setAutowired($autowired);
398
        $definition->setArguments($this->resolveServices($service['arguments'], $file));
399
    }
400
401
    private function processSetup(array &$service)
402
    {
403
        if (!isset($service['setup'])) {
404
            return;
405
        }
406
407
        foreach ($service['setup'] as $setup) {
408
            if ($setup instanceof Entity) {
409
                $name = $setup->value;
410
                $args = $setup->attributes;
411
            } elseif (is_array($setup)) {
412
                $name = $setup[0];
413
                $args = isset($setup[1]) ? $setup[1] : [];
414
            } else {
415
                $name = $setup;
416
                $args = [];
417
            }
418
419
            if ('$' === $name[0]) {
420
                $service['properties'][substr($name, 1)] = $args;
421
            } else {
422
                $service['calls'][] = [$name, $args];
423
            }
424
        }
425
    }
426
427
    private function processCalls($id, array &$service, Definition $definition, $file)
428
    {
429
        if (!isset($service['calls'])) {
430
            return;
431
        }
432
433
        if (!is_array($service['calls'])) {
434
            throw new InvalidArgumentException(sprintf('Parameter "calls" must be an array for service "%s" in %s. Check your NEON syntax.', $id, $file));
435
        }
436
437
        foreach ($service['calls'] as $call) {
438
            if ($call instanceof Entity) { // nette
439
                $method = $call->value;
440
                $args = $this->resolveServices($call->attributes, $file);
441
            } elseif (isset($call['method'])) {
442
                $method = $call['method'];
443
                $args = isset($call['arguments']) ? $this->resolveServices($call['arguments'], $file) : [];
444
            } elseif (is_array($call)) {
445
                $method = $call[0];
446
                $args = isset($call[1]) ? $this->resolveServices($call[1], $file) : [];
447
            } else { // nette
448
                $method = $call;
449
                $args = [];
450
            }
451
452
            $definition->addMethodCall($method, $args);
453
        }
454
    }
455
456
    private function processTags($id, array &$service, Definition $definition, $file)
457
    {
458
        if (!isset($service['tags'])) {
459
            return;
460
        }
461
462
        if (!is_array($service['tags'])) {
463
            throw new InvalidArgumentException(sprintf('Parameter "tags" must be an array for service "%s" in %s. Check your NEON syntax.', $id, $file));
464
        }
465
466
        foreach ($service['tags'] as $tag) {
467
            if ($tag instanceof Entity) {
468
                $tag = ['name' => $tag->value] + $tag->attributes;
469
            }
470
471
            if (is_string($tag)) {
472
                $tag = ['name' => $tag];
473
            }
474
475
            if (!is_array($tag)) {
476
                throw new InvalidArgumentException(sprintf('A "tags" entry must be an array for service "%s" in %s. Check your NEON syntax.', $id, $file));
477
            }
478
479
            if (!isset($tag['name'])) {
480
                throw new InvalidArgumentException(sprintf('A "tags" entry is missing a "name" key for service "%s" in %s.', $id, $file));
481
            }
482
483
            if (!is_string($tag['name']) || '' === $tag['name']) {
484
                throw new InvalidArgumentException(sprintf('The tag name for service "%s" in %s must be a non-empty string.', $id, $file));
485
            }
486
487
            $name = $tag['name'];
488
            unset($tag['name']);
489
490
            foreach ($tag as $attribute => $value) {
491
                if (!is_scalar($value) && null !== $value) {
492
                    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));
493
                }
494
            }
495
496
            $definition->addTag($name, $tag);
497
        }
498
    }
499
500
    private function processAutowire($id, array &$service, Definition $definition, $file)
501
    {
502
        // nette
503 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...
504
            if (isset($service['autowire']) && $service['autowire'] !== $service['autowired']) {
505
                throw new InvalidArgumentException(sprintf('Contradictory definition of autowiring for service "%s" in "%s". Check you NEON syntax.', $id, $file));
506
            }
507
508
            $service['autowire'] = $service['autowired'];
509
        }
510
511
        if (isset($service['autowire'])) {
512
            // nette
513
            if ($definition->isAutowired() && !$service['autowire']) {
514
                throw new InvalidArgumentException(sprintf('Contradictory definition of autowiring for service "%s" in "%s". Check you NEON syntax.', $id, $file));
515
            }
516
517
            $definition->setAutowired($service['autowire']);
518
        }
519
    }
520
521
    private function processAutowiringTypes($id, array &$service, Definition $definition, $file)
522
    {
523
        if (!isset($service['autowiring_types'])) {
524
            return;
525
        }
526
527
        if (is_string($service['autowiring_types'])) {
528
            $definition->addAutowiringType($service['autowiring_types']);
529
            return;
530
        }
531
532
        if (!is_array($service['autowiring_types'])) {
533
            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));
534
        }
535
536
        foreach ($service['autowiring_types'] as $autowiringType) {
537
            if (!is_string($autowiringType)) {
538
                throw new InvalidArgumentException(sprintf('A "autowiring_types" attribute must be of type string for service "%s" in %s. Check your NEON syntax.', $id, $file));
539
            }
540
541
            $definition->addAutowiringType($autowiringType);
542
        }
543
    }
544
545
    private function parseFactory($factory, $file)
546
    {
547
        if (!is_string($factory)) {
548
            return [$this->resolveServices($factory[0], $file), $factory[1]];
549
        }
550
551
        if (false === strpos($factory, ':')) {
552
            return $factory;
553
        }
554
555
        $parts = explode(':', $factory, 2);
556
557
        if (':' === $parts[1][0]) {
558
            return ['@' === $parts[0][0] ? $this->resolveServices($parts[0], $file) : $parts[0], substr($parts[1], 1)];
559
        }
560
561
        return [$this->resolveServices(('@' === $parts[0][0] ?: '@').$parts[0], $file), $parts[1]];
562
    }
563
564
    private function resolveServices($value, $file)
565
    {
566
        // nette
567
        if ($value instanceof Entity) {
568
            if ('expression' === $value->value || 'expr' === $value->value) {
569
                return new Expression(reset($value->attributes));
570
            } elseif (0 === strpos($value->value, '@')) {
571
                $value = $value->value;
572
            } else {
573
                $id = $this->generateAnonymousServiceId($file);
574
                $this->parseDefinition($id, $value, $file);
575
                $value = new Reference($id);
576
            }
577
        }
578
579
        if (is_array($value)) {
580
            $value = array_map(function ($value) use ($file) {
581
                return $this->resolveServices($value, $file);
582
            }, $value);
583
        } elseif (is_string($value) &&  0 === strpos($value, '@=')) {
584
            return new Expression(substr($value, 2));
585
        } elseif (is_string($value) &&  0 === strpos($value, '@')) {
586
            if (0 === strpos($value, '@@')) {
587
                $value = substr($value, 1);
588
                $invalidBehavior = null;
589
            } elseif (0 === strpos($value, '@?')) {
590
                $value = substr($value, 2);
591
                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
592
            } else {
593
                $value = substr($value, 1);
594
                $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
595
            }
596
597
            if ('=' === substr($value, -1)) {
598
                $value = substr($value, 0, -1);
599
            }
600
601
            if (null !== $invalidBehavior) {
602
                $value = new Reference($value, $invalidBehavior);
603
            }
604
        }
605
606
        return $value;
607
    }
608
609
    private function loadFromExtensions($content)
610
    {
611
        foreach ($content as $namespace => $values) {
612
            // nette
613
            if (in_array($namespace, ['imports', 'includes', 'parameters', 'services'])) {
614
                continue;
615
            }
616
617
            if (!is_array($values)) {
618
                $values = [];
619
            }
620
621
            $this->container->loadFromExtension($namespace, $values);
622
        }
623
    }
624
625
    private function generateAnonymousServiceId($file)
626
    {
627
        return sprintf('%s_%d', hash('sha256', $file), ++$this->anonymousServicesCount);
628
    }
629
630
    private function checkDefinition($id, array $definition, $file)
631
    {
632
        if (!is_array($definition)) {
633
            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($definition), $id, $file));
634
        }
635
636
        foreach ($definition as $key => $value) {
637
            if (!isset(self::$keywords[$key])) {
638
                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)));
639
            }
640
        }
641
    }
642
}
643