Passed
Push — master ( 6aee2b...11f44b )
by Mihail
03:39 queued 02:16
created

DIContainer::getNameFromBindings()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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