Magento composer.lock Not Up to Date: Repair It Without Upgrading Other Packages

Illustrated storeroom on a cool paper-white ground. On the right, a long conveyor carries a row of identical parcels that stay still under red and white tape, with a ruby sticker reading do not move and a marigold sticker reading 32 others. Below it, a big indigo ledger book labelled lock, with a gold padlock, sits on a desk. A hand presses a stamp down on one single parcel resting on the ledger, with a speech bubble from the ledger saying just this one and a marigold starburst reading 1. Headline: Change just 1, with just 1 in ruby and a wavy underline, then package, not 32 others, with a marigold sticker reading Fix a stale composer.lock. Two plain tiles read Magento and Composer. A ruby corner ribbon reads Quick fix.

In short: When Composer reports a stale lock or a package missing from it, its hint to run composer update moved 32 unrelated packages in a Magento 2.4.9 test. Run composer update –minimal-changes, or name the package, and compare the lock before you commit.

A Magento project's composer.lock decides which version of more than 600 packages the next deploy installs. When composer.json and the lock disagree, Composer's own message ends by suggesting composer update, and on a project this size that is the reply that changes the most. This guide gives the repair that changes only what you meant to change, then the reasons behind it. Facts and versions were checked against Composer 2.10.3 and Magento 2.4.9. A full update is the right tool when you mean to upgrade, as in our guides to Composer's insecure-package block, to the Magento 2.4.9 upgrade and to extension version jumps.

How do you repair a Magento composer.lock without upgrading other packages?

Before you change anything: these commands change which code your store runs. Try them on a local or staging copy first, never directly on a live site, commit composer.json and composer.lock before you start so that you can compare, and back up the code and database before you deploy. Make every change at your own risk. iLogr cannot see your project and accepts no responsibility for damage, data loss or downtime.

1. Read what Composer says. From the project root:

Shell
composer validate

Under "Lock file errors" there are three cases:

  • Only the line "The lock file is not up to date with the latest changes in composer.json". The hash differs, but every requirement is still met. Go to step 2.
  • Required package "league/csv" is not present in the lock file. Someone added a requirement without updating the lock. Go to step 3.
  • Required package "magento/composer" is in the lock file as "1.10.2" but that does not satisfy your constraint "^1.99". A constraint was edited by hand. Go to step 3.

2. Hash only: refresh it. Run composer update --lock. It overwrites the hash and leaves package versions alone[1]. Then run composer validate again and check that the lock comparison below shows no package change.

3. Missing or mismatched: preview a minimal update.

Shell
composer update --minimal-changes --dry-run

Read the "Lock file operations" line. In the test it said 1 install, 0 updates, 0 removals. If it lists more, read every name before going on, then run the same command without --dry-run on the copy. A real update installs and removes code, and enabled Composer plugins and project scripts may run; the lock-only test did not exercise those effects. Afterwards run composer validate again, inspect the lock comparison below and test the store on staging before you deploy. Full updates with --minimal-changes need Composer 2.9 or later[2]. On an older Composer, name the packages that composer validate listed: composer update vendor/name[3].

4. If the solver refuses, it names the locked package it could not move and says "Make sure you list it as an argument for the update command". Add each package it names to the same command and repeat with --dry-run; adding one name at a time makes the changes easier to inspect. If a requirement you listed in composer.json has to move, preview composer update vendor/name -W -m --dry-run on Composer 2.7 or later, where -m is short for --minimal-changes. It limits movement of transitive dependencies, but the packages you name are still updated fully within their constraints[1]. Before 2.7, keep using explicit package names. Review the operations before you run without --dry-run.

5. Compare, then commit both files together. Use the comparison below, then commit composer.json and composer.lock in one commit. Run the repair with production's PHP version and required extensions, or set config.platform.php to the production PHP version for dependency resolution. That simulates PHP rather than testing it, so run composer check-platform-reqs in the deployment environment too[4]. Production should install from the lock with composer install[5].

What did a store show?

On a Magento store, a lock repair done with a broad update changed a few dozen packages that nobody had asked to change. On another, an extension had been put under suggest instead of require, and a later setup:upgrade dropped its module from app/etc/config.php. The test below reproduces both.

What did a test show?

In a test we used a Composer-layout Magento 2.4.9 project (Composer 2.10.3, PHP 8.4) whose lock held 557 packages and 66 development packages and was about a week old. We added one requirement to composer.json by hand and tried each repair, comparing the lock before and after. The lock was written without installing packages.

  • composer update --lock: the generic warning went. composer validate still failed, and a dry run of composer install stopped with exit status 4 and "Required package "league/csv" is not present in the lock file". This is the error Composer has raised since version 2.5[3].
  • composer update league/csv, or composer update --minimal-changes: one package added, nothing else. Validate and install both passed.
  • A bare composer update: that package plus 32 upgrades: 12 Symfony packages, 12 Magento modules (payment services, data export and related), three Guzzle packages, the AWS SDK, PHPUnit and three more. By version number all were patch or minor releases, but none had anything to do with the missing requirement.

Composer's message ends by offering both composer update and composer update with a package name. The second is the one to use.

Two bar charts from tests. The main chart, Packages changed when one requirement is missing from a Magento 2.4.9 lock, shows composer update --lock changing 0 packages and still failing with install exit 4, composer update league/csv changing 1, composer update --minimal-changes changing 1, and a bare composer update changing 33: 1 added in cobalt and 32 upgraded in ruby. The small second chart is a model with made-up packages and a library the project also requires: a plain require fails, require with -w fails, require with -W changes 6 of which 2 are not needed, require with -W and -m changes 4, and naming the packages changes 4. A ruby ribbon reads In a test.
Changed packages for one missing requirement, from a test on a Magento 2.4.9 lock: a bare composer update changed 33 (1 added, 32 upgraded); composer update league/csv or –minimal-changes changed 1. The small chart is a model with made-up packages.

What does the content hash cover?

The hash is an MD5 of these parts of composer.json: name, version, require, require-dev, conflict, replace, provide, minimum-stability, prefer-stable, repositories, extra and config.platform[6]. In the test, adding or editing a require entry, a repository, an extra key, the minimum stability or config.platform changed it, and so did putting the require entries in a different order. Editing the description, scripts, autoload, other config settings such as allow-plugins, or suggest did not.

So a harmless edit can raise the warning, and --lock is safe for exactly that case. Composer's guide to merge conflicts says the same when only the hash line conflicts[3]. It is not a repair for a missing requirement, as the first bullet above shows.

Why do require, -w and -W move more than you asked for?

A composer require does not add one package. It asks Composer to make the lock satisfy the whole composer.json. In the test, composer require sentry/sentry locked three packages instead of two, because league/csv had been added by hand and was still missing from the lock.

The flags decide how far the update spreads. -w also updates the dependencies of the packages you name, except root requirements, and -W includes root requirements[1]. Those dependencies go to the newest allowed version even when the locked one already fitted. On the Magento lock, composer require mailgun/mailgun-php added six packages; with -w or -W it added the same six and moved three Guzzle packages as well. Adding -m brought it back to six and no upgrades.

A model with made-up packages shows when a flag is needed. The new package requires a newer release of a library that the project also requires directly. A plain require failed with "fixed to 1.0.0 (lock file version) by a partial update". -w was not enough, because Composer keeps a root requirement locked and says so. -W worked but also moved two libraries that had no need to move. -W -m, like naming the packages that had to move, changed exactly four.

Why can a package missing from the lock remove a Magento module?

The suggest section of composer.json is a hint to users[7]. In the test it did not install anything and was not part of the hash, so a requirement typed there raised no warning at all.

Two throw-away extension modules were installed and listed in app/etc/config.php, one of them disabled. Their requirements were then moved to suggest and composer update --lock cleared the warning. Validate passed and the lock was unchanged, so nothing looked wrong. The next update of an unrelated package, symfony/yaml, removed both extensions from the lock, because nothing required them any more.

A build from that lock has no code for them. Magento's setup:upgrade rewrites the modules list in config.php from the modules it finds installed[8][9], so both lines disappeared. When the packages were required again, the disabled module came back enabled, because the code sets any module without an entry to enabled[8].

Run git diff app/etc/config.php after every setup:upgrade. A removed line is the signal. To recover, restore composer.json, composer.lock and app/etc/config.php from the last good commit, install, and rerun setup:upgrade on a copy first; this was not tested as a whole. For a real extension, check its recovery instructions and any database changes first: restoring those files does not undo schema or data changes, and recovery may need a matching database backup. The lock comparison below should show a removal only when you intended one.

How do you check the lock changes before you commit?

A raw diff of composer.lock is long, because every entry carries source, distribution, time and requirement data. This lists only names and versions:

Shell
git show HEAD:composer.lock | jq -r '(.packages + .["packages-dev"])[] | "\(.name) \(.version)"' | sort > before.txt
jq -r '(.packages + .["packages-dev"])[] | "\(.name) \(.version)"' composer.lock | sort > after.txt
diff before.txt after.txt

Every added, removed or moved line should be explained by the change you meant to make. The Lock file operations line of a --dry-run is the cheapest first check.

Two tools do the same job. The composer-diff plugin by Ion Bazan prints a Markdown table of the changes against the last commit, which suits a pull request[10]. composer-lock-diff compares the lock with git HEAD or any two files, in plain text, Markdown or JSON[11]. Choose by the output you want and by whether you want another global package on every developer machine. The jq recipe needs neither.

How do you catch a stale lock before it reaches a deploy?

Add composer validate to the pipeline that builds the project. In the test it exited with status 2 for the hash-only case and for both kinds of lock error, and with 0 once the lock was repaired. A hash-only mismatch is only a warning when you install, with exit status 0, which is easy to miss in a long build log. Composer's own error text also asks you to prefer composer require to editing composer.json by hand[3].

When is a full composer update the right repair?

When you mean to upgrade, or when the lock is beyond repair: Composer's merge-conflict guide lists a plain update as the simplest way to start again, at the price of moving every locked version[3]. Even then, preview it with composer update --dry-run and composer update --minimal-changes --dry-run side by side, and test the result on staging.

References

Numbers in square brackets in the text link to these sources.

  1. Composer documentation: the update and require commands (opens in a new tab).Used for: –lock, -w, -W and –minimal-changes definitions.
  2. Composer 2.10.3 changelog (opens in a new tab).Used for: when –minimal-changes arrived for partial updates and for full updates.
  3. Composer documentation: resolving merge conflicts (opens in a new tab).Used for: update per missing package, –lock for hash-only, and install failing on a missing package.
  4. Composer documentation: config, platform (opens in a new tab).Used for: faking the production PHP version in the platform setting.
  5. Composer documentation: basic usage, installing from composer.lock (opens in a new tab).Used for: install reading the lock, and the stale-lock warning.
  6. Composer 2.10.3 source: Locker.php (opens in a new tab).Used for: the project-file keys that make up the content hash.
  7. Composer documentation: composer.json schema, suggest (opens in a new tab).Used for: suggest being informational.
  8. Magento 2.4.9 source: Installer.php (opens in a new tab).Used for: how the Magento modules list is rebuilt from the installed modules.
  9. Magento 2.4.9 source: UpgradeCommand.php (opens in a new tab).Used for: setup:upgrade calling the modules-list update.
  10. ion-bazan/composer-diff on Packagist (opens in a new tab).Used for: what the plugin reports and that it compares with the last commit.
  11. davidrjonas/composer-lock-diff on GitHub (opens in a new tab).Used for: its comparison options and output formats.

Leave a Reply

Your email address will not be published. Required fields are marked * See the privacy policy for how comments are handled.