Passed
Push — master ( d343d0...ce5123 )
by Mihail
05:32
created

DIContainer::named()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 4
c 4
b 0
f 1
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 2
crap 2
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
127
        try {
128 32
            return $this->newInstance($binding, $arguments);
129
        } finally {
130 32
            unset($this->inProgress[$binding]);
131
        }
132
    }
133
134
    /**
135
     * Create once and share an object throughout the application lifecycle.
136
     * Internally the object is immutable, but it can be replaced with share() method.
137
     *
138
     * @param string $class     FQCN
139
     * @param array  $arguments [optional] See new() description
140
     * @return object
141
     */
142 8
    public function singleton(string $class, array $arguments = []): object
143
    {
144 8
        $binding = $this->getNameFromBindings($class);
145 8
        if (isset($this->singletons[$binding])) {
146 2
            return $this->singletons[$binding];
147
        }
148 8
        return $this->singletons[$class] = $this->new($class, $arguments);
149
    }
150
151
    /**
152
     * Share already created instance of an object throughout the app lifecycle.
153
     *
154
     * @param object $instance        The object that will be shared as dependency
155
     * @param array  $exclude         [optional] A list of FQCNs that should
156
     *                                be excluded from injecting this instance.
157
     *                                In this case, a new object will be created and
158
     *                                injected for these classes
159
     * @return DIContainer
160
     */
161 5
    public function share(object $instance, array $exclude = []): DIContainerInterface
162
    {
163 5
        $class = $instance::class;
164 5
        $this->bindInterfaces($instance, $class);
165 5
        $this->singletons[$class] = $instance;
166 5
        $this->bindings[$class]   = $class;
167 5
        foreach ($exclude as $name) {
168 1
            $this->exclude[$name][$class] = $class;
169
        }
170 5
        return $this;
171
    }
172
173
    /**
174
     * Binds the interface to concrete class implementation.
175
     * It does not create objects, but prepares the container for dependency injection.
176
     *
177
     * This method should be used in the app modules (DIModule).
178
     *
179
     * @param string $interface FQN of the interface
180
     * @param string $class     FQCN of the concrete class implementation,
181
     *                          or empty value for deferred binding
182
     * @return DIContainer
183
     */
184 13
    public function bind(string $interface, string $class = ''): DIContainerInterface
185
    {
186 13
        \assert(false === empty($interface), 'Dependency name for bind() method');
187 13
        if ('$' === ($class[0] ?? null)) {
188 1
            $this->bindings[$interface] = $interface;
189 1
            $class && $this->bindings[$class] = $interface;
190
        } else {
191 12
            $this->bindings[$interface] = $class ?: $interface;
192 12
            $class && $this->bindings[$class] = $class;
193
        }
194 13
        return $this;
195
    }
196
197
    /**
198
     * Shares an object globally by argument name.
199
     *
200
     * @param string $name  The name of the argument
201
     * @param mixed  $value The actual value
202
     * @return DIContainer
203
     */
204 13
    public function named(string $name, mixed $value): DIContainerInterface
205
    {
206 13
        if (1 !== \preg_match('/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name)) {
207 7
            throw DIException::forInvalidParameterName($name);
208
        }
209 6
        $this->named[$name] = $value;
210 6
        return $this;
211
    }
212
213
    /**
214
     * @internal
215
     */
216 22
    public function getStorage(): iterable
217
    {
218
        return [
219 22
            self::SINGLETONS => $this->singletons,
220 22
            self::BINDINGS   => $this->bindings,
221 22
            self::EXCLUDE    => $this->exclude,
222 22
            self::NAMED      => $this->named,
223
        ];
224
    }
225
226
    /**
227
     * @inheritDoc
228
     */
229 8
    public function has($id): bool
230
    {
231 8
        \assert(false === empty($id), 'Dependency name for has() method');
232 8
        return isset($this->bindings[$id]) || isset($this->named[$id]);
233
    }
234
235
    /**
236
     * @inheritDoc
237
     */
238 4
    public function get($id)
239
    {
240 4
        $this->has($id) || throw DIInstanceNotFound::for($id);
241 3
        $dependency = $this->getNameFromBindings($id);
242 3
        return $this->singletons[$dependency]
243 1
            ?? $this->named[$dependency]
244 3
            ?? $this->new($dependency);
245
    }
246
247 32
    private function newInstance(string $class, array $arguments): object
248
    {
249 32
        $this->bindings[$class] = $class;
250 32
        return $this->reflection->newInstance($this, $class, $arguments);
251
    }
252
253 32
    private function getNameFromBindings(string $dependency): string
254
    {
255 32
        \assert(false === empty($dependency), 'Dependency name for class/interface');
256 32
        return $this->bindings[$dependency] ?? $dependency;
257
    }
258
259 5
    private function bindInterfaces(object $dependency, string $class): void
260
    {
261 5
        foreach (\class_implements($dependency) as $interface) {
262 4
            if (isset($this->bindings[$interface])) {
263 1
                $this->bindings[$interface] = $class;
264 1
                break;
265
            }
266
        }
267 5
    }
268
}
269