ashallendesign/laravel-executor
Laravel Executor lets you define and run a sequence of tasks in a specified order. Chain closures, Artisan commands, jobs, and other executable classes, with support for stopping on failure and running tasks synchronously or in the background. Ideal for simple, testable workflow orchestration.
Install the package with Composer:
composer require ashallendesign/laravel-executor --dev
v2.2.0 adds PHP 8 support, so you no longer need --ignore-platform-reqs just to get past a PHP 8 platform check. You may still hit Laravel version constraints — verify against your app before committing:
composer require ashallendesign/laravel-executor --dev -W
If Composer resolves, you're on a Laravel it was tested against. If it refuses on your Laravel version, that's a package constraint issue, not a PHP one — fork the composer.json or pin an older major.
Publish the config file (this is what ties an Artisan command to your executor class):
php artisan vendor:publish --provider="AshAllenDesign\LaravelExecutor\LaravelExecutorServiceProvider"
This creates config/executor.php, which holds a class reference (typically keyed for install and update phases). Point it at your own executor class:
// config/executor.php
return [
\App\Executors\ApplicationExecutor::class,
];
Generate/extend an executor — the package ships an abstract Executor class you subclass:
// app/Executors/ApplicationExecutor.php
namespace App\Executors;
use AshAllenDesign\LaravelExecutor\Classes\Executor;
class ApplicationExecutor extends Executor
{
public function run(): void
{
$this
->artisan('migrate', ['--force' => true])
->artisan('db:seed')
->artisan('storage:link')
->composer('install --no-dev')
->npm('install && npm run prod')
->done();
}
}
Run it via the package's Artisan command (e.g. php artisan executor:run). First use case to target: a fresh dev environment bring-up — clone, .env, php artisan executor:run — replacing a README full of manual steps that people forget. That's the highest-leverage use and the easiest to validate before you trust it with production.
Before you build anything on top of it, check the composer.json constraints and the Laravel version they target. v2.2.0 is a PHP 8 compatibility release, not a Laravel 9/10 one — the Laravel constraint is still the 8-era line, so confirm with php artisan list | grep executor after publishing, and don't assume PHP 8 support implies modern Laravel support.
Phase separation (install vs. update). The core value is distinguishing "first run on a blank box" from "deploy N+1 onto an existing DB." Keep two methods (or two executor classes) and let the config map each to its own command invocation. Install does migrate:fresh, seeding, key generation, storage links. Update does only migrate --force, cache warming, and asset rebuilds. Never let update touch seeders.
Wrap, don't replace, your deploy script. Treat the executor as a thin, testable description of steps and keep it callable from Envoyer, GitHub Actions, or a composer deploy script. Because the steps are PHP chained calls, you get IDE completion and conditional logic that a bash deploy.sh can't express:
public function run(): void
{
$this
->artisan('down')
->artisan('migrate', ['--force' => true])
->if(fn () => app()->environment('production'), function ($executor) {
$executor->artisan('horizon:terminate')->artisan('queue:restart');
})
->artisan('config:cache')
->artisan('route:cache')
->artisan('up')
->done();
}
Maintenance mode as the first and last step is the pattern that saves you from half-deployed apps; keep it symmetrical even when a step in the middle fails.
Compose executors for shared fragments. If dev and production both need storage:link and migrate, extract a private method returning a configured executor and chain it in from both entry points. This beats copy-paste and keeps the "what runs where" matrix readable in one file.
Gate destructive steps behind an explicit flag. migrate:fresh in an executor is a foot-gun. Either place it only in the install executor or guard it with an environment check plus an interactive confirmation via $this->confirm(...) if available in your version. A CI environment should never be able to hit it accidentally.
Idempotency as a design rule. Every step should be safe to re-run. migrate is; db:seed usually isn't. If a step isn't idempotent, either make it conditional (check a sentinel record first) or move it out of the executor into a one-shot command. The executor will get re-run on failed deploys, and non-idempotent steps are how you end up with duplicate rows in production.
PHP 8 support is not Laravel-version support. v2.2.0 lifts the PHP 8 blocker, which removes the main reason people were reaching for --ignore-platform-reqs. It does not mean the package's Laravel constraints moved — it's still a 2020-era package with no release cadence since. Check composer.json for the illuminate/* bounds after installing, and treat "PHP 8 works" and "Laravel 10 works" as two separate questions you have to answer yourself.
The package is dormant. Two-plus years with effectively no functional releases (this one is a compat flag), zero dependents, and a dependency tree tied to Laravel 8. Budget for either a composer.json fork or accepting that you'll migrate off it. Don't make it load-bearing in a client project without a written note on the exit path — the executor class is just PHP, so porting to a plain Artisan command is a mechanical hour, which is your insurance.
Exit codes are the thing that bites you. A chained executor that keeps going after a failed migrate will produce a "successful" deploy with a broken schema. Verify explicitly that a non-zero exit from a shell step halts the chain (the package should throw), and if it doesn't, wrap shell steps so you throw yourself. Then confirm your CI reads the executor command's exit status — a green pipeline that ignored a failure is worse than a red one.
Output buffering swallows debugging. When a subprocess fails you often get the exit code but not the stderr. Capture and dump output around the failing step, or run the executor locally with full verbosity first. Reproduce the exact failing command manually before assuming your executor logic is wrong — nine times out of ten it's the step, not the runner.
Environment and working directory differ from your shell. php artisan executor:run inherits the CWD and env of wherever it's invoked from, not your project root. composer and npm calls in the executor can silently run in the wrong directory or with a different COMPOSER_HOME. Assert the working directory at the top of run() with an exec of pwd during setup, then trust it.
Long-running steps vs. timeouts. npm run prod, composer install, and migrate on a large DB will exceed default web/queue timeouts if you ever wire the executor into a queued job. Keep it CLI-only, or set generous timeouts explicitly. Interleaving it with an HTTP request is a reliable way to get a 504 halfway through migrate.
Container state is not persistent. Do not expect in-memory changes (a cached config, a config() tweak) made in one executor step to survive into another — especially if steps shell out. Each artisan step is effectively a new PHP process. Anything that must persist belongs in a file, a DB, or a .env write.
Extension points are limited; plan to own it. The abstract Executor is small, so adding a custom step type (a Slack webhook ping, a Sentry release marker) means subclassing or writing your own trait. That's fine, but pin the version in composer.json exactly — a dormant package plus a wildcard constraint means an unreviewed update lands in your deploy path. Also snapshot the published config/executor.php into the repo; if the package is abandoned, the file is what keeps your project bootable.
Migrate before you cache. config:cache and route:cache before migrate is the classic ordering bug — cached config can mask new env vars the migration needs, and a failing migration leaves you with stale caches. Order: maintenance mode on → migrate → clear → cache → maintenance mode off.
How can I help you explore Laravel packages today?