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

> I walked the full release history of Packagist's 500 most-installed packages and read every `require.php` constraint. The lag figure I was after never appeared; what appeared instead was the reason it cannot be asked for.

- Kind: Survey
- Question: 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).
- 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.
- Metrics: 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%)
- Checked on: 2026-08-22
- Confidence: High confidence
- Status: Current
- Programme: Language & runtime
- 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
- 3 sources: Packagist API (https://packagist.org/apidoc) · PHP: Supported Versions (https://www.php.net/supported-versions.php) · Composer — Versions and constraints (https://getcomposer.org/doc/articles/versions.md)
- Technologies: PHP, Composer, Packagist
- To reproduce: node scan.mjs --top 500 --php 8.2,8.3,8.4
- Source code: https://github.com/muhammetsafak/packagist-php-support
- Raw data: https://github.com/muhammetsafak/packagist-php-support/blob/main/results/2026-08-22/php-support.json
- Raw data licence: https://github.com/muhammetsafak/packagist-php-support/blob/main/LICENSE
- Published: 2026-08-22
- Source: https://www.muhammetsafak.com.tr/en/research/php-version-support-declarations/
- Language: en-US
- Author: Muhammet Şafak

---
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.

> **Result**
>
> The ecosystem's quick uptake of a new PHP **minor** is not a sign of agility; it
> is the caret's mechanical consequence. That mechanism resets at PHP 9: every
> package on `^8.0` will refuse 9.0, and the same 280 packages will have to decide
> at once. This record leaves behind a measurement to re-run on that day.

## 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 | 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.

> **Caveat**
>
> What this record measures is **declared** support, not tested support. A `^8.0`
> constraint admits 8.4; it does not say the package runs on it. The population is
> the most-installed packages — a fair sample of the ecosystem a developer meets,
> not of Packagist as a whole. A long-tail package is far likelier to sit on an old
> constraint, which makes these figures the **optimistic** end. And the individual
> lags, `guzzlehttp/promises` included, are not a note to the maintainer: closing
> the upper bound is voluntary maintenance work, and most of those who skip it are
> skipping it on purpose.

## 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.
