Dependency rot in 20 PHP applications, and in 15 fresh installs¶
Every Monday lockrot 0.10.0 reads two kinds of composer.lock and reports the packages in them
that stopped being maintained: the lock the newest stable release of 20 open-source PHP
applications ships, and the lock 15 new projects get when they are created that morning
with composer create-project. This page is the last run, 2026-09-21. Nothing is installed, no script
or plugin from any package is run, and no project is contacted: the run reads lock files, then asks
Packagist and the repository host about the packages in them.
What these applications ship¶
Each row names the release it read and the commit that release points at, so any number here can be checked against the same two files lockrot read. A release rather than a branch head on purpose: a release is what people install, and it is the only version of a project that two weeks of this page can be compared across — the tip of a development branch moves for reasons that have nothing to do with dependency rot.
What a new project gets today¶
A project started this morning has no history to rot in, and that is exactly why it is worth
measuring: its dependency tree is whatever the current constraints resolve to, and the answer
changes weekly without anyone touching the project. This half of the run creates each one from
scratch — composer create-project, with the starter package pinned to its newest stable version,
nothing installed and no script or plugin executed — and reads the lock file that falls out.
| New project | Created from | Packages | Advisories | abandoned |
silent |
pinned |
left-behind |
old-promise |
stale |
Report |
|---|---|---|---|---|---|---|---|---|---|---|
| Laravel | laravel/laravel v13.10.1 | 76 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | open |
| Symfony (skeleton) | symfony/skeleton v8.1.99 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | open |
| Symfony (--webapp) | symfony/skeleton v8.1.99 | 133 | 0 | 0 | 0 | 0 | 0 | 0 | 2 | open |
| API Platform | symfony/skeleton v8.1.99 | 47 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | open |
| Drupal | drupal/recommended-project 11.4.7 | 68 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | open |
| WordPress (Bedrock) | roots/bedrock 1.31.5 | 15 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | open |
| Shopware | shopware/production v6.7.14.1 | 173 | 0 | 0 | 0 | 0 | 0 | 5 | 3 | open |
| Sylius | sylius/sylius-standard v2.2.4 | 195 | 0 | 3 | 0 | 0 | 1 | 5 | 1 | open |
| TYPO3 | typo3/cms-base-distribution v14.3.0 | 106 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | open |
| Craft CMS | craftcms/craft 5.8.0 | 111 | 0 | 0 | 2 | 0 | 1 | 7 | 3 | open |
| Statamic | statamic/statamic v6.5.1 | 123 | 0 | 0 | 1 | 0 | 0 | 2 | 4 | open |
| Pimcore | pimcore/skeleton v2026.2.0 | 187 | 0 | 1 | 1 | 0 | 0 | 6 | 4 | open |
| Slim | slim/slim-skeleton 4.5.0 | 16 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | open |
| CakePHP | cakephp/app 5.4.0 | 20 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | open |
| Yii 2 | yiisoft/yii2-app-basic 2.0.55 | 25 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | open |
| 15 starters | 1296 | 0 | 4 | 6 | 0 | 4 | 29 | 19 |
The advisories column counts packages with a security advisory against the installed version, from
the same feed composer audit reads. It is here and not in the table above because these reports
are made with --all: every package is in the document, so an advisory on an otherwise healthy one
is visible. The applications are read without --all, where a healthy package is not a finding and
never reaches the page.
Several projects in this table cannot be in the one above, and for one reason: Shopware, Sylius,
TYPO3, Craft CMS, Statamic and WordPress-through-Bedrock do not commit a lock file to their
repository. Their lock is born at create-project time, which is the only place it can be read —
and is the version their users actually run.
A verdict is an observation, not a judgement, and the two that carry most of the table say
different things. abandoned is the Packagist marker or an archived repository — someone said so.
silent is no stable release and no push to any branch for five years, which is lockrot's default
threshold and not a law of nature: a package that encodes base64url does not need a release in 2035
either. That is what the allowlist is for, and a project that has
looked at one of these and decided it is fine can say so in its own composer.json in one line.
old-promise counts against PHP 8.4 for every project here, whatever each one actually targets,
because a column has to mean the same thing in every row. On a real project it is
--target-php that decides it.
Why these projects¶
The 20 are applications people deploy, chosen on two public signals and one hard constraint. The signals: how widely a thing is actually run, where someone else measures it — W3Techs, 21 September 2026, puts Joomla on 1.1% of all websites, Drupal on 0.6% and Adobe's Magento on 0.6% — and GitHub stars, which is what puts Coolify, Appwrite and Bagisto at the top of any list of PHP applications today.
The constraint decided more of the first table than either signal: a project has to commit
composer.json and composer.lock at a tagged release. Of the 300 most-starred PHP repositories
and a hand list of the widely-deployed ones — 109 looked at — 61 came through that filter, and
these 20 are chosen from those 61 for spread across what the applications actually do: CMS,
shop, forum, LMS, analytics, marketing, accounting, asset management, the rest.
The second table is where the rest of them turn up. WordPress, Shopware, Sylius, TYPO3, Craft CMS,
Statamic, Pimcore, Dolibarr, Roundcube, osTicket, SilverStripe, Contao, October, MODX, ProcessWire
and Vanilla commit no lock file at all; their dependency tree exists only once someone installs
them, so that is where it is read. The starters are the ways people actually begin a PHP project —
the framework skeletons and the vendor-recommended distributions — each pinned to its newest stable
version so the row names something checkable. The list, the reasoning and the near misses are in
data/watch/projects.json.
What moves these numbers¶
Three things, and they are worth telling apart. A project cuts a release, and the row moves because
the project moved — the release column says when that happened. A maintainer marks a package
abandoned on Packagist, and the row moves although the release did not change at all, which is the
gap this post is about and the reason
composer audit does not see most of what is here. Or lockrot learns to measure something it
skipped before, and the row moves because the tool moved; every row in
history.csv carries the release, the commit and the lockrot version that produced it
for exactly that reason.
Running it on your own lock¶
The PHAR does the same without touching the lock (download and verify), and the GitHub Action is what this page runs. A project with a long list starts from a baseline and fails on what arrives next, rather than on what is already there.