Normalizing composer.json

Berlin, Germany

If you are using composer, you have probably modified composer.json at least once to keep things nice and tidy.

In fact, when I started using composer in late summer 2012, I of course edited composer.json manually, even when I wanted to add or update dependencies. I soon learned that one should run

$ composer require foo/bar:~x.y.z

instead, as this prevents pulling in undesired updates. I did that, but in order to keep the list of dependencies sorted, I actually had to do it twice:

  1. require package
  2. move package in require or require-dev section so packages are sorted
  3. run command again (to keep the lock file updated, as the order of dependencies matters for the calculation of the hash)

That’s a bit crazy, right? And just to keep things nice and tidy!

Then I came across a tweet by Henrik Joreteg:

Nice, I wasn’t alone with liking things nice and tidy!

At the time I was contracting for a company here in Berlin. One of the best parts working there was that a lot of respected members of the PHP community stopped by the office oftentimes, for example, Benjamin Eberlei, David Zuelke, and Nils Aderman. Nils (whom you probably know as one of the maintainers of composer, together with Jordi Boggiano) even regularly worked out of the office. Hesitant at first, I approached him about this issue, and asked him whether they would consider a pull request. I think he liked it, so I was quite happy and figured it should be ok to do something about it.

Nonetheless, it took me until December 2014 until I eventually opened a pull request to add the --sort-packages option to composer.

Since then it has been possible to run, for example,

$ composer require --dev --sort-packages phpunit/phpunit

to require a package and keep packages sorted (if you already have that package required, you can run the command to trigger sorting).

With a bit of tweaking not only packages and platform requirements would be sorted, but platform requirements listed before packages. Thanks to Hanov Ruslan, this behaviour can be configured, making it unnecessary to use the option on the command line.

Not only have a lot of people blogged about the feature, but a quick search on GitHub reveals that the configuration option is used at least 327,598 times! Excellent!

Now, how about taking it a step further? Today I present to you localheinz/composer-normalize, a composer plugin for tidying up all ofcomposer.json!

Run

$ composer global require localheinz/composer-normalize

to install the composer plugin globally.

Run

$ composer normalize

to normalize composer.json in the working directory or run

$ composer normalize ~/Sites/foo/bar/composer.json

to normalize composer.json in the ~/Sites/foo/bar directory.

The NormalizeCommand provided by the NormalizePlugin within this package will

  • determine whether a composer.json exists
  • determine whether a composer.lock exists, and if so, whether it is up to date
  • use the ComposerJsonNormalizer to normalize the content of composer.json
  • format the normalized content (either as sniffed, or as specified using the --indent-size and --indent-style options)
  • write the normalized and formatted content of composer.json back to the file
  • update the hash in composer.lock if it exists and if an update is necessary

Currently, the following normalizations will be applied

  • restructure composer.json according to the underlying JSON schema
  • sort entries in the bin section (by value)
  • sort entries in the config, extra, and scripts-descriptions sections (by key)
  • sort entries in the conflict, provide, replace, require, require-dev, and suggest sections (as you are used to from the sort-packages configuration)
  • normalize version constraints in conflict, provide, replace, require, and require-dev sections

The command accepts the following argument

  • file: Path to composer.json file (optional, defaults to composer.json in working directory)

The command comes with the following options

  • --dry-run: Show the results of normalizing, but do not modify any files
  • --indent-size: Indent size (an integer greater than 0); should be used with the --indent-style option
  • --indent-style: Indent style (one of “space”, “tab”); should be used with the --indent-size option
  • --no-update-lock: Do not update lock file if it exists

Here is an example of running

$ composer normalize

on composer/composer itself:

diff --git a/composer.json b/composer.json
index dd672c3b..330f73d0 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,13 @@
 {
     "name": "composer/composer",
+    "type": "library",
     "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
-    "keywords": ["package", "dependency", "autoload"],
+    "keywords": [
+        "package",
+        "dependency",
+        "autoload"
+    ],
     "homepage": "https://getcomposer.org/",
-    "type": "library",
     "license": "MIT",
     "authors": [
         {
@@ -17,52 +21,58 @@
             "homepage": "http://seld.be"
         }
     ],
-    "support": {
-        "irc": "irc://irc.freenode.org/composer",
-        "issues": "https://github.com/composer/composer/issues"
-    },
     "require": {
         "php": "^5.3.2 || ^7.0",
-        "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
         "composer/ca-bundle": "^1.0",
         "composer/semver": "^1.0",
         "composer/spdx-licenses": "^1.2",
+        "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
+        "psr/log": "^1.0",
+        "seld/cli-prompt": "^1.0",
         "seld/jsonlint": "^1.4",
+        "seld/phar-utils": "^1.0",
         "symfony/console": "^2.7 || ^3.0 || ^4.0",
-        "symfony/finder": "^2.7 || ^3.0 || ^4.0",
-        "symfony/process": "^2.7 || ^3.0 || ^4.0",
         "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
-        "seld/phar-utils": "^1.0",
-        "seld/cli-prompt": "^1.0",
-        "psr/log": "^1.0"
+        "symfony/finder": "^2.7 || ^3.0 || ^4.0",
+        "symfony/process": "^2.7 || ^3.0 || ^4.0"
     },
     "require-dev": {
         "phpunit/phpunit": "^4.8.35 || ^5.7",
         "phpunit/phpunit-mock-objects": "^2.3 || ^3.0"
     },
+    "suggest": {
+        "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
+        "ext-zip": "Enabling the zip extension allows you to unzip archives",
+        "ext-zlib": "Allow gzip compression of HTTP requests"
+    },
     "config": {
         "platform": {
             "php": "5.3.9"
         }
     },
-    "suggest": {
-        "ext-zip": "Enabling the zip extension allows you to unzip archives",
-        "ext-zlib": "Allow gzip compression of HTTP requests",
-        "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.7-dev"
+        }
     },
     "autoload": {
-        "psr-4": { "Composer\\": "src/Composer" }
+        "psr-4": {
+            "Composer\\": "src/Composer"
+        }
     },
     "autoload-dev": {
-        "psr-4": { "Composer\\Test\\": "tests/Composer/Test" }
-    },
-    "bin": ["bin/composer"],
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.7-dev"
+        "psr-4": {
+            "Composer\\Test\\": "tests/Composer/Test"
         }
     },
+    "bin": [
+        "bin/composer"
+    ],
     "scripts": {
         "test": "phpunit"
+    },
+    "support": {
+        "issues": "https://github.com/composer/composer/issues",
+        "irc": "irc://irc.freenode.org/composer"
     }
 }

If you need to change the indentation, you can use the --indent-size and --indent-style options. Here’s an example of running

$ composer normalize --indent-size=2 --indent-style=space

on composer/composer itself:

diff --git a/composer.json b/composer.json
index dd672c3b..071c99c6 100644
--- a/composer.json
+++ b/composer.json
@@ -1,68 +1,78 @@
 {
-    "name": "composer/composer",
-    "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
-    "keywords": ["package", "dependency", "autoload"],
-    "homepage": "https://getcomposer.org/",
-    "type": "library",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "Nils Adermann",
-            "email": "naderman@naderman.de",
-            "homepage": "http://www.naderman.de"
-        },
-        {
-            "name": "Jordi Boggiano",
-            "email": "j.boggiano@seld.be",
-            "homepage": "http://seld.be"
-        }
-    ],
-    "support": {
-        "irc": "irc://irc.freenode.org/composer",
-        "issues": "https://github.com/composer/composer/issues"
+  "name": "composer/composer",
+  "type": "library",
+  "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
+  "keywords": [
+    "package",
+    "dependency",
+    "autoload"
+  ],
+  "homepage": "https://getcomposer.org/",
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "Nils Adermann",
+      "email": "naderman@naderman.de",
+      "homepage": "http://www.naderman.de"
     },
-    "require": {
-        "php": "^5.3.2 || ^7.0",
-        "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
-        "composer/ca-bundle": "^1.0",
-        "composer/semver": "^1.0",
-        "composer/spdx-licenses": "^1.2",
-        "seld/jsonlint": "^1.4",
-        "symfony/console": "^2.7 || ^3.0 || ^4.0",
-        "symfony/finder": "^2.7 || ^3.0 || ^4.0",
-        "symfony/process": "^2.7 || ^3.0 || ^4.0",
-        "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
-        "seld/phar-utils": "^1.0",
-        "seld/cli-prompt": "^1.0",
-        "psr/log": "^1.0"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "^4.8.35 || ^5.7",
-        "phpunit/phpunit-mock-objects": "^2.3 || ^3.0"
-    },
-    "config": {
-        "platform": {
-            "php": "5.3.9"
-        }
-    },
-    "suggest": {
-        "ext-zip": "Enabling the zip extension allows you to unzip archives",
-        "ext-zlib": "Allow gzip compression of HTTP requests",
-        "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"
-    },
-    "autoload": {
-        "psr-4": { "Composer\\": "src/Composer" }
-    },
-    "autoload-dev": {
-        "psr-4": { "Composer\\Test\\": "tests/Composer/Test" }
-    },
-    "bin": ["bin/composer"],
-    "extra": {
-        "branch-alias": {
-            "dev-master": "1.7-dev"
-        }
-    },
-    "scripts": {
-        "test": "phpunit"
+    {
+      "name": "Jordi Boggiano",
+      "email": "j.boggiano@seld.be",
+      "homepage": "http://seld.be"
+    }
+  ],
+  "require": {
+    "php": "^5.3.2 || ^7.0",
+    "composer/ca-bundle": "^1.0",
+    "composer/semver": "^1.0",
+    "composer/spdx-licenses": "^1.2",
+    "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
+    "psr/log": "^1.0",
+    "seld/cli-prompt": "^1.0",
+    "seld/jsonlint": "^1.4",
+    "seld/phar-utils": "^1.0",
+    "symfony/console": "^2.7 || ^3.0 || ^4.0",
+    "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
+    "symfony/finder": "^2.7 || ^3.0 || ^4.0",
+    "symfony/process": "^2.7 || ^3.0 || ^4.0"
+  },
+  "require-dev": {
+    "phpunit/phpunit": "^4.8.35 || ^5.7",
+    "phpunit/phpunit-mock-objects": "^2.3 || ^3.0"
+  },
+  "suggest": {
+    "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
+    "ext-zip": "Enabling the zip extension allows you to unzip archives",
+    "ext-zlib": "Allow gzip compression of HTTP requests"
+  },
+  "config": {
+    "platform": {
+      "php": "5.3.9"
+    }
+  },
+  "extra": {
+    "branch-alias": {
+      "dev-master": "1.7-dev"
+    }
+  },
+  "autoload": {
+    "psr-4": {
+      "Composer\\": "src/Composer"
+    }
+  },
+  "autoload-dev": {
+    "psr-4": {
+      "Composer\\Test\\": "tests/Composer/Test"
     }
+  },
+  "bin": [
+    "bin/composer"
+  ],
+  "scripts": {
+    "test": "phpunit"
+  },
+  "support": {
+    "issues": "https://github.com/composer/composer/issues",
+    "irc": "irc://irc.freenode.org/composer"
+  }
 }

Hope you like it!