TL;DR
- Rewrote a bootstrap seeder so the deployment stack — DB engine, queue runner, PHP version — is config, not hardcoded.
- Renamed it away from a phase-based name (
MvpSeeder). If a class name describes when you wrote it, it expires. - Trimmed a CI matrix from three PHP versions to one. Matrix breadth should match what you actually deploy.
The seeder that only worked for me
Bootstrap seeders rot predictably. Version one seeds exactly the environment the author had open at the time: one database engine, one queue setup, one PHP version. It works, so nobody touches it. Six months later someone needs Postgres and discovers the choice was never a choice.
I hit that on a control-plane app — the thing that describes and provisions deployments. Its seeder encoded a single stack shape, so I pulled the variable parts into config.
'control_plane' => [
'default_blueprint' => 'stack-laravel', // or -ha, -single-node
'database' => env('CONTROL_PLANE_DB', 'mysql'), // mysql | postgresql
'queue' => env('CONTROL_PLANE_QUEUE', 'horizon'), // horizon | default
'php_version' => env('CONTROL_PLANE_PHP', '8.4'),
'apps' => [
// each entry may override blueprint / queue per app
],
],
Enter fullscreen mode Exit fullscreen mode
Knob Options Default from Blueprint single-node, standard, HA config Database MySQL, PostgreSQL env Queue runner Horizon, plainqueue:work
env
PHP runtime
8.3 – 8.5
env
Two rules kept it tidy. Every knob has a sane default, so a fresh clone still seeds without a populated .env. And every knob is overridable per app — the moment you have more than one workload, one will be the exception.
config/seeder.php
|
v
PrepareSeeder ----> ControlPlaneSeeder
|
+-------------+-------------+
v v v
blueprint database queue runner
(single/std/ha) (mysql/pgsql) (horizon/default)
\ | /
+-----> Workload + Deployment
Enter fullscreen mode Exit fullscreen mode
Horizon or not is a real fork
Both options run under supervisord, but they’re different operational bargains:
Horizon Plainqueue:work
Dashboard
Yes
No
Auto-balancing across queues
Yes
Manual worker counts
Extra dependency
Redis required
Any queue driver
Config surface
config/horizon.php
Supervisor program blocks
Horizon is the better default for real queue load. But a single-node app running three jobs a day doesn’t need Redis in the stack just for a dashboard nobody opens. Per-app key means neither choice forks the codebase.
Rename anything named after a phase
The seeder used to be MvpSeeder. It’s now ControlPlaneSeeder.
MvpSeeder answers “when was this written”. ControlPlaneSeeder answers “what does this do”. The first stops being interesting the moment the MVP ships; the second stays true for the life of the class. Same for NewUserService, V2Controller, TempJob — timestamps pretending to be names.
While I was in there: the CI matrix
Related cleanup elsewhere. A test workflow was running PHP 8.3, 8.4 and 8.5. Trimmed to 8.5.
Matrix breadth should match your deployment surface. A published package genuinely needs to prove it works across versions — its users pick the runtime, not you. An application you deploy yourself runs exactly one PHP version, and testing two others buys nothing but Actions minutes.
Project type Matrix Published package Every supported PHP/framework combo Deployed application The version you actually deployAlso removed a duplicate workflow running the same suite twice per push. Free 50% cut, which is the best kind.
Test the knobs
Seeders are worth a test the moment they encode decisions rather than fixtures:
it('seeds the queue runner from config', function () {
config(['seeder.control_plane.queue' => 'default']);
$this->seed(ControlPlaneSeeder::class);
expect(Deployment::first()->queue_runner)->toBe('default');
});
Enter fullscreen mode Exit fullscreen mode
Without it, “the config knob works” is an assumption that holds until the one time you actually flip it — usually on a fresh server at an inconvenient hour.
Takeaway
A seeder is documentation that executes. If it says “this system runs MySQL with Horizon on PHP 8.4”, it should be describing a default, not a constraint. Push the specifics to config, keep the shape in the class, and give every knob a default that works out of the box.
답글 남기기