Skip to content

AlwaysCuriousCo/laravel-project-devtool

Repository files navigation

Laravel Project Devtool

Latest Version on Packagist Tests Total Downloads

Simplify your developers' startup time on new projects from an existing repo — one command to a clean-slate dev environment, and a clean way for your app to hook into it.

php artisan project:dev --setup tears the local environment down and rebuilds it from scratch: clear caches → migrate:fresh → seed → build assets. Onboard a new teammate, recover from a broken branch, or reset between feature spikes in a single command instead of a wiki page of steps.

The part that makes it worth installing instead of writing your own shell script: at every stage of the reset it fires a lifecycle event, and your app attaches its own work by dropping a listener into app/Listeners. Generate permissions, seed demo data, print login credentials — without ever editing this package. The engine is mechanism; your app supplies the policy.

⚠️ Dev-only. --setup runs migrate:fresh, which drops every table. Install it as a --dev dependency and never point it at data you care about.


Install

composer require --dev alwayscurious/laravel-project-devtool

That's the whole setup — the command auto-registers. Publish the config only if you want to change defaults:

php artisan vendor:publish --tag="project-devtool-config"

Try it:

php artisan project:dev            # prints available actions, does nothing destructive
php artisan project:dev --setup    # the full reset, with a confirmation prompt

The command

project:dev
    {--setup            : Fresh reset — rebuild DB, seed, and build assets}
    {--new              : With --setup, also install dependencies (composer + npm install)}
    {--force            : With --setup, skip confirmation prompts}
    {--dry-run          : With --setup, simulate the run without making any changes}
    {--only=            : With --setup, run ONLY these steps (comma-separated)}
    {--skip=            : With --setup, skip these steps (comma-separated)}
    {--force-production : Allow --setup to run when the app environment is production}
You want to… Run
See what's available (safe, no-op) php artisan project:dev
Reset the environment php artisan project:dev --setup
Reset a freshly cloned repo, deps and all php artisan project:dev --setup --new
Reset unattended (CI, scripts) php artisan project:dev --setup --force
Preview exactly what would happen, change nothing php artisan project:dev --setup --dry-run
Just reseed (no wipe, no asset build) php artisan project:dev --setup --only=seed
Everything except the slow asset build php artisan project:dev --setup --skip=build

Before wiping anything, --setup confirms with a prompt that names the exact connection and database it's about to drop — so a wrong .env can't surprise you. --force skips it. And it refuses to run in production unless you pass --force-production.

Run only the steps you need

--setup is no longer all-or-nothing. The selectable steps, in order, are install, caches, migrate, seed, build. Use --only to run a subset or --skip to drop a few:

php artisan project:dev --setup --only=migrate,seed   # rebuild + reseed, skip caches/build
php artisan project:dev --setup --skip=build          # everything but the asset build

The confirmation prompt only appears when a destructive step (migrate or seed) is actually in the run — --only=caches won't ask.

Preview with --dry-run

--dry-run walks the entire sequence, prints what each step would do, fires every lifecycle event (so your listeners can announce their intent too), and changes nothing — no migrations, no seeding, no processes, no prompt:

DRY RUN — simulating; no changes will be made.

  [dry-run] would run: optimize:clear
  [dry-run] would run: migrate:fresh
  [dry-run] would run: db:seed
  [dry-run] would run: asset build

✔ Dry run complete — no changes were made.

Timing report

Every real run prints a per-step timing table at the end, so you can see where onboarding time actually goes:

 ------------------ ---------
  Step               Time
 ------------------ ---------
  optimize:clear     0.04s
  migrate:fresh      2.11s
  db:seed            0.83s
  asset build       14.29s
  total             17.27s
 ------------------ ---------

How --setup runs (and where you plug in)

The sequence is fixed and ordering-sensitive — your listeners can rely on it:

  ┌─ SetupStarting      ← guard point; a listener may veto here (AbortSetup)
  │   (wipe confirmation)
  ├─ optimize:clear  →  CachesCleared
  ├─ migrate:fresh   →  DatabaseMigrated   ← the gap before seeding
  ├─ db:seed         →  DatabaseSeeded
  ├─ asset build     →  AssetsBuilding (fired just before the build)
  └─ SetupCompleted    ← end-of-run report point

Each event carries the running command, so a listener gets the live console and can run nested artisan commands:

$event->command->info('');                                 // same output stream
$event->command->option('new');                             // read invocation flags
$event->command->call('some:command', ['--force' => true]); // nested artisan
$event->dryRun;                                             // true during --dry-run

Why seeding comes after a separate DatabaseMigrated event

This is the design detail the package exists for. Some apps must generate data (permissions, lookup tables) after the schema exists but before the seeder runs — because the seeder consumes it (e.g. granting permissions to a role). So the reset deliberately fires DatabaseMigrated in the gap between migrate:fresh and db:seed, giving you a hook at exactly that moment. Merge the two and the seam disappears; keep them split and your app slots right in.


Integrating your app: write a hook

A hook is just a listener that type-hints one of the lifecycle events. Laravel's event discovery wires it up — no registration, no config, no service provider edits.

Scaffold one

php artisan make:dev-hook BuildDemoData --event=DatabaseSeeded

Generates app/Listeners/BuildDemoData.php, pre-typed to the event and documented with the full event list. --event is validated and defaults to DatabaseSeeded.

Or write it by hand

namespace App\Listeners;

use AlwaysCurious\LaravelProjectDevtool\Events\DatabaseSeeded;

class BuildDemoData
{
    public function handle(DatabaseSeeded $event): void
    {
        // Respect a simulation: announce intent, change nothing.
        if ($event->dryRun) {
            $event->command->line('  [dry-run] would seed demo content');

            return;
        }

        $event->command->info('Seeding demo content…');
        $event->command->call('db:seed', [
            '--class' => \Database\Seeders\DemoSeeder::class,
            '--force' => true,
        ]);
    }
}

Lifecycle reference

Event Fires… Reach for it to…
SetupStarting before the wipe confirmation; may throw AbortSetup guard the run (env present? right branch?)
CachesCleared after optimize:clear prep that needs a clean cache/config
DatabaseMigrated after migrate:fresh, before seeding generate permissions / data the seeder needs
DatabaseSeeded after db:seed demo / sample data
AssetsBuilding just before the asset build prepare build inputs
SetupCompleted after the build, before the command returns print login URLs, credentials, next steps

Hooks can't break the reset

A thrown exception from any listener is reported and swallowed so a broken custom hook never leaves you with a half-built database. The single exception is the deliberate veto below.


Guarding the reset: AbortSetup

Veto a doomed run before anything destructive happens by throwing AbortSetup from a SetupStarting listener:

namespace App\Listeners;

use AlwaysCurious\LaravelProjectDevtool\Events\AbortSetup;
use AlwaysCurious\LaravelProjectDevtool\Events\SetupStarting;

class EnsureSuperAdminPassword
{
    public function handle(SetupStarting $event): void
    {
        if (empty(env('SUPER_ADMIN_PASSWORD'))) {
            throw new AbortSetup('SUPER_ADMIN_PASSWORD is not set — refusing to reset.');
        }
    }
}

The command prints the message, says “Nothing was changed.”, and exits with a failure code — the database is never touched. AbortSetup is honoured only from the SetupStarting pre-flight point; thrown later it's treated as a misplaced veto (warned and swallowed) so it can't corrupt a half-built database.


Batteries (opt-in recipes)

Common integrations ship as listeners that are off by default — a project that doesn't use them pulls in zero coupling — and self-guard when an optional dependency is missing.

Filament Shield permissions

GenerateShieldPermissions regenerates filament-shield permissions on DatabaseMigrated, in the gap before seeding, so your seeder can grant them to a super-admin role. If filament-shield isn't installed it skips with a notice instead of failing. Enable it in config:

'recipes' => [
    'shield' => [
        'enabled' => true,
        'panel'   => 'admin',
    ],
],

Configuration

config/project-devtool.php — everything you'd want to vary, nothing hardcoded:

return [
    // Seeder class run during --setup (null = framework default DatabaseSeeder).
    'seeder' => null,

    // Asset build command (argv array). Set to null/[] to skip the build step.
    'build' => ['npm', 'run', 'build'],

    // Dependency install commands used by --new.
    'install' => [
        'composer' => ['composer', 'install'],
        'npm'      => ['npm', 'install'],
    ],

    // Opt-in recipes — off by default.
    'recipes' => [
        'shield' => ['enabled' => false, 'panel' => 'admin'],
    ],
];
  • buildnull/[] skips the asset build (the command tells you it did).
  • seeder → a class name to seed something other than the default DatabaseSeeder.
  • install → swap in yarn, pnpm, bun, etc.

Requirements

  • PHP ^8.3
  • Laravel 11, 12, or 13

Testing

composer test

License

The MIT License (MIT). See License File.

About

Simplify your developer's startup time on new projects from existing repo.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages