Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/Illuminate/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Illuminate\Contracts\Container\CircularDependencyException;
use Illuminate\Contracts\Container\Container as ContainerContract;
use Illuminate\Contracts\Container\ContextualAttribute;
use Illuminate\Contracts\Container\SelfBuilding;
use Illuminate\Support\Collection;
use LogicException;
use ReflectionAttribute;
Expand Down Expand Up @@ -1169,6 +1170,11 @@ public function build($concrete)
return $this->notInstantiable($concrete);
}

if (is_a($concrete, SelfBuilding::class, true) &&
! in_array($concrete, $this->buildStack, true)) {
return $this->buildSelfBuildingInstance($concrete, $reflector);
}

$this->buildStack[] = $concrete;

$constructor = $reflector->getConstructor();
Expand Down Expand Up @@ -1208,6 +1214,34 @@ public function build($concrete)
return $instance;
}

/**
* Instantiate a concrete instance of the given self building type.
*
* @param \Closure(static, array): TClass|class-string<TClass> $concrete
* @param \ReflectionClass $reflector
* @return TClass
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function buildSelfBuildingInstance($concrete, $reflector)
{
if (! method_exists($concrete, 'newInstance')) {
throw new BindingResolutionException("No newInstance method exists for [$concrete].");
}

$this->buildStack[] = $concrete;

$instance = $this->call([$concrete, 'newInstance']);

array_pop($this->buildStack);

$this->fireAfterResolvingAttributeCallbacks(
$reflector->getAttributes(), $instance
);

return $instance;
}

/**
* Resolve all of the dependencies from the ReflectionParameters.
*
Expand Down
10 changes: 10 additions & 0 deletions src/Illuminate/Contracts/Container/SelfBuilding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Illuminate\Contracts\Container;

/**
* @method static newInstance(): static
*/
interface SelfBuilding
{
}
46 changes: 46 additions & 0 deletions tests/Container/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Container\EntryNotFoundException;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Container\ContextualAttribute;
use Illuminate\Contracts\Container\SelfBuilding;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use stdClass;
Expand Down Expand Up @@ -899,6 +900,20 @@ public function testSingletonWithBind()
$this->assertSame($original, $new);
}

public function testWithFactoryHasDependency()
{
$container = new Container;
$_SERVER['__withFactory.email'] = '[email protected]';
$_SERVER['__withFactory.userId'] = 999;

$container->bind(RequestDtoDependencyContract::class, RequestDtoDependency::class);
$r = $container->make(RequestDto::class);

$this->assertInstanceOf(RequestDto::class, $r);
$this->assertEquals(999, $r->userId);
$this->assertEquals('[email protected]', $r->email);
}

// public function testContainerCanCatchCircularDependency()
// {
// $this->expectException(\Illuminate\Contracts\Container\CircularDependencyException::class);
Expand Down Expand Up @@ -1171,3 +1186,34 @@ class IsScopedConcrete implements IsScoped
interface IsSingleton
{
}

class RequestDto implements SelfBuilding
{
public function __construct(
public readonly int $userId,
public readonly string $email,
) {
}

public static function newInstance(RequestDtoDependencyContract $dependency): self
{
return new self(
$dependency->userId,
$_SERVER['__withFactory.email'],
);
}
}

interface RequestDtoDependencyContract
{
}

class RequestDtoDependency implements RequestDtoDependencyContract
{
public int $userId;

public function __construct()
{
$this->userId = $_SERVER['__withFactory.userId'];
}
}
70 changes: 70 additions & 0 deletions tests/Illuminate/Tests/Container/BuildableIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Illuminate\Tests\Container;

use Illuminate\Container\Attributes\Config;
use Illuminate\Contracts\Container\SelfBuilding;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Orchestra\Testbench\TestCase;

class BuildableIntegrationTest extends TestCase
{
public function test_build_method_can_resolve_itself_via_container(): void
{
config([
'aim' => [
'api_key' => 'api-key',
'user_name' => 'cosmastech',
'away_message' => [
'duration' => 500,
'body' => 'sad emo lyrics',
],
],
]);

$config = $this->app->make(AolInstantMessengerConfig::class);

$this->assertEquals(500, $config->awayMessageDuration);
$this->assertEquals('sad emo lyrics', $config->awayMessage);
$this->assertEquals('api-key', $config->apiKey);
$this->assertEquals('cosmastech', $config->userName);

config(['aim.away_message.duration' => 5]);

try {
$this->app->make(AolInstantMessengerConfig::class);
} catch (ValidationException $exception) {
$this->assertArrayHasKey('away_message.duration', $exception->errors());
$this->assertStringContainsString('60', $exception->errors()['away_message.duration'][0]);
}
}
}

class AolInstantMessengerConfig implements SelfBuilding
{
public function __construct(
#[Config('aim.api_key')]
public string $apiKey,
#[Config('aim.user_name')]
public string $userName,
#[Config('aim.away_message.duration')]
public int $awayMessageDuration,
#[Config('aim.away_message.body')]
public string $awayMessage
) {
}

public static function newInstance()
{
Validator::make(config('aim'), [
'api-key' => 'string',
'user_name' => 'string',
'away_message' => 'array',
'away_message.duration' => ['integer', 'min:60', 'max:3600'],
'away_message.body' => ['string', 'min:1'],
])->validate();

return app()->build(static::class);
}
}