shipmonk / composer-normalizer
Zero-dependency normalizer of composer.json: sorts keys in a stable order, removes empty keys and keeps your indentation.
Requires
- php: ^8.1
- ext-json: *
Requires (Dev)
- editorconfig-checker/editorconfig-checker: ^10.7.0
- phpstan/phpstan: ^2.2.14
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^10.5.62
- shipmonk/coding-standard: ^0.3.0
- shipmonk/composer-dependency-analyser: ^1.8
- shipmonk/dead-code-detector: ^1.0
- shipmonk/name-collision-detector: ^2.1.1
- shipmonk/phpstan-rules: ^4.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-11-04 08:02:45 UTC
README
- ๐ชถ Zero dependencies: a small CLI, not a Composer plugin
- ๐ค Sorts keys: the key order of ergebnis/composer-normalize, without changes to your values, except for duplicate extension names
- ๐งน Removes empty keys:
"require-dev": {},"keywords": []and other empty optional keys - ๐ Keeps your format: indentation (spaces or tabs, any size) and new lines are taken from the file
- โจ Compatible: PHP 8.1+
Installation
composer require --dev shipmonk/composer-normalizer
Usage
Add it to the scripts that check and fix your coding standard:
{
"scripts": {
"check:cs": [
"composer-normalizer --dry-run",
"phpcs"
],
"fix:cs": [
"composer-normalizer",
"phpcbf"
]
}
}
Put composer-normalizer before phpcbf: phpcbf exits with code 1 when it fixes a file, and Composer then stops the script list.
You can also run it directly:
vendor/bin/composer-normalizer # sorts composer.json in the working directory vendor/bin/composer-normalizer path/to/composer.json # sorts the given files vendor/bin/composer-normalizer --dry-run # only checks, does not change files
Without a file, it uses the file from the COMPOSER environment variable, the same as Composer.
| Exit code | Meaning |
|---|---|
| 0 | All files are normalized (or were fixed) |
| 1 | --dry-run found a file that is not normalized |
| 2 | Error: file cannot be read or written, invalid JSON, unknown option |
Key order
- Root keys use the order of the Composer schema:
name,description,license,type, ...,config,extra,scripts. Unknown keys go last, in alphabetical order. - Packages in
require,require-dev,conflict,provide,replaceandsuggestuse the order of Composer'ssort-packages:php,ext-*,lib-*,composerand its API packages, then other packages. Unmerged names that differ only in case keep their relative order. - Objects that the schema defines use the schema order, for example
authors,support,autoload,source,dist, items ofrepositories(also inline packages),archive,php-ext,config.audit,config.policyandconfig.http-basic. Inscripts, Composer events (post-install-cmd, ...) go first, then your scripts in alphabetical order. - All other objects are sorted alphabetically, recursively:
config,extra,autoload.psr-4, ... Numeric keys go first, by value (9before10). config.allow-pluginsandconfig.preferred-installare sorted alphabetically, butvendor/*goes aftervendor/package. Composer uses the first entry that matches a package, so the order stays when sorting would change which entry matches.- The order stays where the order has a meaning:
repositories,extra.installer-paths,extra.patches.*,scripts.auto-scriptsand all lists, exceptbin,repositories.*.onlyandrepositories.*.exclude. binis sorted only when the binaries have distinct destination names, including case-insensitive names and.batproxies on Windows. Otherwise, the order stays: Composer installs the first binary with a given name.
Empty keys
Optional keys of the Composer schema are removed when their value is {} or [], for example "require-dev": {}, "keywords": [] or "autoload": {"psr-4": {}}. Other keys stay, for example keys in extra, your own scripts, config keys that the schema does not define and items of lists.
An empty value stays where Composer reads it differently from a missing key:
repositories.*.only: an empty list matches no packagerepositories.*.only-branches: an empty list matches only the default branchconfig.github-protocolsandconfig.forgejo-domains: an empty list replaces the default listconfig.policyand all keys in it: Composer reads{}astrue, which turns a policy on, also when the global config turns it off. Inadvisoriesandabandoned,{}also stops the fallback toconfig.audit. An emptyignore-unreachableignores nothing, the default ignoresinstallandupdate.
Composer applies config on top of the global config (COMPOSER_HOME/config.json). An empty config.platform, config.secure-svn-domains, config.audit.ignore-severity or config.audit.ignore-abandoned replaces the global value. The tool removes these empty values, so the global value applies again.
Extension names
In require, require-dev, conflict, provide, replace and suggest, names that start with ext- (in any case) are written in lowercase, with - instead of spaces, the same as Composer names the packages of PHP extensions: ext-JSON becomes ext-json. When this gives the same name two or more times, the values are joined with ||, in the order of the file. Equal values are written once:
{
"require": {
"ext-JSON": "^1.0",
"ext-json": "^2.0"
}
}
becomes "ext-json": "^1.0 || ^2.0". When one of the values is not a string or is self.version, the values cannot be joined and these names keep their spelling and relative order, so Composer still uses the same last entry.
The merge can change what Composer does. Composer uses only the last of the duplicate names (in suggest, it shows all of them). After the merge, require allows more versions and conflict blocks more versions. A new name can also make composer validate show a problem that the old name hid, for example ext-json in replace and ext-JSON in conflict. Check the result with composer validate.
Format and values
- The output always ends with a new line.
- Slashes and unicode characters are not escaped, the same as when Composer writes
composer.json. - Values do not change, except for duplicate extension names and where the JSON functions of PHP cannot keep them:
- numbers get their shortest form:
1e3becomes1000.0,1.50becomes1.5, an integer larger thanPHP_INT_MAXbecomes a float - when an object has the same key two times, only the last one stays (Composer also uses the last one)
- numbers get their shortest form:
Differences from ergebnis/composer-normalize
The key order is the same as in ergebnis/composer-normalize 2.55, except:
composergoes with the platform packages, the same as whencomposer requiresorts packages.- Unmerged package names that differ only in case keep their relative order.
config.allow-pluginsandconfig.preferred-installkeep their order when sorting would change which entry matches a package.binkeeps its order when destination names conflict, including Windows names and.batproxies.- Numeric keys always go before other keys.
- Repositories use one key order for all repository types. ergebnis puts keys that the schema does not define for the repository type last, for example
optionsin avcsrepository. In repositories in object form ("repositories": {"name": {...}}), ergebnis sorts all keys alphabetically.
Empty keys are removed the same as in ergebnis, except:
- The empty values in the list in Empty keys stay.
- Keys with the value
nullstay. - In repositories, empty keys of all repository types are removed, also in repositories in object form. ergebnis removes only empty keys that the schema defines for the repository type, and none in the object form.
Extension names change the same as in ergebnis, except:
- The joined values keep the order of the file, and equal values are written once. ergebnis also normalizes the joined version constraints (when it can parse them, it sorts and simplifies them), and joins the descriptions in
suggestwith||. - Values with
self.versionare not joined. Duplicate extension names that cannot be joined keep their relative order.
A composer.json that ergebnis normalized does not change, except in the cases above and when the file has no final new line, escaped slashes (\/) or escaped unicode characters (\u00e9).
This tool does not:
- register a Composer plugin: there is no
composer normalizecommand and noallow-pluginsentry. The tool does not load Composer internals, so it does not block an upgrade of PHP or Composer. - change values (except for duplicate extension names): ergebnis also changes version constraints (
^1.0|^2.0becomes^1.0 || ^2.0) and writes1.0as1. - show a diff: run it without
--dry-runand usegit diff. - validate the schema: use
composer validate --strict. - update
composer.lock: thecontent-hashincomposer.lockdepends onrequire,require-dev,conflict,replace,provide,repositories,extra,config.platformandconfig.policy, also on their key order, empty values and package names. When the tool changes them, runcomposer update --lock. - have indent options: the indentation comes from the file.
Migration
composer remove --dev ergebnis/composer-normalize composer require --dev shipmonk/composer-normalizer
Then remove ergebnis/composer-normalize from config.allow-plugins and change your scripts:
| Before | After |
|---|---|
composer normalize --dry-run --no-check-lock --no-update-lock |
composer-normalizer --dry-run |
composer normalize --no-check-lock --no-update-lock |
composer-normalizer |
Contributing
- Check your code with
composer check - Autofix coding-style with
composer fix:cs - All functionality must be tested
Supported PHP versions
- Runtime requires PHP 8.1+