Passed
Push — master ( 6be64d...b8891d )
by Mihail
27:29 queued 12:28
created

DIContainer::mapDeferred()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4

Importance

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