Completed
Push — compile ( 4bfe5a )
by Akihito
02:32
created

Compiler::__invoke()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

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