YamlDumper::addServices()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 20
rs 9.6111
c 0
b 0
f 0
cc 5
nc 7
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\DependencyInjection\Dumper;
13
14
use Symfony\Component\DependencyInjection\Alias;
15
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
16
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
17
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
18
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
19
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
20
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
21
use Symfony\Component\DependencyInjection\ContainerInterface;
22
use Symfony\Component\DependencyInjection\Definition;
23
use Symfony\Component\DependencyInjection\Exception\LogicException;
24
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
25
use Symfony\Component\DependencyInjection\Parameter;
26
use Symfony\Component\DependencyInjection\Reference;
27
use Symfony\Component\ExpressionLanguage\Expression;
28
use Symfony\Component\Yaml\Dumper as YmlDumper;
29
use Symfony\Component\Yaml\Parser;
30
use Symfony\Component\Yaml\Tag\TaggedValue;
31
use Symfony\Component\Yaml\Yaml;
32
33
/**
34
 * YamlDumper dumps a service container as a YAML string.
35
 *
36
 * @author Fabien Potencier <[email protected]>
37
 */
38
class YamlDumper extends Dumper
39
{
40
    private YmlDumper $dumper;
41
42
    /**
43
     * Dumps the service container as an YAML string.
44
     */
45
    public function dump(array $options = []): string
46
    {
47
        if (!class_exists(YmlDumper::class)) {
48
            throw new LogicException('Unable to dump the container as the Symfony Yaml Component is not installed. Try running "composer require symfony/yaml".');
49
        }
50
51
        $this->dumper ??= new YmlDumper();
52
53
        return $this->container->resolveEnvPlaceholders($this->addParameters()."\n".$this->addServices());
54
    }
55
56
    private function addService(string $id, Definition $definition): string
57
    {
58
        $code = "    $id:\n";
59
        if ($class = $definition->getClass()) {
60
            if (str_starts_with($class, '\\')) {
61
                $class = substr($class, 1);
62
            }
63
64
            $code .= \sprintf("        class: %s\n", $this->dumper->dump($class));
65
        }
66
67
        if (!$definition->isPrivate()) {
68
            $code .= \sprintf("        public: %s\n", $definition->isPublic() ? 'true' : 'false');
69
        }
70
71
        $tagsCode = '';
72
        $tags = $definition->getTags();
73
        $tags['container.error'] = array_map(fn ($e) => ['message' => $e], $definition->getErrors());
74
        foreach ($tags as $name => $tags) {
75
            foreach ($tags as $attributes) {
76
                $att = [];
77
                foreach ($attributes as $key => $value) {
78
                    $att[] = \sprintf('%s: %s', $this->dumper->dump($key), $this->dumper->dump($value));
79
                }
80
                $att = $att ? ': { '.implode(', ', $att).' }' : '';
81
82
                $tagsCode .= \sprintf("            - %s%s\n", $this->dumper->dump($name), $att);
83
            }
84
        }
85
        if ($tagsCode) {
86
            $code .= "        tags:\n".$tagsCode;
87
        }
88
89
        if ($definition->getFile()) {
90
            $code .= \sprintf("        file: %s\n", $this->dumper->dump($definition->getFile()));
91
        }
92
93
        if ($definition->isSynthetic()) {
94
            $code .= "        synthetic: true\n";
95
        }
96
97
        if ($definition->isDeprecated()) {
98
            $code .= "        deprecated:\n";
99
            foreach ($definition->getDeprecation('%service_id%') as $key => $value) {
100
                if ('' !== $value) {
101
                    $code .= \sprintf("            %s: %s\n", $key, $this->dumper->dump($value));
102
                }
103
            }
104
        }
105
106
        if ($definition->isAutowired()) {
107
            $code .= "        autowire: true\n";
108
        }
109
110
        if ($definition->isAutoconfigured()) {
111
            $code .= "        autoconfigure: true\n";
112
        }
113
114
        if ($definition->isAbstract()) {
115
            $code .= "        abstract: true\n";
116
        }
117
118
        if ($definition->isLazy()) {
119
            $code .= "        lazy: true\n";
120
        }
121
122
        if ($definition->getArguments()) {
123
            $code .= \sprintf("        arguments: %s\n", $this->dumper->dump($this->dumpValue($definition->getArguments()), 0));
124
        }
125
126
        if ($definition->getProperties()) {
127
            $code .= \sprintf("        properties: %s\n", $this->dumper->dump($this->dumpValue($definition->getProperties()), 0));
128
        }
129
130
        if ($definition->getMethodCalls()) {
131
            $code .= \sprintf("        calls:\n%s\n", $this->dumper->dump($this->dumpValue($definition->getMethodCalls()), 1, 12));
132
        }
133
134
        if (!$definition->isShared()) {
135
            $code .= "        shared: false\n";
136
        }
137
138
        if (null !== $decoratedService = $definition->getDecoratedService()) {
139
            [$decorated, $renamedId, $priority] = $decoratedService;
140
            $code .= \sprintf("        decorates: %s\n", $decorated);
141
            if (null !== $renamedId) {
142
                $code .= \sprintf("        decoration_inner_name: %s\n", $renamedId);
143
            }
144
            if (0 !== $priority) {
145
                $code .= \sprintf("        decoration_priority: %s\n", $priority);
146
            }
147
148
            $decorationOnInvalid = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
149
            if (\in_array($decorationOnInvalid, [ContainerInterface::IGNORE_ON_INVALID_REFERENCE, ContainerInterface::NULL_ON_INVALID_REFERENCE])) {
150
                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE === $decorationOnInvalid ? 'null' : 'ignore';
151
                $code .= \sprintf("        decoration_on_invalid: %s\n", $invalidBehavior);
152
            }
153
        }
154
155
        if ($callable = $definition->getFactory()) {
156
            if (\is_array($callable) && ['Closure', 'fromCallable'] !== $callable && $definition->getClass() === $callable[0]) {
157
                $code .= \sprintf("        constructor: %s\n", $callable[1]);
158
            } else {
159
                $code .= \sprintf("        factory: %s\n", $this->dumper->dump($this->dumpCallable($callable), 0));
160
            }
161
        }
162
163
        if ($callable = $definition->getConfigurator()) {
164
            $code .= \sprintf("        configurator: %s\n", $this->dumper->dump($this->dumpCallable($callable), 0));
165
        }
166
167
        return $code;
168
    }
169
170
    private function addServiceAlias(string $alias, Alias $id): string
171
    {
172
        $deprecated = '';
173
174
        if ($id->isDeprecated()) {
175
            $deprecated = "        deprecated:\n";
176
177
            foreach ($id->getDeprecation('%alias_id%') as $key => $value) {
178
                if ('' !== $value) {
179
                    $deprecated .= \sprintf("            %s: %s\n", $key, $value);
180
                }
181
            }
182
        }
183
184
        if (!$id->isDeprecated() && $id->isPrivate()) {
185
            return \sprintf("    %s: '@%s'\n", $alias, $id);
186
        }
187
188
        if ($id->isPublic()) {
189
            $deprecated = "        public: true\n".$deprecated;
190
        }
191
192
        return \sprintf("    %s:\n        alias: %s\n%s", $alias, $id, $deprecated);
193
    }
194
195
    private function addServices(): string
196
    {
197
        if (!$this->container->getDefinitions()) {
198
            return '';
199
        }
200
201
        $code = "services:\n";
202
        foreach ($this->container->getDefinitions() as $id => $definition) {
203
            $code .= $this->addService($id, $definition);
204
        }
205
206
        $aliases = $this->container->getAliases();
207
        foreach ($aliases as $alias => $id) {
208
            while (isset($aliases[(string) $id])) {
209
                $id = $aliases[(string) $id];
210
            }
211
            $code .= $this->addServiceAlias($alias, $id);
212
        }
213
214
        return $code;
215
    }
216
217
    private function addParameters(): string
218
    {
219
        if (!$this->container->getParameterBag()->all()) {
220
            return '';
221
        }
222
223
        $parameters = $this->prepareParameters($this->container->getParameterBag()->all(), $this->container->isCompiled());
224
225
        return $this->dumper->dump(['parameters' => $parameters], 2);
226
    }
227
228
    /**
229
     * Dumps callable to YAML format.
230
     */
231
    private function dumpCallable(mixed $callable): mixed
232
    {
233
        if (\is_array($callable)) {
234
            if ($callable[0] instanceof Reference) {
235
                $callable = [$this->getServiceCall((string) $callable[0], $callable[0]), $callable[1]];
236
            } else {
237
                $callable = [$callable[0], $callable[1]];
238
            }
239
        }
240
241
        return $callable;
242
    }
243
244
    /**
245
     * Dumps the value to YAML format.
246
     *
247
     * @throws RuntimeException When trying to dump object or resource
248
     */
249
    private function dumpValue(mixed $value): mixed
250
    {
251
        if ($value instanceof ServiceClosureArgument) {
252
            $value = $value->getValues()[0];
253
254
            return new TaggedValue('service_closure', $this->dumpValue($value));
255
        }
256
        if ($value instanceof ArgumentInterface) {
257
            $tag = $value;
258
259
            if ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) {
260
                if (null === $tag->getIndexAttribute()) {
0 ignored issues
show
Bug introduced by
The method getIndexAttribute() does not exist on Symfony\Component\Depend...ument\ArgumentInterface. It seems like you code against a sub-type of Symfony\Component\Depend...ument\ArgumentInterface such as Symfony\Component\Depend...\TaggedIteratorArgument. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

260
                if (null === $tag->/** @scrutinizer ignore-call */ getIndexAttribute()) {
Loading history...
261
                    $content = $tag->getTag();
0 ignored issues
show
Bug introduced by
The method getTag() does not exist on Symfony\Component\Depend...ument\ArgumentInterface. It seems like you code against a sub-type of Symfony\Component\Depend...ument\ArgumentInterface such as Symfony\Component\Depend...\TaggedIteratorArgument. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

261
                    /** @scrutinizer ignore-call */ 
262
                    $content = $tag->getTag();
Loading history...
262
                } else {
263
                    $content = [
264
                        'tag' => $tag->getTag(),
265
                        'index_by' => $tag->getIndexAttribute(),
266
                    ];
267
268
                    if (null !== $tag->getDefaultIndexMethod()) {
0 ignored issues
show
Bug introduced by
The method getDefaultIndexMethod() does not exist on Symfony\Component\Depend...ument\ArgumentInterface. It seems like you code against a sub-type of Symfony\Component\Depend...ument\ArgumentInterface such as Symfony\Component\Depend...\TaggedIteratorArgument. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

268
                    if (null !== $tag->/** @scrutinizer ignore-call */ getDefaultIndexMethod()) {
Loading history...
269
                        $content['default_index_method'] = $tag->getDefaultIndexMethod();
270
                    }
271
                    if (null !== $tag->getDefaultPriorityMethod()) {
0 ignored issues
show
Bug introduced by
The method getDefaultPriorityMethod() does not exist on Symfony\Component\Depend...ument\ArgumentInterface. It seems like you code against a sub-type of Symfony\Component\Depend...ument\ArgumentInterface such as Symfony\Component\Depend...\TaggedIteratorArgument. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

271
                    if (null !== $tag->/** @scrutinizer ignore-call */ getDefaultPriorityMethod()) {
Loading history...
272
                        $content['default_priority_method'] = $tag->getDefaultPriorityMethod();
273
                    }
274
                }
275
                if ($excludes = $tag->getExclude()) {
0 ignored issues
show
Bug introduced by
The method getExclude() does not exist on Symfony\Component\Depend...ument\ArgumentInterface. It seems like you code against a sub-type of Symfony\Component\Depend...ument\ArgumentInterface such as Symfony\Component\Depend...\TaggedIteratorArgument. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

275
                if ($excludes = $tag->/** @scrutinizer ignore-call */ getExclude()) {
Loading history...
276
                    if (!\is_array($content)) {
277
                        $content = ['tag' => $content];
278
                    }
279
                    $content['exclude'] = 1 === \count($excludes) ? $excludes[0] : $excludes;
280
                }
281
                if (!$tag->excludeSelf()) {
0 ignored issues
show
Bug introduced by
The method excludeSelf() does not exist on Symfony\Component\Depend...ument\ArgumentInterface. It seems like you code against a sub-type of Symfony\Component\Depend...ument\ArgumentInterface such as Symfony\Component\Depend...\TaggedIteratorArgument. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

281
                if (!$tag->/** @scrutinizer ignore-call */ excludeSelf()) {
Loading history...
282
                    $content['exclude_self'] = false;
283
                }
284
285
                return new TaggedValue($value instanceof TaggedIteratorArgument ? 'tagged_iterator' : 'tagged_locator', $content);
286
            }
287
288
            if ($value instanceof IteratorArgument) {
289
                $tag = 'iterator';
290
            } elseif ($value instanceof ServiceLocatorArgument) {
291
                $tag = 'service_locator';
292
            } else {
293
                throw new RuntimeException(\sprintf('Unspecified Yaml tag for type "%s".', get_debug_type($value)));
294
            }
295
296
            return new TaggedValue($tag, $this->dumpValue($value->getValues()));
297
        }
298
299
        if (\is_array($value)) {
300
            $code = [];
301
            foreach ($value as $k => $v) {
302
                $code[$k] = $this->dumpValue($v);
303
            }
304
305
            return $code;
306
        } elseif ($value instanceof Reference) {
307
            return $this->getServiceCall((string) $value, $value);
308
        } elseif ($value instanceof Parameter) {
309
            return $this->getParameterCall((string) $value);
310
        } elseif ($value instanceof Expression) {
311
            return $this->getExpressionCall((string) $value);
312
        } elseif ($value instanceof Definition) {
313
            return new TaggedValue('service', (new Parser())->parse("_:\n".$this->addService('_', $value), Yaml::PARSE_CUSTOM_TAGS)['_']['_']);
314
        } elseif ($value instanceof \UnitEnum) {
0 ignored issues
show
Bug introduced by
The type UnitEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
315
            return new TaggedValue('php/enum', \sprintf('%s::%s', $value::class, $value->name));
316
        } elseif ($value instanceof AbstractArgument) {
317
            return new TaggedValue('abstract', $value->getText());
318
        } elseif (\is_object($value) || \is_resource($value)) {
319
            throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
320
        }
321
322
        return $value;
323
    }
324
325
    private function getServiceCall(string $id, ?Reference $reference = null): string
326
    {
327
        if (null !== $reference) {
328
            switch ($reference->getInvalidBehavior()) {
329
                case ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE: break;
330
                case ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE: break;
331
                case ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE: return \sprintf('@!%s', $id);
332
                default: return \sprintf('@?%s', $id);
333
            }
334
        }
335
336
        return \sprintf('@%s', $id);
337
    }
338
339
    private function getParameterCall(string $id): string
340
    {
341
        return \sprintf('%%%s%%', $id);
342
    }
343
344
    private function getExpressionCall(string $expression): string
345
    {
346
        return \sprintf('@=%s', $expression);
347
    }
348
349
    private function prepareParameters(array $parameters, bool $escape = true): array
350
    {
351
        $filtered = [];
352
        foreach ($parameters as $key => $value) {
353
            if (\is_array($value)) {
354
                $value = $this->prepareParameters($value, $escape);
355
            } elseif ($value instanceof Reference || \is_string($value) && str_starts_with($value, '@')) {
356
                $value = '@'.$value;
357
            }
358
359
            $filtered[$key] = $value;
360
        }
361
362
        return $escape ? $this->escape($filtered) : $filtered;
363
    }
364
365
    private function escape(array $arguments): array
366
    {
367
        $args = [];
368
        foreach ($arguments as $k => $v) {
369
            if (\is_array($v)) {
370
                $args[$k] = $this->escape($v);
371
            } elseif (\is_string($v)) {
372
                $args[$k] = str_replace('%', '%%', $v);
373
            } else {
374
                $args[$k] = $v;
375
            }
376
        }
377
378
        return $args;
379
    }
380
}
381