Issues (9)

DIContainer.php (1 issue)

1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded;
14
15
use Psr\Container\ContainerInterface;
16
use function assert;
17
use function call_user_func_array;
18
use function class_implements;
19
use function interface_exists;
20
use function preg_match;
21
22
/**
23
 * Interface DIModule contributes the application configuration,
24
 * typically the interface binding which are used to inject the dependencies.
25
 *
26
 * The application is composed of a set of DIModules and some bootstrapping code.
27
 */
28
interface DIModule
29
{
30
    /**
31
     * Provides bindings and other configurations for this app module.
32
     * Also reduces the repetition and results in a more readable configuration.
33
     * Implement the `configure()` method to bind your interfaces.
34
     *
35
     * ex: `$container->bind(MyInterface::class, MyImplementation::class);`
36
     *
37
     * @param DIContainer $container
38
     */
39
    public function configure(DIContainer $container): void;
40
}
41
42
interface IContainer extends ContainerInterface
43
{
44
    /**
45
     * Creates a new instance of a class. Builds the graph of objects that make up the application.
46
     * It can also inject already created dependencies behind the scene (with singleton and share).
47
     *
48
     * @param string $class     FQCN
49
     * @param array  $arguments [optional] The arguments for the class constructor.
50
     *                          They have top precedence over the shared dependencies
51
     * @return object|null
52
     */
53
    public function new(string $class, array $arguments = []): ?object;
54
55
    /**
56
     * Binds the interface to concrete class implementation.
57
     * It does not create objects, but prepares the container for dependency injection.
58
     *
59
     * This method should be used in the app modules (DIModule).
60
     *
61
     * @param string $interface FQN of the interface
62
     * @param string $class     FQCN of the concrete class implementation,
63
     *                          or empty value for deferred binding
64
     * @return DIContainer
65
     */
66
    public function bind(string $interface, string $class = ''): IContainer;
67
68
    /**
69
     * Create once and share an object throughout the application lifecycle.
70
     * Internally the object is immutable, but it can be replaced with share() method.
71
     *
72
     * @param string $class     FQCN
73
     * @param array  $arguments [optional] See new() description
74
     * @return object
75
     */
76
    public function singleton(string $class, array $arguments = []): object;
77
78
    /**
79
     * Share already created instance of an object throughout the app lifecycle.
80
     *
81
     * @param object $instance        The object that will be shared as dependency
82
     * @param array  $exclude         [optional] A list of FQCNs that should
83
     *                                be excluded from injecting this instance.
84
     *                                In this case, a new object will be created and
85
     *                                injected for these classes
86
     * @return DIContainer
87
     */
88
    public function share(object $instance, array $exclude = []): IContainer;
89
90
    /**
91
     * Shares an object globally by argument name.
92
     *
93
     * @param string $name  The name of the argument
94
     * @param mixed  $value The actual value
95
     * @return DIContainer
96
     */
97
    public function named(string $name, mixed $value): IContainer;
98
}
99
100
/**
101
 * Storage types for internal bindings, instances, etc.
102
 */
103 1
enum DIStorage
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING on line 103 at column 5
Loading history...
104
{
105
    case SINGLETONS;
106
    case BINDINGS;
107
    case EXCLUDE;
108
    case NAMED;
109
}
110
111
/**
112
 * The entry point of the DIContainer that draws the lines between the
113
 * APIs, implementation of these APIs, modules that configure these
114
 * implementations and applications that consist of a collection of modules.
115
 *
116
 * ```
117
 * $container = new DIContainer(new ModuleA, new ModuleB, ... new ModuleZ);
118
 * ($container)([AppEntry::class, 'method']);
119
 * ```
120
 */
121
class DIContainer implements IContainer
122
{
123
    protected DIReflector $reflector;
124
    private array $inProgress = [];
125
    private array $singletons = [];
126
    private array $bindings = [];
127
    private array $exclude = [];
128
    private array $named = [];
129
130 48
    public function __construct(DIModule ...$modules)
131
    {
132 48
        $this->reflector = new DIReflector;
133 48
        foreach ((array)$modules as $module) {
134 10
            $module->configure($this);
135
        }
136 48
    }
137
138 1
    public function __clone()
139
    {
140 1
        $this->inProgress = [];
141 1
        $this->singletons = [];
142 1
        $this->named      = [];
143 1
    }
144
145 48
    public function __destruct()
146
    {
147 48
        $this->singletons = [];
148 48
        $this->bindings   = [];
149 48
        $this->exclude    = [];
150 48
        $this->named      = [];
151 48
    }
152
153
    /**
154
     * @throws \ReflectionException
155
     */
156 7
    public function __invoke(callable $callable, array $arguments = [])
157
    {
158 7
        return call_user_func_array($callable, $this->reflector->processMethodArguments(
159 7
            $this, $this->reflector->newMethodFromCallable($callable), $arguments
160
        ));
161
    }
162
163 33
    public function new(string $class, array $arguments = []): ?object
164
    {
165 33
        $binding = $this->getBinding($class);
166 33
        if (isset($this->inProgress[$binding])) {
167 1
            throw DIException::forCircularDependency($binding);
168
        }
169 33
        $this->inProgress[$binding] = true;
170
        try {
171 33
            return $this->newInstance($binding, $arguments);
172
        } finally {
173 33
            unset($this->inProgress[$binding]);
174
        }
175
    }
176
177 8
    public function singleton(string $class, array $arguments = []): object
178
    {
179 8
        $class = $this->getBinding($class);
180 8
        return $this->singletons[$class] ??= $this->new($class, $arguments);
181
    }
182
183 5
    public function share(object $instance, array $exclude = []): IContainer
184
    {
185 5
        $class = $instance::class;
186 5
        $this->singletons[$class] = $instance;
187 5
        $this->bindInterfaces($class, $class);
188 5
        foreach ($exclude as $name) {
189 1
            $this->exclude[$name][$class] = $class;
190
        }
191 5
        return $this;
192
    }
193
194 15
    public function bind(string $interface, string $class = ''): IContainer
195
    {
196 15
        assert(false === empty($interface), 'Dependency name for bind() method');
197 15
        if ('$' === ($class[0] ?? null)) {
198 1
            $this->bindings[$interface] = $interface;
199 1
            $class && $this->bindings[$class] = $interface;
200 1
            return $this;
201
        }
202 14
        $this->bindInterfaces($interface, $class);
203 13
        return $this;
204
    }
205
206 13
    public function named(string $name, mixed $value): IContainer
207
    {
208 13
        if (1 !== preg_match('/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name)) {
209 7
            throw DIException::forInvalidParameterName($name);
210
        }
211 6
        $this->named[$name] = $value;
212 6
        return $this;
213
    }
214
215
    /**
216
     * @inheritDoc
217
     */
218 8
    public function has($id): bool
219
    {
220 8
        assert(false === empty($id), 'Dependency name for has() method');
221 8
        return isset($this->bindings[$id]) || isset($this->named[$id]);
222
    }
223
224
    /**
225
     * @inheritDoc
226
     */
227 4
    public function get($id): mixed
228
    {
229 4
        $this->has($id) || throw DIInstanceNotFound::for($id);
230 3
        $dependency = $this->getBinding($id);
231 3
        return $this->singletons[$dependency]
232 1
            ?? $this->named[$dependency]
233 3
            ?? $this->new($dependency);
234
    }
235
236
    /**
237
     * @internal
238
     */
239 23
    public function getFromStorage(DIStorage $type, string $dependency = ''): mixed
240
    {
241 23
        return $dependency ? match ($type) {
242 22
            DIStorage::BINDINGS => $this->bindings[$dependency] ?? $dependency,
243 22
            DIStorage::SINGLETONS => $this->singletons[$dependency] ?? null,
244 22
            DIStorage::EXCLUDE => $this->exclude[$dependency] ?? [],
245 22
            DIStorage::NAMED => $this->named['$' . $dependency] ?? $this->named[$dependency] ?? null,
246
        } : match ($type) {
247 3
            DIStorage::BINDINGS => $this->bindings,
248 2
            DIStorage::SINGLETONS => $this->singletons,
249
            DIStorage::EXCLUDE => $this->exclude,
250 23
            DIStorage::NAMED => $this->named,
251
        };
252
    }
253
254
    /**
255
     * @internal
256
     */
257 34
    public function getBinding(string $dependency): string
258
    {
259 34
        assert(false === empty($dependency), 'Dependency name for class/interface');
260 34
        return $this->bindings[$dependency] ?? $dependency;
261
    }
262
263 33
    private function newInstance(string $class, array $arguments): object
264
    {
265 33
        $this->bindings[$class] = $class;
266 33
        return $this->reflector->newInstance($this, $class, $arguments);
267
    }
268
269 17
    private function bindInterfaces(string $dependency, string $class): void
270
    {
271 17
        if (interface_exists($class)) {
272 1
            throw DIException::forInterfaceBinding($dependency, $class);
273
        }
274 16
        $this->bindings[$dependency] = $class;
275 16
        foreach ($this->bindings as $dependency => $class) {
276 16
            $this->mapDeferred($dependency, $class);
277
        }
278 16
    }
279
280 16
    private function mapDeferred(string $dependency, string $class): void
281
    {
282 16
        foreach (class_implements($dependency) as $interface) {
283 6
            if (false === isset($this->bindings[$interface])) {
284 6
                continue;
285
            }
286 3
            if (false === empty($class)) {
287 2
                $this->bindings[$interface] = $class;
288 2
                continue;
289
            }
290 1
            $this->bindings[$dependency] = $this->bindings[$interface];
291 1
            break;
292
        }
293 16
    }
294
}
295