Skip to content

What composer audit --abandoned misses in 31 PHP projects

129 of the 4,109 production packages in 31 open-source PHP applications are marked abandoned on Packagist, archived on GitHub, or have gone five years without a stable release or a push. composer audit --abandoned reports 62 of them. The lock files are wallabag's, Matomo's, Magento's, Drupal's, Joomla's, phpBB's, Moodle's, PrestaShop's and 23 more, read on 2026-09-19 with --no-dev.

The other 67 split two ways. 56 carry no abandoned marker anywhere, so no Composer command has anything to say about them. 11 are marked abandoned on Packagist, and composer audit still does not report them, because it never asks Packagist.

The numbers

Every lock file is the project's default branch as of 2026-09-19, read with --no-dev, so only what runs in production counts. The middle column is lockrot's abandoned and silent verdicts together (lockrot is the tool this site documents: it reads composer.lock and asks Packagist and the repository host about each package). abandoned is the Packagist marker or an archived repository; silent is no stable release and no push to any branch for five years, the default thresholds. "Marked" is the subset with the marker. The composer audit column is what composer audit --abandoned=report --no-dev --locked (Composer 2.10.3) printed for the same file; it never named a package outside the middle column. The last column opens that project's whole report — every finding with its dates, its requirement chain and the advisories against it — as the page --format=html writes, which needs no server and no upload.

Project Packages abandoned + silent Marked composer audit Report
wallabag/wallabag 200 27 19 16 report
akaunting/akaunting 183 16 16 16 report
concretecms/concretecms 158 13 11 10 report
humhub/humhub 210 12 1 1 report
PrestaShop/PrestaShop 228 7 4 0 report
phpbb/phpbb 86 7 5 5 report
salesagility/SuiteCRM-Core 210 7 5 3 report
snipe/snipe-it 167 6 1 1 report
chamilo/chamilo-lms 271 6 0 0 report
matomo-org/matomo 52 4 1 1 report
magento/magento2 148 4 4 4 report
mautic/mautic 193 4 2 2 report
pixelfed/pixelfed 147 4 0 0 report
librenms/librenms 138 4 1 1 report
monicahq/monica 165 2 1 1 report
joomla/joomla-cms 93 2 2 1 report
firefly-iii/firefly-iii 132 1 0 0 report
moodle/moodle 61 1 0 0 report
drupal/drupal 64 1 0 0 report
espocrm/espocrm 125 1 0 0 report
11 projects with none 1,078 0 0 0
31 projects 4,109 129 73 62

The eleven with none are BookStack, koel, Cachet, Invoice Ninja, Kanboard, Pterodactyl, OpenCart, phpMyAdmin, Ampache, symfony/demo and TYPO3. The commit each lock file was read at, the per-project reports and a summary with the abandoned / silent split per project are in the data.

Three projects account for 56 of the 129 rows, and the rows overcount the problems: the 129 are 97 distinct packages, and 26 of the rows are hoa/*, one upstream whose last releases are from 2017 with no push since 2021, pulled into wallabag through wallabag/rulerz and into akaunting through lorisleiva/laravel-search-string. wallabag requires one of its 19 marked packages directly (sensio/framework-extra-bundle); the rest arrive through something else.

Three commands, one of which asks Packagist

composer audit --abandoned and the Package X is abandoned, you should avoid using it line on every composer install read the same thing, and it is not Packagist. It is the abandoned field Composer writes into composer.lock for a package that was abandoned when it last resolved it; for every other package there is no field at all. Installer.php:356 takes the locked repository and asks each package isAbandoned(); AuditCommand.php:124 audits the locked packages under --locked, and otherwise vendor/composer/installed.json, which is a copy of the lock written at install time. Packagist is contacted for security advisories, not for the marker.

This is by design: a lock file is a snapshot, and composer install must produce the same vendor/ next year. Since Composer 2.7 (2024-02) audit.abandoned defaults to fail, so the 62 already break a build that runs composer audit; the question is which packages ever reach that check. A package marked abandoned after its lock entry was written is invisible until something makes Composer resolve it again. doctrine/annotations in wallabag's lock:

$ jq '.packages[] | select(.name == "doctrine/annotations") | .abandoned' composer.lock
null
$ curl -s https://repo.packagist.org/p2/doctrine/annotations.json | jq '.packages["doctrine/annotations"][0].abandoned'
true

Eleven lock entries in the sample sit in that gap: four in PrestaShop, three in wallabag, two in SuiteCRM, one each in Concrete CMS and Joomla. They are eight distinct packages, and three of the eight a PHP developer will already know about: doctrine/annotations gave way to attributes, composer/package-versions-deprecated says so in its name, symfony/security-guard was removed in Symfony 6. microsoft/azure-storage-blob is not one of those: marked abandoned and archived by Microsoft with no replacement named, an Azure storage client that handles account keys, in SuiteCRM's production lock, and invisible to composer audit. The other four are doctrine/cache, behat/transliterator, jakeasmith/http_build_url and marcusschwarz/lesserphp.

"Just run composer update" closes the gap for the entries it resolves, and that is also where the gap comes from: you learn about the marker when you update, on your laptop, and not in CI on a release branch that on purpose does not update. A partial update rewrites only the entries it resolved. On a throwaway project with doctrine/annotations 2.0.2 and the field removed from its entry, Composer 2.10.3 (funding and plugin lines trimmed):

$ composer update psr/log --no-install
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Writing lock file
$ jq '.packages[] | select(.name == "doctrine/annotations") | .abandoned' composer.lock
null
$ composer update doctrine/annotations --no-install
Loading composer repositories with package information
Updating dependencies
Lock file operations: 0 installs, 1 update, 0 removals
  - Upgrading doctrine/annotations (2.0.2 => 2.0.2)
Writing lock file
Package doctrine/annotations is abandoned, you should avoid using it. No replacement was suggested.
$ jq '.packages[] | select(.name == "doctrine/annotations") | .abandoned' composer.lock
true

PrestaShop's lock carries no abandoned field on any of its 228 entries, and four of those packages are marked on Packagist; its doctrine/cache entry has read 2.2.0 since the first quarter of 2024.

The third command is the one that does ask: composer outdated (and composer show --latest) fetches current metadata and prints the warning for all three wallabag packages, whatever the lock says, and --strict exits 1 for them too, since an abandoned package counts as outdated even at its newest release. So does every package with a newer version, which makes it a noisy gate, and it says nothing about the 56 below.

composer audit already accepts that one property of a locked package changes after the lock is written; the advisory request exists for exactly that. Abandonment is the same kind of property: it describes the package upstream today, not the version installed. lockrot asks because it reads each package's repository metadata for the release dates anyway, through Composer's own repository layer and cache; the marker is in the same file.

The 56 nobody marked

I expected the marker to cover most of the dead packages; it covers 73 of the 129. The maintainer sets it on Packagist, and nothing derives it from the repository: two packages in the sample are archived on GitHub and carry no marker (pear/console_color2, sebastian/resource-operations). The other 54 have had no stable release and no push to any branch for at least five years, lockrot's silent. None of the 31 projects gets a word about any of the 56 from Composer.

Three of the 54, as the report prints them (the --target-php=8.4 clause is on the rows because it was passed; it is not what makes them silent):

  silent       namshi/jose 7.2.3  via lexik/jwt-authentication-bundle
               last release 2016-12-05 (9.8 years ago); last push 2021-06-18
               (5.3 years ago); released 2016-12-05, before PHP 8.4 GA
               (2024-11-21); php constraint ">=5.5" has no upper bound
  silent       grandt/phpzipmerge 1.0.4  via wallabag/phpepub › phpzip/phpzip
               last release 2015-08-18 (11.1 years ago); last push 2015-08-18
               (11.1 years ago); released 2015-08-18, before PHP 8.4 GA
               (2024-11-21); php constraint ">=5.3.0" has no upper bound
  silent       raoul2000/yii2-jcrop-widget 1.0.0  direct
               last release 2014-07-30 (12.1 years ago); last push 2018-01-28
               (8.6 years ago)

namshi/jose signs and verifies JWTs; its last release is from 2016 and it is in Chamilo LMS's production lock through lexik/jwt-authentication-bundle. grandt/phpzipmerge is in wallabag's through its EPUB export. raoul2000/yii2-jcrop-widget is a direct requirement of HumHub.

Five years is lockrot's default, and it sets the number. Counting the rows that are not marked or archived and have both a last release and a last push older than the threshold: 114 at three years, 54 at the default five, 33 at seven, 11 at ten.

silent is an observation, not a judgement. ircmaxell/random-lib does what random_bytes() has done in core since PHP 7; spomky-labs/base64url encodes base64url and will not need a release in 2035 either. That is what the allowlist is for: name the package, say why, and the report calls it finished from then on. Both take someone looking at the package; the marker does not.

In CI

lockrot lists all 129 under the two verdicts, with the evidence, the requirement chain and a priority: critical for a direct production requirement, high for a transitive one. composer lockrot --fail-on=silent fails on both verdicts (abandoned is the more severe and --fail-on is inclusive); --fail-on=abandoned on the marked and archived ones only. A project with a wallabag-sized list starts from a baseline and fails on what arrives next; the CI page has the GitHub Action and the GitLab recipe.

Reproducing this

Composer 2.10.3, PHP 8.5.10, default thresholds (release-high-years and push-high-years both 5), --no-dev, GITHUB_TOKEN set. Each project's composer.json and composer.lock were taken from the commit named in manifest.json; nothing was installed. --target-php=8.4 was passed but affects only the old-promise verdict, which this post does not count. The per-project reports and a summary.csv are next to the manifest.

The table was first produced by lockrot 0.8.0 on 2026-09-19. The linked reports are from lockrot 0.10.0, which read the same thirty-one lock files at the same commits on 2026-09-22 — so the only things that could have moved are what lockrot measures and what Packagist and the repository hosts say. Nothing did: on all thirty-one rows, every column lockrot writes came back with the same number, and the new summary.csv is byte for byte the file the first run wrote. The composer audit column is the one thing that was not re-run — it reads the lock file and nothing else, and the lock file is the same file — so it was carried over. Both runs are in the manifest, each with the sha256 of the PHAR it used.

129 is a floor. Of the 4,109 packages, 77 came from path or VCS repositories (58 of them HumHub's) and were not checked: 63 of those returned no data from any repository and are reported unknown, the other 14 are branch snapshots and reported pinned; none of the 77 is counted here. Ten of the 46 distinct silent packages, drawn at random, were re-checked by hand on 2026-09-19 against Packagist's metadata and the GitHub repository API (spot-check.txt); all ten release and push dates matched.

The same numbers on any project, with the PHAR so that the lock file is not touched (download and verify):

$ GITHUB_TOKEN=... php lockrot.phar --fail-on=silent
$ composer audit --abandoned=report --no-dev --locked --format=json | jq '.abandoned | keys'

Seventeen of these thirty-one projects are now read again every Monday, at their newest stable release rather than at a commit on a branch, and the numbers land on the weekly watch beside fifteen projects created from scratch that morning. This post is one day; that page is the line the day sits on.

Without a token, GitHub allows 60 requests an hour and lockrot checks repository activity for at most 50 packages per host per run, among those already old on release age, so a large lock reports fewer silent findings than it has; see Repository hosts and credentials. This run had a token, and none of the 31 reports carries the note lockrot prints when the cap applied.