Passed
Branch master (b33026)
by Divine Niiquaye
159:36 queued 107:08
created

FileLoader::resolveParameters()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 9

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 26
ccs 14
cts 14
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 9
nop 1
crap 9
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI\Loader;
19
20
use Rade\DI\AbstractContainer;
21
use Rade\DI\Container;
22
use Rade\DI\ContainerBuilder;
23
use Rade\DI\Definition;
24
use Symfony\Component\Config\FileLocatorInterface;
25
use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader;
26
use Symfony\Component\Config\Resource\ClassExistenceResource;
27
use Symfony\Component\Config\Resource\FileExistenceResource;
28
use Symfony\Component\Config\Resource\FileResource;
29
use Symfony\Component\Config\Resource\GlobResource;
30
31
abstract class FileLoader extends BaseFileLoader
32
{
33
    /** @var Container|ContainerBuilder */
34
    protected AbstractContainer $container;
35
36
    /** @var array<string,bool|string|string[]> */
37
    protected array $autowired = [];
38
39
    /** @var array<string,string[]> */
40
    protected array $deprecations = [];
41
42
    /** @var array<string,mixed> */
43
    protected array $tags = [];
44
45 105
    public function __construct(AbstractContainer $container, FileLocatorInterface $locator)
46
    {
47 105
        $this->container = $container;
0 ignored issues
show
Documentation Bug introduced by
$container is of type Rade\DI\AbstractContainer, but the property $container was declared to be of type Rade\DI\Container|Rade\DI\ContainerBuilder. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
48 105
        parent::__construct($locator);
49 105
    }
50
51
    /**
52
     * Registers a set of classes as services using PSR-4 for discovery.
53
     *
54
     * @param Definition           $prototype A definition to use as template
55
     * @param string               $namespace The namespace prefix of classes in the scanned directory
56
     * @param string               $resource  The directory to look for classes, glob-patterns allowed
57
     * @param string|string[]|null $exclude   A globbed path of files to exclude or an array of globbed paths of files to exclude
58
     */
59 33
    public function registerClasses(Definition $prototype, string $namespace, string $resource, $exclude = null): void
60
    {
61 33
        if ('\\' !== \substr($namespace, -1)) {
62
            throw new \InvalidArgumentException(\sprintf('Namespace prefix must end with a "\\": "%s".', $namespace));
63
        }
64
65 33
        if (!\preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++$/', $namespace)) {
66
            throw new \InvalidArgumentException(\sprintf('Namespace is not a valid PSR-4 prefix: "%s".', $namespace));
67
        }
68
69 33
        $classes = $this->findClasses($namespace, $this->resolveParameters($resource), (array) $exclude);
70
71
        // prepare for deep cloning
72 25
        $serializedPrototype = \serialize($prototype);
73
74 25
        foreach ($classes as $class) {
75 23
            $definition = $this->container->set($class, \unserialize($serializedPrototype))->replace($class, true);
0 ignored issues
show
Bug introduced by
The method set() does not exist on Rade\DI\AbstractContainer. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

75
            $definition = $this->container->/** @scrutinizer ignore-call */ set($class, \unserialize($serializedPrototype))->replace($class, true);
Loading history...
76
77 23
            if ($definition instanceof Definition) {
78 23
                if (isset($this->autowired[$namespace])) {
79 8
                    $definition->autowire(\is_array($this->autowired[$namespace]) ? $this->autowired[$namespace] : []);
80
                }
81
82 23
                if (isset($this->deprecations[$namespace])) {
83
                    [$package, $version, $message] = $this->deprecations[$namespace];
84
85
                    $definition->deprecate($package, $version, $message);
86
                }
87
88 23
                if (null !== $tags = $this->tags[$class] ?? $this->tags[$namespace] ?? null) {
89 2
                    $this->container->tag($class, $this->tags[$class] = $tags);
90
                }
91
            }
92
        }
93 25
    }
94
95
    /**
96
     * Replaces "%value%" from container's parameters keys.
97
     */
98 77
    protected function resolveParameters(string $value): string
99
    {
100 77
        $res = '';
101 77
        $parts = \preg_split('#(%[^%\s]+%)#i', $value, -1, \PREG_SPLIT_DELIM_CAPTURE);
102
103 77
        if (1 == \count($parts) && $value === $parts[0]) {
104 69
            return $value;
105
        }
106
107 14
        foreach ($parts as $part) {
108 14
            if ('' !== $part && '%' === $part[0]) {
109 14
                $val = \substr($part, 1, -1);
110
111 14
                if (!isset($this->container->parameters[$val])) {
112 2
                    throw new \RuntimeException(\sprintf('You have requested a non-existent parameter "%s".', $val));
113
                }
114
115 12
                if (!\is_scalar($part = $this->container->parameters[$val])) {
116 2
                    throw new \InvalidArgumentException(\sprintf('Unable to concatenate non-scalar parameter "%s" into %s.', $val, $value));
117
                }
118
            }
119
120 14
            $res .= (string) $part;
121
        }
122
123 10
        return '' !== $res ? $res : $value;
124
    }
125
126 29
    private function findClasses(string $namespace, string $pattern, array $excludePatterns): array
127
    {
128 29
        $excludePaths = [];
129 29
        $excludePrefix = null;
130
131 29
        foreach ($excludePatterns as $excludePattern) {
132 15
            $excludePattern = $this->resolveParameters($excludePattern);
133
134 15
            foreach ($this->glob($excludePattern, true, $resource, true, true) as $path => $info) {
135 15
                if (null === $excludePrefix) {
136 15
                    $excludePrefix = $resource->getPrefix();
137
                }
138
139
                // normalize Windows slashes
140 15
                $excludePaths[\str_replace('\\', '/', $path)] = true;
141
            }
142
        }
143
144 29
        $classes = [];
145 29
        $prefixLen = null;
146
147 29
        foreach ($this->glob($pattern, true, $resource, false, false, $excludePaths) as $path => $info) {
148 29
            if (null === $prefixLen) {
149 29
                $prefixLen = \strlen($resource->getPrefix());
150
151 29
                if ($excludePrefix && 0 !== \strpos($excludePrefix, $resource->getPrefix())) {
152 2
                    throw new \InvalidArgumentException(\sprintf('Invalid "exclude" pattern when importing classes for "%s": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s).', $namespace, $excludePattern, $pattern));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $excludePattern seems to be defined by a foreach iteration on line 131. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
153
                }
154
            }
155
156 27
            if (isset($excludePaths[\str_replace('\\', '/', $path)])) {
157
                continue;
158
            }
159
160 27
            if (!\preg_match('/\\.php$/', $path, $m) || !$info->isReadable()) {
0 ignored issues
show
Bug introduced by
The method isReadable() does not exist on Symfony\Component\Config\Resource\GlobResource. ( Ignorable by Annotation )

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

160
            if (!\preg_match('/\\.php$/', $path, $m) || !$info->/** @scrutinizer ignore-call */ isReadable()) {

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...
161
                continue;
162
            }
163
164 27
            $class = $namespace . \ltrim(\str_replace('/', '\\', \substr($path, $prefixLen, -\strlen($m[0]))), '\\');
165
166 27
            if (!\preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class)) {
167
                continue;
168
            }
169
170
            try {
171 27
                $r = new \ReflectionClass($class);
172 17
            } catch (\Error | \ReflectionException $e) {
173 17
                if (\preg_match('/^Class .* not found$/', $e->getMessage())) {
174 15
                    continue;
175
                }
176
177 2
                if ($e instanceof \ReflectionException) {
178 2
                    throw new \InvalidArgumentException(\sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern), 0, $e);
179
                }
180
181
                throw $e;
182
            }
183
184 23
            if ($this->container instanceof ContainerBuilder) {
185 8
                $this->container->addResource(new ClassExistenceResource($class, false));
0 ignored issues
show
Bug introduced by
The method addResource() does not exist on Rade\DI\AbstractContainer. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

185
                $this->container->/** @scrutinizer ignore-call */ 
186
                                  addResource(new ClassExistenceResource($class, false));
Loading history...
186 8
                $this->container->addResource(new FileExistenceResource($rPath = $r->getFileName()));
187 8
                $this->container->addResource(new FileResource($rPath));
188
            }
189
190 23
            if ($r->isInstantiable()) {
191 23
                $classes[] = $class;
192
            }
193
        }
194
195
        // track only for new & removed files
196 25
        if ($resource instanceof GlobResource && $this->container instanceof ContainerBuilder) {
197 9
            $this->container->addResource($resource);
198
        } else {
199 16
            if ($this->container instanceof ContainerBuilder) {
200
                foreach ($resource as $path) {
201
                    $this->container->addResource(new FileExistenceResource($path));
202
                }
203
            }
204
        }
205
206 25
        return $classes;
207
    }
208
}
209