Skip to content

Commit 2a2f3c3

Browse files
committed
adds psr-11 container interface.
1 parent d43fb5b commit 2a2f3c3

7 files changed

Lines changed: 592 additions & 0 deletions

File tree

VERSIONLOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## 0.7.6
2+
* Added the PRS-11 ContainerInterface.
23

34
## 0.7.5 2025-11-28
45

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<?php
2+
3+
namespace Neuron\Patterns\Container;
4+
5+
use ReflectionClass;
6+
use ReflectionException;
7+
use ReflectionParameter;
8+
9+
/**
10+
* Dependency injection container with auto-wiring
11+
*
12+
* Implements PSR-11 compatible container interface with automatic
13+
* dependency resolution using reflection.
14+
*
15+
* @package Neuron\Patterns\Container
16+
*/
17+
class Container implements IContainer
18+
{
19+
/**
20+
* Registered type bindings (interface => concrete class)
21+
*/
22+
private array $_bindings = [];
23+
24+
/**
25+
* Registered singleton factories
26+
*/
27+
private array $_singletons = [];
28+
29+
/**
30+
* Resolved singleton instances
31+
*/
32+
private array $_instances = [];
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function get( string $id )
38+
{
39+
if( !$this->has( $id ) )
40+
{
41+
throw new NotFoundException( "Entry '{$id}' not found in container" );
42+
}
43+
44+
// Return existing singleton instance
45+
if( isset( $this->_instances[$id] ) )
46+
{
47+
return $this->_instances[$id];
48+
}
49+
50+
// Create and cache singleton instance
51+
if( isset( $this->_singletons[$id] ) )
52+
{
53+
$instance = ($this->_singletons[$id])( $this );
54+
$this->_instances[$id] = $instance;
55+
return $instance;
56+
}
57+
58+
// Resolve binding
59+
if( isset( $this->_bindings[$id] ) )
60+
{
61+
return $this->make( $this->_bindings[$id] );
62+
}
63+
64+
// Auto-wire the class
65+
return $this->make( $id );
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function has( string $id ): bool
72+
{
73+
return isset( $this->_singletons[$id] )
74+
|| isset( $this->_instances[$id] )
75+
|| isset( $this->_bindings[$id] )
76+
|| class_exists( $id )
77+
|| interface_exists( $id );
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
public function bind( string $abstract, string $concrete ): void
84+
{
85+
$this->_bindings[$abstract] = $concrete;
86+
}
87+
88+
/**
89+
* {@inheritdoc}
90+
*/
91+
public function singleton( string $abstract, callable $factory ): void
92+
{
93+
$this->_singletons[$abstract] = $factory;
94+
}
95+
96+
/**
97+
* {@inheritdoc}
98+
*/
99+
public function instance( string $abstract, object $instance ): void
100+
{
101+
$this->_instances[$abstract] = $instance;
102+
}
103+
104+
/**
105+
* {@inheritdoc}
106+
*/
107+
public function make( string $class, array $parameters = [] )
108+
{
109+
try
110+
{
111+
$reflector = new ReflectionClass( $class );
112+
}
113+
catch( ReflectionException $e )
114+
{
115+
throw new ContainerException( "Class {$class} does not exist", 0, $e );
116+
}
117+
118+
// Check if class is instantiable
119+
if( !$reflector->isInstantiable() )
120+
{
121+
throw new ContainerException(
122+
"Class {$class} is not instantiable (may be abstract or interface)"
123+
);
124+
}
125+
126+
$constructor = $reflector->getConstructor();
127+
128+
// No constructor - just instantiate
129+
if( is_null( $constructor ) )
130+
{
131+
return new $class;
132+
}
133+
134+
// Resolve constructor dependencies
135+
$dependencies = $this->resolveDependencies(
136+
$constructor->getParameters(),
137+
$parameters
138+
);
139+
140+
return $reflector->newInstanceArgs( $dependencies );
141+
}
142+
143+
/**
144+
* Resolve method/constructor dependencies
145+
*
146+
* @param ReflectionParameter[] $parameters Constructor/method parameters
147+
* @param array $primitives User-provided primitive values (name => value)
148+
* @return array Resolved dependencies
149+
* @throws ContainerException If dependency cannot be resolved
150+
*/
151+
private function resolveDependencies( array $parameters, array $primitives = [] ): array
152+
{
153+
$dependencies = [];
154+
155+
foreach( $parameters as $parameter )
156+
{
157+
$paramName = $parameter->getName();
158+
159+
// Use provided primitive if exists
160+
if( isset( $primitives[$paramName] ) )
161+
{
162+
$dependencies[] = $primitives[$paramName];
163+
continue;
164+
}
165+
166+
// Get type hint
167+
$type = $parameter->getType();
168+
169+
if( $type && !$type->isBuiltin() )
170+
{
171+
// Type is a class/interface - recursively resolve from container
172+
$typeName = $type->getName();
173+
174+
// Check if we can resolve this type
175+
if( $this->isBound( $typeName ) || class_exists( $typeName ) )
176+
{
177+
try
178+
{
179+
$dependencies[] = $this->get( $typeName );
180+
}
181+
catch( NotFoundException | ContainerException $e )
182+
{
183+
// If type allows null and we can't resolve, use null
184+
if( $type->allowsNull() )
185+
{
186+
$dependencies[] = null;
187+
}
188+
else
189+
{
190+
throw new ContainerException(
191+
"Cannot resolve dependency [{$typeName}] for parameter [{$paramName}] in class",
192+
0,
193+
$e
194+
);
195+
}
196+
}
197+
}
198+
elseif( $type->allowsNull() )
199+
{
200+
// Type not bound and nullable - use null
201+
$dependencies[] = null;
202+
}
203+
else
204+
{
205+
throw new ContainerException(
206+
"Cannot resolve dependency [{$typeName}] for parameter [{$paramName}] - no binding exists and type is not nullable"
207+
);
208+
}
209+
}
210+
elseif( $parameter->isDefaultValueAvailable() )
211+
{
212+
// Use default value for primitives
213+
$dependencies[] = $parameter->getDefaultValue();
214+
}
215+
elseif( $type && $type->allowsNull() )
216+
{
217+
// Type allows null
218+
$dependencies[] = null;
219+
}
220+
else
221+
{
222+
throw new ContainerException(
223+
"Cannot resolve primitive parameter [{$paramName}] - no default value provided"
224+
);
225+
}
226+
}
227+
228+
return $dependencies;
229+
}
230+
231+
/**
232+
* Clear all bindings and instances (useful for testing)
233+
*
234+
* @return void
235+
*/
236+
public function clear(): void
237+
{
238+
$this->_bindings = [];
239+
$this->_singletons = [];
240+
$this->_instances = [];
241+
}
242+
243+
/**
244+
* Get all registered bindings
245+
*
246+
* @return array
247+
*/
248+
public function getBindings(): array
249+
{
250+
return $this->_bindings;
251+
}
252+
253+
/**
254+
* Check if a binding exists
255+
*
256+
* @param string $abstract
257+
* @return bool
258+
*/
259+
public function isBound( string $abstract ): bool
260+
{
261+
return isset( $this->_bindings[$abstract] )
262+
|| isset( $this->_singletons[$abstract] )
263+
|| isset( $this->_instances[$abstract] );
264+
}
265+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Neuron\Patterns\Container;
4+
5+
use Exception;
6+
7+
/**
8+
* Base container exception
9+
*
10+
* @package Neuron\Patterns\Container
11+
*/
12+
class ContainerException extends Exception
13+
{
14+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace Neuron\Patterns\Container;
4+
5+
/**
6+
* PSR-11 compatible dependency injection container interface
7+
*
8+
* Provides dependency injection with auto-wiring capabilities.
9+
* Compatible with PSR-11 Container Interface standard.
10+
*
11+
* @package Neuron\Patterns\Container
12+
*/
13+
interface IContainer
14+
{
15+
/**
16+
* Finds an entry of the container by its identifier and returns it.
17+
*
18+
* @param string $id Identifier of the entry to look for (typically a class name or interface)
19+
* @return mixed Entry.
20+
* @throws NotFoundException No entry was found for **this** identifier.
21+
* @throws ContainerException Error while retrieving the entry.
22+
*/
23+
public function get( string $id );
24+
25+
/**
26+
* Returns true if the container can return an entry for the given identifier.
27+
* Returns false otherwise.
28+
*
29+
* @param string $id Identifier of the entry to look for.
30+
* @return bool
31+
*/
32+
public function has( string $id ): bool;
33+
34+
/**
35+
* Bind an abstract type to a concrete implementation
36+
*
37+
* @param string $abstract Interface or abstract class name
38+
* @param string $concrete Concrete class name
39+
* @return void
40+
*/
41+
public function bind( string $abstract, string $concrete ): void;
42+
43+
/**
44+
* Register a singleton (shared instance) in the container
45+
*
46+
* @param string $abstract Interface or class name
47+
* @param callable $factory Factory function that creates the instance
48+
* @return void
49+
*/
50+
public function singleton( string $abstract, callable $factory ): void;
51+
52+
/**
53+
* Resolve and instantiate a class with automatic dependency injection
54+
*
55+
* Uses reflection to analyze constructor parameters and automatically
56+
* resolves dependencies from the container.
57+
*
58+
* @param string $class Fully qualified class name
59+
* @param array $parameters Optional parameters to override auto-wiring
60+
* @return object Instance of the requested class
61+
* @throws ContainerException If the class cannot be instantiated
62+
*/
63+
public function make( string $class, array $parameters = [] );
64+
65+
/**
66+
* Register an existing instance as a singleton
67+
*
68+
* @param string $abstract Interface or class name
69+
* @param object $instance The instance to register
70+
* @return void
71+
*/
72+
public function instance( string $abstract, object $instance ): void;
73+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Neuron\Patterns\Container;
4+
5+
/**
6+
* Service provider interface
7+
*
8+
* Service providers are responsible for registering bindings
9+
* and singletons with the container.
10+
*
11+
* @package Neuron\Patterns\Container
12+
*/
13+
interface IServiceProvider
14+
{
15+
/**
16+
* Register services in the container
17+
*
18+
* @param IContainer $container
19+
* @return void
20+
*/
21+
public function register( IContainer $container ): void;
22+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Neuron\Patterns\Container;
4+
5+
/**
6+
* Exception thrown when an entry is not found in the container
7+
*
8+
* @package Neuron\Patterns\Container
9+
*/
10+
class NotFoundException extends ContainerException
11+
{
12+
}

0 commit comments

Comments
 (0)