Completed
Push — master ( 3e297a...bce447 )
by David
15s
created

ServiceProvider::dumpServiceProviderHelper()   F

Complexity

Conditions 11
Paths 360

Size

Total Lines 113
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 66
nc 360
nop 1
dl 0
loc 113
rs 3.8181
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    /**
304
     * @param string $tagMethodName
305
     * @param array[] $taggedServices
306
     * @return string
307
     */
308
    private function buildTagsCode(string $tagMethodName, array $taggedServices): string
309
    {
310
        $inserts = [];
311
312
        foreach ($taggedServices as $tag) {
313
            ['taggedService' => $taggedService, 'priority' => $priority] = $tag;
314
            $inserts[] = sprintf(
315
                '        $queue->insert($container->get(%s), %s);',
316
                var_export($taggedService, true),
317
                var_export($priority, true)
318
            );
319
        }
320
321
322
        return sprintf(
323
            <<<EOF
324
    public static function %s(ContainerInterface \$container, ?\SplPriorityQueue \$queue): \SplPriorityQueue
325
    {
326
        \$queue = \$queue ?: new \SplPriorityQueue();
327
%s
328
        return \$queue;
329
    }
330
    
331
EOF
332
            ,
333
            $tagMethodName,
334
            implode("\n", $inserts)
335
        );
336
    }
337
}
338