Completed
Push — cleanup ( 4198cf )
by Akihito
02:04
created

Compiler::loadResources()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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