Passed
Pull Request — master (#3)
by David
01:58
created

ServiceProvider::getFactories()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 9.4285
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 Doctrine\Common\Annotations\AnnotationRegistry;
8
use Interop\Container\ServiceProviderInterface;
9
use ReflectionClass;
10
use TheCodingMachine\Funky\Annotations\Extension;
11
use TheCodingMachine\Funky\Annotations\Factory;
12
use TheCodingMachine\Funky\Annotations\Tag;
13
use TheCodingMachine\Funky\Utils\FileSystem;
14
15
class ServiceProvider implements ServiceProviderInterface
16
{
17
    /**
18
     * @var ReflectionClass
19
     */
20
    private $refClass;
21
    /**
22
     * @var string
23
     */
24
    private $className;
25
26
    private static $annotationReader;
27
28
29
    private static function getAnnotationReader() : AnnotationReader
30
    {
31
        if (self::$annotationReader === null) {
32
            AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...istry::registerLoader() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists') ( Ignorable by Annotation )

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

32
            /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerLoader('class_exists');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
33
34
35
            self::$annotationReader = new AnnotationReader();
36
        }
37
        return self::$annotationReader;
38
    }
39
40
    /**
41
     * @return FactoryDefinition[]
42
     */
43
    private function getFactoryDefinitions(): array
44
    {
45
        $refClass = $this->getRefClass();
46
        $factories = [];
47
48
        foreach ($refClass->getMethods() as $method) {
49
            $factoryAnnotation = self::getAnnotationReader()->getMethodAnnotation($method, Factory::class);
50
            if ($factoryAnnotation) {
51
                $factories[] = new FactoryDefinition($method, $factoryAnnotation);
52
            }
53
        }
54
55
        return $factories;
56
    }
57
58
    /**
59
     * @return ExtensionDefinition[]
60
     */
61
    private function getExtensionDefinitions(): array
62
    {
63
        $refClass = $this->getRefClass();
64
        $extensions = [];
65
66
        foreach ($refClass->getMethods() as $method) {
67
            $extensionAnnotation = self::getAnnotationReader()->getMethodAnnotation($method, Extension::class);
68
            if ($extensionAnnotation) {
69
                $extensions[] = new ExtensionDefinition($method, $extensionAnnotation);
70
            }
71
        }
72
73
        return $extensions;
74
    }
75
76
    private function init(): void
77
    {
78
        if ($this->className === null) {
79
            [$className, $fileName] = $this->getFileAndClassName();
80
            if (!file_exists($fileName) || filemtime($this->getRefClass()->getFileName()) > filemtime($fileName)) {
81
                $this->dumpHelper();
82
            }
83
84
            require_once $fileName;
85
86
            $this->className = $className;
87
        }
88
    }
89
90
    /**
91
     * Returns a list of all container entries registered by this service provider.
92
     *
93
     * - the key is the entry name
94
     * - the value is a callable that will return the entry, aka the **factory**
95
     *
96
     * Factories have the following signature:
97
     *        function(\Psr\Container\ContainerInterface $container)
98
     *
99
     * @return callable[]
100
     */
101
    public function getFactories()
102
    {
103
        $this->init();
104
        $factoriesCallable = $this->className.'::getFactories';
105
        return $factoriesCallable();
106
    }
107
108
    /**
109
     * Returns a list of all container entries extended by this service provider.
110
     *
111
     * - the key is the entry name
112
     * - the value is a callable that will return the modified entry
113
     *
114
     * Callables have the following signature:
115
     *        function(Psr\Container\ContainerInterface $container, $previous)
116
     *     or function(Psr\Container\ContainerInterface $container, $previous = null)
117
     *
118
     * About factories parameters:
119
     *
120
     * - the container (instance of `Psr\Container\ContainerInterface`)
121
     * - the entry to be extended. If the entry to be extended does not exist and the parameter is nullable,
122
     *   `null` will be passed.
123
     *
124
     * @return callable[]
125
     */
126
    public function getExtensions()
127
    {
128
        $this->init();
129
        $extensionsCallable = $this->className.'::getExtensions';
130
        return $extensionsCallable();
131
    }
132
133
    /**
134
     * Writes the helper class and returns the file path.
135
     *
136
     * @return string
137
     * @throws \TheCodingMachine\Funky\IoException
138
     */
139
    public function dumpHelper(): string
140
    {
141
        [$className, $tmpFile] = $this->getFileAndClassName();
142
143
        FileSystem::mkdir(dirname($tmpFile));
144
        $result = file_put_contents($tmpFile, $this->dumpServiceProviderHelper($className));
145
        if ($result === false) {
146
            throw IoException::cannotWriteFile($tmpFile);
147
        }
148
        return $tmpFile;
149
    }
150
151
    /**
152
     * @return string[] Returns an array with 2 items: the class name and the file name.
153
     */
154
    private function getFileAndClassName(): array
155
    {
156
        $refClass = $this->getRefClass();
157
        $className = $this->getClassName();
158
159
        $fileName = sys_get_temp_dir().'/funky_cache/'.
160
            str_replace(':', '', dirname($refClass->getFileName()).'/'.
161
            str_replace('\\', '__', $className).'.php');
162
163
        return [$className, $fileName];
164
    }
165
166
    private function getRefClass(): ReflectionClass
167
    {
168
        if ($this->refClass === null) {
169
            $this->refClass = new ReflectionClass($this);
170
        }
171
        return $this->refClass;
172
    }
173
174
    private function getClassName(): string
175
    {
176
        $className = get_class($this).'Helper';
177
        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

177
        if ($this->getRefClass()->/** @scrutinizer ignore-call */ isAnonymous()) {
Loading history...
178
            $className = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $className);
179
        }
180
        return $className;
181
    }
182
183
    /**
184
     * Returns the code of a "service provider helper class" that contains generated factory code.
185
     *
186
     * @return string
187
     */
188
    private function dumpServiceProviderHelper(string $className): string
189
    {
190
        $slashPos = strrpos($className, '\\');
191
        if ($slashPos !== false) {
192
            $namespace = 'namespace '.substr($className, 0, $slashPos).";\n";
193
            $shortClassName = substr($className, $slashPos+1);
194
        } else {
195
            $namespace = null;
196
            $shortClassName = $className;
197
        }
198
199
        $factoriesArrayCode = [];
200
        $factories = [];
201
        $factoryCount = 0;
202
203
        $extensionsArrayCode = [];
204
        $extensions = [];
205
        $extensionCount = 0;
206
207
        $factoriesDefinitions = $this->getFactoryDefinitions();
208
209
        foreach ($factoriesDefinitions as $definition) {
210
            if ($definition->isPsrFactory()) {
211
                $factoriesArrayCode[] = '            '.var_export($definition->getName(), true).
212
                    ' => ['.var_export($definition->getReflectionMethod()->getDeclaringClass()->getName(), true).
213
                    ', '.var_export($definition->getReflectionMethod()->getName(), true)."],\n";
214
            } else {
215
                $factoryCount++;
216
                $localFactoryName = 'factory'.$factoryCount;
217
                $factoriesArrayCode[] = '            '.var_export($definition->getName(), true).
218
                    ' => [self::class, '.var_export($localFactoryName, true)."],\n";
219
                $factories[] = $definition->buildFactoryCode($localFactoryName);
220
            }
221
            foreach ($definition->getAliases() as $alias) {
222
                $factoriesArrayCode[] = '            '.var_export($alias, true).
223
                    ' => new Alias('.var_export($definition->getName(), true)."),\n";
224
            }
225
        }
226
227
        $extensionsDefinitions = $this->getExtensionDefinitions();
228
229
        foreach ($extensionsDefinitions as $definition) {
230
            $extensionCount++;
231
            $localExtensionName = 'extension'.$extensionCount;
232
            $extensionsArrayCode[] = '            '.var_export($definition->getName(), true).
233
                ' => [self::class, '.var_export($localExtensionName, true)."],\n";
234
            $extensions[] = $definition->buildExtensionCode($localExtensionName);
235
        }
236
237
        // Now, let's handle tags.
238
        // Let's build an array of tags with a list of services in it.
239
        $tags = [];
240
        foreach ($factoriesDefinitions as $factoryDefinition) {
241
            foreach ($factoryDefinition->getTags() as $tag) {
242
                $tags[$tag->getName()][] = [
243
                    'taggedService' => $factoryDefinition->getName(),
244
                    'priority' => $tag->getPriority()
245
                ];
246
            }
247
        }
248
        foreach ($extensionsDefinitions as $extensionDefinition) {
249
            foreach ($extensionDefinition->getTags() as $tag) {
250
                $tags[$tag->getName()][] = [
251
                    'taggedService' => $extensionDefinition->getName(),
252
                    'priority' => $tag->getPriority()
253
                ];
254
            }
255
        }
256
257
        foreach ($tags as $tagName => $taggedServices) {
258
            $tagMethodName = 'tag__'.$tagName;
259
            $tagMethodName = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $tagMethodName);
260
261
            $extensionsArrayCode[] = '            '.var_export($tagName, true).
262
                ' => [self::class, '.var_export($tagMethodName, true)."],\n";
263
            $extensions[] = $this->buildTagsCode($tagMethodName, $taggedServices);
264
        }
265
266
        $factoriesArrayStr = implode("\n", $factoriesArrayCode);
267
        $factoriesStr = implode("\n", $factories);
268
269
        $extensionsArrayStr = implode("\n", $extensionsArrayCode);
270
        $extensionsStr = implode("\n", $extensions);
271
272
        $code = <<<EOF
273
<?php
274
$namespace
275
276
use Interop\Container\Factories\Alias;
277
use Psr\Container\ContainerInterface;
278
279
final class $shortClassName
280
{
281
    public static function getFactories(): array
282
    {
283
        return [
284
$factoriesArrayStr
285
        ];
286
    }
287
    
288
    public static function getExtensions(): array
289
    {
290
        return [
291
$extensionsArrayStr
292
        ];
293
    }
294
    
295
$factoriesStr
296
$extensionsStr
297
}
298
EOF;
299
300
        return $code;
301
    }
302
303
    private function buildTagsCode(string $tagMethodName, array $taggedServices): string
304
    {
305
        $inserts = [];
306
307
        foreach ($taggedServices as $tag) {
308
            ['taggedService' => $taggedService, 'priority' => $priority] = $tag;
309
            $inserts[] = sprintf(
310
                '        $queue->insert($container->get(%s), %s);',
311
                var_export($taggedService, true),
312
                var_export($priority, true)
313
            );
314
        }
315
316
317
        return sprintf(
318
            <<<EOF
319
    public static function %s(ContainerInterface \$container, ?\SplPriorityQueue \$queue): \SplPriorityQueue
320
    {
321
        \$queue = \$queue ?: new \SplPriorityQueue();
322
%s
323
        return \$queue;
324
    }
325
    
326
EOF
327
            ,
328
            $tagMethodName,
329
            implode("\n", $inserts)
330
        );
331
    }
332
}
333