Passed
Pull Request — master (#1)
by David
01:54
created

ServiceProvider::dumpHelper()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 1
nop 0
dl 0
loc 10
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 Interop\Container\ServiceProviderInterface;
8
use ReflectionClass;
9
use TheCodingMachine\Funky\Annotations\Factory;
10
use TheCodingMachine\Funky\Utils\FileSystem;
11
12
class ServiceProvider implements ServiceProviderInterface
13
{
14
    /**
15
     * @var ReflectionClass
16
     */
17
    private $refClass;
18
19
    private static $annotationReader;
20
21
22
    private static function getAnnotationReader() : AnnotationReader
23
    {
24
        if (self::$annotationReader === null) {
25
            self::$annotationReader = new AnnotationReader();
26
        }
27
        return self::$annotationReader;
28
    }
29
30
    /**
31
     * @return FactoryDefinition[]
32
     * @throws \TheCodingMachine\Funky\BadModifierException
33
     */
34
    private function getFactoryDefinitions(): array
35
    {
36
        $refClass = new ReflectionClass($this);
37
        $factories = [];
38
39
        foreach ($refClass->getMethods() as $method) {
40
            $factoryAnnotation = self::getAnnotationReader()->getMethodAnnotation($method, Factory::class);
41
            if ($factoryAnnotation) {
42
                $factories[] = new FactoryDefinition($method, $factoryAnnotation);
43
            }
44
        }
45
46
        return $factories;
47
    }
48
49
    /**
50
     * Returns a list of all container entries registered by this service provider.
51
     *
52
     * - the key is the entry name
53
     * - the value is a callable that will return the entry, aka the **factory**
54
     *
55
     * Factories have the following signature:
56
     *        function(\Psr\Container\ContainerInterface $container)
57
     *
58
     * @return callable[]
59
     */
60
    public function getFactories()
61
    {
62
        [$className, $fileName] = $this->getFileAndClassName();
63
        if (!file_exists($fileName) || filemtime($this->getRefClass()->getFileName()) > filemtime($fileName)) {
64
            $this->dumpHelper();
65
        }
66
67
        require_once $fileName;
68
69
        $factoriesCallable = $className.'::getFactories';
70
71
        return $factoriesCallable();
72
    }
73
74
    /**
75
     * Writes the helper class and returns the file path.
76
     *
77
     * @return string
78
     * @throws \TheCodingMachine\Funky\IoException
79
     */
80
    public function dumpHelper(): string
81
    {
82
        [$className, $tmpFile] = $this->getFileAndClassName();
83
84
        FileSystem::mkdir(dirname($tmpFile));
85
        $result = file_put_contents($tmpFile, $this->dumpServiceProviderHelper($className));
86
        if ($result === false) {
87
            throw IoException::cannotWriteFile($tmpFile);
88
        }
89
        return $tmpFile;
90
    }
91
92
    /**
93
     * @return string[] Returns an array with 2 items: the class name and the file name.
94
     */
95
    private function getFileAndClassName(): array
96
    {
97
        $refClass = $this->getRefClass();
98
        $className = $this->getClassName();
99
100
        $fileName = sys_get_temp_dir().'/funky_cache/'.
101
            str_replace(':', '', dirname($refClass->getFileName()).'/'.
102
            str_replace('\\', '__', $className).'.php');
103
104
        return [$className, $fileName];
105
    }
106
107
    private function getRefClass(): ReflectionClass
108
    {
109
        if ($this->refClass === null) {
110
            $this->refClass = new ReflectionClass($this);
111
        }
112
        return $this->refClass;
113
    }
114
115
    private function getClassName(): string
116
    {
117
        $className = get_class($this).'Helper';
118
        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

118
        if ($this->getRefClass()->/** @scrutinizer ignore-call */ isAnonymous()) {
Loading history...
119
            $className = preg_replace("/[^A-Za-z0-9_\x7f-\xff ]/", '', $className);
120
        }
121
        return $className;
122
    }
123
124
    /**
125
     * Returns the code of a "service provider helper class" that contains generated factory code.
126
     *
127
     * @return string
128
     */
129
    private function dumpServiceProviderHelper(string $className): string
130
    {
131
        $slashPos = strrpos($className, '\\');
132
        if ($slashPos !== false) {
133
            $namespace = 'namespace '.substr($className, 0, $slashPos).";\n";
134
            $shortClassName = substr($className, $slashPos+1);
135
        } else {
136
            $namespace = null;
137
            $shortClassName = $className;
138
        }
139
140
        $factoriesArrayCode = [];
141
        $factories = [];
142
        $factoryCount = 0;
143
        foreach ($this->getFactoryDefinitions() as $definition) {
144
            if ($definition->isPsrFactory()) {
145
                $factoriesArrayCode[] = '            '.var_export($definition->getName(), true).
146
                    ' => ['.var_export($definition->getReflectionMethod()->getDeclaringClass()->getName(), true).
147
                    ', '.var_export($definition->getReflectionMethod()->getName(), true)."],\n";
148
            } else {
149
                $factoryCount++;
150
                $localFactoryName = 'factory'.$factoryCount;
151
                $factoriesArrayCode[] = '            '.var_export($definition->getName(), true).
152
                    ' => [self::class, '.var_export($localFactoryName, true)."],\n";
153
                $factories[] = $definition->buildFactoryCode($localFactoryName);
154
            }
155
            foreach ($definition->getAliases() as $alias) {
156
                $factoriesArrayCode[] = '            '.var_export($alias, true).
157
                    ' => new Alias('.var_export($definition->getName(), true)."),\n";
158
            }
159
        }
160
161
        $factoriesArrayStr = implode("\n", $factoriesArrayCode);
162
        $factoriesStr = implode("\n", $factories);
163
164
        $code = <<<EOF
165
<?php
166
$namespace
167
168
use Interop\Container\Factories\Alias;
169
use Psr\Container\ContainerInterface;
170
171
final class $shortClassName
172
{
173
    public static function getFactories(): array
174
    {
175
        return [
176
$factoriesArrayStr
177
        ];
178
    }
179
    
180
$factoriesStr
181
}
182
EOF;
183
184
        return $code;
185
    }
186
187
    /**
188
     * Returns a list of all container entries extended by this service provider.
189
     *
190
     * - the key is the entry name
191
     * - the value is a callable that will return the modified entry
192
     *
193
     * Callables have the following signature:
194
     *        function(Psr\Container\ContainerInterface $container, $previous)
195
     *     or function(Psr\Container\ContainerInterface $container, $previous = null)
196
     *
197
     * About factories parameters:
198
     *
199
     * - the container (instance of `Psr\Container\ContainerInterface`)
200
     * - the entry to be extended. If the entry to be extended does not exist and the parameter is nullable,
201
     *   `null` will be passed.
202
     *
203
     * @return callable[]
204
     */
205
    public function getExtensions()
206
    {
207
        // TODO: Implement getExtensions() method.
208
    }
209
}
210