Skip to content

Commit 77f90cb

Browse files
cosmastechrodrigopedrataylorotwell
authored
[12.x] Colocate Container build functions with the Buildable interface (#56731)
* WithFactory * skip when the concrete is already on the buildStack * fixes Co-authored-by: Rodrigo Pedra Brum <[email protected]> * buildable integration test * style * test naming * test dependency injection * formatting * rename interface * fix tests --------- Co-authored-by: Rodrigo Pedra Brum <[email protected]> Co-authored-by: Taylor Otwell <[email protected]>
1 parent 45cb5bb commit 77f90cb

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

src/Illuminate/Container/Container.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Illuminate\Contracts\Container\CircularDependencyException;
1313
use Illuminate\Contracts\Container\Container as ContainerContract;
1414
use Illuminate\Contracts\Container\ContextualAttribute;
15+
use Illuminate\Contracts\Container\SelfBuilding;
1516
use Illuminate\Support\Collection;
1617
use LogicException;
1718
use ReflectionAttribute;
@@ -1169,6 +1170,11 @@ public function build($concrete)
11691170
return $this->notInstantiable($concrete);
11701171
}
11711172

1173+
if (is_a($concrete, SelfBuilding::class, true) &&
1174+
! in_array($concrete, $this->buildStack, true)) {
1175+
return $this->buildSelfBuildingInstance($concrete, $reflector);
1176+
}
1177+
11721178
$this->buildStack[] = $concrete;
11731179

11741180
$constructor = $reflector->getConstructor();
@@ -1208,6 +1214,34 @@ public function build($concrete)
12081214
return $instance;
12091215
}
12101216

1217+
/**
1218+
* Instantiate a concrete instance of the given self building type.
1219+
*
1220+
* @param \Closure(static, array): TClass|class-string<TClass> $concrete
1221+
* @param \ReflectionClass $reflector
1222+
* @return TClass
1223+
*
1224+
* @throws \Illuminate\Contracts\Container\BindingResolutionException
1225+
*/
1226+
protected function buildSelfBuildingInstance($concrete, $reflector)
1227+
{
1228+
if (! method_exists($concrete, 'newInstance')) {
1229+
throw new BindingResolutionException("No newInstance method exists for [$concrete].");
1230+
}
1231+
1232+
$this->buildStack[] = $concrete;
1233+
1234+
$instance = $this->call([$concrete, 'newInstance']);
1235+
1236+
array_pop($this->buildStack);
1237+
1238+
$this->fireAfterResolvingAttributeCallbacks(
1239+
$reflector->getAttributes(), $instance
1240+
);
1241+
1242+
return $instance;
1243+
}
1244+
12111245
/**
12121246
* Resolve all of the dependencies from the ReflectionParameters.
12131247
*
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Container;
4+
5+
/**
6+
* @method static newInstance(): static
7+
*/
8+
interface SelfBuilding
9+
{
10+
}

tests/Container/ContainerTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Container\EntryNotFoundException;
1111
use Illuminate\Contracts\Container\BindingResolutionException;
1212
use Illuminate\Contracts\Container\ContextualAttribute;
13+
use Illuminate\Contracts\Container\SelfBuilding;
1314
use PHPUnit\Framework\TestCase;
1415
use Psr\Container\ContainerExceptionInterface;
1516
use stdClass;
@@ -899,6 +900,20 @@ public function testSingletonWithBind()
899900
$this->assertSame($original, $new);
900901
}
901902

903+
public function testWithFactoryHasDependency()
904+
{
905+
$container = new Container;
906+
$_SERVER['__withFactory.email'] = '[email protected]';
907+
$_SERVER['__withFactory.userId'] = 999;
908+
909+
$container->bind(RequestDtoDependencyContract::class, RequestDtoDependency::class);
910+
$r = $container->make(RequestDto::class);
911+
912+
$this->assertInstanceOf(RequestDto::class, $r);
913+
$this->assertEquals(999, $r->userId);
914+
$this->assertEquals('[email protected]', $r->email);
915+
}
916+
902917
// public function testContainerCanCatchCircularDependency()
903918
// {
904919
// $this->expectException(\Illuminate\Contracts\Container\CircularDependencyException::class);
@@ -1171,3 +1186,34 @@ class IsScopedConcrete implements IsScoped
11711186
interface IsSingleton
11721187
{
11731188
}
1189+
1190+
class RequestDto implements SelfBuilding
1191+
{
1192+
public function __construct(
1193+
public readonly int $userId,
1194+
public readonly string $email,
1195+
) {
1196+
}
1197+
1198+
public static function newInstance(RequestDtoDependencyContract $dependency): self
1199+
{
1200+
return new self(
1201+
$dependency->userId,
1202+
$_SERVER['__withFactory.email'],
1203+
);
1204+
}
1205+
}
1206+
1207+
interface RequestDtoDependencyContract
1208+
{
1209+
}
1210+
1211+
class RequestDtoDependency implements RequestDtoDependencyContract
1212+
{
1213+
public int $userId;
1214+
1215+
public function __construct()
1216+
{
1217+
$this->userId = $_SERVER['__withFactory.userId'];
1218+
}
1219+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Container;
4+
5+
use Illuminate\Container\Attributes\Config;
6+
use Illuminate\Contracts\Container\SelfBuilding;
7+
use Illuminate\Support\Facades\Validator;
8+
use Illuminate\Validation\ValidationException;
9+
use Orchestra\Testbench\TestCase;
10+
11+
class BuildableIntegrationTest extends TestCase
12+
{
13+
public function test_build_method_can_resolve_itself_via_container(): void
14+
{
15+
config([
16+
'aim' => [
17+
'api_key' => 'api-key',
18+
'user_name' => 'cosmastech',
19+
'away_message' => [
20+
'duration' => 500,
21+
'body' => 'sad emo lyrics',
22+
],
23+
],
24+
]);
25+
26+
$config = $this->app->make(AolInstantMessengerConfig::class);
27+
28+
$this->assertEquals(500, $config->awayMessageDuration);
29+
$this->assertEquals('sad emo lyrics', $config->awayMessage);
30+
$this->assertEquals('api-key', $config->apiKey);
31+
$this->assertEquals('cosmastech', $config->userName);
32+
33+
config(['aim.away_message.duration' => 5]);
34+
35+
try {
36+
$this->app->make(AolInstantMessengerConfig::class);
37+
} catch (ValidationException $exception) {
38+
$this->assertArrayHasKey('away_message.duration', $exception->errors());
39+
$this->assertStringContainsString('60', $exception->errors()['away_message.duration'][0]);
40+
}
41+
}
42+
}
43+
44+
class AolInstantMessengerConfig implements SelfBuilding
45+
{
46+
public function __construct(
47+
#[Config('aim.api_key')]
48+
public string $apiKey,
49+
#[Config('aim.user_name')]
50+
public string $userName,
51+
#[Config('aim.away_message.duration')]
52+
public int $awayMessageDuration,
53+
#[Config('aim.away_message.body')]
54+
public string $awayMessage
55+
) {
56+
}
57+
58+
public static function newInstance()
59+
{
60+
Validator::make(config('aim'), [
61+
'api-key' => 'string',
62+
'user_name' => 'string',
63+
'away_message' => 'array',
64+
'away_message.duration' => ['integer', 'min:60', 'max:3600'],
65+
'away_message.body' => ['string', 'min:1'],
66+
])->validate();
67+
68+
return app()->build(static::class);
69+
}
70+
}

0 commit comments

Comments
 (0)