Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Upgrade Bundle Laravel Package

modera/upgrade-bundle

Modera Upgrade Bundle helps you migrate applications between major versions with less friction. It provides upgrade routines, compatibility checks, and guided migration steps so you can move to newer releases safely and with minimal manual effort.

View on GitHub
Deep Wiki
Context7

Getting Started

The dependency itself is not the day-to-day tool — the pattern is. modera/upgrade-bundle is a Symfony bundle (registration via AppKernel::registerBundles(), ContainerAwareCommand, Symfony 2.x/3.x event dispatcher signatures), archived since 2016-06-10, with zero dependents and no Laravel integration surface. There is no service provider to publish, no config to merge, and no supported install path on a modern stack. The realistic first step is a one-hour read of the source with three questions, then a reimplementation in your own app.

Read the source in this order:

  1. The console command class, not the bundle class. A bundle named "UpgradeBundle" is one command that discovers versioned scripts, orders them, and skips already-applied ones. That command is the entire product; the DI wiring around it is disposable.
  2. Script discovery — how does it find scripts (directory scan, service tags, package convention)? This determines whether upgrade scripts live in your app, in each vendor package, or in a central registry. That is the one design decision worth copying deliberately.
  3. The "already applied" marker — a file, a DB table, or container state. Whatever it is, you will build the same thing against your own ledger table.

If you are actually inside a Modera/Symfony 2.8–3.x app with a pinned legacy stack: register the bundle manually, run bin/console list | grep -i upgrade, and verify it discovers your script directory before writing any scripts. If that fails on the PHP/framework version you run, stop — shimming Symfony 2 container patterns onto PHP 8 costs more than the ~200 lines the package contains.

First Laravel use case (the one that justifies the effort). A composer update bumped a package whose data shape changed — a config key was renamed, an upload path moved, a serialized payload gained a field. Composer won't do this, migrations won't do it (it isn't schema), and a runbook step will be forgotten on the next deploy. That is the exact case this pattern exists for, and you can stand it up in an afternoon:

// database/migrations/xxxx_create_package_upgrades_table.php
Schema::create('package_upgrades', function (Blueprint $t) {
    $t->id();
    $t->string('package');                 // e.g. 'acme/billing'
    $t->string('step');                    // e.g. '002_move_uploads'
    $t->string('version')->nullable();     // target package version, if relevant
    $t->string('status', 16)->index();     // started | completed | failed
    $t->text('error')->nullable();
    $t->timestamp('started_at')->nullable();
    $t->timestamp('completed_at')->nullable();
    $t->unique(['package', 'step']);
});
app/Upgrades/
  acme-billing/
    001_rename_config_key.php
    002_move_uploads.php
    003_backfill_invoices.php
// app/Console/Commands/RunUpgrades.php
protected $signature = 'upgrade:run {--package=} {--pretend} {--step=}';

Wire it once into composer.json (post-update-cmd) or your deploy pipeline, and from then on the workflow is: bump a dep → write a numbered script → run php artisan upgrade:run --pretend → run it for real.

Implementation Patterns

Deterministic ordering over lexical convenience. Name scripts with zero-padded numeric prefixes (001_, 002_) so shell-style glob() order matches execution order. If you want semver names instead (1.10.0.php vs 1.9.0.php), you must sort with version_compare, because raw string sort puts 1.10.0 before 1.9.0 and silently runs steps out of order. Pick one scheme and assert it in a test.

Scripts are classes, not closures. The discoverable-script model in the bundle works because each upgrade is a named, identifiable unit. In Laravel, give yourself a contract:

interface UpgradeStep
{
    public function package(): string;
    public function version(): string;   // minimum version this step applies to
    public function up(): void;
    public function down(): void;        // implement for anything destructive
}

Plain classes are testable in isolation, autoloadable, and can be resolved from the container when they need a service — which is the one genuinely useful thing the Symfony bundle gave you for free.

The ledger records intent, not just completion. The naive implementation inserts a row after success. That makes a mid-step crash invisible on the next run: the step simply runs again from an unknown starting state. Record started before executing and completed after; on re-run, a started-but-not-completed row is an alert, not a skip. This is the single highest-value deviation from the original.

Idempotency is a hard requirement, enforced per step. Every up() must be safe to run twice: updateOrInsert instead of insert, if (! File::exists($new)) { rename }, firstOrCreate for seed rows, guarded DDL that doesn't exist. Concurrency is real — two CI runners can both fire post-update-cmd. Wrap execution in Cache::lock('upgrade:run', 600) (or a row lock on the ledger) and take the lock before discovery, not per step.

Version detection comes from composer.lock, not from the runtime. If steps are version-gated, read installed versions by parsing composer.lock (or composer show --format=json) and compare with version_compare. Do not rely on the package's runtime constants; they tell you what's loaded, not what was declared, and they drift in monorepo setups.

Multi-hop upgrades execute every intermediate step. Going 1.1 → 1.4 runs 002, 003, and 004 in sequence. Never jump to "the step matching the target version" — the bundle's ordering model exists precisely so you don't. Sort all candidate steps for the package by version_compare, filter to step_version > max(completed.version), run in order, halt on first failure.

Dry-run and package filter are not optional. --pretend prints the ordered plan (package, step, version, whether it's applied) without mutating anything; --package=acme/billing scopes a run so you can validate one dependency's upgrades in staging without touching others. Both are cheap to add and become the primary way you and reviewers interact with the tool.

Ship scripts with the package, or centralize them — choose on purpose. The bundle assumes upgrade logic travels inside each package, which makes packages responsible for their own migrations but couples every package to the orchestrator and to your release process. The alternative is a central app/Upgrades/<vendor>-<package>/ registry owned by the app team. Central is easier to review and test in a Laravel app; per-package is better if third parties extend your platform. Write the choice down before the directory has twenty files in it.

Keep schema migrations out of it. Any Schema::create / column add belongs in Laravel migrations, which already have a ledger, batching, and rollback. Upgrade steps are for post-schema work: data reshapes, file moves, cache warmups, config rewrites, one-off API bridging calls. If you find yourself writing DDL in a step, you're fighting a solved problem.

Gotchas and Tips

Shell sort vs. semver sort is the classic silent skip. glob() returns shell order; version_compare() returns semantic order; the two disagree at 1.9.0/1.10.0 and on prerelease suffixes. A mismatch produces a successful exit code with steps silently skipped. Add an assertion test that the discovered order matches the expected order, and log every skip decision (skipping 003: already completed) rather than only logging executions.

Prerelease tags break naive comparisons. 1.4.0-beta compared with 1.4.0 via version_compare behaves differently than string equality or >=. If your platform ships prereleases, normalize versions before comparison and test that case explicitly — this is exactly the kind of thing that only manifests during a hotfix deploy.

Partial application has no transaction boundary you can rely on. File renames and external HTTP calls aren't transactional. Design steps to be individually resumable and small: two 20-line steps that each finish cleanly beat one 80-line step that can fail halfway. For DB-only steps, wrap in DB::transaction and let it roll back; for mixed steps, order the operations so the DB change is last and the step is re-runnable from the top.

Don't mutate files during post-update-cmd if CI verifies composer.lock. Composer's integrity check can run before or after your hook depending on the Composer version and flags, and any file write that touches vendor/ or the lock-adjacent state produces failures that look unrelated to upgrades. Prefer: composer update → a separate deploy step runs php artisan upgrade:run before the app boots.

A failed step must exit non-zero. The most dangerous failure mode of a "runner" script is swallowing an exception, printing a warning, and exiting 0 — the deploy proceeds on a half-upgraded database and you find out from customers. Wrap up() in a try/catch that writes status = failed, error = $e->getMessage(), and returns self::FAILURE.

Config quirks inherited from the Symfony original. The bundle resolves its script directory and services through container-aware access patterns (getContainer(), service tags) that have no Laravel analogue and don't survive the port anyway. In Laravel, put the directory list and the ledger table name in config/upgrades.php, and make the command resolve them once at boot — reading config per step is a small thing that becomes annoying when you run 40 steps.

Vendoring the 2016 source is a last resort, and then only as a private fork. If you genuinely must run the original code, fork it into an internal repo, pin the commit, rename the package so Packagist can never serve it to you accidentally, and budget a compatibility spike for breakage in DI wiring, ContainerAwareCommand, and event dispatcher signatures on PHP 8. The logic is small; the Symfony scaffolding is not, and it's the scaffolding that will break.

Extension points worth preserving from the original design: a --step selector for running a single script out of order during an incident, a --pretend plan view, a down() on anything destructive (you will rarely run it, but the discipline of writing it catches non-reversible steps before they ship), and a per-package filter. Skip everything else — scheduling, notifications, and a web UI are scope you don't need yet.

Debug the ledger before you debug the code. When a step "didn't run," the answer is almost always in the ledger table or the version comparison, not in the step body: is there a started row without completed? Is the installed version parsed correctly from composer.lock? Does the script name sort where you think it does? Query the ledger first, then read the step.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings