Passed
Branch refactor (dbb736)
by Mihail
03:14
created

DIContainer::mapInterfaces()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

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