Passed
Push — master ( 76f495...db6fe4 )
by Mihail
11:45
created

DIContainer.php (1 issue)

Labels
Severity
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
    public function __construct(DIModule ...$modules)
78 42
    {
79
        $this->reflection = new DIReflector;
80 42
        foreach ((array)$modules as $module) {
81 42
            $module->configure($this);
82 8
        }
83
    }
84 42
85
    public function __clone()
86 1
    {
87
        $this->inProgress = [];
88 1
        $this->singletons = [];
89 1
        $this->named      = [];
90 1
    }
91 1
92
    public function __destruct()
93 42
    {
94
        $this->reflection = null;
95 42
        $this->singletons = [];
96 42
        $this->bindings   = [];
97 42
        $this->exclude    = [];
98 42
        $this->named      = [];
99 42
    }
100 42
101
    public function __invoke(callable $callable, array $arguments = [])
102 6
    {
103
        return \call_user_func_array($callable, $this->reflection->processMethodArguments(
104 6
            $this, $this->reflection->newMethodFromCallable($callable), $arguments
0 ignored issues
show
The method newMethodFromCallable() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

104
            $this, $this->reflection->/** @scrutinizer ignore-call */ newMethodFromCallable($callable), $arguments

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
105 6
        ));
106
    }
107
108
    /**
109
     * Creates a new instance of a class. Builds the graph of objects that make up the application.
110
     * It can also inject already created dependencies behind the scene (with singleton and share).
111
     *
112
     * @param string $class     FQCN
113
     * @param array  $arguments [optional] The arguments for the class constructor.
114
     *                          They have top precedence over the shared dependencies
115
     *
116
     * @return object|null
117
     */
118
    public function new(string $class, array $arguments = []): ?object
119
    {
120 28
        $binding = $this->getNameFromBindings($class);
121
        if (isset($this->inProgress[$binding])) {
122 28
            throw DIException::forCircularDependency($binding);
123 28
        }
124 1
        $this->inProgress[$binding] = true;
125
126 28
        try {
127
            return $this->newInstance($binding, $arguments);
128
        } finally {
129 28
            unset($this->inProgress[$binding]);
130
        }
131 28
    }
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
     *
140
     * @return object
141
     */
142
    public function singleton(string $class, array $arguments = []): object
143
    {
144 8
        $binding = $this->getNameFromBindings($class);
145
        if (isset($this->singletons[$binding])) {
146 8
            return $this->singletons[$binding];
147 8
        }
148 2
        return $this->singletons[$class] = $this->new($class, $arguments);
149
    }
150 8
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
     *
160
     * @return DIContainer
161
     */
162
    public function share(object $instance, array $exclude = []): DIContainerInterface
163
    {
164 5
        $class = $instance::class;
165
        $this->bindInterfaces($instance, $class);
166 5
        $this->singletons[$class] = $instance;
167 5
        $this->bindings[$class]   = $class;
168
        foreach ($exclude as $name) {
169 5
            $this->exclude[$name][$class] = $class;
170 5
        }
171
        return $this;
172 5
    }
173 1
174
    /**
175 5
     * Binds the interface to concrete class implementation.
176
     * It does not create objects, but prepares the container for dependency injection.
177
     *
178
     * This method should be used in the app modules (DIModule).
179
     *
180
     * @param string $interface FQN of the interface
181
     * @param string $class     FQCN of the concrete class implementation,
182
     *                          or empty value for deferred binding
183
     *
184
     * @return DIContainer
185
     */
186
    public function bind(string $interface, string $class = ''): DIContainerInterface
187
    {
188
        \assert(false === empty($interface), 'Dependency name for bind() method');
189
        if ('$' === ($class[0] ?? null)) {
190 11
            $this->bindings[$interface] = $interface;
191
            $class && $this->bindings[$class] = $interface;
192 11
        } else {
193
            $this->bindings[$interface] = $class ?: $interface;
194 11
            $class && $this->bindings[$class] = $class;
195 1
        }
196 1
        return $this;
197
    }
198 10
199 10
    /**
200
     * Shares an object globally by argument name.
201 11
     *
202
     * @param string $name  The name of the argument
203
     * @param mixed  $value The actual value
204
     *
205
     * @return DIContainer
206
     */
207
    public function named(string $name, mixed $value): DIContainerInterface
208
    {
209
        if (1 !== \preg_match('/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name)) {
210
            throw DIException::forInvalidParameterName($name);
211
        }
212 13
        $this->named[$name] = $value;
213
        return $this;
214 13
    }
215 7
216
    /**
217 6
     * @internal
218 6
     */
219
    public function getStorage(): iterable
220
    {
221
        return [
222
            self::SINGLETONS => $this->singletons,
223
            self::BINDINGS   => $this->bindings,
224 18
            self::EXCLUDE    => $this->exclude,
225
            self::NAMED      => $this->named,
226
        ];
227 18
    }
228 18
229 18
    /**
230 18
     * @inheritDoc
231
     */
232
    public function has($id): bool
233
    {
234
        \assert(false === empty($id), 'Dependency name for has() method');
235
        return isset($this->bindings[$id]) || isset($this->named[$id]);
236
    }
237 8
238
    /**
239 8
     * @inheritDoc
240 8
     */
241
    public function get($id)
242
    {
243
        $this->has($id) || throw DIInstanceNotFound::for($id);
244
        $dependency = $this->getNameFromBindings($id);
245
        return $this->singletons[$dependency]
246 4
            ?? $this->named[$dependency]
247
            ?? $this->new($dependency);
248 4
    }
249 1
250
    private function newInstance(string $class, array $arguments): object
251
    {
252 3
        $this->bindings[$class] = $class;
253 3
        return $this->reflection->newInstance($this, $class, $arguments);
254 1
    }
255 3
256
    private function getNameFromBindings(string $dependency): string
257
    {
258 28
        \assert(false === empty($dependency), 'Dependency name for class/interface');
259
        return $this->bindings[$dependency] ?? $dependency;
260 28
    }
261 28
262
    private function bindInterfaces(object $dependency, string $class): void
263
    {
264 28
        foreach (\class_implements($dependency) as $interface) {
265
            if (isset($this->bindings[$interface])) {
266 28
                $this->bindings[$interface] = $class;
267 28
                break;
268
            }
269
        }
270 5
    }
271
}
272