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