Completed
Pull Request — 1.x (#320)
by Akihito
01:39
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\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 1
    private $ns;
34
35 1
    /**
36 1
     * Compile application
37
     *
38 1
     * @param string $appName application name "MyVendor|MyProject"
39
     * @param string $context application context "prod-app"
40
     * @param string $appDir  application path
41 1
     */
42
    public function __invoke(string $appName, string $context, string $appDir) : string
43 1
    {
44 1
        $this->registerLoader($appDir);
45 1
        $autoload = $this->compileAutoload($appName, $context, $appDir);
46 1
        $preload = $this->compilePreload($appName, $context, $appDir);
47 1
        $log = $this->compileDiScripts($appName, $context, $appDir);
48 1
        $this->ns = (string) filemtime(realpath($appDir) . '/src');
49
50 1
        return sprintf("Compile Log: %s\nautoload.php: %s\npreload.php: %s", $log, $autoload, $preload);
51
    }
52
53
    public function registerLoader(string $appDir) : void
54 1
    {
55
        $loaderFile = $appDir . '/vendor/autoload.php';
56
        if (! file_exists($loaderFile)) {
57 1
            throw new \RuntimeException('no loader');
58 1
        }
59
        $loaderFile = require $loaderFile;
60 1
        spl_autoload_register(
61 1
            function ($class) use ($loaderFile) {
62
                $loaderFile->loadClass($class);
63 1
                if ($class !== NullPage::class) {
64
                    $this->classes[] = $class;
65
                }
66 1
            },
67
            false,
68 1
            true
69 1
        );
70 1
    }
71
72
    public function compileDiScripts(string $appName, string $context, string $appDir) : string
73
    {
74
        $appMeta = new Meta($appName, $context, $appDir);
75
        $injector = new AppInjector($appName, $context, $appMeta, $this->ns);
76
        $cache = $injector->getInstance(Cache::class);
77
        $reader = $injector->getInstance(AnnotationReader::class);
78
        /* @var $reader \Doctrine\Common\Annotations\Reader */
79
        $namedParams = $injector->getInstance(NamedParameterInterface::class);
80
        /* @var $namedParams NamedParameterInterface */
81
82
        // create DI factory class and AOP compiled class for all resources and save $app cache.
83
        (new Bootstrap)->newApp($appMeta, $context, $cache);
84
85
        // check resource injection and create annotation cache
86
        foreach ($appMeta->getResourceListGenerator() as [$className]) {
87
            $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...
88
        }
89
        $logFile = realpath($appMeta->logDir) . '/compile.log';
90
        $this->saveCompileLog($appMeta, $context, $logFile);
91
92
        return $logFile;
93
    }
94
95
    private function compileAutoload(string $appName, string $context, string $appDir) : string
96
    {
97
        $this->invokeTypicalRequest($appName, $context);
98
        $paths = $this->getPaths($this->classes, $appDir);
99
100
        return $this->dumpAutoload($appDir, $paths);
101
    }
102
103
    private function dumpAutoload(string $appDir, array $paths) : string
104
    {
105
        $autoloadFile = '<?php' . PHP_EOL;
106
        foreach ($paths as $path) {
107
            $autoloadFile .= sprintf(
108
                "require %s';\n",
109
                $this->getRelativePath($appDir, $path)
110
            );
111
        }
112
        $autoloadFile .= "require __DIR__ . '/vendor/autoload.php';" . PHP_EOL;
113
        $loaderFile = realpath($appDir) . '/autoload.php';
114
        file_put_contents($loaderFile, $autoloadFile);
115
116
        return $loaderFile;
117
    }
118
119
    private function compilePreload(string $appName, string $context, string $appDir) : string
0 ignored issues
show
Unused Code introduced by
The parameter $appName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
120
    {
121 1
        //$this->loadResources($appName, $context, $appDir);
122
        $paths = $this->getPaths($this->classes, $appDir);
123
        $output = '<?php' . PHP_EOL;
124 1
        $output .= "opcache_compile_file(__DIR__ . '/vendor/autoload.php');" . PHP_EOL;
125
        foreach ($paths as $path) {
126
            $output .= sprintf(
127
                "opcache_compile_file(%s');\n",
128
                $this->getRelativePath($appDir, $path)
129
            );
130 1
        }
131 1
        $preloadFile = realpath($appDir) . '/preload.php';
132 1
        file_put_contents($preloadFile, $output);
133 1
134 1
        return $preloadFile;
135 1
    }
136 1
137
    private function getRelativePath(string $rootDir, string $file) : string
138 1
    {
139
        $dir = (string) realpath($rootDir);
140 1
        if (strpos($file, $dir) !== false) {
141
            return (string) preg_replace('#^' . preg_quote($dir, '#') . '#', "__DIR__ . '", $file);
142 1
        }
143
144 1
        return $file;
145
    }
146 1
147
    private function invokeTypicalRequest(string $appName, string $context) : void
148
    {
149 1
        $app = (new Bootstrap)->getApp($appName, $context);
150
        $ro = new NullPage;
151
        $ro->uri = new Uri('app://self/');
152 1
        $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...
153 1
    }
154
155
    private function scanClass(InjectorInterface $injector, Reader $reader, NamedParameterInterface $namedParams, string $className) : void
156 1
    {
157 1
        try {
158 1
            $instance = $injector->getInstance($className);
159
        } catch (\Exception $e) {
160 1
            error_log(sprintf('Failed to instantiate [%s]: %s(%s) in %s on line %s', $className, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
161
162 1
            return;
163
        }
164 1
        assert(class_exists($className));
165
        $class = new ReflectionClass($className);
166 1
        $reader->getClassAnnotations($class);
167 1
        $methods = $class->getMethods();
168 1
        foreach ($methods as $method) {
169
            $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...
170 1
            if ($this->isMagicMethod($methodName)) {
171 1
                continue;
172
            }
173
            $this->saveNamedParam($namedParams, $instance, $methodName);
174
            // method annotation
175
            $reader->getMethodAnnotations($method);
176
        }
177
    }
178
179
    private function isMagicMethod(string $method) : bool
180
    {
181
        return \in_array($method, ['__sleep', '__wakeup', 'offsetGet', 'offsetSet', 'offsetExists', 'offsetUnset', 'count', 'ksort', 'asort', 'jsonSerialize'], true);
182
    }
183
184
    private function saveNamedParam(NamedParameterInterface $namedParameter, $instance, string $method) : void
185
    {
186
        // named parameter
187
        if (! \in_array($method, ['onGet', 'onPost', 'onPut', 'onPatch', 'onDelete', 'onHead'], true)) {
188
            return;
189
        }
190
        $callable = [$instance, $method];
191
        if (! is_callable($callable)) {
192
            return;
193
        }
194
        try {
195
            $namedParameter->getParameters($callable, []);
196
        } catch (ParameterException $e) {
197
            return;
198
        }
199
    }
200
201
    private function saveCompileLog(AbstractAppMeta $appMeta, string $context, string $logFile) : void
202
    {
203
        $module = (new Module)($appMeta, $context);
204
        /** @var AbstractModule $module */
205
        $container = $module->getContainer();
206
        foreach ($appMeta->getResourceListGenerator() as [$class]) {
207
            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...
208
        }
209
        file_put_contents($logFile, (string) $module);
210
    }
211
212
    private function getPaths(array $classes, string $appDir) : array
213
    {
214
        $paths = [];
215
        foreach ($classes as $class) {
216
            // could be phpdoc tag by annotation loader
217
            $isAutoloadFailed = ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false);
218
            if ($isAutoloadFailed) {
219
                continue;
220
            }
221
            $filePath = (string) (new ReflectionClass($class))->getFileName();
222
            if (! file_exists($filePath) || strpos($filePath, 'phar') === 0) {
223
                continue;
224
            }
225
            $paths[] = $this->getRelativePath($appDir, $filePath);
226
        }
227
228
        return $paths;
229
    }
230
231
    private function loadResources(string $appName, string $context, string $appDir) : void
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
232
    {
233
        $meta = new Meta($appName, $context, $appDir);
234
        /* @var ResMeta $resMeta */
235
        $injector = new AppInjector($appName, $context, $meta, $this->ns);
236
        foreach ($meta->getGenerator('*') as $resMeta) {
237
            $injector->getInstance($resMeta->class);
238
        }
239
    }
240
}
241