What 3052 composer.json files tell us about the Composer plugin ecosystemWhat 3052 composer.json files tell us about the Composer plugin ecosystem

I like to read composer.json files the way other people read tea leaves. Take enough of them and patterns emerge: which packages everyone depends on, which ones are stuck two major versions behind, and what a minimum-stability field says about a maintainer’s risk tolerance.

For this post, I pulled every composer.json I could find under projects/*/code/ in my local Exakat corpus: 3285 project directories, 3054 of which actually ship a composer.json. Yes, I usually estimate that 2% of PHP application do not use composer.json. Not great, no bad.

I parsed all of them, and 3052 came back as valid JSON (two are broken, a separate post, maybe). Most of these projects are Composer plugins and installers: 42% declare type: composer-plugin, and composer-plugin-api shows up as a requirement in 40% of all files. So what follows is really a snapshot of the Composer plugin ecosystem specifically, not PHP at large: keep that in mind when you compare it to your own dependency tree.

Here’s what the numbers say.

PHP itself: how low do people set the floor

Before anything else, there’s the language itself. Only 1548 of the 3052 manifests, that is 50.7%, declare a php constraint at all. The other half either leaves compatibility entirely to composer-plugin-api, or simply doesn’t say. For a plugin that’s meant to install on a stranger’s machine, that’s a striking number of authors who never wrote down the one constraint that would actually stop it from running.

For the 1548 that do declare one, here’s the floor they set, the lowest PHP minor version each constraint would still accept, whether written as ^8.1, >=7.4, ~7.4 or an OR’d chain of ranges:

PHP 5
  5.0                                    2  (0.1%)
  5.1                                    2  (0.1%)
  5.2                                    3  (0.2%)
  5.3  ████████████████                106  (6.8%)
  5.4  ████████                         56  (3.6%)
  5.5  █████████                        63  (4.1%)
  5.6  ████████████                     82  (5.3%)

PHP 7
  7.0  ████████████                     77  (5.0%)
  7.1  █████████████████████           143  (9.2%)
  7.2  █████████████████               111  (7.2%)
  7.3  ███████████                      72  (4.7%)
  7.4  █████████████████████████       168  (10.9%)

PHP 8
  8.0  ████████████████████            135  (8.7%)
  8.1  ██████████████████████████████  200  (12.9%)
  8.2  ████████████████████████        158  (10.2%)
  8.3  ██████████████                   92  (5.9%)
  8.4  ██████████                       64  (4.1%)
  8.5  ██                               13  (0.8%)

Floor version implied by each project’s php constraint, out of the 1548 manifests that declare one.

PHP 8.1 alone is the single most common floor, for 200 projects or 12.9%, but it’s not a landslide: ^7.1, ^7.4, ^8.0 and ^8.2 are all close behind it. Group by major and the picture is more telling: PHP 8.x floors account for 42.8% of the declared constraints, PHP 7.x for 36.9%, and PHP 5.x, a version whose last release shipped over a decade ago, still accounts for 20.3%. One in five plugins that bothers to state a PHP version at all is still willing to run on PHP 5. Or so they claim.

The extensions people build against

2259 ext-* requirements, spread across 87 distinct extensions, appear in 583 composer.json: 19.1% of the corpus declares at least one. The head of the list is exactly what you’d expect from PHP core: JSON, mbstring, cURL, PDO, DOM. The tail is where it gets interesting: a long list of extensions required by exactly one project each, which is its own kind of signal about how specialized some of these extensions are.

Extension Projects Share Home
ext-json 370 12.12% link
ext-mbstring 183 6.00% link
ext-curl 130 4.26% link
ext-pdo 116 3.80% link
ext-dom 111 3.64% link
ext-openssl 94 3.08% link
ext-gd 89 2.92% link
ext-simplexml 86 2.82% link
ext-intl 80 2.62% link
ext-zip 77 2.52% link
ext-fileinfo 74 2.42% link
ext-xml 67 2.20% link
ext-iconv 63 2.06% link
ext-ctype 60 1.97% link
ext-libxml 54 1.77% link
ext-zlib 44 1.44% link
ext-tokenizer 36 1.18% link
ext-bcmath 36 1.18% link
ext-filter 31 1.02% link
ext-pcre 26 0.85% link
ext-hash 26 0.85% link
ext-session 25 0.82% link
ext-spl 21 0.69% link
ext-pdo_mysql 21 0.69% link
ext-posix 20 0.66% link
ext-xmlreader 19 0.62% link
ext-exif 19 0.62% link
ext-soap 18 0.59% link
ext-sodium 17 0.56% link
ext-mysqli 17 0.56% link
ext-redis 17 0.56% link
ext-xmlwriter 15 0.49% link
ext-reflection 11 0.36% link
ext-sockets 11 0.36% link
ext-xsl 11 0.36% link
ext-apcu 11 0.36% link
ext-pcntl 11 0.36% link
ext-gettext 10 0.33% link
ext-gmp 10 0.33% link
ext-sqlite3 9 0.29% link
ext-imagick 9 0.29% link
ext-ldap 8 0.26% link
ext-phar 7 0.23% link
ext-yaml 6 0.20% link
ext-readline 6 0.20% link
ext-pgsql 6 0.20% link
ext-pdo_sqlite 5 0.16% link
ext-calendar 5 0.16% link
ext-zend-opcache 4 0.13% link
ext-ftp 4 0.13% link
ext-swoole 4 0.13% link
ext-mongodb 3 0.10% link
ext-memcached 3 0.10% link
ext-http 2 0.07% link
ext-pdo_pgsql 2 0.07% link
ext-amqp 2 0.07% link
ext-tidy 2 0.07% link
ext-mcrypt 2 0.07% link
ext-igbinary 2 0.07% link
ext-gnupg 2 0.07% link
ext-ffi 2 0.07% link
ext-date 2 0.07% link
ext-gearman 1 0.03% link
ext-rdkafka 1 0.03% link
ext-geos 1 0.03% link
ext-lua 1 0.03% link
ext-bz2 1 0.03% link
ext-dba 1 0.03% link
ext-mustache 1 0.03% link
ext-tideways 1 0.03% link
ext-wikidiff2 1 0.03% link
ext-imap 1 0.03% link
ext-sysvmsg 1 0.03% link
ext-sysvsem 1 0.03% link
ext-sysvshm 1 0.03% link
ext-parallel 1 0.03% link
ext-shmop 1 0.03% link
ext-phalcon 1 0.03% link
ext-openswoole 1 0.03% link
ext-chunkutils2 1 0.03% link
ext-crypto 1 0.03% link
ext-encoding 1 0.03%
ext-leveldb 1 0.03% link
ext-morton 1 0.03% link
ext-pmmpthread 1 0.03% link
ext-mysqlnd 1 0.03% link
ext-maxminddb 1 0.03% link

Extension names normalized to lowercase (a handful of manifests write ext-SPL or ext-PDO); links go to the PHP manual for bundled extensions and to PECL or the project’s own repository for everything else. ext-encoding is left unlinked: I couldn’t confidently confirm what current package it maps to.

The long tail tells its own story: ext-swoole and ext-openswoole for async runtimes, ext-chunkutils2/ext-morton/ext-pmmpthread from the PocketMine-MP Minecraft-server project, ext-geos for geospatial computation, ext-wikidiff2 straight out of MediaWiki, ext-phalcon for a framework that ships as a compiled extension rather than a library. None of these are things you’d predict from a generic “PHP ecosystem” survey. They only show up because this corpus is Composer plugins specifically, each one built to solve one real, narrow problem.

That’s PHP and its extensions accounted for. What’s left is the actual software. They are the libraries, frameworks and tools people install on top of the platform to build their plugins.

Top 50 most required packages

19945 require entries across the corpus, spread over 5707 distinct package names: this time, real Packagist packages only, with php, ext-*, lib-* and composer-plugin-api/composer-runtime-api stripped out. Most of that 5707 is one-off dependencies used by a single plugin. Here’s the top of the distribution, with the most frequent components:

# Package Projects Share
1 symfony/console 215 7.0%
2 symfony/yaml 182 6.0%
3 guzzlehttp/guzzle 176 5.8%
4 psr/log 138 4.5%
5 symfony/filesystem 137 4.5%
6 symfony/finder 130 4.3%
7 symfony/process 115 3.8%
8 laravel/framework 104 3.4%
9 twig/twig 93 3.0%
10 monolog/monolog 92 3.0%
11 composer/installers 89 2.9%
12 composer/composer 88 2.9%
13 doctrine/dbal 84 2.8%
14 symfony/event-dispatcher 76 2.5%
15 laravel/tinker 76 2.5%
16 psr/container 76 2.5%
17 psr/http-message 75 2.5%
18 nikic/php-parser 75 2.5%
19 vlucas/phpdotenv 73 2.4%
20 phpmailer/phpmailer 68 2.2%
21 symfony/config 63 2.1%
22 symfony/http-client 62 2.0%
23 symfony/dependency-injection 60 2.0%
24 symfony/http-foundation 60 2.0%
25 symfony/mailer 57 1.9%
26 doctrine/orm 57 1.9%
27 symfony/validator 56 1.8%
28 ramsey/uuid 53 1.7%
29 symfony/cache 51 1.7%
30 symfony/dotenv 51 1.7%
31 composer/semver 49 1.6%
32 psr/simple-cache 49 1.6%
33 ezyang/htmlpurifier 46 1.5%
34 illuminate/support 46 1.5%
35 symfony/property-access 44 1.4%
36 league/flysystem 43 1.4%
37 symfony/framework-bundle 43 1.4%
38 psr/http-client 43 1.4%
39 nyholm/psr7 41 1.3%
40 symfony/translation 41 1.3%
41 symfony/expression-language 40 1.3%
42 symfony/var-dumper 40 1.3%
43 league/flysystem-aws-s3-v3 40 1.3%
44 doctrine/doctrine-bundle 39 1.3%
45 predis/predis 38 1.2%
46 erusev/parsedown 38 1.2%
47 symfony/http-kernel 38 1.2%
48 symfony/mime 38 1.2%
49 symfony/twig-bundle 38 1.2%
50 psr/http-factory 38 1.2%

Symfony components own this list: 24 of the top 50 are symfony/*. Guzzle, Monolog, Doctrine and the PSR interfaces fill most of the rest. Laravel shows up too, with laravel/framework, laravel/tinker, illuminate/support, a reminder that plenty of these “Composer plugins” are actually Laravel packages that happen to also ship an installer.

Top 50 most required dev packages

require-dev is a much smaller and more concentrated world: 8474 entries, 1385 distinct packages, and a very predictable top of the table.

# Package Projects Share
1 phpunit/phpunit 1043 34.2%
2 composer/composer 810 26.5%
3 phpstan/phpstan 373 12.2%
4 squizlabs/php_codesniffer 292 9.6%
5 friendsofphp/php-cs-fixer 225 7.4%
6 mockery/mockery 206 6.7%
7 rector/rector 103 3.4%
8 symfony/var-dumper 99 3.2%
9 mikey179/vfsstream 97 3.2%
10 fakerphp/faker 97 3.2%
11 laravel/pint 85 2.8%
12 phpstan/extension-installer 84 2.8%
13 nunomaduro/collision 82 2.7%
14 pestphp/pest 82 2.7%
15 phpstan/phpstan-phpunit 82 2.7%
16 vimeo/psalm 78 2.6%
17 roave/security-advisories 71 2.3%
18 symfony/phpunit-bridge 64 2.1%
19 phpstan/phpstan-strict-rules 57 1.9%
20 phpstan/phpstan-deprecation-rules 53 1.7%
21 symfony/process 51 1.7%
22 phpmd/phpmd 51 1.7%
23 orchestra/testbench 49 1.6%
24 symfony/browser-kit 48 1.6%
25 phpspec/prophecy-phpunit 43 1.4%
26 infection/infection 40 1.3%
27 symfony/css-selector 39 1.3%
28 php-coveralls/php-coveralls 37 1.2%
29 symfony/console 36 1.2%
30 laravel/sail 36 1.2%
31 php-parallel-lint/php-parallel-lint 36 1.2%
32 ergebnis/composer-normalize 35 1.1%
33 symfony/web-profiler-bundle 33 1.1%
34 phpcompatibility/php-compatibility 33 1.1%
35 phpbench/phpbench 32 1.0%
36 phpspec/phpspec 31 1.0%
37 symfony/yaml 30 1.0%
38 dealerdirect/phpcodesniffer-composer-installer 30 1.0%
39 spatie/laravel-ignition 30 1.0%
40 bamarni/composer-bin-plugin 29 1.0%
41 symfony/filesystem 27 0.9%
42 larastan/larastan 27 0.9%
43 symfony/debug-bundle 27 0.9%
44 satooshi/php-coveralls 26 0.9%
45 symfony/stopwatch 25 0.8%
46 pestphp/pest-plugin-laravel 25 0.8%
47 symfony/dom-crawler 25 0.8%
48 guzzlehttp/guzzle 25 0.8%
49 phpstan/phpstan-symfony 24 0.8%
50 behat/behat 23 0.8%

A third of the corpus tests with PHPUnit. Just over a quarter requires composer/composer itself in dev: this is expected, since a Composer plugin needs the Composer API to type-hint against and to run its test suite against real plugin activation. PHPStan alone, at 12.2%, is already well ahead of Psalm, at 2.6%, and counting the whole family together, phpstan/phpstan plus its satellites extension-installer, phpunit, strict-rules, deprecation-rules, symfony, 390 projects, or 12.8% of the corpus, use at least one of them. Pest is present, 2.7% but PHPUnit is still the default by an order of magnitude.

Requested versions vs. what’s actually on Packagist today

This is the part I find the most telling. For each of the most-required packages, I looked at every version constraint written across the corpus, broke ||-separated ranges into individual major versions, and compared the most commonly targeted major against the current stable release on Packagist.

Package Latest on Packagist Most-required major Projects compatible with the latest major
symfony/console 8.1.5 v6 (44% of projects) 20%
symfony/yaml 8.1.5 v7 (35% of projects) 9%
guzzlehttp/guzzle 8.1.0 v7 (83% of projects) 2%
psr/log 3.0.2 v3 (70% of projects) 70%
symfony/filesystem 8.1.5 v6 (47% of projects) 12%
symfony/finder 8.1.5 v6 (42% of projects) 18%
symfony/process 8.1.5 v7 (45% of projects) 23%
laravel/framework 13.29.0 v12 (32% of projects) 24%
twig/twig 3.28.0 v3 (78% of projects) 78%
monolog/monolog 3.10.0 v3 (55% of projects) 55%
composer/installers 2.3.0 v1 (82% of projects) 29%
composer/composer 2.10.2 v2 (68% of projects) 68%
doctrine/dbal 4.4.4 v3 (39% of projects) 31%
symfony/event-dispatcher 8.1.5 v6 (41% of projects) 18%
laravel/tinker 3.0.2 v2 (66% of projects) 24%
psr/container 2.0.2 v1 (78% of projects) 71%
psr/http-message 2.0 v1 (77% of projects) 67%
nikic/php-parser 5.8.0 v5 (56% of projects) 56%
vlucas/phpdotenv 5.7.0 v5 (58% of projects) 58%

A few things jump out:

  • Guzzle is the most striking laggard — though partly excusable. 83% of the corpus is still pinned to ^7.x, and only 2% of projects declare a constraint that would accept Guzzle 8. In fairness, Guzzle 8.0.0 only shipped on 2026-07-20, barely a month before this scan: this is less “ecosystem laggard” and more “the ink isn’t dry yet.” Worth re-running this query in six months.
  • The Symfony components are all in the same boat. Console, Yaml, Filesystem, Finder, Process, EventDispatcher: every one of them peaks around major 6 or 7, while Symfony itself is already shipping 8.x. Given Symfony’s yearly major-version cadence, this isn’t really about laziness; a ^6.0 constraint from a couple of years ago is simply not the kind of thing a plugin maintainer revisits unless something forces their hand.
  • composer/installers v1 → v2 is a genuinely stalled migration, and this one has no excuse.82% of projects are still on the ^1.0 line: v2.0.0 shipped back in August 2021. Five years on, four out of five plugins in this corpus still haven’t made the jump. If you maintain a Composer plugin, this is probably worth a five-minute check.
  • PSR interfaces show the opposite pattern. psr/log, psr/container, psr/http-message all sit around 67–71% latest-major compatibility: no surprise, since these interfaces barely change and a wide ^1.0 || ^2.0 || ^3.0 constraint costs nothing to write.
  • Twig and nikic/php-parser are the well-behaved ones — the most-required major is the current major, with 56–78% of projects already on the latest line.

Type: what kind of package is this, really

type is optional, and it shows:

Type Projects Share
composer-plugin 1293 42.4%
(not set) 1074 35.2%
library 320 10.5%
project 257 8.4%
symfony-bundle 13 0.4%
wordpress-plugin 13 0.4%
application 12 0.4%
cakephp-plugin 7 0.2%
package 5 0.2%
neos-package 5 0.2%
drupal-module 5 0.2%
magento2-module 4 0.1%
framework 4 0.1%
29 more types, each with 1–3 occurrences 39 1.3%

More than a third of the corpus leaves type unset entirely. The default for Composer is to treat it as a library, so the effective library share is closer to 45%. The long tail of one-off types, such as kirby-cms, s9y-core, midcom-package, october-plugin… is a nice reminder of how many CMS-specific plugin ecosystems still route through Composer’s type field for their custom installers.

Minimum stability: how much risk are people willing to take

minimum-stability Projects Share
(not set — defaults to stable) 1521 49.8%
dev 1296 42.5%
stable 211 6.9%
alpha 12 0.4%
beta 8 0.3%
RC 4 0.1%

This is the one that surprised me most. minimum-stability: dev sits at 42.5%: nearly as high as everyone who left the field alone. That’s a huge chunk of the ecosystem explicitly opting in to unreleased, unstable dependency versions by default. It is almost always paired with a prefer-stable: true to soften the blow, though I didn’t check that here. Composer plugins skew younger and more experimental than the average PHP library, and it shows: this is not a community that’s shy about tracking dev-main.

And I don’t think that’s a problem by itself. A component can reasonably stay on dev for as long as it has no real users yet. The moment someone actually depends on it in production is the moment they need a version number to freeze, and not a day before. What’s healthy here is the appetite: a PHP ecosystem willing to try new ideas before they’re polished is a good sign, not a red flag. The gap I keep running into in this data isn’t the experimentation itself, it’s that nothing beyond a handful of leaders, like Symfony, PHPUnit, PHPStan, etc. gets validated by adoption at any scale. Past the top of each list, popularity falls off a cliff, and there’s no real tooling to help a maintainer discover which of the thousands of one-off dependencies in that long tail are actually worth building on. Now that AI makes it trivial to spin up ten experiments where you used to write one, that discovery problem only gets bigger. The bottleneck stops being “can I build this” and becomes “how would anyone downstream ever find out it’s good”.


Corpus: 3052 valid composer.json files scanned across 3285 project directories. Version comparisons pulled live from Packagist at the time of writing: check the linked package pages for current numbers, since this particular table will age fast, by design.