Skip to content

Commit e156ca7

Browse files
committed
API changes, delegate support, resolve params by name only in factories
1 parent 0334e08 commit e156ca7

File tree

5 files changed

+98
-75
lines changed

5 files changed

+98
-75
lines changed

Diff for: README.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ Container requires PHP 8.0+
2020
## Interface
2121

2222
```php
23-
new Container(iterable $definitions = [], bool $autowire = true)
23+
new Container(iterable $definitions = [])
2424
```
2525

2626
The container ships with four public methods:
2727

2828
```php
29-
with(string $id, $entry): Container // add a container entry
30-
get(string $id) // get entry (PSR-11)
29+
withAutowiring(bool $flag): Container // toggle autowiring
30+
withEntry(string $id, mixed $entry): Container // add a container entry
31+
withDelegate(ContainerInterface $delegate): Container // register a delegate container
32+
get(string $id): mixed // get entry (PSR-11)
3133
has(string $id): bool // has entry (PSR-11)
32-
create(string $id, array $params = []); // create a class with optional constructor substitution args
34+
create(string $id, array $params = []): mixed // create a class with optional constructor substitution args
3335
entries(): array // list all container entries
3436
```
3537

@@ -70,7 +72,7 @@ $hello === $hello2 // true
7072
$hello->print(); // 'Hello World'
7173
```
7274

73-
Note that the container only creates instances once. It does not work as a factory.
75+
Note that the container only creates (shared) instances once. It does not work as a factory.
7476
You should consider the [Factory Pattern](https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html) or use the ```create()``` method instead:
7577

7678
```php
@@ -99,7 +101,7 @@ The ```create()``` method will automatically resolve the ```Config``` dependency
99101

100102
## Configuration
101103

102-
You can configure the container with definitions. ```Callables``` (except invokable objects) are always treated as factories and can (!should) be used to bootstrap class instances:
104+
You can configure the container with definitions. ```Closures``` are always treated as factories and should be used to bootstrap class instances. If you like to use ```callables``` as factories: ```Closure::fromCallable([$object, 'method'])```.
103105

104106
```php
105107
use Semperton\Container\Container;
@@ -128,17 +130,17 @@ $container->get('closure')(); // 42
128130
$container->get(MailFactory::class); // instance of MailFactory
129131
```
130132

131-
The ```with()``` method also treats ```callables``` as factories.
133+
The ```withEntry()``` method also treats ```callables``` as factories.
132134

133135
## Immutability
134136

135-
Once the container is created, it is immutable. If you like to add an entry after instantiation, keep in mind that the ```with()``` method always returns a new container instance:
137+
Once the container is created, it is immutable. If you like to add an entry after instantiation, keep in mind that the ```withEntry()``` method always returns a new container instance:
136138

137139
```php
138140
use Semperton\Container\Container;
139141

140142
$container1 = new Container();
141-
$container2 = $container1->with('number', 42);
143+
$container2 = $container1->withEntry('number', 42);
142144

143145
$container1->has('number'); // false
144146
$container2->has('number'); // true

Diff for: src/Container.php

+72-54
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,16 @@
1818
use const SORT_NATURAL;
1919
use const SORT_FLAG_CASE;
2020

21-
use function is_callable;
2221
use function class_exists;
2322
use function array_key_exists;
2423
use function array_keys;
2524
use function array_unique;
26-
use function array_merge;
2725
use function sort;
2826

2927
final class Container implements ContainerInterface, FactoryInterface
3028
{
3129
/**
32-
* @var array<string, callable|Closure>
30+
* @var array<string, Closure>
3331
*/
3432
protected array $factories = [];
3533

@@ -48,32 +46,53 @@ final class Container implements ContainerInterface, FactoryInterface
4846
*/
4947
protected array $resolving = [];
5048

49+
protected bool $autowire = true;
50+
51+
protected ?ContainerInterface $delegate = null;
52+
5153
/**
52-
* @param iterable<string, mixed> $definitions
54+
* @param iterable<string|class-string, mixed> $definitions
5355
*/
54-
public function __construct(
55-
iterable $definitions = [],
56-
protected bool $autowire = true
57-
) {
56+
public function __construct(iterable $definitions = [])
57+
{
58+
$this->set(self::class, $this);
59+
$this->set(ContainerInterface::class, $this);
60+
5861
/** @var mixed $entry */
5962
foreach ($definitions as $id => $entry) {
6063
$this->set($id, $entry);
6164
}
6265
}
6366

67+
/**
68+
* @param mixed $entry
69+
*/
6470
protected function set(string $id, mixed $entry): void
6571
{
6672
unset($this->factories[$id], $this->cache[$id], $this->entries[$id]);
6773

68-
if ($entry instanceof Closure || (is_callable($entry) && !is_object($entry))) {
69-
/** @var callable|Closure */
74+
if ($entry instanceof Closure) {
7075
$this->factories[$id] = $entry;
7176
} else {
7277
$this->entries[$id] = $entry;
7378
}
7479
}
7580

76-
public function with(string $id, mixed $entry): Container
81+
public function withAutowiring(bool $flag): Container
82+
{
83+
$container = clone $this;
84+
$container->autowire = $flag;
85+
return $container;
86+
}
87+
88+
public function withDelegate(ContainerInterface $delegate): Container
89+
{
90+
$container = clone $this;
91+
$container->delegate = $delegate;
92+
return $container;
93+
}
94+
95+
public function withEntry(string $id, mixed $entry): Container
7796
{
7897
$container = clone $this;
7998
$container->set($id, $entry);
@@ -91,16 +110,12 @@ public function get(string $id): mixed
91110
return $this->entries[$id];
92111
}
93112

94-
if ($id === self::class || $id === ContainerInterface::class) {
95-
return $this;
96-
}
97-
98-
if ($this->autowire) {
99-
$this->entries[$id] = $this->create($id);
100-
return $this->entries[$id];
113+
if ($this->delegate?->has($id)) {
114+
return $this->delegate->get($id);
101115
}
102116

103-
throw new NotFoundException("Entry for < $id > could not be resolved");
117+
$this->entries[$id] = $this->create($id);
118+
return $this->entries[$id];
104119
}
105120

106121
/**
@@ -113,63 +128,66 @@ public function create(string $id, array $params = []): mixed
113128
}
114129

115130
if (isset($this->factories[$id])) {
116-
$this->cache[$id] = $this->getFactoryClosure($this->factories[$id]);
131+
$this->cache[$id] = $this->getClosureFactory($this->factories[$id]);
117132
return $this->resolve($id, $params);
118133
}
119134

120135
if ($this->canCreate($id)) {
136+
/** @var class-string $id */
121137
$this->cache[$id] = $this->getClassFactory($id);
122138
return $this->resolve($id, $params);
123139
}
124140

125-
throw new NotFoundException("Factory or class for < $id > could not be found");
141+
throw new NotFoundException("Entry, factory or class for < $id > could not be resolved");
126142
}
127143

128-
protected function resolve(string $id, array $params = []): mixed
144+
protected function resolve(string $id, array $params): mixed
129145
{
130-
if (isset($this->resolving[$id])) {
131-
throw new CircularReferenceException("Circular reference detected for < $id >");
132-
}
133-
134-
$this->resolving[$id] = true;
146+
try {
147+
if (isset($this->resolving[$id])) {
148+
$entries = array_keys($this->resolving);
149+
$path = implode(' -> ', [...$entries, $id]);
150+
throw new CircularReferenceException("Circular reference detected: $path");
151+
}
135152

136-
/** @var mixed */
137-
$entry = $this->cache[$id]($params);
153+
$this->resolving[$id] = true;
138154

139-
unset($this->resolving[$id]);
155+
/** @var mixed */
156+
$entry = $this->cache[$id]($params);
157+
} finally {
158+
unset($this->resolving[$id]);
159+
}
140160

141161
return $entry;
142162
}
143163

144-
protected function getFactoryClosure(callable $callable): Closure
164+
protected function getClosureFactory(Closure $closure): Closure
145165
{
146-
$closure = Closure::fromCallable($callable);
147-
148166
$function = new ReflectionFunction($closure);
149-
150167
$params = $function->getParameters();
151168

152-
return function () use ($function, $params): mixed {
153-
$args = $this->resolveFunctionParams($params);
154-
return $function->invokeArgs($args);
169+
return function (array $args) use ($function, $params): mixed {
170+
$newArgs = $this->resolveFunctionParams($params, $args, true);
171+
return $function->invokeArgs($newArgs);
155172
};
156173
}
157174

175+
/**
176+
* @param class-string $name
177+
*/
158178
protected function getClassFactory(string $name): Closure
159179
{
160-
/** @psalm-suppress ArgumentTypeCoercion */
161180
$class = new ReflectionClass($name);
162181

163182
if (!$class->isInstantiable()) {
164183
throw new NotInstantiableException("Unable to create < $name >, not instantiable");
165184
}
166185

167186
$constructor = $class->getConstructor();
168-
$params = $constructor ? $constructor->getParameters() : [];
187+
$params = $constructor?->getParameters() ?? [];
169188

170189
return function (array $args) use ($class, $params) {
171-
172-
$newArgs = $this->resolveFunctionParams($params, $args);
190+
$newArgs = $this->resolveFunctionParams($params, $args, false);
173191
return $class->newInstanceArgs($newArgs);
174192
};
175193
}
@@ -178,40 +196,37 @@ protected function getClassFactory(string $name): Closure
178196
* @param array<array-key, ReflectionParameter> $params
179197
* @return array<int, mixed>
180198
*/
181-
protected function resolveFunctionParams(array $params, array $replace = []): array
199+
protected function resolveFunctionParams(array $params, array $replace, bool $allowNames): array
182200
{
183201
$args = [];
184202

185203
foreach ($params as $param) {
186204

187205
$paramName = $param->getName();
188206

189-
if ($replace && (isset($replace[$paramName]) || array_key_exists($paramName, $replace))) {
190-
207+
if (isset($replace[$paramName]) || array_key_exists($paramName, $replace)) {
191208
/** @var mixed */
192209
$args[] = $replace[$paramName];
193210
continue;
194211
}
195212

196-
/** @var null|ReflectionNamedType */
197213
$type = $param->getType();
198214

199-
if ($type && !$type->isBuiltin()) {
215+
// we do not support union / intersection types for now
216+
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
200217
$className = $type->getName();
201218
/** @var mixed */
202219
$args[] = $this->get($className);
203220
continue;
204221
}
205222

206-
if ($this->has($paramName)) {
207-
223+
if ($allowNames && $this->has($paramName)) {
208224
/** @var mixed */
209225
$args[] = $this->get($paramName);
210226
continue;
211227
}
212228

213229
if ($param->isOptional()) {
214-
215230
/** @var mixed */
216231
$args[] = $param->getDefaultValue();
217232
continue;
@@ -229,7 +244,7 @@ protected function resolveFunctionParams(array $params, array $replace = []): ar
229244

230245
protected function canCreate(string $name): bool
231246
{
232-
return class_exists($name);
247+
return $this->autowire && class_exists($name);
233248
}
234249

235250
public function has(string $id): bool
@@ -238,13 +253,16 @@ public function has(string $id): bool
238253
isset($this->entries[$id]) ||
239254
isset($this->factories[$id]) ||
240255
isset($this->cache[$id]) ||
241-
array_key_exists($id, $this->entries) ||
242-
$this->canCreate($id)
256+
array_key_exists($id, $this->entries)
243257
) {
244258
return true;
245259
}
246260

247-
return false;
261+
if ($this->delegate?->has($id)) {
262+
return true;
263+
}
264+
265+
return $this->canCreate($id);
248266
}
249267

250268
/**
@@ -254,7 +272,7 @@ public function entries(): array
254272
{
255273
$entries = array_keys($this->entries);
256274
$factories = array_keys($this->factories);
257-
$combined = array_unique(array_merge($entries, $factories));
275+
$combined = array_unique([...$entries, ...$factories]);
258276

259277
sort($combined, SORT_NATURAL | SORT_FLAG_CASE);
260278

Diff for: tests/ContainerTest.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public function testContainerImmutability()
118118
{
119119
$container = new Container();
120120
$oldContainer = $container;
121-
$newContainer = $container->with('foo', 'bar');
121+
$newContainer = $container->withEntry('foo', 'bar');
122122
$this->assertEquals($container, $oldContainer);
123123
$this->assertNotEquals($container, $newContainer);
124124
$this->assertEquals('bar', $newContainer->get('foo'));
@@ -145,6 +145,8 @@ public function testListEntries()
145145
$expected = [
146146
'bar',
147147
'foo',
148+
ContainerInterface::class,
149+
Container::class,
148150
DepA::class,
149151
DepB::class,
150152
DepC::class

Diff for: tests/CreateTest.php

+11-10
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,29 @@ public function testParameterException()
3535
$this->expectException(ParameterResolveException::class);
3636

3737
$container = new Container();
38-
$c = $container->create(DepC::class);
38+
$container->create(DepC::class);
3939
}
4040

41-
public function testCreateArgs()
41+
public function testParameterException2()
4242
{
43-
$container = new Container();
44-
$c = $container->create(DepC::class, [
43+
$this->expectException(ParameterResolveException::class);
44+
45+
$container = new Container([
4546
'name' => 'Semperton'
4647
]);
4748

48-
$this->assertInstanceOf(DepC::class, $c);
49-
$this->assertInstanceOf(DepB::class, $c->b);
50-
$this->assertEquals('Semperton', $c->name);
49+
$container->get(DepC::class);
5150
}
5251

53-
public function testCreateAutoResolve()
52+
public function testCreateArgs()
5453
{
55-
$container = new Container([
54+
$container = new Container();
55+
$c = $container->create(DepC::class, [
5656
'name' => 'Semperton'
5757
]);
5858

59-
$c = $container->get(DepC::class);
59+
$this->assertInstanceOf(DepC::class, $c);
60+
$this->assertInstanceOf(DepB::class, $c->b);
6061
$this->assertEquals('Semperton', $c->name);
6162
}
6263
}

0 commit comments

Comments
 (0)