Completed
Push — preload ( 78b58f )
by Akihito
02:19
created

Compiler::compileDiScripts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 9.536
c 0
b 0
f 0
cc 2
nc 2
nop 3
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 Doctrine\Common\Annotations\AnnotationReader;
14
use Doctrine\Common\Annotations\Reader;
15
use Doctrine\Common\Cache\Cache;
16
use function file_exists;
17
use Ray\Di\AbstractModule;
18
use Ray\Di\Bind;
19
use Ray\Di\InjectorInterface;
20
use ReflectionClass;
21
22
final class Compiler
23
{
24
    private $classes = [];
25
26
    /**
27
     * Compile application
28
     *
29
     * @param string $appName application name "MyVendor|MyProject"
30
     * @param string $context application context "prod-app"
31
     * @param string $appDir  application path
32
     */
33
    public function __invoke(string $appName, string $context, string $appDir) : string
34
    {
35
        $this->registerLoader($appDir);
36
        $autoload = $this->compileAutoload($appName, $context, $appDir);
37
        $preload = $this->compilePreload($appDir);
38
        $log = $this->compileDiScripts($appName, $context, $appDir);
39
40
        return sprintf("Compile Log: %s\nautoload.php: %s\npreload.php: %s", $log, $autoload, $preload);
41
    }
42
43
    public function registerLoader(string $appDir) : void
44
    {
45
        $loaderFile = $appDir . '/vendor/autoload.php';
46
        if (! file_exists($loaderFile)) {
47
            throw new \RuntimeException('no loader');
48
        }
49
        $loaderFile = require $loaderFile;
50
        spl_autoload_register(
51
            function ($class) use ($loaderFile) {
52
                $loaderFile->loadClass($class);
53
                if ($class !== NullPage::class) {
54
                    $this->classes[] = $class;
55
                }
56
            },
57
            false,
58
            true
59
        );
60
    }
61
62
    public function compileDiScripts(string $appName, string $context, string $appDir) : string
63
    {
64
        $appMeta = new Meta($appName, $context, $appDir);
65
        (new Unlink)->force($appMeta->tmpDir);
66
        $cacheNs = (string) filemtime($appMeta->appDir . '/src');
67
        $injector = new AppInjector($appName, $context, $appMeta, $cacheNs);
68
        $cache = $injector->getInstance(Cache::class);
69
        $reader = $injector->getInstance(AnnotationReader::class);
70
        /* @var $reader \Doctrine\Common\Annotations\Reader */
71
        $namedParams = $injector->getInstance(NamedParameterInterface::class);
72
        /* @var $namedParams NamedParameterInterface */
73
74
        // create DI factory class and AOP compiled class for all resources and save $app cache.
75
        (new Bootstrap)->newApp($appMeta, $context, $cache);
76
77
        // check resource injection and create annotation cache
78
        foreach ($appMeta->getResourceListGenerator() as [$className]) {
79
            $this->scanClass($injector, $reader, $namedParams, (string) $className);
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...
80
        }
81
        $logFile = realpath($appMeta->logDir) . '/compile.log';
82
        $this->saveCompileLog($appMeta, $context, $logFile);
83
84
        return $logFile;
85
    }
86
87
    private function compileAutoload(string $appName, string $context, string $appDir) : string
88
    {
89
        $this->invokeTypicalRequest($appName, $context);
90
        $paths = $this->getPaths($this->classes, $appDir);
91
92
        return $this->dumpAutoload($appDir, $paths);
93
    }
94
95
    private function dumpAutoload(string $appDir, array $paths) : string
96
    {
97
        $autoloadFile = '<?php' . PHP_EOL;
98
        foreach ($paths as $path) {
99
            $autoloadFile .= sprintf(
100
                "require %s';\n",
101
                $this->getRelativePath($appDir, $path)
102
            );
103
        }
104
        $autoloadFile .= "require __DIR__ . '/vendor/autoload.php';" . PHP_EOL;
105
        $loaderFile = realpath($appDir) . '/autoload.php';
106
        file_put_contents($loaderFile, $autoloadFile);
107
108
        return $loaderFile;
109
    }
110
111
    private function compilePreload(string $appDir) : string
112
    {
113
        $paths = $this->getPaths($this->classes, $appDir);
114
        $output = '<?php' . PHP_EOL;
115
        foreach ($paths as $path) {
116
            $output .= sprintf(
117
                "opcache_compile_file(%s');\n",
118
                $this->getRelativePath($appDir, $path)
119
            );
120
        }
121
        $preloadFile = realpath($appDir) . '/preload.php';
122
        file_put_contents($preloadFile, $output);
123
124
        return $preloadFile;
125
    }
126
127
    private function getRelativePath(string $rootDir, string $file) : string
128
    {
129
        $dir = (string) realpath($rootDir);
130
        if (strpos($file, $dir) !== false) {
131
            return (string) preg_replace('#^' . preg_quote($dir, '#') . '#', "__DIR__ . '", $file);
132
        }
133
134
        return $file;
135
    }
136
137
    private function invokeTypicalRequest(string $appName, string $context) : void
138
    {
139
        $app = (new Bootstrap)->getApp($appName, $context);
140
        $ro = new NullPage;
141
        $ro->uri = new Uri('app://self/');
142
        $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...
143
    }
144
145
    private function scanClass(InjectorInterface $injector, Reader $reader, NamedParameterInterface $namedParams, string $className) : void
146
    {
147
        try {
148
            $instance = $injector->getInstance($className);
149
        } catch (\Exception $e) {
150
            error_log(sprintf('Failed to instantiate [%s]: %s(%s) in %s on line %s', $className, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
151
152
            return;
153
        }
154
        assert(class_exists($className));
155
        $class = new ReflectionClass($className);
156
        $reader->getClassAnnotations($class);
157
        $methods = $class->getMethods();
158
        foreach ($methods as $method) {
159
            $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...
160
            if ($this->isMagicMethod($methodName)) {
161
                continue;
162
            }
163
            $this->saveNamedParam($namedParams, $instance, $methodName);
164
            // method annotation
165
            $reader->getMethodAnnotations($method);
166
        }
167
    }
168
169
    private function isMagicMethod(string $method) : bool
170
    {
171
        return \in_array($method, ['__sleep', '__wakeup', 'offsetGet', 'offsetSet', 'offsetExists', 'offsetUnset', 'count', 'ksort', 'asort', 'jsonSerialize'], true);
172
    }
173
174
    private function saveNamedParam(NamedParameterInterface $namedParameter, $instance, string $method) : void
175
    {
176
        // named parameter
177
        if (! \in_array($method, ['onGet', 'onPost', 'onPut', 'onPatch', 'onDelete', 'onHead'], true)) {
178
            return;
179
        }
180
        $callable = [$instance, $method];
181
        if (! is_callable($callable)) {
182
            return;
183
        }
184
        try {
185
            $namedParameter->getParameters($callable, []);
186
        } catch (ParameterException $e) {
187
            return;
188
        }
189
    }
190
191
    private function saveCompileLog(AbstractAppMeta $appMeta, string $context, string $logFile) : void
192
    {
193
        $module = (new Module)($appMeta, $context);
194
        /** @var AbstractModule $module */
195
        $container = $module->getContainer();
196
        foreach ($appMeta->getResourceListGenerator() as [$class]) {
197
            new Bind($container, (string) $class);
0 ignored issues
show
Bug introduced by
The variable $class 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...
198
        }
199
        file_put_contents($logFile, (string) $module);
200
    }
201
202
    private function getPaths(array $classes, string $appDir) : array
203
    {
204
        $paths = [];
205
        foreach ($classes as $class) {
206
            // could be phpdoc tag by annotation loader
207
            $isAutoloadFailed = ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false);
208
            if ($isAutoloadFailed) {
209
                continue;
210
            }
211
            $filePath = (string) (new ReflectionClass($class))->getFileName();
212
            if (! file_exists($filePath) || strpos($filePath, 'phar') === 0) {
213
                continue;
214
            }
215
            $paths[] = $this->getRelativePath($appDir, $filePath);
216
        }
217
218
        return $paths;
219
    }
220
}
221