Completed
Push — fix-compile-autoload ( 9c5dea )
by Akihito
03:14
created

Compiler::hookNullObjectClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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