Completed
Push — 1.x ( 354cf5...bb1524 )
by Akihito
23s queued 11s
created

Compiler::getFileInfo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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