根据需要在 Laravel 服务容器中更改具体的 class 绑定

Change concrete class binding on demand in Laravel Service Container

如何动态更改具体的 class 绑定。

我正在尝试测试使用外部 API 的 artisan 命令。

class ConsumeApiCommand extends Command
{
    public function __construct(ClientInterface $client)
    {
        parent::__construct();

        $this->client = $client;
    }

    public function handle()
    {
        $api_response = $this->client->request('POST', 'http://external.api/resource');

        $response = json_decode($api_response);

        if(isset($response['error'])) {
            $this->error($response['error']);
        } else {
            $this->status($response['status']);
        }
    }
}

目前;我可以在测试中伪造具体 class。

class FakeServiceProvider extends AppServiceProvider
{
    public function register(): void
    {
        $this->app->bind(ClientInterface::class, function () {
            return new class implements ClientInterface {
                public function request($method, $uri, $headers = [], $body = [])
                {
                    return json_encode(['status' => "You've reached us."]);
                }
            };
        });
    }
}

通过。

public function test_can_consume_api_if_authenticated()
{
    $this->artisan('consume:api')
         ->expectsOutput("You've reached us.")
         ->assertExitCode(0);
}

失败; returns 初始绑定 class 响应 You've reached us.

public function test_cant_consume_api_if_not_authenticated()
{
    $this->app->bind(ClientInterface::class, function () {
        return new class implements ClientInterface {
            public function request($method, $uri, $headers = [], $body = [])
            {
                return json_encode(['error' => "Unauthorized."]);
            }
        };
    });

    $this->artisan('consume:api')
         ->expectsOutput("Unauthorized.")
         ->assertExitCode(0);
}

是否可以通过这种方式实现愿望行为?或者服务容器绑定在请求生命周期内不能改变?

始终允许将新的具体 class 绑定到 Laravel 中的接口。 我在这里看到的问题(我过去遇到过类似的情况)是当你 bind() 新的具体 class 时,artisan 命令已经初始化(使用旧绑定)。

在测试用例的 setUp() 方法中,您应该 re-register 命令以便它从接口获取新绑定。

要测试这是否是一个可能的解决方案,只需在命令的 __construct() 方法中添加 dump() 并在测试的 setUp 中添加另一个方法。如果我是正确的,你应该看到 che 命令的第一个出现,然后是另一个。

我只是为那些可能想知道我是如何解决这个问题的人留下一个代码片段。

基于,问题中的源是可以的,但我必须手动操作所需的接口,将其注入命令并将命令添加到服务容器中。

我无法动态覆盖绑定。

use Illuminate\Container\Container;
use Illuminate\Contracts\Console\Kernel;

class ConsumeApiCommandTest extends TestCase
{
    public function test_can_consume_api_if_authenticated()
    {
        $this->artisan('consume:api')
             ->expectsOutput("You've reached us.")
             ->assertExitCode(0);
    }

    public function test_cant_consume_api_if_not_authenticated()
    {
        // Mock client interface.
        $mock = \Mockery::mock(ClientInterface::class);

        // Here you override the methods you want.
        $mock->shouldReceive('request')->once()
             ->andReturn(json_encode(['error' => "Unauthorized."]));

        $command = new ConsumeApiCommand($mock);

        // Re-register artisan command.
        Container::getInstance()->make(Kernel::class)->registerCommand($command);

        $this->artisan('consume:api')
             ->expectsOutput("Unauthorized.")
             ->assertExitCode(0);
    }
}