Instantiating an Abstract Class with Dependency Injection

In my Laravel app, I wrote an abstract class that has a fair bit of stand-alone behaviour. I wanted to test this directly instead of via the implementations.

PHP has anonymous classes, so instantiating it isn’t very tricky to do in a unit test. But the constructor also has about a dozen dependencies its asking Laravel’s service container for, and I wanted [almost] all those to be injected for me.

Problem is, I don’t think PHP lets you create an anonymous class and assign it to a variable, without instantiating it at the same time.

Instead, I found a clever trick: I can implement my own constructor and use App::call() on parent::__construct.

new class extends WorkflowFromJsonRepository {
    public function __construct()
    {
        App::call(parent::__construct(...), [
            'someParamToStub' => $stubbedThing,
        ]);
    }
};

If you need to pass stubs in, you can pick and choose by passing them in the second call() parameter. Ya’know, standard Laravel stuff at that point.

I am not sure if this trick would works on anything below PHP 8.1. The syntax with the three dots — parent::__construct(...) — is the new first-class callable feature. I’m not sure if you can get a handle on that using the older array-based syntax?

Wake up your RDS Aurora Serverless before running your migrations

When you’re using an RDS Aurora Serverless DB instance with Laravel Vapor, you have the option to scale it down to zero capacity units when it’s been idle. This is great for development environments — it only takes a few seconds to come back up, and while it’s hibernating, you’re saving loads of money.

One downside is that your migrations may fail during deployments. When Vapor goes to run the php artisan migrate command, RDS often won’t wake up before Laravel’s DB connection attempt times out:

An error occurred during deployment.

Message: Deployment hook failed.
Hook: migrate --force

In Connection.php line 678:
                                                                               
  SQLSTATE[08006] [7] timeout expired (SQL: select * from information_schema.  
  tables where table_schema = public and table_name = migrations and table_ty  
  pe = 'BASE TABLE')                                                           
                                                                               

In Connector.php line 70:
                                       
  SQLSTATE[08006] [7] timeout expired  

I wrote an artisan command to poke the DB a couple times, and added this to my vapor.yml as its first deployment step.

<?php

// app/Console/Commands/WakeUpDatabase.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class WakeUpDatabase extends Command
{
    protected $signature = 'db:wake {retries=5 : attempts to make} {wait=5 : time to wait between retries, in seconds}';
    protected $description = 'Wakes up a potentially-inactive serverless RDS database';

    public function handle()
    {
        $retries = (int) $this->argument('retries');
        $wait_between = ((int) $this->argument('wait')) * 1000;

        retry($retries, fn () => DB::select('SELECT 1'), $wait_between);

        $this->info('Database is up');
    }
}

Add php artisan db:wake to your vapor.yml deploy: section, right above the migrate command, in each environment you have an Aurora Serverless DB.

Problem solved!