Completed
Push — preload ( 462cf7...41faef )
by Akihito
02:08
created

Compiler::loadResources()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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