Skip to content
Muhammet Şafak
tr

The PHP ecosystem does not wait for a new release — but it does not declare support either

After a PHP release ships, when do the 500 most-installed Composer packages declare that they support it, and how much does that declaration actually say?

Finding

43.5% of installs arrive on a constraint with no upper bound at all — `symfony/console` says `>=8.4.1` today, which claims support for PHP 12 as well. Of the 280 packages that do close the top, 247 made the commitment before the version existed: `guzzlehttp/guzzle` covered 8.4 in October 2020, four years early. That leaves 33 packages that genuinely waited, at a median of 325 days. The raw medians fall version over version and read as an ecosystem speeding up; restricted to an equal observation window the trend reverses (238 → 215 → 325 days).

Installs on an unbounded constraint
43.5%
Committed in advance
247 / 280 packages
Median for those who waited
325 days
Equal window, 8.2 → 8.4
238 → 325 days +37%

3 sources

  1. 01 Packagist API packagist.org · accessed Contract for the popularity list and the p2 metadata endpoint.
  2. 02 PHP: Supported Versions php.net · accessed The GA dates that are the zero point of every lag.
  3. 03 Composer — Versions and constraints getcomposer.org · accessed Source for the caret and tilde readings, and the evidence they differ from semver.

Method

The 500 most-installed packages were taken from Packagist's popularity endpoint, every stable release of each was pulled from the `repo.packagist.org/p2` metadata endpoint, and the `require.php` constraint each release shipped with was read. Constraints are resolved by a hand-written reader: npm's `semver` reads the same syntax differently from Composer (`~8.1` is `>=8.1 <9.0` in Composer, `>=8.1.0 <8.2.0` in semver), so it was not borrowed; the reader throws on anything it cannot parse rather than swallowing it, and the run counts those (zero this time). Which bucket a package lands in passes three filters: packages born after the release are excluded (they cannot have waited), releases with no constraint count as admitting every PHP (they block nothing), and lag is measured only on constraints that close at the top. Cross-version comparison is clipped to the shortest observation window (639 days). The scan is deterministic: re-running it the same day returns the same figures.

High confidence Repeated runs, controlled environment, raw data published.
Checked on

checked yesterday

Published

Environment

Population
Packagist, top 500 packages by installs
Endpoints
packagist.org/explore/popular.json · repo.packagist.org/p2
Version filter
stable tags only (dev/alpha/beta/RC excluded)
PHP release dates
php.net/supported-versions (8.2: 2022-12-08 · 8.3: 2023-11-23 · 8.4: 2024-11-21)
Comparison window
639 days — the shortest observation period (8.4)
Unreadable constraints
0

Technologies

PHP Composer Packagist

To reproduce

node scan.mjs --top 500 --php 8.2,8.3,8.4

The question was simple: after PHP 8.4 shipped, when did the ecosystem start supporting it? Every team asks a version of this — “when can I upgrade” — and looking for the answer in require.php constraints seemed reasonable.

The figure never appeared. What appeared is more interesting: why it cannot be asked for.

The first answer: nobody waited

The raw scan says 481 of 500 packages admitted PHP 8.4 before it shipped. That is 99.7% by install count. The number that genuinely waited and widened afterwards is zero.

The result is true and useless, because this is what “admitted” rests on:

Package First admitting release Date Constraint
symfony/console 2.0.4 2011-09-26 none
psr/log 1.0.0 2012-12-21 none
guzzlehttp/psr7 1.0.0 2015-05-19 >=5.4.0
psr/container 1.0.0 2017-02-14 >=5.3.0
A lower bound written in 2011 says nothing about a version released in 2024. Composer will install it; nobody is claiming the code runs.

A package that wrote >=5.4.0 in 2015 has not declared support for PHP 8.4. It has set a floor. Composer does not distinguish the two — anything that does not block installation counts as compatible — but a reader has to.

Half the constraints never close at the top

223 of the 500 most-installed packages never bound their upper end. By install count that is 56.4%. Those constraints admit PHP versions that do not exist yet:

Package Installs Constraint today What it claims
symfony/console 1.18 billion >=8.4.1 everything, PHP 12 included
psr/log 1.26 billion >=8.0.0 everything, PHP 12 included
symfony/polyfill-mbstring 1.26 billion >=7.2 everything, PHP 12 included
psr/container 1.10 billion >=7.4.0 everything, PHP 12 included
Symfony 8 cuts off everything below PHP 8.4 and leaves the top open. That is a policy, not an oversight — and it makes the declaration uninformative about the future.

This is not an accusation. Closing the top is a maintenance cost: every new PHP release means walking the dependency tree and tagging again. An open upper bound is a reasonable way to decline that cost. But the consequence stands: for half the ecosystem, the constraint holds no answer to “does it support this”.

Where a date starts to mean something

That leaves 280 packages whose constraint closes at the top — ^8.0, or 8.0 - 8.4. Here the date says something, because writing ^8.0 is a deliberate forward commitment: all of PHP 8.

PHP Bounded declarers Committed in advance Widened later Never bounded
8.2 265 201 64 194
8.3 274 223 51 199
8.4 280 247 33 202
The large majority of those who commit, commit before the version is born.

guzzlehttp/guzzle covered PHP 8.4 in October 2020 — four years early, by writing ^7.2.5 || ^8.0. phpunit/phpunit in August 2020, doctrine/lexer in May 2020. The caret does the work: say “all of PHP 8” once and you never touch the file again for 8.1, 8.2, 8.3 or 8.4.

How long the waiters wait

The 33 packages that did widen afterwards took a median of 325 days. Over a year.

Package Installs Lag Constraint it widened to
guzzlehttp/promises 1.06 billion 546 days ^7.2.5 || ^8.0
theseer/tokenizer 822 million 362 days ^7.2 || ^8.0
myclabs/deep-copy 923 million 253 days ^7.1 || ^8.0
nette/utils a rare genuinely closed range in the wild 454 million 194 days 8.0 - 8.4
Most of these sit in the test and infrastructure layer: not the code you write, but your dependency's dependency.

The trend is censoring, not speed

The raw medians fall from version to version: 559 days for 8.2, 418 for 8.3, 325 for 8.4. “The ecosystem is speeding up” comes from here, and it is wrong.

The reason is plain: 8.2 has been observable for three years and seven months, 8.4 for one year and nine months. A package that will widen for 8.4 next spring is invisible today, so 8.4’s median is drawn from the fast half of its distribution alone.

Clipped to the shortest window (639 days):

PHP Raw median Widened within 639 days Median in that window
8.2 559 days 35 238 days
8.3 418 days 33 215 days
8.4 325 days 33 325 days
The trend does not flatten, it turns around: given equal time, 8.4 was taken up more slowly than its predecessors.

What it is good for

Planning an upgrade, you read composer why-not php 8.4, and most of what stands in the output comes from this scan’s bounded cohort. The other half never appears — not because it does not block you, but because nobody knows whether it would. An absent declaration is not a green light; it is an unmeasured risk.

The measurement itself is repeatable: scan.mjs and the raw JSON of the 22 August 2026 run are in the repository. When PHP 9 arrives the same command over the same 500 packages will produce a very different table — and that will be the real test.

Related posts

Share:

Other records

All records

Laravel's preload curve: 123 files buy eight times what the last 1,912 do

How far can a curated preload take Laravel, and what does each slice cost in start-up time?

Finding

The curve is not proportional to volume. The first 1,592 files — Laravel's own framework — buy 30 ms and add 1.2 seconds to start-up. The next 1,094 Symfony files buy 9.5 ms for free. The **123 files** after that (psr, carbon) buy 15.7 ms, more than the 1,094 before them. And the last 1,912 buy 1.8 ms while adding another 1.2 seconds. So the blanket preload the earlier record measured as a ceiling is the worst point on the curve that is not the origin: stopping at 2,809 files gives 12.77 ms for 1,514 ms of start-up, while 4,721 files ask 2,691 ms to reach 10.96 ms.

measured yesterday

High confidence

opcache preload cuts the deploy bill by up to fourteen times — but five of seven frameworks do not hand it to you

With `opcache.preload` on, how long is the first request seven PHP frameworks serve after a deploy, what does the gain cost, and who can actually have it?

Finding

Preload shortens the cold first request by between 3.5× and 14.2×: Symfony drops from 35.58 ms to 2.50 ms, down to Phalcon's bare figure. But only two of the seven candidates — Symfony and CodeIgniter — publish a preload file of their own; for the other five the gain sits on the table waiting for the user to write one. Writing one is not as easy as it looks: a preload generated blindly from the classmap never brings Symfony up at all, and on CodeIgniter it does worse (5.29 ms) than the hand-picked official file (3.13 ms). And the cost does not vanish: Laravel's classmap preload takes the 62 ms it saves each visitor and writes it back as 2,340 ms of php-fpm start-up.

measured yesterday

High confidence

The fixed cost of installing a PHP framework: disk size tells you nothing

How many megabytes on disk, how many files per request and how many milliseconds on the first request do seven PHP frameworks cost — and which of those numbers actually predicts throughput under load?

Finding

Disk size predicts nothing: Yii2 has the largest vendor tree at 34.1 MB and loads only 62 files per request, among the fewest in the field. Files per request predicts nothing either: CodeIgniter loads 96 files and serves 6,431 req/s, Symfony loads 224 and serves 13,067. The one number that genuinely separates them is the first request served with a cold opcache: 2.2 ms for Phalcon, 72.2 ms for Laravel — thirty-three times. That is the compile bill the first visitor pays after every deploy, and it is measured in tens of milliseconds, not kilobytes.

measured 3 days ago

High confidence

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind