Completed
Push — preload ( 9bee4e...1dd11b )
by Akihito
04:05 queued 02:36
created

Compiler::runBootstrap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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