Completed
Push — refactor ( 1ac315 )
by Akihito
05:56
created

ScriptInjector   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 256
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
wmc 32
lcom 2
cbo 9
dl 0
loc 256
rs 9.84
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 49 3
A __sleep() 0 6 1
A __wakeup() 0 9 1
A getInstance() 0 18 4
A clear() 0 11 3
A isSingleton() 0 16 4
A getModule() 0 16 2
A getInstanceFile() 0 12 2
A saveModule() 0 10 4
A registerLoader() 0 21 5
A compileOnDemand() 0 15 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\Compiler;
6
7
use Ray\Compiler\Exception\Unbound;
8
use Ray\Di\AbstractModule;
9
use Ray\Di\Dependency;
10
use Ray\Di\DependencyInterface;
11
use Ray\Di\InjectorInterface;
12
use Ray\Di\Name;
13
use Ray\Di\NullModule;
14
use ReflectionParameter;
15
16
use function assert;
17
use function error_reporting;
18
use function file_exists;
19
use function file_get_contents;
20
use function glob;
21
use function in_array;
22
use function is_bool;
23
use function is_dir;
24
use function rmdir;
25
use function rtrim;
26
use function serialize;
27
use function spl_autoload_register;
28
use function sprintf;
29
use function str_replace;
30
use function unlink;
31
use function unserialize;
32
33
use const DIRECTORY_SEPARATOR;
34
use const E_NOTICE;
35
36
final class ScriptInjector implements InjectorInterface
37
{
38
    public const MODULE = '/_module.txt';
39
40
    public const AOP = '/_aop.txt';
41
42
    public const INSTANCE = '%s/%s.php';
43
44
    public const QUALIFIER = '%s/qualifer/%s-%s-%s';
45
46
    /** @var string */
47
    private $scriptDir;
48
49
    /**
50
     * Injection Point
51
     *
52
     * [$class, $method, $parameter]
53
     *
54
     * @var array{0: string, 1: string, 2: string}
55
     */
56
    private $ip = ['', '', ''];
57
58
    /**
59
     * Singleton instance container
60
     *
61
     * @var array<object>
62
     */
63
    private $singletons = [];
64
65
    /** @var array<callable> */
66
    private $functions;
67
68
    /** @var callable */
69
    private $lazyModule;
70
71
    /** @var AbstractModule|null */
72
    private $module;
73
74
    /** @var ?array<DependencyInterface> */
75
    private $container;
76
77
    /** @var bool */
78
    private $isModuleLocked = false;
79
80
    /** @var array<string> */
81
    private static $scriptDirs = [];
82
83
    /**
84
     * @param string   $scriptDir  generated instance script folder path
85
     * @param callable $lazyModule callable variable which return AbstractModule instance
86
     *
87
     * @psalm-suppress UnresolvableInclude
88
     */
89
    public function __construct($scriptDir, ?callable $lazyModule = null)
90
    {
91
        $this->scriptDir = $scriptDir;
92
        $this->lazyModule = $lazyModule ?: static function (): NullModule {
93
            return new NullModule();
94
        };
95
        $this->registerLoader();
96
        $prototype =
97
            /**
98
             * @param array{0: string, 1: string, 2: string} $injectionPoint
99
             *
100
             * @return mixed
101
             */
102
            function (string $dependencyIndex, array $injectionPoint = ['', '', '']) {
103
                $this->ip = $injectionPoint; // @phpstan-ignore-line
104
                [$prototype, $singleton, $injectionPoint, $injector] = $this->functions;
0 ignored issues
show
Bug introduced by
The variable $prototype does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $singleton does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $injector does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
105
106
                return require $this->getInstanceFile($dependencyIndex);
107
            };
108
        $singleton =
109
            /**
110
             * @param array{0: string, 1: string, 2: string} $injectionPoint
111
             *
112
             * @return mixed
113
             */
114
            function (string $dependencyIndex, $injectionPoint = ['', '', '']) {
115
                if (isset($this->singletons[$dependencyIndex])) {
116
                    return $this->singletons[$dependencyIndex];
117
                }
118
119
                $this->ip = $injectionPoint;
120
                [$prototype, $singleton, $injectionPoint, $injector] = $this->functions;
0 ignored issues
show
Bug introduced by
The variable $prototype does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $singleton does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $injector does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
121
122
                $instance = require $this->getInstanceFile($dependencyIndex);
123
                $this->singletons[$dependencyIndex] = $instance;
124
125
                return $instance;
126
            };
127
        $injectionPoint = function () use ($scriptDir): InjectionPoint {
128
            return new InjectionPoint(
129
                new ReflectionParameter([$this->ip[0], $this->ip[1]], $this->ip[2]),
130
                $scriptDir
131
            );
132
        };
133
        $injector = function (): self {
134
            return $this;
135
        };
136
        $this->functions = [$prototype, $singleton, $injectionPoint, $injector];
137
    }
138
139
    /**
140
     * @return list<string>
0 ignored issues
show
Documentation introduced by
The doc-type list<string> could not be parsed: Expected "|" or "end of type", but got "<" at position 4. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
141
     */
142
    public function __sleep()
143
    {
144
        $this->saveModule();
145
146
        return ['scriptDir', 'singletons'];
147
    }
148
149
    public function __wakeup()
150
    {
151
        $this->__construct(
152
            $this->scriptDir,
153
            function () {
154
                return $this->getModule();
155
            }
156
        );
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function getInstance($interface, $name = Name::ANY)
163
    {
164
        $dependencyIndex = $interface . '-' . $name;
165
        if (isset($this->singletons[$dependencyIndex])) {
166
            return $this->singletons[$dependencyIndex];
167
        }
168
169
        [$prototype, $singleton, $injectionPoint, $injector] = $this->functions;
0 ignored issues
show
Bug introduced by
The variable $prototype does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $singleton does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $injectionPoint does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $injector does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
170
        /** @psalm-suppress UnresolvableInclude */
171
        $instance = require $this->getInstanceFile($dependencyIndex);
172
        /** @global bool $isSingleton */
173
        $isSingleton = isset($isSingleton) && $isSingleton; // @phpstan-ignore-line
0 ignored issues
show
Bug introduced by
The variable $isSingleton seems only to be defined at a later point. As such the call to isset() seems to always evaluate to false.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
174
        if ($isSingleton) {
175
            $this->singletons[$dependencyIndex] = $instance;
176
        }
177
178
        return $instance;
179
    }
180
181
    public function clear(): void
182
    {
183
        $unlink = static function (string $path) use (&$unlink): void {
184
            foreach ((array) glob(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '*') as $f) {
185
                $file = (string) $f;
186
                is_dir($file) ? $unlink($file) : unlink($file);
187
                @rmdir($file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
188
            }
189
        };
190
        $unlink($this->scriptDir);
191
    }
192
193
    public function isSingleton(string $dependencyIndex): bool
194
    {
195
        if (! $this->container) {
196
            $module = $this->getModule();
197
            /** @var AbstractModule $module */
198
            $this->container = $module->getContainer()->getContainer();
199
        }
200
201
        if (! isset($this->container[$dependencyIndex])) {
202
            throw new Unbound($dependencyIndex);
203
        }
204
205
        $dependency = $this->container[$dependencyIndex];
206
207
        return $dependency instanceof Dependency ? (new PrivateProperty())($dependency, 'isSingleton') : false;
208
    }
209
210
    private function getModule(): AbstractModule
211
    {
212
        $modulePath = $this->scriptDir . self::MODULE;
213
        if (! file_exists($modulePath)) {
214
            return new NullModule();
215
        }
216
217
        $serialized = file_get_contents($modulePath);
218
        assert(! is_bool($serialized));
219
        $er = error_reporting(error_reporting() ^ E_NOTICE);
220
        $module = unserialize($serialized, ['allowed_classes' => true]);
221
        error_reporting($er);
222
        assert($module instanceof AbstractModule);
223
224
        return $module;
225
    }
226
227
    /**
228
     * Return compiled script file name
229
     */
230
    private function getInstanceFile(string $dependencyIndex): string
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
231
    {
232
        $file = sprintf(self::INSTANCE, $this->scriptDir, str_replace('\\', '_', $dependencyIndex));
233
        if (file_exists($file)) {
234
            return $file;
235
        }
236
237
        $this->compileOnDemand($dependencyIndex);
238
        assert(file_exists($file));
239
240
        return $file;
241
    }
242
243
    private function saveModule(): void
244
    {
245
        if ($this->isModuleLocked || file_exists($this->scriptDir . self::MODULE)) {
246
            return;
247
        }
248
249
        $this->isModuleLocked = true;
250
        $module = $this->module instanceof AbstractModule ? $this->module : ($this->lazyModule)();
251
        (new FilePutContents())($this->scriptDir . self::MODULE, serialize($module));
252
    }
253
254
    private function registerLoader(): void
255
    {
256
        if (in_array($this->scriptDir, self::$scriptDirs, true)) {
257
            return;
258
        }
259
260
        if (self::$scriptDirs === []) {
261
            spl_autoload_register(
262
                static function (string $class): void {
263
                    foreach (self::$scriptDirs as $scriptDir) {
264
                        $file = sprintf('%s/%s.php', $scriptDir, str_replace('\\', '_', $class));
265
                        if (file_exists($file)) {
266
                            require $file; // @codeCoverageIgnore
267
                        }
268
                    }
269
                }
270
            );
271
        }
272
273
        self::$scriptDirs[] = $this->scriptDir;
274
    }
275
276
    private function compileOnDemand(string $dependencyIndex): void
277
    {
278
        if (! $this->module instanceof AbstractModule) {
279
            $this->module = ($this->lazyModule)();
280
        }
281
282
        $isFirstCompile = ! file_exists($this->scriptDir . self::AOP);
283
        if ($isFirstCompile) {
284
            (new DiCompiler(($this->lazyModule)(), $this->scriptDir))->savePointcuts($this->module->getContainer());
285
            $this->saveModule();
286
        }
287
288
        assert($this->module instanceof AbstractModule);
289
        (new OnDemandCompiler($this, $this->scriptDir, $this->module))($dependencyIndex);
290
    }
291
}
292