Completed
Push — master ( 5b7dfb...3e297a )
by David
12s
created

ServiceProvider::init()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 0
dl 0
loc 11
rs 9.2
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\Funky;
5
6
use Doctrine\Common\Annotations\AnnotationReader;
7
use Interop\Container\ServiceProviderInterface;
8
use ReflectionClass;
9
use TheCodingMachine\Funky\Annotations\Extension;
10
use TheCodingMachine\Funky\Annotations\Factory;
11
use TheCodingMachine\Funky\Utils\FileSystem;
12
13
class ServiceProvider implements ServiceProviderInterface
14
{
15
    /**
16
     * @var ReflectionClass
17
     */
18
    private $refClass;
19
    /**
20
     * @var string
21
     */
22
    private $className;
23
24
    private static $annotationReader;
25
26
27
    private static function getAnnotationReader() : AnnotationReader
28
    {
29
        if (self::$annotationReader === null) {
30
            self::$annotationReader = new AnnotationReader();
31
        }
32
        return self::$annotationReader;
33
    }
34
35
    /**
36
     * @return FactoryDefinition[]
37
     */
38
    private function getFactoryDefinitions(): array
39
    {
40
        $refClass = $this->getRefClass();
41
        $factories = [];
42
43
        foreach ($refClass->getMethods() as $method) {
44
            $factoryAnnotation = self::getAnnotationReader()->getMethodAnnotation($method, Factory::class);
45
            if ($factoryAnnotation) {
46
                $factories[] = new FactoryDefinition($method, $factoryAnnotation);
47
            }
48
        }
49
50
        return $factories;
51
    }
52
53
    /**
54
     * @return ExtensionDefinition[]
55
     */
56
    private function getExtensionDefinitions(): array
57
    {
58
        $refClass = $this->getRefClass();
59
        $extensions = [];
60
61
        foreach ($refClass->getMethods() as $method) {
62
            $extensionAnnotation = self::getAnnotationReader()->getMethodAnnotation($method, Extension::class);
63
            if ($extensionAnnotation) {
64
                $extensions[] = new ExtensionDefinition($method, $extensionAnnotation);
65
            }
66
        }
67
68
        return $extensions;
69
    }
70
71
    private function init(): void
72
    {
73
        if ($this->className === null) {
74
            [$className, $fileName] = $this->getFileAndClassName();
75
            if (!file_exists($fileName) || filemtime($this->getRefClass()->getFileName()) > filemtime($fileName)) {
76
                $this->dumpHelper();
77
            }
78
79
            require_once $fileName;
80
81
            $this->className = $className;
82
        }
83
    }
84
85
    /**
86
     * Returns a list of all container entries registered by this service provider.
87
     *
88
     * - the key is the entry name
89
     * - the value is a callable that will return the entry, aka the **factory**
90
     *
91
     * Factories have the following signature:
92
     *        function(\Psr\Container\ContainerInterface $container)
93
     *
94
     * @return callable[]
95
     */
96
    public function getFactories()
97
    {
98
        $this->init();
99
        $factoriesCallable = $this->className.'::getFactories';
100
        return $factoriesCallable();
101
    }
102
103
    /**
104
     * Returns a list of all container entries extended by this service provider.
105
     *
106
     * - the key is the entry name
107
     * - the value is a callable that will return the modified entry
108
     *
109
     * Callables have the following signature:
110
     *        function(Psr\Container\ContainerInterface $container, $previous)
111
     *     or function(Psr\Container\ContainerInterface $container, $previous = null)
112
     *
113
     * About factories parameters:
114
     *
115
     * - the container (instance of `Psr\Container\ContainerInterface`)
116
     * - the entry to be extended. If the entry to be extended does not exist and the parameter is nullable,
117
     *   `null` will be passed.
118
     *
119
     * @return callable[]
120
     */
121
    public function getExtensions()
122
    {
123
        $this->init();
124
        $extensionsCallable = $this->className.'::getExtensions';
125
        return $extensionsCallable();
126
    }
127
128
    /**
129
     * Writes the helper class and returns the file path.
130
     *
131
     * @return string
132
     * @throws \TheCodingMachine\Funky\IoException
133
     */
134
    public function dumpHelper(): string
135
    {
136
        [$className, $tmpFile] = $this->getFileAndClassName();
137
138
        FileSystem::mkdir(dirname($tmpFile));
139
        $result = file_put_contents($tmpFile, $this->dumpServiceProviderHelper($className));
140
        if ($result === false) {
141
            throw IoException::cannotWriteFile($tmpFile);
142
        }
143
        return $tmpFile;
144
    }
145
146
    /**
147
     * @return string[] Returns an array with 2 items: the class name and the file name.
148
     */
149
    private function getFileAndClassName(): array
150
    {
151
        $refClass = $this->getRefClass();
152
        $className = $this->getClassName();
153
154
        $fileName = sys_get_temp_dir().'/funky_cache/'.
155
            str_replace(':', '', dirname($refClass->getFileName()).'/'.
156
            str_replace('\\', '__', $className).'.php');
157
158
        return [$className, $fileName];
159
    }
160
161
    private function getRefClass(): ReflectionClass
162
    {
163
        if ($this->refClass === null) {
164
            $this->refClass = new ReflectionClass($this);
165
        }
166
        return $this->refClass;
167
    }
168
169
    private function getClassName(): string
170
    {
171
        $className = get_class($this).'Helper';
172
        if ($this->getRefClass()->isAnonymous()) {
0 ignored issues
show
Bug introduced by
The method isAnonymous() does not exist on ReflectionClass. It seems like you code against a sub-type of ReflectionClass such as Roave\BetterReflection\R...Adapter\ReflectionClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

172
        if ($this->getRefClass()->/** @scrutinizer ignore-call */ isAnonymous()) {
Loading history...
173
            $className = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $className);
174
        }
175
        return $className;
176
    }
177
178
    /**
179
     * Returns the code of a "service provider helper class" that contains generated factory code.
180
     *
181
     * @return string
182
     */
183
    private function dumpServiceProviderHelper(string $className): string
184
    {
185
        $slashPos = strrpos($className, '\\');
186
        if ($slashPos !== false) {
187
            $namespace = 'namespace '.substr($className, 0, $slashPos).";\n";
188
            $shortClassName = substr($className, $slashPos+1);
189
        } else {
190
            $namespace = null;
191
            $shortClassName = $className;
192
        }
193
194
        $factoriesArrayCode = [];
195
        $factories = [];
196
        $factoryCount = 0;
197
        foreach ($this->getFactoryDefinitions() as $definition) {
198
            if ($definition->isPsrFactory()) {
199
                $factoriesArrayCode[] = '            '.var_export($definition->getName(), true).
200
                    ' => ['.var_export($definition->getReflectionMethod()->getDeclaringClass()->getName(), true).
201
                    ', '.var_export($definition->getReflectionMethod()->getName(), true)."],\n";
202
            } else {
203
                $factoryCount++;
204
                $localFactoryName = 'factory'.$factoryCount;
205
                $factoriesArrayCode[] = '            '.var_export($definition->getName(), true).
206
                    ' => [self::class, '.var_export($localFactoryName, true)."],\n";
207
                $factories[] = $definition->buildFactoryCode($localFactoryName);
208
            }
209
            foreach ($definition->getAliases() as $alias) {
210
                $factoriesArrayCode[] = '            '.var_export($alias, true).
211
                    ' => new Alias('.var_export($definition->getName(), true)."),\n";
212
            }
213
        }
214
215
        $factoriesArrayStr = implode("\n", $factoriesArrayCode);
216
        $factoriesStr = implode("\n", $factories);
217
218
        $extensionsArrayCode = [];
219
        $extensions = [];
220
        $extensionCount = 0;
221
        foreach ($this->getExtensionDefinitions() as $definition) {
222
            $extensionCount++;
223
            $localExtensionName = 'extension'.$extensionCount;
224
            $extensionsArrayCode[] = '            '.var_export($definition->getName(), true).
225
                ' => [self::class, '.var_export($localExtensionName, true)."],\n";
226
            $extensions[] = $definition->buildExtensionCode($localExtensionName);
227
        }
228
229
        $extensionsArrayStr = implode("\n", $extensionsArrayCode);
230
        $extensionsStr = implode("\n", $extensions);
231
232
        $code = <<<EOF
233
<?php
234
$namespace
235
236
use Interop\Container\Factories\Alias;
237
use Psr\Container\ContainerInterface;
238
239
final class $shortClassName
240
{
241
    public static function getFactories(): array
242
    {
243
        return [
244
$factoriesArrayStr
245
        ];
246
    }
247
    
248
    public static function getExtensions(): array
249
    {
250
        return [
251
$extensionsArrayStr
252
        ];
253
    }
254
    
255
$factoriesStr
256
$extensionsStr
257
}
258
EOF;
259
260
        return $code;
261
    }
262
}
263