PhpDumper::addAliases()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 21
rs 9.5555
c 0
b 0
f 0
cc 5
nc 5
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 Composer\Autoload\ClassLoader;
15
use Symfony\Component\Config\Resource\FileResource;
16
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
17
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
18
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
19
use Symfony\Component\DependencyInjection\Argument\LazyClosure;
20
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
21
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
22
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
23
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
24
use Symfony\Component\DependencyInjection\Compiler\CheckCircularReferencesPass;
25
use Symfony\Component\DependencyInjection\Compiler\ServiceReferenceGraphNode;
26
use Symfony\Component\DependencyInjection\Container;
27
use Symfony\Component\DependencyInjection\ContainerBuilder;
28
use Symfony\Component\DependencyInjection\ContainerInterface;
29
use Symfony\Component\DependencyInjection\Definition;
30
use Symfony\Component\DependencyInjection\Exception\EnvParameterException;
31
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
32
use Symfony\Component\DependencyInjection\Exception\LogicException;
33
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
34
use Symfony\Component\DependencyInjection\ExpressionLanguage;
35
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Symfony\Component\Depend...\Dumper\DumperInterface. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
36
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\LazyServiceDumper;
37
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper;
38
use Symfony\Component\DependencyInjection\Loader\FileLoader;
39
use Symfony\Component\DependencyInjection\Parameter;
40
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
41
use Symfony\Component\DependencyInjection\Reference;
42
use Symfony\Component\DependencyInjection\ServiceLocator as BaseServiceLocator;
43
use Symfony\Component\DependencyInjection\TypedReference;
44
use Symfony\Component\DependencyInjection\Variable;
45
use Symfony\Component\ErrorHandler\DebugClassLoader;
46
use Symfony\Component\ExpressionLanguage\Expression;
47
48
/**
49
 * PhpDumper dumps a service container as a PHP class.
50
 *
51
 * @author Fabien Potencier <[email protected]>
52
 * @author Johannes M. Schmitt <[email protected]>
53
 */
54
class PhpDumper extends Dumper
55
{
56
    /**
57
     * Characters that might appear in the generated variable name as first character.
58
     */
59
    public const FIRST_CHARS = 'abcdefghijklmnopqrstuvwxyz';
60
61
    /**
62
     * Characters that might appear in the generated variable name as any but the first character.
63
     */
64
    public const NON_FIRST_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_';
65
66
    /** @var \SplObjectStorage<Definition, Variable>|null */
67
    private ?\SplObjectStorage $definitionVariables = null;
68
    private ?array $referenceVariables = null;
69
    private int $variableCount;
70
    private ?\SplObjectStorage $inlinedDefinitions = null;
71
    private ?array $serviceCalls = null;
72
    private array $reservedVariables = ['instance', 'class', 'this', 'container'];
73
    private ExpressionLanguage $expressionLanguage;
74
    private ?string $targetDirRegex = null;
75
    private int $targetDirMaxMatches;
76
    private string $docStar;
77
    private array $serviceIdToMethodNameMap;
78
    private array $usedMethodNames;
79
    private string $namespace;
80
    private bool $asFiles;
81
    private string $hotPathTag;
82
    private array $preloadTags;
83
    private bool $inlineFactories;
84
    private bool $inlineRequires;
85
    private array $inlinedRequires = [];
86
    private array $circularReferences = [];
87
    private array $singleUsePrivateIds = [];
88
    private array $preload = [];
89
    private bool $addGetService = false;
90
    private array $locatedIds = [];
91
    private string $serviceLocatorTag;
92
    private array $exportedVariables = [];
93
    private array $dynamicParameters = [];
94
    private string $baseClass;
95
    private string $class;
96
    private DumperInterface $proxyDumper;
97
    private bool $hasProxyDumper = true;
98
99
    public function __construct(ContainerBuilder $container)
100
    {
101
        if (!$container->isCompiled()) {
102
            throw new LogicException('Cannot dump an uncompiled container.');
103
        }
104
105
        parent::__construct($container);
106
    }
107
108
    /**
109
     * Sets the dumper to be used when dumping proxies in the generated container.
110
     */
111
    public function setProxyDumper(DumperInterface $proxyDumper): void
112
    {
113
        $this->proxyDumper = $proxyDumper;
114
        $this->hasProxyDumper = !$proxyDumper instanceof NullDumper;
115
    }
116
117
    /**
118
     * Dumps the service container as a PHP class.
119
     *
120
     * Available options:
121
     *
122
     *  * class:      The class name
123
     *  * base_class: The base class name
124
     *  * namespace:  The class namespace
125
     *  * as_files:   To split the container in several files
126
     *
127
     * @return string|array A PHP class representing the service container or an array of PHP files if the "as_files" option is set
128
     *
129
     * @throws EnvParameterException When an env var exists but has not been dumped
130
     */
131
    public function dump(array $options = []): string|array
132
    {
133
        $this->locatedIds = [];
134
        $this->targetDirRegex = null;
135
        $this->inlinedRequires = [];
136
        $this->exportedVariables = [];
137
        $this->dynamicParameters = [];
138
        $options = array_merge([
139
            'class' => 'ProjectServiceContainer',
140
            'base_class' => 'Container',
141
            'namespace' => '',
142
            'as_files' => false,
143
            'debug' => true,
144
            'hot_path_tag' => 'container.hot_path',
145
            'preload_tags' => ['container.preload', 'container.no_preload'],
146
            'inline_factories' => null,
147
            'inline_class_loader' => null,
148
            'preload_classes' => [],
149
            'service_locator_tag' => 'container.service_locator',
150
            'build_time' => time(),
151
        ], $options);
152
153
        $this->addGetService = false;
154
        $this->namespace = $options['namespace'];
155
        $this->asFiles = $options['as_files'];
156
        $this->hotPathTag = $options['hot_path_tag'];
157
        $this->preloadTags = $options['preload_tags'];
158
159
        $this->inlineFactories = false;
160
        if (isset($options['inline_factories'])) {
161
            $this->inlineFactories = $this->asFiles && $options['inline_factories'];
162
        }
163
164
        $this->inlineRequires = $options['debug'];
165
        if (isset($options['inline_class_loader'])) {
166
            $this->inlineRequires = $options['inline_class_loader'];
167
        }
168
169
        $this->serviceLocatorTag = $options['service_locator_tag'];
170
        $this->class = $options['class'];
171
172
        if (!str_starts_with($baseClass = $options['base_class'], '\\') && 'Container' !== $baseClass) {
173
            $baseClass = \sprintf('%s\%s', $options['namespace'] ? '\\'.$options['namespace'] : '', $baseClass);
174
            $this->baseClass = $baseClass;
175
        } elseif ('Container' === $baseClass) {
176
            $this->baseClass = Container::class;
177
        } else {
178
            $this->baseClass = $baseClass;
179
        }
180
181
        $this->initializeMethodNamesMap('Container' === $baseClass ? Container::class : $baseClass);
182
183
        if (!$this->hasProxyDumper) {
184
            (new AnalyzeServiceReferencesPass(true, false))->process($this->container);
185
            (new CheckCircularReferencesPass())->process($this->container);
186
        }
187
188
        $this->analyzeReferences();
189
        $this->docStar = $options['debug'] ? '*' : '';
190
191
        if (!empty($options['file']) && is_dir($dir = \dirname($options['file']))) {
192
            // Build a regexp where the first root dirs are mandatory,
193
            // but every other sub-dir is optional up to the full path in $dir
194
            // Mandate at least 1 root dir and not more than 5 optional dirs.
195
196
            $dir = explode(\DIRECTORY_SEPARATOR, realpath($dir));
197
            $i = \count($dir);
198
199
            if (2 + (int) ('\\' === \DIRECTORY_SEPARATOR) <= $i) {
200
                $regex = '';
201
                $lastOptionalDir = $i > 8 ? $i - 5 : (2 + (int) ('\\' === \DIRECTORY_SEPARATOR));
202
                $this->targetDirMaxMatches = $i - $lastOptionalDir;
203
204
                while (--$i >= $lastOptionalDir) {
205
                    $regex = \sprintf('(%s%s)?', preg_quote(\DIRECTORY_SEPARATOR.$dir[$i], '#'), $regex);
206
                }
207
208
                do {
209
                    $regex = preg_quote(\DIRECTORY_SEPARATOR.$dir[$i], '#').$regex;
210
                } while (0 < --$i);
211
212
                $this->targetDirRegex = '#(^|file://|[:;, \|\r\n])'.preg_quote($dir[0], '#').$regex.'#';
213
            }
214
        }
215
216
        $proxyClasses = $this->inlineFactories ? $this->generateProxyClasses() : null;
217
218
        if ($options['preload_classes']) {
219
            $this->preload = array_combine($options['preload_classes'], $options['preload_classes']);
220
        }
221
222
        $code = $this->addDefaultParametersMethod();
223
        $code =
224
            $this->startClass($options['class'], $baseClass, $this->inlineFactories && $proxyClasses).
225
            $this->addServices($services).
226
            $this->addDeprecatedAliases().
227
            $code
228
        ;
229
230
        $proxyClasses ??= $this->generateProxyClasses();
231
232
        if ($this->addGetService) {
233
            $code = preg_replace(
234
                "/\r?\n\r?\n    public function __construct.+?\\{\r?\n/s",
235
                "\n    protected \Closure \$getService;$0",
236
                $code,
237
                1
238
            );
239
        }
240
241
        if ($this->asFiles) {
242
            $fileTemplate = <<<EOF
243
<?php
244
245
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
246
use Symfony\Component\DependencyInjection\ContainerInterface;
247
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
248
249
/*{$this->docStar}
250
 * @internal This class has been auto-generated by the Symfony Dependency Injection Component.
251
 */
252
class %s extends {$options['class']}
253
{%s}
254
255
EOF;
256
            $files = [];
257
            $preloadedFiles = [];
258
            $ids = $this->container->getRemovedIds();
259
            foreach ($this->container->getDefinitions() as $id => $definition) {
260
                if (!$definition->isPublic() && '.' !== ($id[0] ?? '-')) {
261
                    $ids[$id] = true;
262
                }
263
            }
264
            if ($ids = array_keys($ids)) {
265
                sort($ids);
266
                $c = "<?php\n\nreturn [\n";
267
                foreach ($ids as $id) {
268
                    $c .= '    '.$this->doExport($id)." => true,\n";
269
                }
270
                $files['removed-ids.php'] = $c."];\n";
271
            }
272
273
            if (!$this->inlineFactories) {
274
                foreach ($this->generateServiceFiles($services) as $file => [$c, $preload]) {
275
                    $files[$file] = \sprintf($fileTemplate, substr($file, 0, -4), $c);
276
277
                    if ($preload) {
278
                        $preloadedFiles[$file] = $file;
279
                    }
280
                }
281
                foreach ($proxyClasses as $file => $c) {
282
                    $files[$file] = "<?php\n".$c;
283
                    $preloadedFiles[$file] = $file;
284
                }
285
            }
286
287
            $code .= $this->endClass();
288
289
            if ($this->inlineFactories && $proxyClasses) {
290
                $files['proxy-classes.php'] = "<?php\n\n";
291
292
                foreach ($proxyClasses as $c) {
293
                    $files['proxy-classes.php'] .= $c;
294
                }
295
            }
296
297
            $files[$options['class'].'.php'] = $code;
298
            $hash = ucfirst(strtr(ContainerBuilder::hash($files), '._', 'xx'));
299
            $code = [];
300
301
            foreach ($files as $file => $c) {
302
                $code["Container{$hash}/{$file}"] = substr_replace($c, "<?php\n\nnamespace Container{$hash};\n", 0, 6);
303
304
                if (isset($preloadedFiles[$file])) {
305
                    $preloadedFiles[$file] = "Container{$hash}/{$file}";
306
                }
307
            }
308
            $namespaceLine = $this->namespace ? "\nnamespace {$this->namespace};\n" : '';
309
            $time = $options['build_time'];
310
            $id = hash('crc32', $hash.$time);
311
            $this->asFiles = false;
312
313
            if ($this->preload && null !== $autoloadFile = $this->getAutoloadFile()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->preload of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
314
                $autoloadFile = trim($this->export($autoloadFile), '()\\');
315
316
                $preloadedFiles = array_reverse($preloadedFiles);
317
                if ('' !== $preloadedFiles = implode("';\nrequire __DIR__.'/", $preloadedFiles)) {
318
                    $preloadedFiles = "require __DIR__.'/$preloadedFiles';\n";
319
                }
320
321
                $code[$options['class'].'.preload.php'] = <<<EOF
322
<?php
323
324
// This file has been auto-generated by the Symfony Dependency Injection Component
325
// You can reference it in the "opcache.preload" php.ini setting on PHP >= 7.4 when preloading is desired
326
327
use Symfony\Component\DependencyInjection\Dumper\Preloader;
328
329
if (in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
330
    return;
331
}
332
333
require $autoloadFile;
334
(require __DIR__.'/{$options['class']}.php')->set(\\Container{$hash}\\{$options['class']}::class, null);
335
$preloadedFiles
336
\$classes = [];
337
338
EOF;
339
340
                foreach ($this->preload as $class) {
341
                    if (!$class || str_contains($class, '$') || \in_array($class, ['int', 'float', 'string', 'bool', 'resource', 'object', 'array', 'null', 'callable', 'iterable', 'mixed', 'void', 'never'], true)) {
342
                        continue;
343
                    }
344
                    if (!(class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) || (new \ReflectionClass($class))->isUserDefined()) {
345
                        $code[$options['class'].'.preload.php'] .= \sprintf("\$classes[] = '%s';\n", $class);
346
                    }
347
                }
348
349
                $code[$options['class'].'.preload.php'] .= <<<'EOF'
350
351
$preloaded = Preloader::preload($classes);
352
353
EOF;
354
            }
355
356
            $code[$options['class'].'.php'] = <<<EOF
357
<?php
358
{$namespaceLine}
359
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
360
361
if (\\class_exists(\\Container{$hash}\\{$options['class']}::class, false)) {
362
    // no-op
363
} elseif (!include __DIR__.'/Container{$hash}/{$options['class']}.php') {
364
    touch(__DIR__.'/Container{$hash}.legacy');
365
366
    return;
367
}
368
369
if (!\\class_exists({$options['class']}::class, false)) {
370
    \\class_alias(\\Container{$hash}\\{$options['class']}::class, {$options['class']}::class, false);
371
}
372
373
return new \\Container{$hash}\\{$options['class']}([
374
    'container.build_hash' => '$hash',
375
    'container.build_id' => '$id',
376
    'container.build_time' => $time,
377
    'container.runtime_mode' => \\in_array(\\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? 'web=0' : 'web=1',
378
], __DIR__.\\DIRECTORY_SEPARATOR.'Container{$hash}');
379
380
EOF;
381
        } else {
382
            $code .= $this->endClass();
383
            foreach ($proxyClasses as $c) {
384
                $code .= $c;
385
            }
386
        }
387
388
        $this->targetDirRegex = null;
389
        $this->inlinedRequires = [];
390
        $this->circularReferences = [];
391
        $this->locatedIds = [];
392
        $this->exportedVariables = [];
393
        $this->dynamicParameters = [];
394
        $this->preload = [];
395
396
        $unusedEnvs = [];
397
        foreach ($this->container->getEnvCounters() as $env => $use) {
398
            if (!$use) {
399
                $unusedEnvs[] = $env;
400
            }
401
        }
402
        if ($unusedEnvs) {
403
            throw new EnvParameterException($unusedEnvs, null, 'Environment variables "%s" are never used. Please, check your container\'s configuration.');
404
        }
405
406
        return $code;
407
    }
408
409
    /**
410
     * Retrieves the currently set proxy dumper or instantiates one.
411
     */
412
    private function getProxyDumper(): DumperInterface
413
    {
414
        return $this->proxyDumper ??= new LazyServiceDumper($this->class);
415
    }
416
417
    private function analyzeReferences(): void
418
    {
419
        (new AnalyzeServiceReferencesPass(false, $this->hasProxyDumper))->process($this->container);
420
        $checkedNodes = [];
421
        $this->circularReferences = [];
422
        $this->singleUsePrivateIds = [];
423
        foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) {
424
            if (!$node->getValue() instanceof Definition) {
425
                continue;
426
            }
427
428
            if ($this->isSingleUsePrivateNode($node)) {
429
                $this->singleUsePrivateIds[$id] = $id;
430
            }
431
432
            $this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes);
433
        }
434
435
        $this->container->getCompiler()->getServiceReferenceGraph()->clear();
436
        $this->singleUsePrivateIds = array_diff_key($this->singleUsePrivateIds, $this->circularReferences);
437
    }
438
439
    private function collectCircularReferences(string $sourceId, array $edges, array &$checkedNodes, array &$loops = [], array $path = [], bool $byConstructor = true): void
440
    {
441
        $path[$sourceId] = $byConstructor;
442
        $checkedNodes[$sourceId] = true;
443
        foreach ($edges as $edge) {
444
            $node = $edge->getDestNode();
445
            $id = $node->getId();
446
            if ($sourceId === $id || !$node->getValue() instanceof Definition || $edge->isWeak()) {
447
                continue;
448
            }
449
450
            if (isset($path[$id])) {
451
                $loop = null;
452
                $loopByConstructor = $edge->isReferencedByConstructor() && !$edge->isLazy();
453
                $pathInLoop = [$id, []];
454
                foreach ($path as $k => $pathByConstructor) {
455
                    if (null !== $loop) {
456
                        $loop[] = $k;
457
                        $pathInLoop[1][$k] = $pathByConstructor;
458
                        $loops[$k][] = &$pathInLoop;
459
                        $loopByConstructor = $loopByConstructor && $pathByConstructor;
460
                    } elseif ($k === $id) {
461
                        $loop = [];
462
                    }
463
                }
464
                $this->addCircularReferences($id, $loop, $loopByConstructor);
0 ignored issues
show
Bug introduced by
It seems like $loop can also be of type null; however, parameter $currentPath of Symfony\Component\Depend...addCircularReferences() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

464
                $this->addCircularReferences($id, /** @scrutinizer ignore-type */ $loop, $loopByConstructor);
Loading history...
465
            } elseif (!isset($checkedNodes[$id])) {
466
                $this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes, $loops, $path, $edge->isReferencedByConstructor() && !$edge->isLazy());
467
            } elseif (isset($loops[$id])) {
468
                // we already had detected loops for this edge
469
                // let's check if we have a common ancestor in one of the detected loops
470
                foreach ($loops[$id] as [$first, $loopPath]) {
471
                    if (!isset($path[$first])) {
472
                        continue;
473
                    }
474
                    // We have a common ancestor, let's fill the current path
475
                    $fillPath = null;
476
                    foreach ($loopPath as $k => $pathByConstructor) {
477
                        if (null !== $fillPath) {
478
                            $fillPath[$k] = $pathByConstructor;
479
                        } elseif ($k === $id) {
480
                            $fillPath = $path;
481
                            $fillPath[$k] = $pathByConstructor;
482
                        }
483
                    }
484
485
                    // we can now build the loop
486
                    $loop = null;
487
                    $loopByConstructor = $edge->isReferencedByConstructor() && !$edge->isLazy();
488
                    foreach ($fillPath as $k => $pathByConstructor) {
489
                        if (null !== $loop) {
490
                            $loop[] = $k;
491
                            $loopByConstructor = $loopByConstructor && $pathByConstructor;
492
                        } elseif ($k === $first) {
493
                            $loop = [];
494
                        }
495
                    }
496
                    $this->addCircularReferences($first, $loop, $loopByConstructor);
497
                    break;
498
                }
499
            }
500
        }
501
        unset($path[$sourceId]);
502
    }
503
504
    private function addCircularReferences(string $sourceId, array $currentPath, bool $byConstructor): void
505
    {
506
        $currentId = $sourceId;
507
        $currentPath = array_reverse($currentPath);
508
        $currentPath[] = $currentId;
509
        foreach ($currentPath as $parentId) {
510
            if (empty($this->circularReferences[$parentId][$currentId])) {
511
                $this->circularReferences[$parentId][$currentId] = $byConstructor;
512
            }
513
514
            $currentId = $parentId;
515
        }
516
    }
517
518
    private function collectLineage(string $class, array &$lineage): void
519
    {
520
        if (isset($lineage[$class])) {
521
            return;
522
        }
523
        if (!$r = $this->container->getReflectionClass($class, false)) {
524
            return;
525
        }
526
        if (is_a($class, $this->baseClass, true)) {
527
            return;
528
        }
529
        $file = $r->getFileName();
530
        if (str_ends_with($file, ') : eval()\'d code')) {
531
            $file = substr($file, 0, strrpos($file, '(', -17));
532
        }
533
        if (!$file || $this->doExport($file) === $exportedFile = $this->export($file)) {
534
            return;
535
        }
536
537
        $lineage[$class] = substr($exportedFile, 1, -1);
538
539
        if ($parent = $r->getParentClass()) {
540
            $this->collectLineage($parent->name, $lineage);
541
        }
542
543
        foreach ($r->getInterfaces() as $parent) {
544
            $this->collectLineage($parent->name, $lineage);
545
        }
546
547
        foreach ($r->getTraits() as $parent) {
548
            $this->collectLineage($parent->name, $lineage);
549
        }
550
551
        unset($lineage[$class]);
552
        $lineage[$class] = substr($exportedFile, 1, -1);
553
    }
554
555
    private function generateProxyClasses(): array
556
    {
557
        $proxyClasses = [];
558
        $alreadyGenerated = [];
559
        $definitions = $this->container->getDefinitions();
560
        $strip = '' === $this->docStar;
561
        $proxyDumper = $this->getProxyDumper();
562
        ksort($definitions);
563
        foreach ($definitions as $id => $definition) {
564
            if (!$definition = $this->isProxyCandidate($definition, $asGhostObject, $id)) {
565
                continue;
566
            }
567
            if (isset($alreadyGenerated[$asGhostObject][$class = $definition->getClass()])) {
568
                continue;
569
            }
570
            $alreadyGenerated[$asGhostObject][$class] = true;
571
572
            foreach (array_column($definition->getTag('proxy'), 'interface') ?: [$class] as $r) {
573
                if (!$r = $this->container->getReflectionClass($r)) {
574
                    continue;
575
                }
576
                do {
577
                    $file = $r->getFileName();
578
                    if (str_ends_with($file, ') : eval()\'d code')) {
579
                        $file = substr($file, 0, strrpos($file, '(', -17));
580
                    }
581
                    if (is_file($file)) {
582
                        $this->container->addResource(new FileResource($file));
583
                    }
584
                    $r = $r->getParentClass() ?: null;
585
                } while ($r?->isUserDefined());
586
            }
587
588
            if ("\n" === $proxyCode = "\n".$proxyDumper->getProxyCode($definition, $id)) {
589
                continue;
590
            }
591
592
            if ($this->inlineRequires) {
593
                $lineage = [];
594
                $this->collectLineage($class, $lineage);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type null; however, parameter $class of Symfony\Component\Depend...umper::collectLineage() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

594
                $this->collectLineage(/** @scrutinizer ignore-type */ $class, $lineage);
Loading history...
595
596
                $code = '';
597
                foreach (array_diff_key(array_flip($lineage), $this->inlinedRequires) as $file => $class) {
598
                    if ($this->inlineFactories) {
599
                        $this->inlinedRequires[$file] = true;
600
                    }
601
                    $code .= \sprintf("include_once %s;\n", $file);
602
                }
603
604
                $proxyCode = $code.$proxyCode;
605
            }
606
607
            if ($strip) {
608
                $proxyCode = "<?php\n".$proxyCode;
609
                $proxyCode = substr(self::stripComments($proxyCode), 5);
610
            }
611
612
            $proxyClass = $this->inlineRequires ? substr($proxyCode, \strlen($code)) : $proxyCode;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $code does not seem to be defined for all execution paths leading up to this point.
Loading history...
613
            $i = strpos($proxyClass, 'class');
614
            $proxyClass = substr($proxyClass, 6 + $i, strpos($proxyClass, ' ', 7 + $i) - $i - 6);
615
616
            if ($this->asFiles || $this->namespace) {
617
                $proxyCode .= "\nif (!\\class_exists('$proxyClass', false)) {\n    \\class_alias(__NAMESPACE__.'\\\\$proxyClass', '$proxyClass', false);\n}\n";
618
            }
619
620
            $proxyClasses[$proxyClass.'.php'] = $proxyCode;
621
        }
622
623
        return $proxyClasses;
624
    }
625
626
    private function addServiceInclude(string $cId, Definition $definition, bool $isProxyCandidate): string
627
    {
628
        $code = '';
629
630
        if ($this->inlineRequires && (!$this->isHotPath($definition) || $isProxyCandidate)) {
631
            $lineage = [];
632
            foreach ($this->inlinedDefinitions as $def) {
633
                if (!$def->isDeprecated()) {
634
                    foreach ($this->getClasses($def, $cId) as $class) {
635
                        $this->collectLineage($class, $lineage);
636
                    }
637
                }
638
            }
639
640
            foreach ($this->serviceCalls as $id => [$callCount, $behavior]) {
641
                if ('service_container' !== $id && $id !== $cId
642
                    && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $behavior
643
                    && $this->container->has($id)
644
                    && $this->isTrivialInstance($def = $this->container->findDefinition($id))
645
                ) {
646
                    foreach ($this->getClasses($def, $cId) as $class) {
647
                        $this->collectLineage($class, $lineage);
648
                    }
649
                }
650
            }
651
652
            foreach (array_diff_key(array_flip($lineage), $this->inlinedRequires) as $file => $class) {
653
                $code .= \sprintf("        include_once %s;\n", $file);
654
            }
655
        }
656
657
        foreach ($this->inlinedDefinitions as $def) {
658
            if ($file = $def->getFile()) {
659
                $file = $this->dumpValue($file);
660
                $file = '(' === $file[0] ? substr($file, 1, -1) : $file;
661
                $code .= \sprintf("        include_once %s;\n", $file);
662
            }
663
        }
664
665
        if ('' !== $code) {
666
            $code .= "\n";
667
        }
668
669
        return $code;
670
    }
671
672
    /**
673
     * @throws InvalidArgumentException
674
     * @throws RuntimeException
675
     */
676
    private function addServiceInstance(string $id, Definition $definition, bool $isSimpleInstance): string
677
    {
678
        $class = $this->dumpValue($definition->getClass());
679
680
        if (str_starts_with($class, "'") && !str_contains($class, '$') && !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
681
            throw new InvalidArgumentException(\sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id));
682
        }
683
684
        $asGhostObject = false;
685
        $isProxyCandidate = $this->isProxyCandidate($definition, $asGhostObject, $id);
686
        $instantiation = '';
687
688
        $lastWitherIndex = null;
689
        foreach ($definition->getMethodCalls() as $k => $call) {
690
            if ($call[2] ?? false) {
691
                $lastWitherIndex = $k;
692
            }
693
        }
694
695
        if (!$isProxyCandidate && $definition->isShared() && !isset($this->singleUsePrivateIds[$id]) && null === $lastWitherIndex) {
696
            $instantiation = \sprintf('$container->%s[%s] = %s', $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates', $this->doExport($id), $isSimpleInstance ? '' : '$instance');
697
        } elseif (!$isSimpleInstance) {
698
            $instantiation = '$instance';
699
        }
700
701
        $return = '';
702
        if ($isSimpleInstance) {
703
            $return = 'return ';
704
        } else {
705
            $instantiation .= ' = ';
706
        }
707
708
        return $this->addNewInstance($definition, '        '.$return.$instantiation, $id, $asGhostObject);
709
    }
710
711
    private function isTrivialInstance(Definition $definition): bool
712
    {
713
        if ($definition->hasErrors()) {
714
            return true;
715
        }
716
        if ($definition->isSynthetic() || $definition->getFile() || $definition->getMethodCalls() || $definition->getProperties() || $definition->getConfigurator()) {
717
            return false;
718
        }
719
        if ($definition->isDeprecated() || $definition->isLazy() || $definition->getFactory() || 3 < \count($definition->getArguments())) {
720
            return false;
721
        }
722
723
        foreach ($definition->getArguments() as $arg) {
724
            if (!$arg || $arg instanceof Parameter) {
725
                continue;
726
            }
727
            if (\is_array($arg) && 3 >= \count($arg)) {
728
                foreach ($arg as $k => $v) {
729
                    if ($this->dumpValue($k) !== $this->dumpValue($k, false)) {
730
                        return false;
731
                    }
732
                    if (!$v || $v instanceof Parameter) {
733
                        continue;
734
                    }
735
                    if ($v instanceof Reference && $this->container->has($id = (string) $v) && $this->container->findDefinition($id)->isSynthetic()) {
736
                        continue;
737
                    }
738
                    if (!\is_scalar($v) || $this->dumpValue($v) !== $this->dumpValue($v, false)) {
739
                        return false;
740
                    }
741
                }
742
            } elseif ($arg instanceof Reference && $this->container->has($id = (string) $arg) && $this->container->findDefinition($id)->isSynthetic()) {
743
                continue;
744
            } elseif (!\is_scalar($arg) || $this->dumpValue($arg) !== $this->dumpValue($arg, false)) {
745
                return false;
746
            }
747
        }
748
749
        return true;
750
    }
751
752
    private function addServiceMethodCalls(Definition $definition, string $variableName, ?string $sharedNonLazyId): string
753
    {
754
        $lastWitherIndex = null;
755
        foreach ($definition->getMethodCalls() as $k => $call) {
756
            if ($call[2] ?? false) {
757
                $lastWitherIndex = $k;
758
            }
759
        }
760
761
        $calls = '';
762
        foreach ($definition->getMethodCalls() as $k => $call) {
763
            $arguments = [];
764
            foreach ($call[1] as $i => $value) {
765
                $arguments[] = (\is_string($i) ? $i.': ' : '').$this->dumpValue($value);
766
            }
767
768
            $witherAssignation = '';
769
770
            if ($call[2] ?? false) {
771
                if (null !== $sharedNonLazyId && $lastWitherIndex === $k && 'instance' === $variableName) {
772
                    $witherAssignation = \sprintf('$container->%s[\'%s\'] = ', $definition->isPublic() ? 'services' : 'privates', $sharedNonLazyId);
773
                }
774
                $witherAssignation .= \sprintf('$%s = ', $variableName);
775
            }
776
777
            $calls .= $this->wrapServiceConditionals($call[1], \sprintf("        %s\$%s->%s(%s);\n", $witherAssignation, $variableName, $call[0], implode(', ', $arguments)));
778
        }
779
780
        return $calls;
781
    }
782
783
    private function addServiceProperties(Definition $definition, string $variableName = 'instance'): string
784
    {
785
        $code = '';
786
        foreach ($definition->getProperties() as $name => $value) {
787
            $code .= \sprintf("        \$%s->%s = %s;\n", $variableName, $name, $this->dumpValue($value));
788
        }
789
790
        return $code;
791
    }
792
793
    private function addServiceConfigurator(Definition $definition, string $variableName = 'instance'): string
794
    {
795
        if (!$callable = $definition->getConfigurator()) {
796
            return '';
797
        }
798
799
        if (\is_array($callable)) {
800
            if ($callable[0] instanceof Reference
801
                || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))
0 ignored issues
show
Bug introduced by
The method contains() does not exist on null. ( Ignorable by Annotation )

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

801
                || ($callable[0] instanceof Definition && $this->definitionVariables->/** @scrutinizer ignore-call */ contains($callable[0]))

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
802
            ) {
803
                return \sprintf("        %s->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName);
804
            }
805
806
            $class = $this->dumpValue($callable[0]);
807
            // If the class is a string we can optimize away
808
            if (str_starts_with($class, "'") && !str_contains($class, '$')) {
809
                return \sprintf("        %s::%s(\$%s);\n", $this->dumpLiteralClass($class), $callable[1], $variableName);
810
            }
811
812
            if (str_starts_with($class, 'new ')) {
813
                return \sprintf("        (%s)->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName);
814
            }
815
816
            return \sprintf("        [%s, '%s'](\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName);
817
        }
818
819
        return \sprintf("        %s(\$%s);\n", $callable, $variableName);
820
    }
821
822
    private function addService(string $id, Definition $definition): array
823
    {
824
        $this->definitionVariables = new \SplObjectStorage();
825
        $this->referenceVariables = [];
826
        $this->variableCount = 0;
827
        $this->referenceVariables[$id] = new Variable('instance');
828
829
        $return = [];
830
831
        if ($class = $definition->getClass()) {
832
            $class = $class instanceof Parameter ? '%'.$class.'%' : $this->container->resolveEnvPlaceholders($class);
0 ignored issues
show
introduced by
$class is never a sub-type of Symfony\Component\DependencyInjection\Parameter.
Loading history...
833
            $return[] = \sprintf(str_starts_with($class, '%') ? '@return object A %1$s instance' : '@return \%s', ltrim($class, '\\'));
834
        } elseif ($factory = $definition->getFactory()) {
835
            if (\is_string($factory) && !str_starts_with($factory, '@=')) {
836
                $return[] = \sprintf('@return object An instance returned by %s()', $factory);
837
            } elseif (\is_array($factory) && (\is_string($factory[0]) || $factory[0] instanceof Definition || $factory[0] instanceof Reference)) {
838
                $class = $factory[0] instanceof Definition ? $factory[0]->getClass() : (string) $factory[0];
839
                $class = $class instanceof Parameter ? '%'.$class.'%' : $this->container->resolveEnvPlaceholders($class);
840
                $return[] = \sprintf('@return object An instance returned by %s::%s()', $class, $factory[1]);
841
            }
842
        }
843
844
        if ($definition->isDeprecated()) {
845
            if ($return && str_starts_with($return[\count($return) - 1], '@return')) {
846
                $return[] = '';
847
            }
848
849
            $deprecation = $definition->getDeprecation($id);
850
            $return[] = \sprintf('@deprecated %s', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']);
851
        }
852
853
        $return = str_replace("\n     * \n", "\n     *\n", implode("\n     * ", $return));
854
        $return = $this->container->resolveEnvPlaceholders($return);
855
856
        $shared = $definition->isShared() ? ' shared' : '';
857
        $public = $definition->isPublic() ? 'public' : 'private';
858
        $autowired = $definition->isAutowired() ? ' autowired' : '';
859
        $asFile = $this->asFiles && !$this->inlineFactories && !$this->isHotPath($definition);
860
        $methodName = $this->generateMethodName($id);
861
862
        if ($asFile || $definition->isLazy()) {
863
            $lazyInitialization = ', $lazyLoad = true';
864
        } else {
865
            $lazyInitialization = '';
866
        }
867
868
        $code = <<<EOF
869
870
    /*{$this->docStar}
871
     * Gets the $public '$id'$shared$autowired service.
872
     *
873
     * $return
874
EOF;
875
        $code = str_replace('*/', ' ', $code).<<<EOF
876
877
     */
878
    protected static function {$methodName}(\$container$lazyInitialization)
879
    {
880
881
EOF;
882
883
        if ($asFile) {
884
            $file = $methodName.'.php';
885
            $code = str_replace("protected static function {$methodName}(", 'public static function do(', $code);
886
        } else {
887
            $file = null;
888
        }
889
890
        if ($definition->hasErrors() && $e = $definition->getErrors()) {
891
            $code .= \sprintf("        throw new RuntimeException(%s);\n", $this->export(reset($e)));
892
        } else {
893
            $this->serviceCalls = [];
894
            $this->inlinedDefinitions = $this->getDefinitionsFromArguments([$definition], null, $this->serviceCalls);
0 ignored issues
show
Bug introduced by
It seems like $this->serviceCalls can also be of type null; however, parameter $calls of Symfony\Component\Depend...initionsFromArguments() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

894
            $this->inlinedDefinitions = $this->getDefinitionsFromArguments([$definition], null, /** @scrutinizer ignore-type */ $this->serviceCalls);
Loading history...
895
896
            if ($definition->isDeprecated()) {
897
                $deprecation = $definition->getDeprecation($id);
898
                $code .= \sprintf("        trigger_deprecation(%s, %s, %s);\n\n", $this->export($deprecation['package']), $this->export($deprecation['version']), $this->export($deprecation['message']));
899
            } elseif ($definition->hasTag($this->hotPathTag) || !$definition->hasTag($this->preloadTags[1])) {
900
                foreach ($this->inlinedDefinitions as $def) {
901
                    foreach ($this->getClasses($def, $id) as $class) {
902
                        $this->preload[$class] = $class;
903
                    }
904
                }
905
            }
906
907
            if (!$definition->isShared()) {
908
                $factory = \sprintf('$container->factories%s[%s]', $definition->isPublic() ? '' : "['service_container']", $this->doExport($id));
909
            }
910
911
            $asGhostObject = false;
912
            if ($isProxyCandidate = $this->isProxyCandidate($definition, $asGhostObject, $id)) {
913
                $definition = $isProxyCandidate;
914
915
                if (!$definition->isShared()) {
916
                    $code .= \sprintf('        %s ??= ', $factory);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $factory does not seem to be defined for all execution paths leading up to this point.
Loading history...
917
918
                    if ($definition->isPublic()) {
919
                        $code .= \sprintf("fn () => self::%s(\$container);\n\n", $asFile ? 'do' : $methodName);
920
                    } else {
921
                        $code .= \sprintf("self::%s(...);\n\n", $asFile ? 'do' : $methodName);
922
                    }
923
                }
924
                $lazyLoad = $asGhostObject ? '$proxy' : 'false';
925
926
                $factoryCode = $asFile ? \sprintf('self::do($container, %s)', $lazyLoad) : \sprintf('self::%s($container, %s)', $methodName, $lazyLoad);
927
                $code .= $this->getProxyDumper()->getProxyFactoryCode($definition, $id, $factoryCode);
928
            }
929
930
            $c = $this->addServiceInclude($id, $definition, null !== $isProxyCandidate);
931
932
            if ('' !== $c && $isProxyCandidate && !$definition->isShared()) {
933
                $c = implode("\n", array_map(fn ($line) => $line ? '    '.$line : $line, explode("\n", $c)));
934
                $code .= "        static \$include = true;\n\n";
935
                $code .= "        if (\$include) {\n";
936
                $code .= $c;
937
                $code .= "            \$include = false;\n";
938
                $code .= "        }\n\n";
939
            } else {
940
                $code .= $c;
941
            }
942
943
            $c = $this->addInlineService($id, $definition);
944
945
            if (!$isProxyCandidate && !$definition->isShared()) {
946
                $c = implode("\n", array_map(fn ($line) => $line ? '    '.$line : $line, explode("\n", $c)));
947
                $lazyloadInitialization = $definition->isLazy() ? ', $lazyLoad = true' : '';
948
949
                $c = \sprintf("        %s = function (\$container%s) {\n%s        };\n\n        return %1\$s(\$container);\n", $factory, $lazyloadInitialization, $c);
950
            }
951
952
            $code .= $c;
953
        }
954
955
        $code .= "    }\n";
956
957
        $this->definitionVariables = $this->inlinedDefinitions = null;
958
        $this->referenceVariables = $this->serviceCalls = null;
959
960
        return [$file, $code];
961
    }
962
963
    private function addInlineVariables(string $id, Definition $definition, array $arguments, bool $forConstructor): string
964
    {
965
        $code = '';
966
967
        foreach ($arguments as $argument) {
968
            if (\is_array($argument)) {
969
                $code .= $this->addInlineVariables($id, $definition, $argument, $forConstructor);
970
            } elseif ($argument instanceof Reference) {
971
                $code .= $this->addInlineReference($id, $definition, $argument, $forConstructor);
972
            } elseif ($argument instanceof Definition) {
973
                $code .= $this->addInlineService($id, $definition, $argument, $forConstructor);
974
            }
975
        }
976
977
        return $code;
978
    }
979
980
    private function addInlineReference(string $id, Definition $definition, string $targetId, bool $forConstructor): string
981
    {
982
        while ($this->container->hasAlias($targetId)) {
983
            $targetId = (string) $this->container->getAlias($targetId);
984
        }
985
986
        [$callCount, $behavior] = $this->serviceCalls[$targetId];
987
988
        if ($id === $targetId) {
989
            return $this->addInlineService($id, $definition, $definition);
990
        }
991
992
        if ('service_container' === $targetId || isset($this->referenceVariables[$targetId])) {
993
            return '';
994
        }
995
996
        if ($this->container->hasDefinition($targetId) && ($def = $this->container->getDefinition($targetId)) && !$def->isShared()) {
997
            return '';
998
        }
999
1000
        $hasSelfRef = isset($this->circularReferences[$id][$targetId]) && !isset($this->definitionVariables[$definition]) && !($this->hasProxyDumper && $definition->isLazy());
1001
1002
        if ($hasSelfRef && !$forConstructor && !$forConstructor = !$this->circularReferences[$id][$targetId]) {
1003
            $code = $this->addInlineService($id, $definition, $definition);
1004
        } else {
1005
            $code = '';
1006
        }
1007
1008
        if (isset($this->referenceVariables[$targetId]) || (2 > $callCount && (!$hasSelfRef || !$forConstructor))) {
1009
            return $code;
1010
        }
1011
1012
        $name = $this->getNextVariableName();
1013
        $this->referenceVariables[$targetId] = new Variable($name);
1014
1015
        $reference = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $behavior ? new Reference($targetId, $behavior) : null;
1016
        $code .= \sprintf("        \$%s = %s;\n", $name, $this->getServiceCall($targetId, $reference));
1017
1018
        if (!$hasSelfRef || !$forConstructor) {
1019
            return $code;
1020
        }
1021
1022
        return $code.\sprintf(<<<'EOTXT'
1023
1024
        if (isset($container->%s[%s])) {
1025
            return $container->%1$s[%2$s];
1026
        }
1027
1028
EOTXT
1029
            ,
1030
            $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates',
1031
            $this->doExport($id)
1032
        );
1033
    }
1034
1035
    private function addInlineService(string $id, Definition $definition, ?Definition $inlineDef = null, bool $forConstructor = true): string
1036
    {
1037
        $code = '';
1038
1039
        if ($isSimpleInstance = $isRootInstance = null === $inlineDef) {
1040
            foreach ($this->serviceCalls as $targetId => [$callCount, $behavior, $byConstructor]) {
1041
                if ($byConstructor && isset($this->circularReferences[$id][$targetId]) && !$this->circularReferences[$id][$targetId] && !($this->hasProxyDumper && $definition->isLazy())) {
1042
                    $code .= $this->addInlineReference($id, $definition, $targetId, $forConstructor);
1043
                }
1044
            }
1045
        }
1046
1047
        if (isset($this->definitionVariables[$inlineDef ??= $definition])) {
1048
            return $code;
1049
        }
1050
1051
        $arguments = [$inlineDef->getArguments(), $inlineDef->getFactory()];
1052
1053
        $code .= $this->addInlineVariables($id, $definition, $arguments, $forConstructor);
1054
1055
        if ($arguments = array_filter([$inlineDef->getProperties(), $inlineDef->getMethodCalls(), $inlineDef->getConfigurator()])) {
1056
            $isSimpleInstance = false;
1057
        } elseif ($definition !== $inlineDef && 2 > $this->inlinedDefinitions[$inlineDef]) {
1058
            return $code;
1059
        }
1060
1061
        $asGhostObject = false;
1062
        $isProxyCandidate = $this->isProxyCandidate($inlineDef, $asGhostObject, $id);
1063
1064
        if (isset($this->definitionVariables[$inlineDef])) {
1065
            $isSimpleInstance = false;
1066
        } else {
1067
            $name = $definition === $inlineDef ? 'instance' : $this->getNextVariableName();
1068
            $this->definitionVariables[$inlineDef] = new Variable($name);
1069
            $code .= '' !== $code ? "\n" : '';
1070
1071
            if ('instance' === $name) {
1072
                $code .= $this->addServiceInstance($id, $definition, $isSimpleInstance);
1073
            } else {
1074
                $code .= $this->addNewInstance($inlineDef, '        $'.$name.' = ', $id);
1075
            }
1076
1077
            if ('' !== $inline = $this->addInlineVariables($id, $definition, $arguments, false)) {
1078
                $code .= "\n".$inline."\n";
1079
            } elseif ($arguments && 'instance' === $name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $arguments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1080
                $code .= "\n";
1081
            }
1082
1083
            $code .= $this->addServiceProperties($inlineDef, $name);
1084
            $code .= $this->addServiceMethodCalls($inlineDef, $name, !$isProxyCandidate && $inlineDef->isShared() && !isset($this->singleUsePrivateIds[$id]) ? $id : null);
1085
            $code .= $this->addServiceConfigurator($inlineDef, $name);
1086
        }
1087
1088
        if (!$isRootInstance || $isSimpleInstance) {
1089
            return $code;
1090
        }
1091
1092
        return $code."\n        return \$instance;\n";
1093
    }
1094
1095
    private function addServices(?array &$services = null): string
1096
    {
1097
        $publicServices = $privateServices = '';
1098
        $definitions = $this->container->getDefinitions();
1099
        ksort($definitions);
1100
        foreach ($definitions as $id => $definition) {
1101
            if (!$definition->isSynthetic()) {
1102
                $services[$id] = $this->addService($id, $definition);
1103
            } elseif ($definition->hasTag($this->hotPathTag) || !$definition->hasTag($this->preloadTags[1])) {
1104
                $services[$id] = null;
1105
1106
                foreach ($this->getClasses($definition, $id) as $class) {
1107
                    $this->preload[$class] = $class;
1108
                }
1109
            }
1110
        }
1111
1112
        foreach ($definitions as $id => $definition) {
1113
            if (!([$file, $code] = $services[$id]) || null !== $file) {
1114
                continue;
1115
            }
1116
            if ($definition->isPublic()) {
1117
                $publicServices .= $code;
1118
            } elseif (!$this->isTrivialInstance($definition) || isset($this->locatedIds[$id])) {
1119
                $privateServices .= $code;
1120
            }
1121
        }
1122
1123
        return $publicServices.$privateServices;
1124
    }
1125
1126
    private function generateServiceFiles(array $services): iterable
1127
    {
1128
        $definitions = $this->container->getDefinitions();
1129
        ksort($definitions);
1130
        foreach ($definitions as $id => $definition) {
1131
            if (([$file, $code] = $services[$id]) && null !== $file && ($definition->isPublic() || !$this->isTrivialInstance($definition) || isset($this->locatedIds[$id]))) {
1132
                yield $file => [$code, $definition->hasTag($this->hotPathTag) || !$definition->hasTag($this->preloadTags[1]) && !$definition->isDeprecated() && !$definition->hasErrors()];
1133
            }
1134
        }
1135
    }
1136
1137
    private function addNewInstance(Definition $definition, string $return = '', ?string $id = null, bool $asGhostObject = false): string
1138
    {
1139
        $tail = $return ? str_repeat(')', substr_count($return, '(') - substr_count($return, ')')).";\n" : '';
1140
1141
        $arguments = [];
1142
        if (BaseServiceLocator::class === $definition->getClass() && $definition->hasTag($this->serviceLocatorTag)) {
1143
            foreach ($definition->getArgument(0) as $k => $argument) {
1144
                $arguments[$k] = $argument->getValues()[0];
1145
            }
1146
1147
            return $return.$this->dumpValue(new ServiceLocatorArgument($arguments)).$tail;
1148
        }
1149
1150
        foreach ($definition->getArguments() as $i => $value) {
1151
            $arguments[] = (\is_string($i) ? $i.': ' : '').$this->dumpValue($value);
1152
        }
1153
1154
        if ($callable = $definition->getFactory()) {
1155
            if ('current' === $callable && [0] === array_keys($definition->getArguments()) && \is_array($value) && [0] === array_keys($value)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value seems to be defined by a foreach iteration on line 1150. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1156
                return $return.$this->dumpValue($value[0]).$tail;
1157
            }
1158
1159
            if (['Closure', 'fromCallable'] === $callable) {
1160
                $callable = $definition->getArgument(0);
1161
                if ($callable instanceof ServiceClosureArgument) {
1162
                    return $return.$this->dumpValue($callable).$tail;
1163
                }
1164
1165
                $arguments = ['...'];
1166
1167
                if ($callable instanceof Reference || $callable instanceof Definition) {
1168
                    $callable = [$callable, '__invoke'];
1169
                }
1170
            }
1171
1172
            if (\is_string($callable) && str_starts_with($callable, '@=')) {
1173
                return $return.\sprintf('(($args = %s) ? (%s) : null)',
1174
                    $this->dumpValue(new ServiceLocatorArgument($definition->getArguments())),
1175
                    $this->getExpressionLanguage()->compile(substr($callable, 2), ['container' => 'container', 'args' => 'args'])
1176
                ).$tail;
1177
            }
1178
1179
            if (!\is_array($callable)) {
1180
                return $return.\sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail;
1181
            }
1182
1183
            if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) {
1184
                throw new RuntimeException(\sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
1185
            }
1186
1187
            if (['...'] === $arguments && ($definition->isLazy() || 'Closure' !== ($definition->getClass() ?? 'Closure')) && (
1188
                $callable[0] instanceof Reference
1189
                || ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
1190
            )) {
1191
                $initializer = 'fn () => '.$this->dumpValue($callable[0]);
1192
1193
                return $return.LazyClosure::getCode($initializer, $callable, $definition, $this->container, $id).$tail;
1194
            }
1195
1196
            if ($callable[0] instanceof Reference
1197
                || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))
1198
            ) {
1199
                return $return.\sprintf('%s->%s(%s)', $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1200
            }
1201
1202
            $class = $this->dumpValue($callable[0]);
1203
            // If the class is a string we can optimize away
1204
            if (str_starts_with($class, "'") && !str_contains($class, '$')) {
1205
                if ("''" === $class) {
1206
                    throw new RuntimeException(\sprintf('Cannot dump definition: "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id ? 'The "'.$id.'"' : 'inline'));
1207
                }
1208
1209
                return $return.\sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1210
            }
1211
1212
            if (str_starts_with($class, 'new ')) {
1213
                return $return.\sprintf('(%s)->%s(%s)', $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1214
            }
1215
1216
            return $return.\sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1217
        }
1218
1219
        if (null === $class = $definition->getClass()) {
1220
            throw new RuntimeException('Cannot dump definitions which have no class nor factory.');
1221
        }
1222
1223
        if (!$asGhostObject) {
1224
            return $return.\sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments)).$tail;
1225
        }
1226
1227
        if (!method_exists($this->container->getParameterBag()->resolveValue($class), '__construct')) {
1228
            return $return.'$lazyLoad'.$tail;
1229
        }
1230
1231
        return $return.\sprintf('($lazyLoad->__construct(%s) && false ?: $lazyLoad)', implode(', ', $arguments)).$tail;
1232
    }
1233
1234
    private function startClass(string $class, string $baseClass, bool $hasProxyClasses): string
1235
    {
1236
        $namespaceLine = !$this->asFiles && $this->namespace ? "\nnamespace {$this->namespace};\n" : '';
1237
1238
        $code = <<<EOF
1239
<?php
1240
$namespaceLine
1241
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
1242
use Symfony\Component\DependencyInjection\ContainerInterface;
1243
use Symfony\Component\DependencyInjection\Container;
1244
use Symfony\Component\DependencyInjection\Exception\LogicException;
1245
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
1246
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
1247
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
1248
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
1249
1250
/*{$this->docStar}
1251
 * @internal This class has been auto-generated by the Symfony Dependency Injection Component.
1252
 */
1253
class $class extends $baseClass
1254
{
1255
    private const DEPRECATED_PARAMETERS = [];
1256
1257
    private const NONEMPTY_PARAMETERS = [];
1258
1259
    protected \$parameters = [];
1260
1261
    public function __construct()
1262
    {
1263
1264
EOF;
1265
        $code = str_replace("    private const DEPRECATED_PARAMETERS = [];\n\n", $this->addDeprecatedParameters(), $code);
1266
        $code = str_replace("    private const NONEMPTY_PARAMETERS = [];\n\n", $this->addNonEmptyParameters(), $code);
1267
1268
        if ($this->asFiles) {
1269
            $code = str_replace('__construct()', '__construct(private array $buildParameters = [], protected string $containerDir = __DIR__)', $code);
1270
1271
            if (null !== $this->targetDirRegex) {
1272
                $code = str_replace('$parameters = []', "\$targetDir;\n    protected \$parameters = []", $code);
1273
                $code .= '        $this->targetDir = \\dirname($containerDir);'."\n";
1274
            }
1275
        }
1276
1277
        if (Container::class !== $this->baseClass) {
1278
            $r = $this->container->getReflectionClass($this->baseClass, false);
1279
            if (null !== $r
1280
                && (null !== $constructor = $r->getConstructor())
1281
                && 0 === $constructor->getNumberOfRequiredParameters()
1282
                && Container::class !== $constructor->getDeclaringClass()->name
1283
            ) {
1284
                $code .= "        parent::__construct();\n";
1285
                $code .= "        \$this->parameterBag = null;\n\n";
1286
            }
1287
        }
1288
1289
        if ($this->container->getParameterBag()->all()) {
1290
            $code .= "        \$this->parameters = \$this->getDefaultParameters();\n\n";
1291
        }
1292
        $code .= "        \$this->services = \$this->privates = [];\n";
1293
1294
        $code .= $this->addSyntheticIds();
1295
        $code .= $this->addMethodMap();
1296
        $code .= $this->asFiles && !$this->inlineFactories ? $this->addFileMap() : '';
1297
        $code .= $this->addAliases();
1298
        $code .= $this->addInlineRequires($hasProxyClasses);
1299
        $code .= <<<EOF
1300
    }
1301
1302
    public function compile(): void
1303
    {
1304
        throw new LogicException('You cannot compile a dumped container that was already compiled.');
1305
    }
1306
1307
    public function isCompiled(): bool
1308
    {
1309
        return true;
1310
    }
1311
1312
EOF;
1313
        $code .= $this->addRemovedIds();
1314
1315
        if ($this->asFiles && !$this->inlineFactories) {
1316
            $code .= <<<'EOF'
1317
1318
    protected function load($file, $lazyLoad = true): mixed
1319
    {
1320
        if (class_exists($class = __NAMESPACE__.'\\'.$file, false)) {
1321
            return $class::do($this, $lazyLoad);
1322
        }
1323
1324
        if ('.' === $file[-4]) {
1325
            $class = substr($class, 0, -4);
1326
        } else {
1327
            $file .= '.php';
1328
        }
1329
1330
        $service = require $this->containerDir.\DIRECTORY_SEPARATOR.$file;
1331
1332
        return class_exists($class, false) ? $class::do($this, $lazyLoad) : $service;
1333
    }
1334
1335
EOF;
1336
        }
1337
1338
        foreach ($this->container->getDefinitions() as $definition) {
1339
            if (!$definition->isLazy() || !$this->hasProxyDumper) {
1340
                continue;
1341
            }
1342
1343
            if ($this->asFiles && !$this->inlineFactories) {
1344
                $proxyLoader = "class_exists(\$class, false) || require __DIR__.'/'.\$class.'.php';\n\n        ";
1345
            } else {
1346
                $proxyLoader = '';
1347
            }
1348
1349
            $code .= <<<EOF
1350
1351
    protected function createProxy(\$class, \Closure \$factory)
1352
    {
1353
        {$proxyLoader}return \$factory();
1354
    }
1355
1356
EOF;
1357
            break;
1358
        }
1359
1360
        return $code;
1361
    }
1362
1363
    private function addSyntheticIds(): string
1364
    {
1365
        $code = '';
1366
        $definitions = $this->container->getDefinitions();
1367
        ksort($definitions);
1368
        foreach ($definitions as $id => $definition) {
1369
            if ($definition->isSynthetic() && 'service_container' !== $id) {
1370
                $code .= '            '.$this->doExport($id)." => true,\n";
1371
            }
1372
        }
1373
1374
        return $code ? "        \$this->syntheticIds = [\n{$code}        ];\n" : '';
1375
    }
1376
1377
    private function addRemovedIds(): string
1378
    {
1379
        $ids = $this->container->getRemovedIds();
1380
        foreach ($this->container->getDefinitions() as $id => $definition) {
1381
            if (!$definition->isPublic() && '.' !== ($id[0] ?? '-')) {
1382
                $ids[$id] = true;
1383
            }
1384
        }
1385
        if (!$ids) {
1386
            return '';
1387
        }
1388
        if ($this->asFiles) {
1389
            $code = "require \$this->containerDir.\\DIRECTORY_SEPARATOR.'removed-ids.php'";
1390
        } else {
1391
            $code = '';
1392
            $ids = array_keys($ids);
1393
            sort($ids);
1394
            foreach ($ids as $id) {
1395
                if (preg_match(FileLoader::ANONYMOUS_ID_REGEXP, $id)) {
1396
                    continue;
1397
                }
1398
                $code .= '            '.$this->doExport($id)." => true,\n";
1399
            }
1400
1401
            $code = "[\n{$code}        ]";
1402
        }
1403
1404
        return <<<EOF
1405
1406
    public function getRemovedIds(): array
1407
    {
1408
        return {$code};
1409
    }
1410
1411
EOF;
1412
    }
1413
1414
    private function addDeprecatedParameters(): string
1415
    {
1416
        if (!($bag = $this->container->getParameterBag()) instanceof ParameterBag) {
1417
            return '';
1418
        }
1419
1420
        if (!$deprecated = $bag->allDeprecated()) {
1421
            return '';
1422
        }
1423
        $code = '';
1424
        ksort($deprecated);
1425
        foreach ($deprecated as $param => $deprecation) {
1426
            $code .= '        '.$this->doExport($param).' => ['.implode(', ', array_map($this->doExport(...), $deprecation))."],\n";
1427
        }
1428
1429
        return "    private const DEPRECATED_PARAMETERS = [\n{$code}    ];\n\n";
1430
    }
1431
1432
    private function addNonEmptyParameters(): string
1433
    {
1434
        if (!($bag = $this->container->getParameterBag()) instanceof ParameterBag) {
1435
            return '';
1436
        }
1437
1438
        if (!$nonEmpty = $bag->allNonEmpty()) {
1439
            return '';
1440
        }
1441
        $code = '';
1442
        ksort($nonEmpty);
1443
        foreach ($nonEmpty as $param => $message) {
1444
            $code .= '        '.$this->doExport($param).' => '.$this->doExport($message).",\n";
1445
        }
1446
1447
        return "    private const NONEMPTY_PARAMETERS = [\n{$code}    ];\n\n";
1448
    }
1449
1450
    private function addMethodMap(): string
1451
    {
1452
        $code = '';
1453
        $definitions = $this->container->getDefinitions();
1454
        ksort($definitions);
1455
        foreach ($definitions as $id => $definition) {
1456
            if (!$definition->isSynthetic() && $definition->isPublic() && (!$this->asFiles || $this->inlineFactories || $this->isHotPath($definition))) {
1457
                $code .= '            '.$this->doExport($id).' => '.$this->doExport($this->generateMethodName($id)).",\n";
1458
            }
1459
        }
1460
1461
        $aliases = $this->container->getAliases();
1462
        foreach ($aliases as $alias => $id) {
1463
            if (!$id->isDeprecated()) {
1464
                continue;
1465
            }
1466
            $code .= '            '.$this->doExport($alias).' => '.$this->doExport($this->generateMethodName($alias)).",\n";
1467
        }
1468
1469
        return $code ? "        \$this->methodMap = [\n{$code}        ];\n" : '';
1470
    }
1471
1472
    private function addFileMap(): string
1473
    {
1474
        $code = '';
1475
        $definitions = $this->container->getDefinitions();
1476
        ksort($definitions);
1477
        foreach ($definitions as $id => $definition) {
1478
            if (!$definition->isSynthetic() && $definition->isPublic() && !$this->isHotPath($definition)) {
1479
                $code .= \sprintf("            %s => '%s',\n", $this->doExport($id), $this->generateMethodName($id));
1480
            }
1481
        }
1482
1483
        return $code ? "        \$this->fileMap = [\n{$code}        ];\n" : '';
1484
    }
1485
1486
    private function addAliases(): string
1487
    {
1488
        if (!$aliases = $this->container->getAliases()) {
1489
            return "\n        \$this->aliases = [];\n";
1490
        }
1491
1492
        $code = "        \$this->aliases = [\n";
1493
        ksort($aliases);
1494
        foreach ($aliases as $alias => $id) {
1495
            if ($id->isDeprecated()) {
1496
                continue;
1497
            }
1498
1499
            $id = (string) $id;
1500
            while (isset($aliases[$id])) {
1501
                $id = (string) $aliases[$id];
1502
            }
1503
            $code .= '            '.$this->doExport($alias).' => '.$this->doExport($id).",\n";
1504
        }
1505
1506
        return $code."        ];\n";
1507
    }
1508
1509
    private function addDeprecatedAliases(): string
1510
    {
1511
        $code = '';
1512
        $aliases = $this->container->getAliases();
1513
        foreach ($aliases as $alias => $definition) {
1514
            if (!$definition->isDeprecated()) {
1515
                continue;
1516
            }
1517
            $public = $definition->isPublic() ? 'public' : 'private';
1518
            $id = (string) $definition;
1519
            $methodNameAlias = $this->generateMethodName($alias);
1520
            $idExported = $this->export($id);
1521
            $deprecation = $definition->getDeprecation($alias);
1522
            $packageExported = $this->export($deprecation['package']);
1523
            $versionExported = $this->export($deprecation['version']);
1524
            $messageExported = $this->export($deprecation['message']);
1525
            $code .= <<<EOF
1526
1527
    /*{$this->docStar}
1528
     * Gets the $public '$alias' alias.
1529
     *
1530
     * @return object The "$id" service.
1531
     */
1532
    protected static function {$methodNameAlias}(\$container)
1533
    {
1534
        trigger_deprecation($packageExported, $versionExported, $messageExported);
1535
1536
        return \$container->get($idExported);
1537
    }
1538
1539
EOF;
1540
        }
1541
1542
        return $code;
1543
    }
1544
1545
    private function addInlineRequires(bool $hasProxyClasses): string
1546
    {
1547
        $lineage = [];
1548
        $hotPathServices = $this->hotPathTag && $this->inlineRequires ? $this->container->findTaggedServiceIds($this->hotPathTag) : [];
1549
1550
        foreach ($hotPathServices as $id => $tags) {
1551
            $definition = $this->container->getDefinition($id);
1552
1553
            if ($definition->isLazy() && $this->hasProxyDumper) {
1554
                continue;
1555
            }
1556
1557
            $inlinedDefinitions = $this->getDefinitionsFromArguments([$definition]);
1558
1559
            foreach ($inlinedDefinitions as $def) {
1560
                foreach ($this->getClasses($def, $id) as $class) {
1561
                    $this->collectLineage($class, $lineage);
1562
                }
1563
            }
1564
        }
1565
1566
        $code = '';
1567
1568
        foreach ($lineage as $file) {
1569
            if (!isset($this->inlinedRequires[$file])) {
1570
                $this->inlinedRequires[$file] = true;
1571
                $code .= \sprintf("\n            include_once %s;", $file);
1572
            }
1573
        }
1574
1575
        if ($hasProxyClasses) {
1576
            $code .= "\n            include_once __DIR__.'/proxy-classes.php';";
1577
        }
1578
1579
        return $code ? \sprintf("\n        \$this->privates['service_container'] = static function (\$container) {%s\n        };\n", $code) : '';
1580
    }
1581
1582
    private function addDefaultParametersMethod(): string
1583
    {
1584
        $bag = $this->container->getParameterBag();
1585
1586
        if (!$bag->all() && (!$bag instanceof ParameterBag || !$bag->allNonEmpty())) {
1587
            return '';
1588
        }
1589
1590
        $php = [];
1591
        $dynamicPhp = [];
1592
1593
        foreach ($bag->all() as $key => $value) {
1594
            if ($key !== $resolvedKey = $this->container->resolveEnvPlaceholders($key)) {
1595
                throw new InvalidArgumentException(\sprintf('Parameter name cannot use env parameters: "%s".', $resolvedKey));
1596
            }
1597
            $hasEnum = false;
1598
            $export = $this->exportParameters([$value], '', 12, $hasEnum);
1599
            $export = explode('0 => ', substr(rtrim($export, " ]\n"), 2, -1), 2);
1600
1601
            if ($hasEnum || preg_match("/\\\$container->(?:getEnv\('(?:[-.\w\\\\]*+:)*+\w*+'\)|targetDir\.'')/", $export[1])) {
1602
                $dynamicPhp[$key] = \sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
1603
                $this->dynamicParameters[$key] = true;
1604
            } else {
1605
                $php[] = \sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
1606
            }
1607
        }
1608
        $parameters = \sprintf("[\n%s\n%s]", implode("\n", $php), str_repeat(' ', 8));
1609
1610
        $code = <<<'EOF'
1611
1612
    public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null
1613
    {
1614
        if (isset(self::DEPRECATED_PARAMETERS[$name])) {
1615
            trigger_deprecation(...self::DEPRECATED_PARAMETERS[$name]);
1616
        }
1617
1618
        if (isset($this->buildParameters[$name])) {
1619
            return $this->buildParameters[$name];
1620
        }
1621
1622
        if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || \array_key_exists($name, $this->parameters))) {
1623
            throw new ParameterNotFoundException($name, extraMessage: self::NONEMPTY_PARAMETERS[$name] ?? null);
1624
        }
1625
1626
        if (isset($this->loadedDynamicParameters[$name])) {
1627
            $value = $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
1628
        } else {
1629
            $value = $this->parameters[$name];
1630
        }
1631
1632
        if (isset(self::NONEMPTY_PARAMETERS[$name]) && (null === $value || '' === $value || [] === $value)) {
1633
            throw new \Symfony\Component\DependencyInjection\Exception\EmptyParameterValueException(self::NONEMPTY_PARAMETERS[$name]);
1634
        }
1635
1636
        return $value;
1637
    }
1638
1639
    public function hasParameter(string $name): bool
1640
    {
1641
        if (isset($this->buildParameters[$name])) {
1642
            return true;
1643
        }
1644
1645
        return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || \array_key_exists($name, $this->parameters);
1646
    }
1647
1648
    public function setParameter(string $name, $value): void
1649
    {
1650
        throw new LogicException('Impossible to call set() on a frozen ParameterBag.');
1651
    }
1652
1653
    public function getParameterBag(): ParameterBagInterface
1654
    {
1655
        if (!isset($this->parameterBag)) {
1656
            $parameters = $this->parameters;
1657
            foreach ($this->loadedDynamicParameters as $name => $loaded) {
1658
                $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
1659
            }
1660
            foreach ($this->buildParameters as $name => $value) {
1661
                $parameters[$name] = $value;
1662
            }
1663
            $this->parameterBag = new FrozenParameterBag($parameters, self::DEPRECATED_PARAMETERS, self::NONEMPTY_PARAMETERS);
1664
        }
1665
1666
        return $this->parameterBag;
1667
    }
1668
1669
EOF;
1670
1671
        if (!$this->asFiles) {
1672
            $code = preg_replace('/^.*buildParameters.*\n.*\n.*\n\n?/m', '', $code);
1673
        }
1674
1675
        if (!$bag instanceof ParameterBag || !$bag->allDeprecated()) {
1676
            $code = preg_replace("/\n.*DEPRECATED_PARAMETERS.*\n.*\n.*\n/m", '', $code, 1);
1677
            $code = str_replace(', self::DEPRECATED_PARAMETERS', ', []', $code);
1678
        }
1679
1680
        if (!$bag instanceof ParameterBag || !$bag->allNonEmpty()) {
1681
            $code = str_replace(', extraMessage: self::NONEMPTY_PARAMETERS[$name] ?? null', '', $code);
1682
            $code = str_replace(', self::NONEMPTY_PARAMETERS', '', $code);
1683
            $code = preg_replace("/\n.*NONEMPTY_PARAMETERS.*\n.*\n.*\n/m", '', $code, 1);
1684
        }
1685
1686
        if ($dynamicPhp) {
1687
            $loadedDynamicParameters = $this->exportParameters(array_combine(array_keys($dynamicPhp), array_fill(0, \count($dynamicPhp), false)), '', 8);
1688
            $getDynamicParameter = <<<'EOF'
1689
        $container = $this;
1690
        $value = match ($name) {
1691
%s
1692
            default => throw new ParameterNotFoundException($name),
1693
        };
1694
        $this->loadedDynamicParameters[$name] = true;
1695
1696
        return $this->dynamicParameters[$name] = $value;
1697
EOF;
1698
            $getDynamicParameter = \sprintf($getDynamicParameter, implode("\n", $dynamicPhp));
1699
        } else {
1700
            $loadedDynamicParameters = '[]';
1701
            $getDynamicParameter = str_repeat(' ', 8).'throw new ParameterNotFoundException($name);';
1702
        }
1703
1704
        return $code.<<<EOF
1705
1706
    private \$loadedDynamicParameters = {$loadedDynamicParameters};
1707
    private \$dynamicParameters = [];
1708
1709
    private function getDynamicParameter(string \$name)
1710
    {
1711
{$getDynamicParameter}
1712
    }
1713
1714
    protected function getDefaultParameters(): array
1715
    {
1716
        return $parameters;
1717
    }
1718
1719
EOF;
1720
    }
1721
1722
    /**
1723
     * @throws InvalidArgumentException
1724
     */
1725
    private function exportParameters(array $parameters, string $path = '', int $indent = 12, bool &$hasEnum = false): string
1726
    {
1727
        $php = [];
1728
        foreach ($parameters as $key => $value) {
1729
            if (\is_array($value)) {
1730
                $value = $this->exportParameters($value, $path.'/'.$key, $indent + 4, $hasEnum);
1731
            } elseif ($value instanceof ArgumentInterface) {
1732
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain special arguments. "%s" found in "%s".', get_debug_type($value), $path.'/'.$key));
1733
            } elseif ($value instanceof Variable) {
1734
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain variable references. Variable "%s" found in "%s".', $value, $path.'/'.$key));
1735
            } elseif ($value instanceof Definition) {
1736
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain service definitions. Definition for "%s" found in "%s".', $value->getClass(), $path.'/'.$key));
1737
            } elseif ($value instanceof Reference) {
1738
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain references to other services (reference to service "%s" found in "%s").', $value, $path.'/'.$key));
1739
            } elseif ($value instanceof Expression) {
1740
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain expressions. Expression "%s" found in "%s".', $value, $path.'/'.$key));
1741
            } 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...
1742
                $hasEnum = true;
1743
                $value = \sprintf('\%s::%s', $value::class, $value->name);
1744
            } else {
1745
                $value = $this->export($value);
1746
            }
1747
1748
            $php[] = \sprintf('%s%s => %s,', str_repeat(' ', $indent), $this->export($key), $value);
1749
        }
1750
1751
        return \sprintf("[\n%s\n%s]", implode("\n", $php), str_repeat(' ', $indent - 4));
1752
    }
1753
1754
    private function endClass(): string
1755
    {
1756
        return <<<'EOF'
1757
}
1758
1759
EOF;
1760
    }
1761
1762
    private function wrapServiceConditionals(mixed $value, string $code): string
1763
    {
1764
        if (!$condition = $this->getServiceConditionals($value)) {
1765
            return $code;
1766
        }
1767
1768
        // re-indent the wrapped code
1769
        $code = implode("\n", array_map(fn ($line) => $line ? '    '.$line : $line, explode("\n", $code)));
1770
1771
        return \sprintf("        if (%s) {\n%s        }\n", $condition, $code);
1772
    }
1773
1774
    private function getServiceConditionals(mixed $value): string
1775
    {
1776
        $conditions = [];
1777
        foreach (ContainerBuilder::getInitializedConditionals($value) as $service) {
1778
            if (!$this->container->hasDefinition($service)) {
1779
                return 'false';
1780
            }
1781
            $conditions[] = \sprintf('isset($container->%s[%s])', $this->container->getDefinition($service)->isPublic() ? 'services' : 'privates', $this->doExport($service));
1782
        }
1783
        foreach (ContainerBuilder::getServiceConditionals($value) as $service) {
1784
            if ($this->container->hasDefinition($service) && !$this->container->getDefinition($service)->isPublic()) {
1785
                continue;
1786
            }
1787
1788
            $conditions[] = \sprintf('$container->has(%s)', $this->doExport($service));
1789
        }
1790
1791
        if (!$conditions) {
1792
            return '';
1793
        }
1794
1795
        return implode(' && ', $conditions);
1796
    }
1797
1798
    private function getDefinitionsFromArguments(array $arguments, ?\SplObjectStorage $definitions = null, array &$calls = [], ?bool $byConstructor = null): \SplObjectStorage
1799
    {
1800
        $definitions ??= new \SplObjectStorage();
1801
1802
        foreach ($arguments as $argument) {
1803
            if (\is_array($argument)) {
1804
                $this->getDefinitionsFromArguments($argument, $definitions, $calls, $byConstructor);
1805
            } elseif ($argument instanceof Reference) {
1806
                $id = (string) $argument;
1807
1808
                while ($this->container->hasAlias($id)) {
1809
                    $id = (string) $this->container->getAlias($id);
1810
                }
1811
1812
                if (!isset($calls[$id])) {
1813
                    $calls[$id] = [0, $argument->getInvalidBehavior(), $byConstructor];
1814
                } else {
1815
                    $calls[$id][1] = min($calls[$id][1], $argument->getInvalidBehavior());
1816
                }
1817
1818
                ++$calls[$id][0];
1819
            } elseif (!$argument instanceof Definition) {
1820
                // no-op
1821
            } elseif (isset($definitions[$argument])) {
1822
                $definitions[$argument] = 1 + $definitions[$argument];
1823
            } else {
1824
                $definitions[$argument] = 1;
1825
                $arguments = [$argument->getArguments(), $argument->getFactory()];
1826
                $this->getDefinitionsFromArguments($arguments, $definitions, $calls, null === $byConstructor || $byConstructor);
1827
                $arguments = [$argument->getProperties(), $argument->getMethodCalls(), $argument->getConfigurator()];
1828
                $this->getDefinitionsFromArguments($arguments, $definitions, $calls, null !== $byConstructor && $byConstructor);
1829
            }
1830
        }
1831
1832
        return $definitions;
1833
    }
1834
1835
    /**
1836
     * @throws RuntimeException
1837
     */
1838
    private function dumpValue(mixed $value, bool $interpolate = true): string
1839
    {
1840
        if (\is_array($value)) {
1841
            if ($value && $interpolate && false !== $param = array_search($value, $this->container->getParameterBag()->all(), true)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1842
                return $this->dumpValue("%$param%");
1843
            }
1844
            $isList = array_is_list($value);
0 ignored issues
show
Bug introduced by
The function array_is_list was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1844
            $isList = /** @scrutinizer ignore-call */ array_is_list($value);
Loading history...
1845
            $code = [];
1846
            foreach ($value as $k => $v) {
1847
                $code[] = $isList ? $this->dumpValue($v, $interpolate) : \sprintf('%s => %s', $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate));
1848
            }
1849
1850
            return \sprintf('[%s]', implode(', ', $code));
1851
        } elseif ($value instanceof ArgumentInterface) {
1852
            $scope = [$this->definitionVariables, $this->referenceVariables];
1853
            $this->definitionVariables = $this->referenceVariables = null;
1854
1855
            try {
1856
                if ($value instanceof ServiceClosureArgument) {
1857
                    $value = $value->getValues()[0];
1858
                    $code = $this->dumpValue($value, $interpolate);
1859
1860
                    $returnedType = '';
1861
                    if ($value instanceof TypedReference) {
1862
                        $returnedType = \sprintf(': %s\%s', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $value->getInvalidBehavior() ? '' : '?', str_replace(['|', '&'], ['|\\', '&\\'], $value->getType()));
1863
                    }
1864
1865
                    $attribute = '';
1866
                    if ($value instanceof Reference) {
1867
                        $attribute = 'name: '.$this->dumpValue((string) $value, $interpolate);
1868
1869
                        if ($this->container->hasDefinition($value) && ($class = $this->container->findDefinition($value)->getClass()) && $class !== (string) $value) {
1870
                            $attribute .= ', class: '.$this->dumpValue($class, $interpolate);
1871
                        }
1872
1873
                        $attribute = \sprintf('#[\Closure(%s)] ', $attribute);
1874
                    }
1875
1876
                    return \sprintf('%sfn ()%s => %s', $attribute, $returnedType, $code);
1877
                }
1878
1879
                if ($value instanceof IteratorArgument) {
1880
                    if (!$values = $value->getValues()) {
1881
                        return 'new RewindableGenerator(fn () => new \EmptyIterator(), 0)';
1882
                    }
1883
1884
                    $code = [];
1885
                    $code[] = 'new RewindableGenerator(function () use ($container) {';
1886
1887
                    $operands = [0];
1888
                    foreach ($values as $k => $v) {
1889
                        ($c = $this->getServiceConditionals($v)) ? $operands[] = "(int) ($c)" : ++$operands[0];
1890
                        $v = $this->wrapServiceConditionals($v, \sprintf("        yield %s => %s;\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate)));
1891
                        foreach (explode("\n", $v) as $v) {
0 ignored issues
show
Comprehensibility Bug introduced by
$v is overwriting a variable from outer foreach loop.
Loading history...
1892
                            if ($v) {
1893
                                $code[] = '    '.$v;
1894
                            }
1895
                        }
1896
                    }
1897
1898
                    $code[] = \sprintf('        }, %s)', \count($operands) > 1 ? 'fn () => '.implode(' + ', $operands) : $operands[0]);
1899
1900
                    return implode("\n", $code);
1901
                }
1902
1903
                if ($value instanceof ServiceLocatorArgument) {
1904
                    $serviceMap = '';
1905
                    $serviceTypes = '';
1906
                    foreach ($value->getValues() as $k => $v) {
1907
                        if (!$v instanceof Reference) {
1908
                            $serviceMap .= \sprintf("\n            %s => [%s],", $this->export($k), $this->dumpValue($v));
1909
                            $serviceTypes .= \sprintf("\n            %s => '?',", $this->export($k));
1910
                            continue;
1911
                        }
1912
                        $id = (string) $v;
1913
                        while ($this->container->hasAlias($id)) {
1914
                            $id = (string) $this->container->getAlias($id);
1915
                        }
1916
                        $definition = $this->container->getDefinition($id);
1917
                        $load = !($definition->hasErrors() && $e = $definition->getErrors()) ? $this->asFiles && !$this->inlineFactories && !$this->isHotPath($definition) : reset($e);
1918
                        $serviceMap .= \sprintf("\n            %s => [%s, %s, %s, %s],",
1919
                            $this->export($k),
1920
                            $this->export($definition->isShared() ? ($definition->isPublic() ? 'services' : 'privates') : false),
1921
                            $this->doExport($id),
1922
                            $this->export(ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $v->getInvalidBehavior() && !\is_string($load) ? $this->generateMethodName($id) : null),
1923
                            $this->export($load)
1924
                        );
1925
                        $serviceTypes .= \sprintf("\n            %s => %s,", $this->export($k), $this->export($v instanceof TypedReference ? $v->getType() : '?'));
1926
                        $this->locatedIds[$id] = true;
1927
                    }
1928
                    $this->addGetService = true;
1929
1930
                    return \sprintf('new \%s($container->getService ??= $container->getService(...), [%s%s], [%s%s])', ServiceLocator::class, $serviceMap, $serviceMap ? "\n        " : '', $serviceTypes, $serviceTypes ? "\n        " : '');
1931
                }
1932
            } finally {
1933
                [$this->definitionVariables, $this->referenceVariables] = $scope;
1934
            }
1935
        } elseif ($value instanceof Definition) {
1936
            if ($value->hasErrors() && $e = $value->getErrors()) {
1937
                return \sprintf('throw new RuntimeException(%s)', $this->export(reset($e)));
1938
            }
1939
            if ($this->definitionVariables?->contains($value)) {
1940
                return $this->dumpValue($this->definitionVariables[$value], $interpolate);
1941
            }
1942
            if ($value->getMethodCalls()) {
1943
                throw new RuntimeException('Cannot dump definitions which have method calls.');
1944
            }
1945
            if ($value->getProperties()) {
1946
                throw new RuntimeException('Cannot dump definitions which have properties.');
1947
            }
1948
            if (null !== $value->getConfigurator()) {
1949
                throw new RuntimeException('Cannot dump definitions which have a configurator.');
1950
            }
1951
1952
            return $this->addNewInstance($value);
1953
        } elseif ($value instanceof Variable) {
1954
            return '$'.$value;
1955
        } elseif ($value instanceof Reference) {
1956
            $id = (string) $value;
1957
1958
            while ($this->container->hasAlias($id)) {
1959
                $id = (string) $this->container->getAlias($id);
1960
            }
1961
1962
            if (null !== $this->referenceVariables && isset($this->referenceVariables[$id])) {
1963
                return $this->dumpValue($this->referenceVariables[$id], $interpolate);
1964
            }
1965
1966
            return $this->getServiceCall($id, $value);
1967
        } elseif ($value instanceof Expression) {
1968
            return $this->getExpressionLanguage()->compile((string) $value, ['container' => 'container']);
1969
        } elseif ($value instanceof Parameter) {
1970
            return $this->dumpParameter($value);
1971
        } elseif (true === $interpolate && \is_string($value)) {
1972
            if (preg_match('/^%([^%]+)%$/', $value, $match)) {
1973
                // we do this to deal with non string values (Boolean, integer, ...)
1974
                // the preg_replace_callback converts them to strings
1975
                return $this->dumpParameter($match[1]);
1976
            }
1977
1978
            $replaceParameters = fn ($match) => "'.".$this->dumpParameter($match[2]).".'";
1979
1980
            return str_replace('%%', '%', preg_replace_callback('/(?<!%)(%)([^%]+)\1/', $replaceParameters, $this->export($value)));
1981
        } elseif ($value instanceof \UnitEnum) {
1982
            return \sprintf('\%s::%s', $value::class, $value->name);
1983
        } elseif ($value instanceof AbstractArgument) {
1984
            throw new RuntimeException($value->getTextWithContext());
1985
        } elseif (\is_object($value) || \is_resource($value)) {
1986
            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)));
1987
        }
1988
1989
        return $this->export($value);
1990
    }
1991
1992
    /**
1993
     * Dumps a string to a literal (aka PHP Code) class value.
1994
     *
1995
     * @throws RuntimeException
1996
     */
1997
    private function dumpLiteralClass(string $class): string
1998
    {
1999
        if (str_contains($class, '$')) {
2000
            return \sprintf('${($_ = %s) && false ?: "_"}', $class);
2001
        }
2002
        if (!str_starts_with($class, "'") || !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
2003
            throw new RuntimeException(\sprintf('Cannot dump definition because of invalid class name (%s).', $class ?: 'n/a'));
2004
        }
2005
2006
        $class = substr(str_replace('\\\\', '\\', $class), 1, -1);
2007
2008
        return str_starts_with($class, '\\') ? $class : '\\'.$class;
2009
    }
2010
2011
    private function dumpParameter(string $name): string
2012
    {
2013
        if (!$this->container->hasParameter($name) || ($this->dynamicParameters[$name] ?? false)) {
2014
            return \sprintf('$container->getParameter(%s)', $this->doExport($name));
2015
        }
2016
2017
        $value = $this->container->getParameter($name);
2018
        $dumpedValue = $this->dumpValue($value, false);
2019
2020
        if (!$value || !\is_array($value)) {
2021
            return $dumpedValue;
2022
        }
2023
2024
        return \sprintf('$container->parameters[%s]', $this->doExport($name));
2025
    }
2026
2027
    private function getServiceCall(string $id, ?Reference $reference = null): string
2028
    {
2029
        while ($this->container->hasAlias($id)) {
2030
            $id = (string) $this->container->getAlias($id);
2031
        }
2032
2033
        if ('service_container' === $id) {
2034
            return '$container';
2035
        }
2036
2037
        if ($this->container->hasDefinition($id) && $definition = $this->container->getDefinition($id)) {
2038
            if ($definition->isSynthetic()) {
2039
                $code = \sprintf('$container->get(%s%s)', $this->doExport($id), null !== $reference ? ', '.$reference->getInvalidBehavior() : '');
2040
            } elseif (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) {
2041
                $code = 'null';
2042
                if (!$definition->isShared()) {
2043
                    return $code;
2044
                }
2045
            } elseif ($this->isTrivialInstance($definition)) {
2046
                if ($definition->hasErrors() && $e = $definition->getErrors()) {
2047
                    return \sprintf('throw new RuntimeException(%s)', $this->export(reset($e)));
2048
                }
2049
                $code = $this->addNewInstance($definition, '', $id);
2050
                if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) {
2051
                    return \sprintf('($container->%s[%s] ??= %s)', $definition->isPublic() ? 'services' : 'privates', $this->doExport($id), $code);
2052
                }
2053
                $code = "($code)";
2054
            } else {
2055
                $code = $this->asFiles && !$this->inlineFactories && !$this->isHotPath($definition) ? "\$container->load('%s')" : 'self::%s($container)';
2056
                $code = \sprintf($code, $this->generateMethodName($id));
2057
2058
                if (!$definition->isShared()) {
2059
                    $factory = \sprintf('$container->factories%s[%s]', $definition->isPublic() ? '' : "['service_container']", $this->doExport($id));
2060
                    $code = \sprintf('(isset(%s) ? %1$s($container) : %s)', $factory, $code);
2061
                }
2062
            }
2063
            if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) {
2064
                $code = \sprintf('($container->%s[%s] ?? %s)', $definition->isPublic() ? 'services' : 'privates', $this->doExport($id), $code);
2065
            }
2066
2067
            return $code;
2068
        }
2069
        if (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) {
2070
            return 'null';
2071
        }
2072
        if (null !== $reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $reference->getInvalidBehavior()) {
2073
            $code = \sprintf('$container->get(%s, ContainerInterface::NULL_ON_INVALID_REFERENCE)', $this->doExport($id));
2074
        } else {
2075
            $code = \sprintf('$container->get(%s)', $this->doExport($id));
2076
        }
2077
2078
        return \sprintf('($container->services[%s] ?? %s)', $this->doExport($id), $code);
2079
    }
2080
2081
    /**
2082
     * Initializes the method names map to avoid conflicts with the Container methods.
2083
     */
2084
    private function initializeMethodNamesMap(string $class): void
2085
    {
2086
        $this->serviceIdToMethodNameMap = [];
2087
        $this->usedMethodNames = [];
2088
2089
        if ($reflectionClass = $this->container->getReflectionClass($class)) {
2090
            foreach ($reflectionClass->getMethods() as $method) {
2091
                $this->usedMethodNames[strtolower($method->getName())] = true;
2092
            }
2093
        }
2094
    }
2095
2096
    /**
2097
     * @throws InvalidArgumentException
2098
     */
2099
    private function generateMethodName(string $id): string
2100
    {
2101
        if (isset($this->serviceIdToMethodNameMap[$id])) {
2102
            return $this->serviceIdToMethodNameMap[$id];
2103
        }
2104
2105
        $i = strrpos($id, '\\');
2106
        $name = Container::camelize(false !== $i && isset($id[1 + $i]) ? substr($id, 1 + $i) : $id);
2107
        $name = preg_replace('/[^a-zA-Z0-9_\x7f-\xff]/', '', $name);
2108
        $methodName = 'get'.$name.'Service';
2109
        $suffix = 1;
2110
2111
        while (isset($this->usedMethodNames[strtolower($methodName)])) {
2112
            ++$suffix;
2113
            $methodName = 'get'.$name.$suffix.'Service';
2114
        }
2115
2116
        $this->serviceIdToMethodNameMap[$id] = $methodName;
2117
        $this->usedMethodNames[strtolower($methodName)] = true;
2118
2119
        return $methodName;
2120
    }
2121
2122
    private function getNextVariableName(): string
2123
    {
2124
        $firstChars = self::FIRST_CHARS;
2125
        $firstCharsLength = \strlen($firstChars);
2126
        $nonFirstChars = self::NON_FIRST_CHARS;
2127
        $nonFirstCharsLength = \strlen($nonFirstChars);
2128
2129
        while (true) {
2130
            $name = '';
2131
            $i = $this->variableCount;
2132
            $name .= $firstChars[$i % $firstCharsLength];
2133
            $i = (int) ($i / $firstCharsLength);
2134
2135
            while ($i > 0) {
2136
                --$i;
2137
                $name .= $nonFirstChars[$i % $nonFirstCharsLength];
2138
                $i = (int) ($i / $nonFirstCharsLength);
2139
            }
2140
2141
            ++$this->variableCount;
2142
2143
            // check that the name is not reserved
2144
            if (\in_array($name, $this->reservedVariables, true)) {
2145
                continue;
2146
            }
2147
2148
            return $name;
2149
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
2150
    }
2151
2152
    private function getExpressionLanguage(): ExpressionLanguage
2153
    {
2154
        if (!isset($this->expressionLanguage)) {
2155
            if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) {
2156
                throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
2157
            }
2158
            $providers = $this->container->getExpressionLanguageProviders();
2159
            $this->expressionLanguage = new ExpressionLanguage(null, $providers, function ($arg) {
2160
                $id = '""' === substr_replace($arg, '', 1, -1) ? stripcslashes(substr($arg, 1, -1)) : null;
2161
2162
                if (null !== $id && ($this->container->hasAlias($id) || $this->container->hasDefinition($id))) {
2163
                    return $this->getServiceCall($id);
2164
                }
2165
2166
                return \sprintf('$container->get(%s)', $arg);
2167
            });
2168
2169
            if ($this->container->isTrackingResources()) {
2170
                foreach ($providers as $provider) {
2171
                    $this->container->addObjectResource($provider);
2172
                }
2173
            }
2174
        }
2175
2176
        return $this->expressionLanguage;
2177
    }
2178
2179
    private function isHotPath(Definition $definition): bool
2180
    {
2181
        return $this->hotPathTag && $definition->hasTag($this->hotPathTag) && !$definition->isDeprecated();
2182
    }
2183
2184
    private function isSingleUsePrivateNode(ServiceReferenceGraphNode $node): bool
2185
    {
2186
        if ($node->getValue()->isPublic()) {
2187
            return false;
2188
        }
2189
        $ids = [];
2190
        foreach ($node->getInEdges() as $edge) {
2191
            if (!$value = $edge->getSourceNode()->getValue()) {
2192
                continue;
2193
            }
2194
            if ($edge->isLazy() || !$value instanceof Definition || !$value->isShared()) {
2195
                return false;
2196
            }
2197
2198
            // When the source node is a proxy or ghost, it will construct its references only when the node itself is initialized.
2199
            // Since the node can be cloned before being fully initialized, we do not know how often its references are used.
2200
            if ($this->getProxyDumper()->isProxyCandidate($value)) {
2201
                return false;
2202
            }
2203
            $ids[$edge->getSourceNode()->getId()] = true;
2204
        }
2205
2206
        return 1 === \count($ids);
2207
    }
2208
2209
    private function export(mixed $value): mixed
2210
    {
2211
        if (null !== $this->targetDirRegex && \is_string($value) && preg_match($this->targetDirRegex, $value, $matches, \PREG_OFFSET_CAPTURE)) {
2212
            $suffix = $matches[0][1] + \strlen($matches[0][0]);
2213
            $matches[0][1] += \strlen($matches[1][0]);
2214
            $prefix = $matches[0][1] ? $this->doExport(substr($value, 0, $matches[0][1]), true).'.' : '';
2215
2216
            if ('\\' === \DIRECTORY_SEPARATOR && isset($value[$suffix])) {
2217
                $cookie = '\\'.random_int(100000, \PHP_INT_MAX);
2218
                $suffix = '.'.$this->doExport(str_replace('\\', $cookie, substr($value, $suffix)), true);
2219
                $suffix = str_replace('\\'.$cookie, "'.\\DIRECTORY_SEPARATOR.'", $suffix);
2220
            } else {
2221
                $suffix = isset($value[$suffix]) ? '.'.$this->doExport(substr($value, $suffix), true) : '';
2222
            }
2223
2224
            $dirname = $this->asFiles ? '$container->containerDir' : '__DIR__';
2225
            $offset = 2 + $this->targetDirMaxMatches - \count($matches);
2226
2227
            if (0 < $offset) {
2228
                $dirname = \sprintf('\dirname(__DIR__, %d)', $offset + (int) $this->asFiles);
2229
            } elseif ($this->asFiles) {
2230
                $dirname = "\$container->targetDir.''"; // empty string concatenation on purpose
2231
            }
2232
2233
            if ($prefix || $suffix) {
2234
                return \sprintf('(%s%s%s)', $prefix, $dirname, $suffix);
2235
            }
2236
2237
            return $dirname;
2238
        }
2239
2240
        return $this->doExport($value, true);
2241
    }
2242
2243
    private function doExport(mixed $value, bool $resolveEnv = false): mixed
2244
    {
2245
        $shouldCacheValue = $resolveEnv && \is_string($value);
2246
        if ($shouldCacheValue && isset($this->exportedVariables[$value])) {
2247
            return $this->exportedVariables[$value];
2248
        }
2249
        if (\is_string($value) && str_contains($value, "\n")) {
2250
            $cleanParts = explode("\n", $value);
2251
            $cleanParts = array_map(fn ($part) => var_export($part, true), $cleanParts);
2252
            $export = implode('."\n".', $cleanParts);
2253
        } else {
2254
            $export = var_export($value, true);
2255
        }
2256
2257
        if ($resolveEnv && "'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$container->getEnv('string:%s').'")) {
2258
            $export = $resolvedExport;
2259
            if (str_ends_with($export, ".''")) {
2260
                $export = substr($export, 0, -3);
2261
                if ("'" === $export[1]) {
2262
                    $export = substr_replace($export, '', 23, 7);
2263
                }
2264
            }
2265
            if ("'" === $export[1]) {
2266
                $export = substr($export, 3);
0 ignored issues
show
Bug introduced by
It seems like $export can also be of type array; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2266
                $export = substr(/** @scrutinizer ignore-type */ $export, 3);
Loading history...
2267
            }
2268
        }
2269
2270
        if ($shouldCacheValue) {
2271
            $this->exportedVariables[$value] = $export;
2272
        }
2273
2274
        return $export;
2275
    }
2276
2277
    private function getAutoloadFile(): ?string
2278
    {
2279
        $file = null;
2280
2281
        foreach (spl_autoload_functions() as $autoloader) {
2282
            if (!\is_array($autoloader)) {
2283
                continue;
2284
            }
2285
2286
            if ($autoloader[0] instanceof DebugClassLoader) {
2287
                $autoloader = $autoloader[0]->getClassLoader();
2288
            }
2289
2290
            if (!\is_array($autoloader) || !$autoloader[0] instanceof ClassLoader || !$autoloader[0]->findFile(__CLASS__)) {
2291
                continue;
2292
            }
2293
2294
            foreach (get_declared_classes() as $class) {
2295
                if (str_starts_with($class, 'ComposerAutoloaderInit') && $class::getLoader() === $autoloader[0]) {
2296
                    $file = \dirname((new \ReflectionClass($class))->getFileName(), 2).'/autoload.php';
2297
2298
                    if (null !== $this->targetDirRegex && preg_match($this->targetDirRegex.'A', $file)) {
2299
                        return $file;
2300
                    }
2301
                }
2302
            }
2303
        }
2304
2305
        return $file;
2306
    }
2307
2308
    private function getClasses(Definition $definition, string $id): array
2309
    {
2310
        $classes = [];
2311
2312
        while ($definition instanceof Definition) {
2313
            foreach ($definition->getTag($this->preloadTags[0]) as $tag) {
2314
                if (!isset($tag['class'])) {
2315
                    throw new InvalidArgumentException(\sprintf('Missing attribute "class" on tag "%s" for service "%s".', $this->preloadTags[0], $id));
2316
                }
2317
2318
                $classes[] = trim($tag['class'], '\\');
2319
            }
2320
2321
            if ($class = $definition->getClass()) {
2322
                $classes[] = trim($class, '\\');
2323
            }
2324
            $factory = $definition->getFactory();
2325
2326
            if (\is_string($factory) && !str_starts_with($factory, '@=') && str_contains($factory, '::')) {
2327
                $factory = explode('::', $factory);
2328
            }
2329
2330
            if (!\is_array($factory)) {
2331
                $definition = $factory;
2332
                continue;
2333
            }
2334
2335
            $definition = $factory[0] ?? null;
2336
2337
            if (\is_string($definition)) {
2338
                $classes[] = trim($factory[0], '\\');
2339
            }
2340
        }
2341
2342
        return $classes;
2343
    }
2344
2345
    private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject, string $id): ?Definition
2346
    {
2347
        $asGhostObject = false;
2348
2349
        if (['Closure', 'fromCallable'] === $definition->getFactory()) {
2350
            return null;
2351
        }
2352
2353
        if (!$definition->isLazy() || !$this->hasProxyDumper) {
2354
            return null;
2355
        }
2356
2357
        return $this->getProxyDumper()->isProxyCandidate($definition, $asGhostObject, $id) ? $definition : null;
2358
    }
2359
2360
    /**
2361
     * Removes comments from a PHP source string.
2362
     *
2363
     * We don't use the PHP php_strip_whitespace() function
2364
     * as we want the content to be readable and well-formatted.
2365
     */
2366
    private static function stripComments(string $source): string
2367
    {
2368
        if (!\function_exists('token_get_all')) {
2369
            return $source;
2370
        }
2371
2372
        $rawChunk = '';
2373
        $output = '';
2374
        $tokens = token_get_all($source);
2375
        $ignoreSpace = false;
2376
        for ($i = 0; isset($tokens[$i]); ++$i) {
2377
            $token = $tokens[$i];
2378
            if (!isset($token[1]) || 'b"' === $token) {
2379
                $rawChunk .= $token;
2380
            } elseif (\T_START_HEREDOC === $token[0]) {
2381
                $output .= $rawChunk.$token[1];
2382
                do {
2383
                    $token = $tokens[++$i];
2384
                    $output .= isset($token[1]) && 'b"' !== $token ? $token[1] : $token;
2385
                } while (\T_END_HEREDOC !== $token[0]);
2386
                $rawChunk = '';
2387
            } elseif (\T_WHITESPACE === $token[0]) {
2388
                if ($ignoreSpace) {
2389
                    $ignoreSpace = false;
2390
2391
                    continue;
2392
                }
2393
2394
                // replace multiple new lines with a single newline
2395
                $rawChunk .= preg_replace(['/\n{2,}/S'], "\n", $token[1]);
2396
            } elseif (\in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT])) {
2397
                if (!\in_array($rawChunk[\strlen($rawChunk) - 1], [' ', "\n", "\r", "\t"], true)) {
2398
                    $rawChunk .= ' ';
2399
                }
2400
                $ignoreSpace = true;
2401
            } else {
2402
                $rawChunk .= $token[1];
2403
2404
                // The PHP-open tag already has a new-line
2405
                if (\T_OPEN_TAG === $token[0]) {
2406
                    $ignoreSpace = true;
2407
                } else {
2408
                    $ignoreSpace = false;
2409
                }
2410
            }
2411
        }
2412
2413
        $output .= $rawChunk;
2414
2415
        unset($tokens, $rawChunk);
2416
        gc_mem_caches();
2417
2418
        return $output;
2419
    }
2420
}
2421