dragon-code/laravel-actions
dragon-code/laravel-actions is a lightweight Laravel package for encapsulating business logic in reusable action classes. It provides a simple API to define, resolve, and execute actions, helping keep controllers and models thin while making workflows easier to test and maintain.
The first thing to sort out is the naming: the package is published as dragon-code/laravel-actions, but the repository and the description point at Laravel Deploy Operations (TheDragonCode/laravel-deploy-operations). Do not confuse it with lorisleiva/laravel-actions (the action-class pattern) — this one is about running one-off operations during a deployment, tracked in a database table so each operation executes exactly once per environment.
Minimal path from zero to a working operation:
composer require dragon-code/laravel-actions --dev
composer show | grep dragon-code
You should see the deploy-operations package. If you got something else, stop and re-check before writing any code.php artisan vendor:publish --provider="DragonCode\LaravelDeployOperations\ServiceProvider"
php artisan migrate
php artisan list | grep -i deploy
Expect the family to be deploy:make (scaffold), deploy:run (execute pending), and typically a status/rollback variant. Check whether deploy:run advertises --isolated — 7.3.0 fixed its presence semantics to match Laravel core, so it is now a real option worth using (see Gotchas).php artisan deploy:make warm_product_cache
run() or up()/down(). That determines whether you get reversibility for free. Also check whether operations can be queued (the base class exposes queue-related props in recent versions).First use case: the classic one that motivates the package — a cache/settings refresh that must happen after the new code is live and after migrations, but must not re-run on every deploy. Write a single operation that flushes the config/route/view caches and warms the pages that matter, run it against staging, then wire deploy:run into the deploy script.
CI/CD hook, not a manual step. The value of this package collapses if someone has to remember to run it. Put it at the end of the deploy pipeline, after composer install, after php artisan migrate --force:
php artisan migrate --force
php artisan deploy:run --force # verify the exact non-interactive flag in your version
php artisan queue:restart
The ordering matters: operations frequently read schema or config that only exists post-migration.
One operation per concern. Because the package records execution, an operation is effectively a named, permanently-retired unit of work. Keep them small and single-purpose — backfill_order_totals, migrate_legacy_slugs, enable_new_billing_flag. A monolithic "do everything" operation becomes un-retryable the moment step 4 of 9 fails.
Data backfills in chunks. Operations are the right home for the work that is too slow or too risky for a migration:
Order::query()->whereNull('total_cents')
->chunkById(500, fn ($orders) => /* recompute */);
Chunking keeps memory flat and lets you re-run the residual set by writing a follow-up operation rather than editing the executed one.
Long work: dispatch a job, don't grind in the deploy. Recent releases make queued operations a first-class path. Rather than chunking a multi-million-row backfill inside the deploy step, have the operation dispatch a job (or mark the operation itself as queued, if your version's generated stub supports it) and keep the artisan call short. Since 7.3.0 you can also swap the job class the package dispatches via the queue.job config key — that is the extension point for wrapping the job with your own base class (tenant context, tags, custom retry policy) without forking the package.
Uniqueness locks for queued work. 7.3.0 added a configurable uniqueness-lock duration for queued operation jobs. Practical consequence: a queued operation will not be enqueued twice while its lock is held, and the lock has a TTL. Tune the duration to longer than your worst-case job runtime, or the lock expires mid-flight and a second copy slips through. Read the published config for the exact key name, since it is new in this release.
Deployment chores as operations. storage:link, search-index rebuilds, S3 permission fixes, feature-flag flips, permission-table seeding — anything that is a state transition rather than a schema change. This keeps migrations/ honest and reviewable, and stops the "migration 2023_…_fix_data_again.php" graveyard.
Environment-scoped writes. Anything that touches production data should assert its environment rather than trusting the deploy config:
if (app()->isProduction() && ! $confirmed) { return; }
Testing pattern. Operations are hard to test as operations (they're skipped once recorded), but the body should delegate to a regular service/action class that you unit-test normally. Treat the operation class as a thin, timestamped entry point — that also keeps it readable in a git log. If the operation is queued, test the dispatched job directly instead.
Rollback posture. If your version exposes a down()/rollback path, use it only for reversible things (cache flips, flag toggles). Do not write a down() for a data backfill that overwrote values; that is false comfort.
--dev is the documented install, but composer install --no-dev in production removes the package — and if your deploy script then calls php artisan deploy:run, the build fails with "command not found." Either install it in require, or ensure the deploy ordering runs operations from a build that still has dev dependencies. This is the single most common way this package bites people. The queued-operation path makes this worse: a job class that extends a package class will fail to unserialize in a worker lacking the package.deploy:run on exactly one node, or you'll get concurrent execution and duplicate work. Use a deploy lock or a dedicated "deploy" job in your pipeline. As of 7.3.0 the --isolated flag now follows Laravel core presence semantics, so it is a viable second line of defence — but it relies on the cache lock store being shared across nodes, so on a single-node file cache it buys you nothing.laravel-actions vs laravel-deploy-operations mismatch, pin the version and verify the installed package name in composer.lock. Also worth noting: 243 stars, zero dependents, and a release date listed in the future — treat this as a small, single-maintainer package. Read the source (it's compact) before betting a critical deploy pipeline on it, and consider wrapping the call so you can swap it out.php artisan list before coding. Command signatures and config keys have moved between majors — and 7.3.0 added new queue.* config keys, so a config file published on an older version won't have them. Re-publish or diff the config after upgrading; array_merge defaults usually save you, but a stale cached config won't.config:cache as part of the deploy, publish customisations before that step, and remember the operations directory path is config-driven — a wrong path silently yields "no pending operations," which reads exactly like success. The same applies to queue.job: a stale cached config silently dispatches the package's default job instead of yours.--isolated fix — if you had worked around the old broken presence check (e.g. passing --isolated=1 to force truthiness), drop the workaround and re-test your deploy command, since the option now parses the way Laravel core does.queue.job) are the practical hooks: subclass the base to add environment guards or logging, swap the job class to add your own middleware/tags/retries, and keep a thin custom artisan command that wraps deploy:run if you need extra pre/post behaviour (Slack notifications, sentry breadcrumbs) without forking.config:cache serving a stale path, (5) for queued operations, has the job actually been picked up (check the queue connection and the uniqueness lock).How can I help you explore Laravel packages today?