Completed
Push — 1.x ( 66e6a9...550354 )
by Akihito
24s queued 12s
created

Compiler::registerLoader()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 20
ccs 10
cts 10
cp 1
crap 3
rs 9.6
c 0
b 0
f 0
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\Provide\Error\NullPage;
10
use BEAR\Resource\Exception\ParameterException;
11
use BEAR\Resource\NamedParameterInterface;
12
use BEAR\Resource\Uri;
13
use BEAR\Sunday\Extension\Application\AbstractApp;
14
use BEAR\Sunday\Extension\Application\AppInterface;
15
use Composer\Autoload\ClassLoader;
16
use Doctrine\Common\Annotations\Reader;
17
use Exception;
18
use function file_exists;
19
use const PHP_EOL;
20
use Ray\Di\AbstractModule;
21
use Ray\Di\Exception\Unbound;
22
use Ray\Di\InjectorInterface;
23
use function realpath;
24
use ReflectionClass;
25
use RuntimeException;
26
use function sprintf;
27
28
final class Compiler
29
{
30
    /**
31
     * @var string[]
32
     */
33 1
    private $classes = [];
34
35 1
    /**
36 1
     * @var InjectorInterface
37
     */
38 1
    private $injector;
39
40
    /**
41 1
     * @var string
42
     */
43 1
    private $appName;
44 1
45 1
    /**
46 1
     * @var string
47 1
     */
48 1
    private $context;
49
50 1
    /**
51
     * @var string
52
     */
53
    private $appDir;
54 1
55
    /**
56
     * @var string
57 1
     */
58 1
    private $cacheNs;
59
60 1
    /**
61 1
     * @var Meta
62
     */
63 1
    private $appMeta;
64
65
    /**
66 1
     * @var array<int, string>
67
     */
68 1
    private $compiled = [];
69 1
70 1
    /**
71
     * @var array<string, string>
72
     */
73
    private $failed = [];
74
75
    /**
76
     * @param string $appName application name "MyVendor|MyProject"
77
     * @param string $context application context "prod-app"
78
     * @param string $appDir  application path
79
     * @param string $cacheNs cache namespace
80
     */
81
    public function __construct(string $appName, string $context, string $appDir, string $cacheNs = '')
82
    {
83
        $this->registerLoader($appDir);
84
        $this->hookNullObjectClass($appDir);
85
        $this->appName = $appName;
86
        $this->context = $context;
87
        $this->appDir = $appDir;
88
        $this->cacheNs = $cacheNs;
89
        $this->appMeta = new Meta($appName, $context, $appDir);
90
        /** @psalm-suppress MixedAssignment */
91
        $this->injector = Injector::getInstance($appName, $context, $appDir, $cacheNs);
92
    }
93
94
    /**
95
     * Compile application
96
     */
97
    public function compile() : int
98
    {
99
        if (! is_dir($this->appDir)) {
100
            throw new RuntimeException($this->appDir);
101
        }
102
        $preload = $this->compilePreload($this->appMeta, $this->context);
103
        $module = (new Module)($this->appMeta, $this->context);
104
        $this->compileSrc($module);
105
        echo PHP_EOL;
106
        $this->compileDiScripts($this->appMeta);
107
        /** @var float $start */
108
        $start = $_SERVER['REQUEST_TIME_FLOAT'];
109
        $time = number_format(microtime(true) - $start, 2);
110
        $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3);
111
        echo PHP_EOL;
112
        printf("Compilation (1/2) took %f seconds and used %fMB of memory\n", $time, $memory);
113
        printf("Success: %d Failed: %d\n", count($this->compiled), count($this->failed));
114
        printf("preload.php: %s\n", $preload);
115
        foreach ($this->failed as $depedencyIndex => $error) {
116
            printf("UNBOUND: %s for %s \n", $error, $depedencyIndex);
117
        }
118
119
        return $this->failed ? 1 : 0;
120
    }
121 1
122
    public function dumpAutoload() : int
123
    {
124 1
        echo PHP_EOL;
125
        $this->invokeTypicalRequest();
126
        $paths = $this->getPaths($this->classes);
127
        $autolaod = $this->saveAutoloadFile($this->appMeta->appDir, $paths);
128
        /** @var float $start */
129
        $start = $_SERVER['REQUEST_TIME_FLOAT'];
130 1
        $time = number_format(microtime(true) - $start, 2);
131 1
        $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3);
132 1
        printf("Compilation (2/2) took %f seconds and used %fMB of memory\n", $time, $memory);
133 1
        printf("autoload.php: %s\n", $autolaod);
134 1
135 1
        return 0;
136 1
    }
137
138 1
    public function registerLoader(string $appDir) : void
139
    {
140 1
        $loaderFile = $appDir . '/vendor/autoload.php';
141
        if (! file_exists($loaderFile)) {
142 1
            throw new RuntimeException('no loader');
143
        }
144 1
        /** @var ClassLoader $loader */
145
        $loader = require $loaderFile;
146 1
        spl_autoload_register(
147
            /** @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...
148
            function (string $class) use ($loader) : void {
149 1
                $loader->loadClass($class);
150
                if ($class !== NullPage::class) {
151
                    $this->classes[] = $class;
152 1
                }
153 1
            },
154
            false,
155
            true
156 1
        );
157 1
    }
158 1
159
    public function compileDiScripts(AbstractAppMeta $appMeta) : void
160 1
    {
161
        $reader = $this->injector->getInstance(Reader::class);
162 1
        assert($reader instanceof Reader);
163
        $namedParams = $this->injector->getInstance(NamedParameterInterface::class);
164 1
        assert($namedParams instanceof NamedParameterInterface);
165
        // create DI factory class and AOP compiled class for all resources and save $app cache.
166 1
        $app = $this->injector->getInstance(AppInterface::class);
167 1
        assert($app instanceof AppInterface);
168 1
169
        // check resource injection and create annotation cache
170 1
        $metas = $appMeta->getResourceListGenerator();
171 1
        /** @var array{0: string, 1:string} $meta */
172
        foreach ($metas as $meta) {
173
            /** @var string $className */
174
            [$className] = $meta;
0 ignored issues
show
Bug introduced by
The variable $className 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...
175
            assert(class_exists($className));
176
            $this->scanClass($reader, $namedParams, $className);
177
        }
178
    }
179
180
    public function compileSrc(AbstractModule $module) : AbstractModule
181
    {
182
        $container = $module->getContainer()->getContainer();
183
        $dependencies = array_keys($container);
184
        sort($dependencies);
185
        foreach ($dependencies as $dependencyIndex) {
186
            $pos = strpos((string) $dependencyIndex, '-');
187
            assert(is_int($pos));
188
            $interface = substr((string) $dependencyIndex, 0, $pos);
189
            $name = substr((string) $dependencyIndex, $pos + 1);
190
            $this->getInstance($interface, $name);
191
        }
192
193
        return $module;
194
    }
195
196
    /**
197
     * @param array<string> $paths
198
     */
199
    private function saveAutoloadFile(string $appDir, array $paths) : string
200
    {
201
        $requiredFile = '';
202
        foreach ($paths as $path) {
203
            $requiredFile .= sprintf(
204
                "require %s';\n",
205
                $this->getRelativePath($appDir, $path)
206
            );
207
        }
208
        $autoloadFile = sprintf("<?php
209
210
// %s autoload
211
212
%s
213
require __DIR__ . '/vendor/autoload.php';
214
", $this->context, $requiredFile);
215
        $fileName = realpath($appDir) . '/autoload.php';
216
        if (file_exists($fileName)) {
217
            $fileName .= ' (overwritten)';
218
        }
219
        file_put_contents($fileName, $autoloadFile);
220
221
        return $fileName;
222
    }
223
224
    private function compilePreload(AbstractAppMeta $appMeta, string $context) : string
225
    {
226
        $this->loadResources($appMeta->name, $context, $appMeta->appDir);
227
        $paths = $this->getPaths($this->classes);
228
        $requiredOnceFile = '';
229
        foreach ($paths as $path) {
230
            $requiredOnceFile .= sprintf(
231
                "require_once %s';\n",
232
                $this->getRelativePath($appMeta->appDir, $path)
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
        if (file_exists($fileName)) {
244
            $fileName .= 'overwritten';
245
        }
246
        file_put_contents($fileName, $preloadFile);
247
248
        return $fileName;
249
    }
250
251
    private function getRelativePath(string $rootDir, string $file) : string
252
    {
253
        $dir = (string) realpath($rootDir);
254
        if (strpos($file, $dir) !== false) {
255
            return (string) preg_replace('#^' . preg_quote($dir, '#') . '#', "__DIR__ . '", $file);
256
        }
257
258
        return $file;
259
    }
260
261
    /**
262
     * @psalm-suppress MixedFunctionCall
263
     * @psalm-suppress NoInterfaceProperties
264
     */
265
    private function invokeTypicalRequest() : void
266
    {
267
        $app = $this->injector->getInstance(AppInterface::class);
268
        assert($app instanceof AbstractApp);
269
        $ro = new NullPage;
270
        $ro->uri = new Uri('app://self/');
271
        /** @psalm-suppress MixedMethodCall */
272
        $app->resource->get->object($ro)();
0 ignored issues
show
Bug introduced by
Accessing get on the interface BEAR\Resource\ResourceInterface 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...
273
    }
274
275
    /**
276
     * Save annotation and method meta information
277
     *
278
     * @template T
279
     *
280
     * @param class-string<T> $className
0 ignored issues
show
Documentation introduced by
The doc-type class-string<T> 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...
281
     */
282
    private function scanClass(Reader $reader, NamedParameterInterface $namedParams, string $className) : void
283
    {
284
        $class = new ReflectionClass($className);
285
        /** @var T $instance */
286
        $instance = $class->newInstanceWithoutConstructor();
287
        if (! $instance instanceof $className) {
288
            return;
289
        }
290
        $reader->getClassAnnotations($class);
291
        $methods = $class->getMethods();
292
        $log = sprintf('M %s:', $className);
293
        foreach ($methods as $method) {
294
            $methodName = $method->getName();
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
295
            if ($this->isMagicMethod($methodName)) {
296
                continue;
297
            }
298
            if (substr($methodName, 0, 2) === 'on') {
299
                $log .= sprintf(' %s', $methodName);
300
                $this->saveNamedParam($namedParams, $instance, $methodName);
301
            }
302
            // method annotation
303
            $reader->getMethodAnnotations($method);
304
            $log .= sprintf('@ %s', $methodName);
305
        }
306
//        echo $log . PHP_EOL;
307
    }
308
309
    private function isMagicMethod(string $method) : bool
310
    {
311
        return \in_array($method, ['__sleep', '__wakeup', 'offsetGet', 'offsetSet', 'offsetExists', 'offsetUnset', 'count', 'ksort', 'asort', 'jsonSerialize'], true);
312
    }
313
314
    private function saveNamedParam(NamedParameterInterface $namedParameter, object $instance, string $method) : void
315
    {
316
        // named parameter
317
        if (! \in_array($method, ['onGet', 'onPost', 'onPut', 'onPatch', 'onDelete', 'onHead'], true)) {
318
            return;
319
        }
320
        $callable = [$instance, $method];
321
        if (! is_callable($callable)) {
322
            return;
323
        }
324
        try {
325
            $namedParameter->getParameters($callable, []);
326
        } catch (ParameterException $e) {
327
            return;
328
        }
329
    }
330
331
    /**
332
     * @param array<string> $classes
333
     *
334
     * @return array<string>
335
     */
336
    private function getPaths(array $classes) : array
337
    {
338
        $paths = [];
339
        foreach ($classes as $class) {
340
            // could be phpdoc tag by annotation loader
341
            $isAutoloadFailed = ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false);
342
            if ($isAutoloadFailed) {
343
                continue;
344
            }
345
            assert(class_exists($class) || interface_exists($class) || trait_exists($class));
346
            $filePath = (string) (new ReflectionClass($class))->getFileName();
347
            if (! file_exists($filePath) || strpos($filePath, 'phar') === 0) {
348
                continue;
349
            }
350
            $paths[] = $this->getRelativePath($this->appDir, $filePath);
351
        }
352
353
        return $paths;
354
    }
355
356
    private function loadResources(string $appName, string $context, string $appDir) : void
357
    {
358
        $meta = new Meta($appName, $context, $appDir);
359
        $resMetas = $meta->getGenerator('*');
360
        foreach ($resMetas as $resMeta) {
361
            $this->getInstance($resMeta->class);
362
        }
363
    }
364
365
    private function getInstance(string $interface, string $name = '') : void
366
    {
367
        $dependencyIndex = $interface . '-' . $name;
368
        if (in_array($dependencyIndex, $this->compiled, true)) {
369
            printf("S %s:%s\n", $interface, $name);
370
371
            return;
372
        }
373
        try {
374
            $this->injector->getInstance($interface, $name);
375
            $this->compiled[] = $dependencyIndex;
376
            $this->progress('.');
377
        } catch (Unbound $e) {
378
            if ($dependencyIndex === 'Ray\Aop\MethodInvocation-') {
379
                return;
380
            }
381
            $this->failed[$dependencyIndex] = $e->getMessage();
382
            $this->progress('F');
383
        } catch (Exception $e) {
384
            $this->failed[$dependencyIndex] = sprintf('%s: %s', get_class($e), $e->getMessage());
385
            $this->progress('F');
386
        }
387
    }
388
389
    private function progress(string $char) : void
390
    {
391
        /**
392
         * @var int
393
         */
394
        static $cnt = 0;
395
396
        echo $char;
397
        $cnt++;
398
        if ($cnt === 60) {
399
            $cnt = 0;
400
            echo PHP_EOL;
401
        }
402
    }
403
404
    private function hookNullObjectClass(string $appDir) : void
405
    {
406
        $compileScript = realpath($appDir) . '/.compile.php';
407
        if (file_exists($compileScript)) {
408
            require $compileScript;
409
        }
410
    }
411
}
412