ServiceProvider::getClassName()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 10
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 instanceof Factory) {
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 instanceof Extension) {
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
        $className = $this->getClassName();
157
158
        $fileName = \ComposerLocator::getPath('thecodingmachine/funky').'/generated/'.
0 ignored issues
show
Bug introduced by
The method getPath() does not exist on ComposerLocator. ( Ignorable by Annotation )

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

158
        $fileName = \ComposerLocator::/** @scrutinizer ignore-call */ getPath('thecodingmachine/funky').'/generated/'.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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