1212
1313namespace Koded ;
1414
15- use Psr \Container \ContainerInterface ;
16- use Throwable ;
15+ use Psr \Container \{ContainerExceptionInterface , ContainerInterface };
1716
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+ */
1823interface DIModule
1924{
20- public function configure (DIContainer $ injector ): void ;
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 ;
2135}
2236
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+ */
2347final class DIContainer implements ContainerInterface
2448{
2549 public const SINGLETONS = 'singletons ' ;
2650 public const BINDINGS = 'bindings ' ;
51+ public const EXCLUDE = 'exclude ' ;
2752 public const NAMED = 'named ' ;
2853
2954 private $ reflection ;
3055 private $ inProgress = [];
3156
3257 private $ singletons = [];
3358 private $ bindings = [];
59+ private $ exclude = [];
3460 private $ named = [];
3561
36- private $ interfaces = [];
37-
3862 public function __construct (DIModule ...$ modules )
3963 {
4064 $ this ->reflection = new DIReflector ;
41- $ this ->interfaces = array_filter (get_declared_interfaces (), function (string $ name ) {
42- return false === strpos ($ name , '\\' );
43- });
44- $ this ->interfaces = array_flip ($ this ->interfaces );
45-
4665 foreach ((array )$ modules as $ module ) {
4766 $ module ->configure ($ this );
4867 }
4968 }
5069
5170 public function __clone ()
5271 {
53- throw DIException::forCloningNotAllowed ();
72+ $ this ->inProgress = [];
73+ $ this ->singletons = [];
74+ $ this ->named = [];
5475 }
5576
5677 public function __destruct ()
5778 {
5879 $ this ->reflection = null ;
59-
6080 $ this ->singletons = [];
61- $ this ->interfaces = [];
6281 $ this ->bindings = [];
82+ $ this ->exclude = [];
6383 $ this ->named = [];
6484 }
6585
@@ -70,14 +90,20 @@ public function __invoke(callable $callable, array $arguments = [])
7090 ));
7191 }
7292
73- public function inject (string $ class , array $ arguments = []): ?object
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+ public function new (string $ class , array $ arguments = []): ?object
74105 {
75- $ binding = $ this ->getFromBindings ($ class );
76-
77- if (isset ($ this ->singletons [$ binding ])) {
78- return $ this ->singletons [$ binding ];
79- }
80-
106+ $ binding = $ this ->getNameFromBindings ($ class );
81107 if (isset ($ this ->inProgress [$ binding ])) {
82108 throw DIException::forCircularDependency ($ binding );
83109 }
@@ -90,43 +116,89 @@ public function inject(string $class, array $arguments = []): ?object
90116 }
91117 }
92118
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+ */
93128 public function singleton (string $ class , array $ arguments = []): object
94129 {
95- return $ this ->singletons [$ class ] = $ this ->inject ($ class , $ arguments );
130+ $ binding = $ this ->getNameFromBindings ($ class );
131+ if (isset ($ this ->singletons [$ binding ])) {
132+ return $ this ->singletons [$ binding ];
133+ }
134+ return $ this ->singletons [$ class ] = $ this ->new ($ class , $ arguments );
96135 }
97136
98- public function share (object $ instance ): DIContainer
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+ public function share (object $ instance , array $ exclude = []): DIContainer
99149 {
100150 $ class = get_class ($ instance );
101- $ this ->mapInterfaces ($ class , $ class );
151+ $ this ->bindInterfaces ($ instance , $ class );
152+
102153 $ this ->singletons [$ class ] = $ instance ;
154+ $ this ->bindings [$ class ] = $ class ;
103155
156+ foreach ($ exclude as $ name ) {
157+ $ this ->exclude [$ name ][$ class ] = $ class ;
158+ }
104159 return $ this ;
105160 }
106161
107- public function bind (string $ interface , string $ class ): DIContainer
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+ public function bind (string $ interface , string $ class = '' ): DIContainer
108175 {
109- $ this ->assertEmpty ($ class , 'class ' );
110- $ this ->assertEmpty ($ interface , 'interface ' );
176+ assert (false === empty ($ interface ), 'Dependency name for bind() method ' );
111177
112- if ('$ ' === $ class [0 ]) {
178+ if ('$ ' === ( $ class [0 ] ?? null ) ) {
113179 $ this ->bindings [$ interface ] = $ interface ;
114- $ this ->bindings [$ class ] = $ interface ;
180+ $ class && $ this ->bindings [$ class ] = $ interface ;
115181 } else {
116- $ this ->bindings [$ interface ] = $ class ;
117- $ this ->bindings [$ class ] = $ class ;
182+ $ this ->bindings [$ interface ] = $ class ?: $ interface ;
183+ $ class && $ this ->bindings [$ class ] = $ class ;
118184 }
119-
120185 return $ this ;
121186 }
122187
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+ */
123196 public function named (string $ name , $ value ): DIContainer
124197 {
125198 if (1 !== preg_match ('/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/ ' , $ name )) {
126- throw DIException::forInvalidParameterName ();
199+ throw DIException::forInvalidParameterName ($ name );
127200 }
128201 $ this ->named [$ name ] = $ value ;
129-
130202 return $ this ;
131203 }
132204
@@ -138,6 +210,7 @@ public function getStorage(): array
138210 return [
139211 self ::SINGLETONS => $ this ->singletons ,
140212 self ::BINDINGS => $ this ->bindings ,
213+ self ::EXCLUDE => $ this ->exclude ,
141214 self ::NAMED => $ this ->named ,
142215 ];
143216 }
@@ -147,8 +220,7 @@ public function getStorage(): array
147220 */
148221 public function has ($ id ): bool
149222 {
150- $ this ->assertEmpty ($ id , 'dependency ' );
151-
223+ assert (false === empty ($ id ), 'Dependency name for has() method ' );
152224 return isset ($ this ->bindings [$ id ]) || isset ($ this ->named [$ id ]);
153225 }
154226
@@ -161,42 +233,30 @@ public function get($id)
161233 throw DIInstanceNotFound::for ($ id );
162234 }
163235
164- $ dependency = $ this ->getFromBindings ($ id );
165-
236+ $ dependency = $ this ->getNameFromBindings ($ id );
166237 return $ this ->singletons [$ dependency ]
167238 ?? $ this ->named [$ dependency ]
168- ?? $ this ->inject ($ dependency );
239+ ?? $ this ->new ($ dependency );
169240 }
170241
171242 private function newInstance (string $ class , array $ arguments ): object
172243 {
173- try {
174- $ this ->bindings [$ class ] = $ class ;
175- return $ this ->reflection ->newInstance ($ this , $ class , $ arguments );
176- } catch (Throwable $ e ) {
177- throw $ e ;
178- }
244+ $ this ->bindings [$ class ] = $ class ;
245+ return $ this ->reflection ->newInstance ($ this , $ class , $ arguments );
179246 }
180247
181- private function getFromBindings (string $ dependency ): string
248+ private function getNameFromBindings (string $ dependency ): string
182249 {
183- $ this ->assertEmpty ($ dependency , 'class/interface ' );
184-
250+ assert (false === empty ($ dependency ), 'Dependency name for class/interface ' );
185251 return $ this ->bindings [$ dependency ] ?? $ dependency ;
186252 }
187253
188- private function assertEmpty (string $ value , string $ type ): void
189- {
190- if (empty ($ value )) {
191- throw DIException::forEmptyName ($ type );
192- }
193- }
194-
195- private function mapInterfaces (string $ dependency , string $ class ): void
254+ private function bindInterfaces (object $ dependency , string $ class ): void
196255 {
197- foreach ((@class_implements ($ dependency , false ) ?: []) as $ implements ) {
198- if (false === isset ($ this ->interfaces [$ implements ])) {
199- $ this ->bindings [$ implements ] = $ class ;
256+ foreach (class_implements ($ dependency ) as $ interface ) {
257+ if (isset ($ this ->bindings [$ interface ])) {
258+ $ this ->bindings [$ interface ] = $ class ;
259+ break ;
200260 }
201261 }
202262 }
0 commit comments