Issues (2)

src/ServiceProvider.php (1 issue)

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');
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
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