Completed
Push — develop ( f0edc8...54f05a )
by Filipe
22s queued 20s
created

organiseImplementations()   B

Complexity

Conditions 8
Paths 26

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 15
nc 26
nop 0
dl 0
loc 26
rs 8.4444
c 1
b 0
f 0
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
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): void
79
    {
80
        $this->container = $container;
81
    }
82
83
    public function getContainer(): ?ContainerInterface
84
    {
85
        return $this->container;
86
    }
87
88
    /**
89
     * @param string $path
90
     * @return void
91
     */
92
    public function loadFiles(string $path): void
93
    {
94
        try {
95
            $directory = new RecursiveDirectoryIterator($path);
96
        } catch (Exception) {
97
            throw new InvalidDefinitionsPathException(
98
                'Provided autowire definitions path is not valid or is not found. ' .
99
                'Could not create container. Please check ' . $path
100
            );
101
        }
102
103
        $iterator = new RecursiveIteratorIterator($directory);
104
        $phpFiles = new RegexIterator($iterator, '/.*\.php$/i');
105
106
        /** @var SplFileInfo $phpFile */
107
        foreach ($phpFiles as $phpFile) {
108
            $classFile = new ClassFile($phpFile->getPathname());
109
            if ($classFile->isAnImplementation()) {
110
                $this->files[] = $classFile;
111
            }
112
        }
113
    }
114
115
    /**
116
     * Organizes the implementations based on interfaces and parent classes.
117
     *
118
     * @return void
119
     */
120
    private function organiseImplementations(): void
121
    {
122
        foreach ($this->files as $file) {
123
            foreach ($file->interfaces() as $interface) {
124
                $entry = array_key_exists($interface, $this->implementations) ? $this->implementations[$interface] : [];
125
                if (in_array($file->className(), $entry)) {
126
                    continue;
127
                }
128
                $entry[] = $file->className();
129
                $this->implementations[$interface] = $entry;
130
            }
131
132
            if (!$file->parentClass()) {
133
                continue;
134
            }
135
136
            $entry = array_key_exists($file->parentClass(), $this->implementations)
137
                ? $this->implementations[$file->parentClass()]
138
                : [];
139
140
            if (!in_array($file->className(), $entry)) {
141
                $entry[] = $file->className();
142
                $this->implementations[$file->parentClass()] = $entry;
143
            }
144
        }
145
        $this->saveImplementations();
146
    }
147
148
    private function saveImplementations(): void
149
    {
150
        $file = sys_get_temp_dir() . self::TMP_FILE_NAME;
151
        if (is_file($file)) {
152
            unlink($file);
153
        }
154
        $data = [
155
            'snapshot' => $this->directoryWatcher->snapshot(),
156
            'implementations' => $this->implementations,
157
        ];
158
        file_put_contents($file, serialize($data));
159
    }
160
161
    private function loadImplementations(): bool
162
    {
163
        $file = sys_get_temp_dir() . self::TMP_FILE_NAME;
164
        if (!file_exists($file)) {
165
            return false;
166
        }
167
168
        $cachedData = unserialize(file_get_contents($file));
169
170
        /** @var Directory\Snapshot $snapshot */
171
        $snapshot = $cachedData['snapshot'];
172
        if ($this->directoryWatcher->hasChanged($snapshot)) {
173
            return false;
174
        }
175
176
        $this->implementations = $cachedData['implementations'];
177
        return true;
178
    }
179
180
    protected function createDefinitions():void
181
    {
182
        foreach ($this->implementations as $key => $classes) {
183
            if ($this->container && $this->container->has($key)) {
184
                continue;
185
            }
186
            $this->definitions[$key] = count($classes) === 1
187
                ? new Factory($this->createCallback($classes[0]))
188
                : new Factory($this->createAmbiguousCallback($key));
189
        }
190
    }
191
192
    private function createCallback(string $className): callable
193
    {
194
        return function (Container $container) use ($className) {
195
            return $container->make($className);
196
        };
197
    }
198
199
    private function createAmbiguousCallback(string $interface): callable
200
    {
201
        return function () use ($interface) {
202
            throw new AmbiguousImplementationException("Ambiguous implementation for '$interface'. " .
203
                "There are more then 1 implementations you need to provide a service definition for this interface.");
204
        };
205
    }
206
}
207