Completed
Push — preload ( 78b58f...05d45b )
by Akihito
01:56
created

Compiler::loadResources()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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