Issues (23)

src/DefinitionLoader/AutowireDefinitionLoader.php (1 issue)

1
<?php
2
3
/**
4
 * This file is part of slick/di package
5
 *
6
 * For the full copyright and license information, please view the LICENSE.md
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Di\DefinitionLoader;
11
12
use ArrayObject;
13
use Exception;
14
use RecursiveDirectoryIterator;
15
use RecursiveIteratorIterator;
16
use RegexIterator;
17
use Slick\Di\Container;
18
use Slick\Di\ContainerAwareInterface;
19
use Slick\Di\ContainerInterface;
20
use Slick\Di\Definition\Factory;
21
use Slick\Di\DefinitionLoader\AutowireDefinitionLoader\ClassFile;
22
use Slick\Di\DefinitionLoaderInterface;
23
use Slick\Di\Exception\AmbiguousImplementationException;
24
use Slick\Di\Exception\InvalidDefinitionsPathException;
25
use Slick\FsWatch\Directory;
26
use Slick\FsWatch\Exception\DirectoryNotAccecible;
27
use Slick\FsWatch\Exception\DirectoryNotFound;
28
use SplFileInfo;
29
use Traversable;
30
31
/**
32
 * AutowireDefinitionLoader
33
 *
34
 * @package Slick\Di\DefinitionLoader
35
 * @author  Filipe Silva <[email protected]>
36
 */
37
class AutowireDefinitionLoader implements DefinitionLoaderInterface, ContainerAwareInterface
38
{
39
    private ?ContainerInterface $container = null;
40
41
    private const TMP_FILE_NAME = '/_slick_di_autowire';
42
43
    /**
44
     * @var array<ClassFile>|ClassFile[]
45
     */
46
    private array $files = [];
47
48
    private array $definitions = [];
49
50
    protected array $implementations = [];
51
52
    private Directory $directoryWatcher;
53
54
    public function __construct(string $path)
55
    {
56
        try {
57
            $this->directoryWatcher = new Directory($path);
58
        } catch (DirectoryNotFound|DirectoryNotAccecible) {
59
            throw new InvalidDefinitionsPathException(
60
                'Provided autowire definitions path is not valid or is not found. ' .
61
                'Could not create container. Please check ' . $path
62
            );
63
        }
64
65
        if (!$this->loadImplementations()) {
66
            $this->loadFiles($path);
67
            $this->organiseImplementations();
68
        }
69
70
        $this->createDefinitions();
71
    }
72
73
    public function getIterator(): Traversable
74
    {
75
        return new ArrayObject($this->definitions);
76
    }
77
78
    public function setContainer(ContainerInterface $container): self
79
    {
80
        $this->container = $container;
81
        return $this;
82
    }
83
84
    public function getContainer(): ContainerInterface
85
    {
86
        return $this->container;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->container could return the type null which is incompatible with the type-hinted return Slick\Di\ContainerInterface. Consider adding an additional type-check to rule them out.
Loading history...
87
    }
88
89
    /**
90
     * @param string $path
91
     * @return void
92
     */
93
    public function loadFiles(string $path): void
94
    {
95
        try {
96
            $directory = new RecursiveDirectoryIterator($path);
97
        } catch (Exception) {
98
            throw new InvalidDefinitionsPathException(
99
                'Provided autowire definitions path is not valid or is not found. ' .
100
                'Could not create container. Please check ' . $path
101
            );
102
        }
103
104
        $iterator = new RecursiveIteratorIterator($directory);
105
        $phpFiles = new RegexIterator($iterator, '/.*\.php$/i');
106
107
        /** @var SplFileInfo $phpFile */
108
        foreach ($phpFiles as $phpFile) {
109
            $classFile = new ClassFile($phpFile->getPathname());
110
            if ($classFile->isAnImplementation()) {
111
                $this->files[] = $classFile;
112
            }
113
        }
114
    }
115
116
    /**
117
     * Organizes the implementations based on interfaces and parent classes.
118
     *
119
     * @return void
120
     */
121
    private function organiseImplementations(): void
122
    {
123
        foreach ($this->files as $file) {
124
            foreach ($file->interfaces() as $interface) {
125
                $entry = array_key_exists($interface, $this->implementations) ? $this->implementations[$interface] : [];
126
                if (in_array($file->className(), $entry)) {
127
                    continue;
128
                }
129
                $entry[] = $file->className();
130
                $this->implementations[$interface] = $entry;
131
            }
132
133
            if (!$file->parentClass()) {
134
                continue;
135
            }
136
137
            $entry = array_key_exists($file->parentClass(), $this->implementations)
138
                ? $this->implementations[$file->parentClass()]
139
                : [];
140
141
            if (!in_array($file->className(), $entry)) {
142
                $entry[] = $file->className();
143
                $this->implementations[$file->parentClass()] = $entry;
144
            }
145
        }
146
        $this->saveImplementations();
147
    }
148
149
    private function saveImplementations(): void
150
    {
151
        $file = sys_get_temp_dir() . self::TMP_FILE_NAME;
152
        if (is_file($file)) {
153
            unlink($file);
154
        }
155
        $data = [
156
            'snapshot' => $this->directoryWatcher->snapshot(),
157
            'implementations' => $this->implementations,
158
        ];
159
        file_put_contents($file, serialize($data));
160
    }
161
162
    private function loadImplementations(): bool
163
    {
164
        $file = sys_get_temp_dir() . self::TMP_FILE_NAME;
165
        if (!file_exists($file)) {
166
            return false;
167
        }
168
169
        $cachedData = unserialize(file_get_contents($file));
170
171
        /** @var Directory\Snapshot $snapshot */
172
        $snapshot = $cachedData['snapshot'];
173
        if ($this->directoryWatcher->hasChanged($snapshot)) {
174
            return false;
175
        }
176
177
        $this->implementations = $cachedData['implementations'];
178
        return true;
179
    }
180
181
    protected function createDefinitions():void
182
    {
183
        foreach ($this->implementations as $key => $classes) {
184
            if ($this->container && $this->container->has($key)) {
185
                continue;
186
            }
187
            $this->definitions[$key] = count($classes) === 1
188
                ? new Factory($this->createCallback($classes[0]))
189
                : new Factory($this->createAmbiguousCallback($key));
190
        }
191
    }
192
193
    private function createCallback(string $className): callable
194
    {
195
        return function (Container $container) use ($className) {
196
            return $container->make($className);
197
        };
198
    }
199
200
    private function createAmbiguousCallback(string $interface): callable
201
    {
202
        return function () use ($interface) {
203
            throw new AmbiguousImplementationException("Ambiguous implementation for '$interface'. " .
204
                "There are more then 1 implementations you need to provide a service definition for this interface.");
205
        };
206
    }
207
}
208