Completed
Pull Request — 1.x (#352)
by Akihito
01:20
created

Compiler::isMagicMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Package;
6
7
use BEAR\AppMeta\AbstractAppMeta;
8
use BEAR\AppMeta\Meta;
9
use BEAR\Package\Compiler\CompileDiScripts;
10
use BEAR\Package\Compiler\ScanClass;
11
use BEAR\Package\Provide\Error\NullPage;
12
use BEAR\Resource\Uri;
13
use BEAR\Sunday\Extension\Application\AppInterface;
14
use Composer\Autoload\ClassLoader;
15
use Ray\Di\AbstractModule;
16
use Ray\Di\Exception\Unbound;
17
use Ray\Di\InjectorInterface;
18
use Ray\ObjectGrapher\ObjectGrapher;
19
use ReflectionClass;
20
use RuntimeException;
21
use Throwable;
22
23
use function array_keys;
24
use function assert;
25
use function class_exists;
26
use function count;
27
use function file_exists;
28
use function file_put_contents;
29
use function get_class;
30
use function in_array;
31
use function interface_exists;
32
use function is_float;
33 1
use function is_int;
34
use function memory_get_peak_usage;
35 1
use function microtime;
36 1
use function number_format;
37
use function preg_quote;
38 1
use function preg_replace;
39
use function printf;
40
use function property_exists;
41 1
use function realpath;
42
use function sort;
43 1
use function spl_autoload_register;
44 1
use function sprintf;
45 1
use function strpos;
46 1
use function substr;
47 1
use function trait_exists;
48 1
49
use const PHP_EOL;
50 1
51
final class Compiler
52
{
53
    /** @var string[] */
54 1
    private $classes = [];
55
56
    /** @var InjectorInterface */
57 1
    private $injector;
58 1
59
    /** @var string */
60 1
    private $context;
61 1
62
    /** @var string */
63 1
    private $appDir;
64
65
    /** @var Meta */
66 1
    private $appMeta;
67
68 1
    /** @var array<int, string> */
69 1
    private $compiled = [];
70 1
71
    /** @var array<string, string> */
72
    private $failed = [];
73
74
    /** @var list<string> */
75
    private $overwritten = [];
76
77
    /** @var ScanClass */
78
    private $compilerScanClass;
79
80
    /** @var CompileDiScripts */
81
    private $compilerDiScripts;
82
83
    /**
84
     * @param string $appName application name "MyVendor|MyProject"
85
     * @param string $context application context "prod-app"
86
     * @param string $appDir  application path
87
     */
88
    public function __construct(string $appName, string $context, string $appDir)
89
    {
90
        $this->registerLoader($appDir);
91
        $this->hookNullObjectClass($appDir);
92
        $this->context = $context;
93
        $this->appDir = $appDir;
94
        $this->appMeta = new Meta($appName, $context, $appDir);
95
        /** @psalm-suppress MixedAssignment */
96
        $this->injector = Injector::getInstance($appName, $context, $appDir);
97
        $this->compilerScanClass = new ScanClass();
98
        $this->compilerDiScripts = new CompileDiScripts($this, $this->compilerScanClass);
99
    }
100
101
    /**
102
     * Compile application
103
     *
104
     * @return 0|1 exit code
0 ignored issues
show
Documentation introduced by
The doc-type 0|1 could not be parsed: Unknown type name "0" at position 0. (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...
105
     */
106
    public function compile(): int
107
    {
108
        $preload = $this->compilePreload($this->appMeta, $this->context);
109
        $module = (new Module())($this->appMeta, $this->context);
110
        $this->compileSrc($module);
111
        echo PHP_EOL;
112
        $this->compileDiScripts($this->appMeta);
113
        $dot = $this->failed ? '' : $this->compileObjectGraphDotFile($module);
114
        $start = $_SERVER['REQUEST_TIME_FLOAT'];
115
        assert(is_float($start));
116
        $time = number_format(microtime(true) - $start, 2);
117
        $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3);
118
        echo PHP_EOL;
119
        printf("Compilation (1/2) took %f seconds and used %fMB of memory\n", $time, $memory);
120
        printf("Success: %d Failed: %d\n", count($this->compiled), count($this->failed));
121 1
        printf("preload.php: %s\n", $this->getFileInfo($preload));
122
        printf("module.dot: %s\n", $dot ? $this->getFileInfo($dot) : 'n/a');
123
124 1
        foreach ($this->failed as $depedencyIndex => $error) {
125
            printf("UNBOUND: %s for %s \n", $error, $depedencyIndex);
126
        }
127
128
        return $this->failed ? 1 : 0;
129
    }
130 1
131 1
    public function dumpAutoload(): int
132 1
    {
133 1
        echo PHP_EOL;
134 1
        $this->invokeTypicalRequest();
135 1
        $paths = $this->getPaths($this->classes);
136 1
        $autolaod = $this->saveAutoloadFile($this->appMeta->appDir, $paths);
137
        $start = $_SERVER['REQUEST_TIME_FLOAT'];
138 1
        assert(is_float($start));
139
        $time = number_format(microtime(true) - $start, 2);
140 1
        $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3);
141
        printf("Compilation (2/2) took %f seconds and used %fMB of memory\n", $time, $memory);
142 1
        printf("autoload.php: %s\n", $this->getFileInfo($autolaod));
143
144 1
        return 0;
145
    }
146 1
147
    private function registerLoader(string $appDir): void
148
    {
149 1
        $loaderFile = $appDir . '/vendor/autoload.php';
150
        if (! file_exists($loaderFile)) {
151
            throw new RuntimeException('no loader');
152 1
        }
153 1
154
        $loader = require $loaderFile;
155
        assert($loader instanceof ClassLoader);
156 1
        spl_autoload_register(
157 1
            /** @var class-string $class */
0 ignored issues
show
Documentation introduced by
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (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...
158 1
            function (string $class) use ($loader): void {
159
                $loader->loadClass($class);
160 1
                if ($class !== NullPage::class) {
161
                    $this->classes[] = $class;
162 1
                }
163
            }
164 1
        );
165
    }
166 1
167 1
    private function compileDiScripts(AbstractAppMeta $appMeta): void
168 1
    {
169
        ($this->compilerDiScripts)($appMeta);
170 1
    }
171 1
172
    private function compileSrc(AbstractModule $module): AbstractModule
173
    {
174
        $container = $module->getContainer()->getContainer();
175
        $dependencies = array_keys($container);
176
        sort($dependencies);
177
        foreach ($dependencies as $dependencyIndex) {
178
            $pos = strpos((string) $dependencyIndex, '-');
179
            assert(is_int($pos));
180
            $interface = substr((string) $dependencyIndex, 0, $pos);
181
            $name = substr((string) $dependencyIndex, $pos + 1);
182
            $this->getInstance($interface, $name);
183
        }
184
185
        return $module;
186
    }
187
188
    private function getFileInfo(string $filename): string
189
    {
190
        if (in_array($filename, $this->overwritten, true)) {
191
            return $filename . ' (overwritten)';
192
        }
193
194
        return $filename;
195
    }
196
197
    /**
198
     * @param array<string> $paths
199
     */
200
    private function saveAutoloadFile(string $appDir, array $paths): string
201
    {
202
        $requiredFile = '';
203
        foreach ($paths as $path) {
204
            $requiredFile .= sprintf(
205
                "require %s';\n",
206
                $this->getRelativePath($appDir, $path)
207
            );
208
        }
209
210
        $autoloadFile = sprintf("<?php
211
212
// %s autoload
213
214
%s
215
require __DIR__ . '/vendor/autoload.php';
216
", $this->context, $requiredFile);
217
        $fileName = realpath($appDir) . '/autoload.php';
218
        $this->putFileContents($fileName, $autoloadFile);
219
220
        return $fileName;
221
    }
222
223
    private function compilePreload(AbstractAppMeta $appMeta, string $context): string
224
    {
225
        $this->loadResources($appMeta->name, $context, $appMeta->appDir);
226
        $paths = $this->getPaths($this->classes);
227
        $requiredOnceFile = '';
228
        foreach ($paths as $path) {
229
            $requiredOnceFile .= sprintf(
230
                "require_once %s';\n",
231
                $this->getRelativePath($appMeta->appDir, $path)
232
            );
233
        }
234
235
        $preloadFile = sprintf("<?php
236
237
// %s preload
238
239
require __DIR__ . '/vendor/autoload.php';
240
241
%s", $this->context, $requiredOnceFile);
242
        $fileName = realpath($appMeta->appDir) . '/preload.php';
243
        $this->putFileContents($fileName, $preloadFile);
244
245
        return $fileName;
246
    }
247
248
    private function getRelativePath(string $rootDir, string $file): string
249
    {
250
        $dir = (string) realpath($rootDir);
251
        if (strpos($file, $dir) !== false) {
252
            return (string) preg_replace('#^' . preg_quote($dir, '#') . '#', "__DIR__ . '", $file);
253
        }
254
255
        return $file;
256
    }
257
258
    /**
259
     * @psalm-suppress MixedFunctionCall
260
     * @psalm-suppress NoInterfaceProperties
261
     */
262
    private function invokeTypicalRequest(): void
263
    {
264
        $app = $this->injector->getInstance(AppInterface::class);
265
        assert($app instanceof AppInterface);
266
        assert(property_exists($app, 'resource'));
267
        $ro = new NullPage();
268
        $ro->uri = new Uri('app://self/');
269
        /** @psalm-suppress MixedMethodCall */
270
        $app->resource->get->object($ro)();
0 ignored issues
show
Bug introduced by
Accessing resource on the interface BEAR\Sunday\Extension\Application\AppInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
271
    }
272
273
    /**
274
     * @param array<string> $classes
275
     *
276
     * @return array<string>
277
     */
278
    private function getPaths(array $classes): array
279
    {
280
        $paths = [];
281
        foreach ($classes as $class) {
282
            // could be phpdoc tag by annotation loader
283
            if ($this->isNotAutoloadble($class)) {
284
                continue;
285
            }
286
287
            /** @var class-string $class */
0 ignored issues
show
Documentation introduced by
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (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...
288
            $filePath = (string) (new ReflectionClass($class))->getFileName(); // @phpstan-ignore-line
289
            if (! $this->isNotCompileFile($filePath)) {
290
                continue; // @codeCoverageIgnore
291
            }
292
293
            $paths[] = $this->getRelativePath($this->appDir, $filePath);
294
        }
295
296
        return $paths;
297
    }
298
299
    private function isNotCompileFile(string $filePath): bool
300
    {
301
        return file_exists($filePath) || is_int(strpos($filePath, 'phar'));
302
    }
303
304
    private function isNotAutoloadble(string $class): bool
305
    {
306
        return ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false);
307
    }
308
309
    private function loadResources(string $appName, string $context, string $appDir): void
310
    {
311
        $meta = new Meta($appName, $context, $appDir);
312
313
        $resMetas = $meta->getGenerator('*');
314
        foreach ($resMetas as $resMeta) {
315
            $this->getInstance($resMeta->class);
316
        }
317
    }
318
319
    private function getInstance(string $interface, string $name = ''): void
320
    {
321
        $dependencyIndex = $interface . '-' . $name;
322
        if (in_array($dependencyIndex, $this->compiled, true)) {
323
            // @codeCoverageIgnoreStart
324
            printf("S %s:%s\n", $interface, $name);
325
326
            return;
327
328
            // @codeCoverageIgnoreEnd
329
        }
330
331
        try {
332
            $this->injector->getInstance($interface, $name);
333
            $this->compiled[] = $dependencyIndex;
334
            $this->progress('.');
335
        } catch (Unbound $e) {
336
            if ($dependencyIndex === 'Ray\Aop\MethodInvocation-') {
337
                return;
338
            }
339
340
            $this->failed[$dependencyIndex] = $e->getMessage();
341
            $this->progress('F');
342
            // @codeCoverageIgnoreStart
343
        } catch (Throwable $e) {
344
            $this->failed[$dependencyIndex] = sprintf('%s: %s', get_class($e), $e->getMessage());
345
            $this->progress('F');
346
            // @codeCoverageIgnoreEnd
347
        }
348
    }
349
350
    private function progress(string $char): void
351
    {
352
        /**
353
         * @var int
354
         */
355
        static $cnt = 0;
356
357
        echo $char;
358
        $cnt++;
359
        if ($cnt === 60) {
360
            $cnt = 0;
361
            echo PHP_EOL;
362
        }
363
    }
364
365
    private function hookNullObjectClass(string $appDir): void
366
    {
367
        $compileScript = realpath($appDir) . '/.compile.php';
368
        if (file_exists($compileScript)) {
369
            require $compileScript;
370
        }
371
    }
372
373
    private function putFileContents(string $fileName, string $content): void
374
    {
375
        if (file_exists($fileName)) {
376
            $this->overwritten[] = $fileName;
377
        }
378
379
        file_put_contents($fileName, $content);
380
    }
381
382
    private function compileObjectGraphDotFile(AbstractModule $module): string
383
    {
384
        $dotFile = sprintf('%s/module.dot', $this->appDir);
385
        $this->putFileContents($dotFile, (new ObjectGrapher())($module));
386
387
        return $dotFile;
388
    }
389
390
    public function getInjector(): InjectorInterface
391
    {
392
        return $this->injector;
393
    }
394
}
395