Completed
Pull Request — 1.x (#333)
by Akihito
04:02 queued 02:39
created

Compiler::compilePreload()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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