Skip to content

Commit 800261d

Browse files
committed
Merge branch 'release/0.7.6'
2 parents f5fccc1 + 2960e7b commit 800261d

15 files changed

Lines changed: 1089 additions & 21 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,29 @@ on: [push]
55
jobs:
66
build-test:
77
runs-on: ubuntu-latest
8-
container:
9-
image: php:8.4 # This forces the job to run in a Docker container
108

119
steps:
1210
- name: Checkout
1311
uses: actions/checkout@v3
1412

15-
- name: Install System Dependencies (Git, Zip, Unzip)
16-
run: |
17-
apt-get update
18-
apt-get install -y unzip git zip
19-
20-
- name: Install and Enable extensions
21-
run: |
22-
docker-php-ext-install sockets calendar
23-
docker-php-ext-enable sockets calendar
24-
25-
- name: Install Composer
26-
run: |
27-
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
28-
composer --version
13+
- name: Setup PHP
14+
uses: shivammathur/setup-php@v2
15+
with:
16+
php-version: 8.4
17+
extensions: sockets, calendar, pcov
18+
coverage: pcov
2919

3020
- name: Install Dependencies
31-
run: composer install --prefer-dist --no-progress
32-
33-
- name: Run PHPUnit
34-
run: vendor/bin/phpunit tests
21+
run: composer install --prefer-dist --no-progress --no-interaction --optimize-autoloader
22+
23+
- name: Run PHPUnit with Coverage
24+
run: vendor/bin/phpunit tests --coverage-clover coverage.xml --coverage-filter src
25+
26+
- name: Upload coverage to Codecov
27+
uses: codecov/codecov-action@v5
28+
with:
29+
token: ${{ secrets.CODECOV_TOKEN }}
30+
files: ./coverage.xml
31+
flags: patterns
32+
slug: Neuron-PHP/patterns
33+
fail_ci_if_error: true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ node_modules
77
.DS_STORE
88
public_html/
99
.phpunit.result.cache
10+
11+
coverage.xml

.version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"strategy": "semver",
33
"major": 0,
44
"minor": 7,
5-
"patch": 5,
5+
"patch": 6,
66
"build": 0
77
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[![CI](https://github.com/Neuron-PHP/patterns/actions/workflows/ci.yml/badge.svg)](https://github.com/Neuron-PHP/patterns/actions)
2+
[![codecov](https://codecov.io/gh/Neuron-PHP/patterns/branch/develop/graph/badge.svg)](https://codecov.io/gh/Neuron-PHP/patterns)
23
# Neuron-PHP Patterns
34

45
A comprehensive design patterns library for PHP 8.0+ that provides robust implementations of common software design patterns including Singleton, Registry, Observer, Command, and Criteria patterns.

VERSIONLOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 0.7.6 2025-12-27
2+
* Added the PRS-11 ContainerInterface.
3+
14
## 0.7.5 2025-11-28
25

36
## 0.7.4 2025-11-21
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+
}

0 commit comments

Comments
 (0)