Composer Update Without Fear: A Safe Routine for Legacy PHP Projects

#composer update legacy php

Founder & Lead Developer

Expert in software development and legacy code optimization

LinkedIn

Running composer update on a five-year-old Symfony or Laravel codebase feels less like routine maintenance and more like defusing something. The fear is not irrational. A dependency graph nobody has touched in years can hide packages that only still work because nothing around them has moved, and a single minor version bump can cascade into a dozen failing tests, or worse, no failing tests at all because the coverage was never there to catch it. A safe composer update legacy PHP routine is not a one-off fix you run once and forget. It is four habits practiced together: clean lock file hygiene, small batches instead of one giant jump, a test coverage floor you actually trust, and a rollback plan that does not depend on anyone remembering what they did three hours ago.

The composer update legacy PHP routine starts with the lock file

Before touching a single dependency, confirm composer.lock is committed to the repository and actually matches composer.json. Run composer validate --no-check-all and look for drift: a project where production installs run with --no-dev while a developer's machine pulled in something slightly different during a rushed deploy years ago. This kind of mismatch is common on projects that have changed hands more than once, and it means the lock file in the repository is not a reliable record of what is actually running in production.

Once the lock file is trustworthy, run composer outdated -D to see only direct dependencies, not the full transitive tree, which on a mature project can run into the hundreds of entries and tells you almost nothing useful. For any package you are unsure about, composer why vendor/package shows what depends on it, and composer why-not vendor/package 4.0 shows what is blocking an upgrade. On a legacy project, these two commands usually surface the real constraint faster than reading changelogs: a forgotten internal bundle still pinned to an old Guzzle version, or a testing library nobody uses anymore that is quietly holding back three other packages.

The lock file diff itself is worth reviewing on its own, separately from the composer.json change that triggered it. A single direct dependency bump can pull a dozen transitive packages along with it, and on a legacy project that diff is often the first time anyone has looked closely at what the application actually depends on underneath the surface. Reviewing it before merging catches the odd case where a transitive package jumps a major version on its own, quietly, as a side effect of something that looked like a minor update.

Update in batches, grouped by risk, not all at once

A bare composer update with no arguments resolves the entire dependency tree at once, and on a legacy project that means reviewing a diff with dozens of version bumps mixed together, patch releases next to major ones, in a single pull request nobody can meaningfully review. Scope the command instead: composer update vendor/package another-vendor/package updates only the packages you name, which turns a frightening all-or-nothing operation into something you can sequence deliberately.

Group packages by risk tier before you run anything. Patch releases across the board go first, since they are the least likely to break anything and clear the easy ground. Minor version bumps come next, grouped by subsystem, logging packages together, templating packages together, so a failing test points at an obvious suspect instead of forcing a bisect across an unrelated set of changes. Major version bumps get their own pull request each, reviewed on their own, because that is where breaking changes actually live. This batching discipline is the manual complement to what we have covered before on why automated dependency updates still need a human reviewing the queue: Dependabot or Renovate will happily open fifteen pull requests in one afternoon, and grouping them by risk before merging is the part the bot cannot do for you.

Run composer audit against each batch before merging rather than waiting until the end of the quarter. It checks installed packages against known security advisories and takes seconds, and catching a flagged package in a batch of five is a much smaller problem than finding it buried in a batch of thirty. Pair it with whatever static analysis the project already runs, PHPStan or Psalm, since a type error introduced by an upgraded package's changed return type will usually show up there before it shows up in a test.

The test coverage floor you need before you start

Full code coverage is not the bar here, and chasing one before you touch a dependency update is a good way to never start. What you need is narrower: integration tests around the boundary where each risky package is actually used. If your application calls a PDF library in three places, you need tests that exercise those three call sites with realistic input, not a unit test that mocks the library out of the picture entirely.

Where that coverage does not exist, write it before running the update, not after. A short smoke test that calls the real integration point and checks the output looks right takes an hour to write and tells you immediately whether a version bump changed behavior you depend on. This is also where Rector earns its place in the same routine: once a package update forces an API change across the codebase, an automated refactoring pass can apply the mechanical part of the fix across every call site consistently, leaving your smoke tests to confirm behavior rather than hand-editing each occurrence.

A rollback plan that holds up under pressure

"We have git" is not a rollback plan. A rollback plan is a written, two-minute procedure someone can follow without thinking clearly, because the moment you need it is usually also the moment something in production is already behaving oddly. Tag the commit before each batch starts. Keep the prior composer.lock available somewhere a deploy script can reach directly, in addition to git history, so restoring the exact dependency versions that were running before is composer install against the old lock file, rather than a manual reconstruction of what version everything was on.

Decide in advance what triggers a rollback, rather than deciding in the moment while something is already on fire. A spike in error rate on the deploy dashboard, a failed smoke test in staging that was passing an hour ago, or a support ticket that matches a known regression pattern from the changelog you read earlier, any of these should be enough to revert first and investigate after, not the other way around.

When this stops being worth doing yourself

A monthly or quarterly cadence for this routine is realistic for most teams, and the actual time cost per cycle drops fast once the lock file is clean and the batching habit is established. What does not drop is the judgment part: deciding which packages matter enough to test carefully, reading a changelog well enough to know if a major bump is safe, recognizing when a seemingly routine update is actually load-bearing for something critical. That part does not get easier just because you have done it before, and on a team where nobody owns it consistently, it tends to slip during the one sprint where it matters least to slip.

If your team does not have the bandwidth to run this cycle reliably every quarter, it is one of the more concrete pieces of an ongoing code quality consulting engagement, where someone outside the regular sprint cycle keeps the dependency tree healthy instead of it becoming a one-time emergency every eighteen months. Either way, the routine itself does not change: a clean lock file, batched updates, a coverage floor you trust, and a rollback plan written down before you need it, not improvised while something is already broken.

If you want a second opinion on your current composer.lock, or help building this routine into a cadence your team will actually stick to, write to us at hello@wolf-tech.io or have a look at wolf-tech.io.