Completed
Push — master ( 2f1776...76f495 )
by Mihail
09:40
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\{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
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();
50
}
51
52
53
/**
54
 * The entry point of the DIContainer that draws the lines between the
55
 * APIs, implementation of these APIs, modules that configure these
56
 * implementations and applications that consist of a collection of modules.
57
 *
58
 * ```
59
 * $container = new DIContainer(new ModuleA, new ModuleB, ... new ModuleZ);
60
 * ($container)([AppEntry::class, 'method']);
61
 * ```
62
 */
63
class DIContainer implements DIContainerInterface
64
{
65
    public const SINGLETONS = 'singletons';
66
    public const BINDINGS   = 'bindings';
67
    public const EXCLUDE    = 'exclude';
68
    public const NAMED      = 'named';
69
70
    protected ?DIReflector $reflection;
71
    private array $inProgress = [];
72
73
    private array $singletons = [];
74
    private array $bindings = [];
75
    private array $exclude = [];
76
    private array $named = [];
77
78 42
    public function __construct(DIModule ...$modules)
79
    {
80 42
        $this->reflection = new DIReflector;
81 42
        foreach ((array)$modules as $module) {
82 8
            $module->configure($this);
83
        }
84 42
    }
85
86 1
    public function __clone()
87
    {
88 1
        $this->inProgress = [];
89 1
        $this->singletons = [];
90 1
        $this->named      = [];
91 1
    }
92
93 42
    public function __destruct()
94
    {
95 42
        $this->reflection = null;
96 42
        $this->singletons = [];
97 42
        $this->bindings   = [];
98 42
        $this->exclude    = [];
99 42
        $this->named      = [];
100 42
    }
101
102 6
    public function __invoke(callable $callable, array $arguments = [])
103
    {
104 6
        return \call_user_func_array($callable, $this->reflection->processMethodArguments(
105 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

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