Search code examples
laravelinversion-of-controllaravel-artisan

Change concrete class binding on demand in Laravel Service Container


How do I change a concrete class binding dynamically.

I'm trying to test an artisan command that consumes an external API.

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']);
        }
    }
}

Currently; I can fake the concrete class in my tests.

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."]);
                }
            };
        });
    }
}

Passing.

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

Failing; returns initially binded class response 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);
}

Is it possible to achieve the desire behavior this way? Or service container bindings can't change during the request lifetime?


Solution

  • Am just leaving a code snippet for those that might be wondering how I solved this.

    Basing on Leonardo's answer, the source in the question is okay, but I had to manipulate the required interface manually, inject it into the command and add the command to the service container.

    I was not able to override the binding dynamically.

    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);
        }
    }