atoolo / channel-diff
CLI tool to compare two IES publication channels.
Requires
- php: >=8.2
- ext-json: *
- atoolo/resource-bundle: ^1.8
- symfony/console: ^7.4
- symfony/dotenv: ^7.4
- symfony/framework-bundle: ^7.4
- symfony/runtime: ^7.4
- symfony/yaml: ^7.4
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^1.2
- infection/infection: ^0.27.11
- overtrue/phplint: ^9.7.1
- phpcompatibility/php-compatibility: ^9.3.5
- roave/security-advisories: dev-latest
- squizlabs/php_codesniffer: ^3.13
Conflicts
This package is auto-updated.
Last update: 2026-07-22 12:34:53 UTC
README
channel-diff
A command-line tool to compare two IES publication channels. It loads the PHP resource files of each channel into their nested arrays and diffs them field-by-field; binary media files are compared by hash.
Typical use cases: verifying a channel after a re-publish, a publisher change, or a migration between environments.
How it works
- Each publication channel is a directory containing SiteKit resource PHP files.
These are loaded via
atoolo/resource-bundleinto their raw nested arrays and compared. - The three channel layouts are detected automatically:
- DOCUMENT_ROOT — everything in one directory (resources and media
intermixed); the SiteKit framework subtree (
WEB-IES/) is excluded. - RESOURCE, URL-based — separate
objects/andmedia/public/trees. - RESOURCE, ID-based — same as above, with an ID-bucketed
objects/tree.
- DOCUMENT_ROOT — everything in one directory (resources and media
intermixed); the SiteKit framework subtree (
- Resources and media are matched by their relative file path. Volatile
fields (
version,created,changed,generated,changedBy,cacheInfo) are ignored by default.
Note: Because matching is by physical path, both channels should use the same layout. Comparing channels of different layouts will report every resource as "only in A / only in B".
Requirements
- PHP >= 8.2 with the
intlandjsonextensions - Composer 2.x
- phive (for the QA tools; only needed for
composer testandcomposer analyse)
Installation
git clone <repository-url> channel-diff cd channel-diff composer install
atoolo/resource-bundle is resolved from Packagist like any other dependency.
composer install runs a post-install-cmd that installs the QA PHAR tools
(PHPUnit, PHPStan, PHP-CS-Fixer, composer-normalize) into tools/ via
phive. phplint is installed as a regular Composer
dev dependency.
Contributors should enable the shared git hooks (Conventional Commit check) once after cloning:
git config core.hooksPath .githooks
Usage
php bin/console channel:diff <channelA> <channelB> [options]
<channelA> and <channelB> are the base directories of the two publication
channels, for example:
- DOCUMENT_ROOT:
.../publications/<host>/www - RESOURCE:
.../publications/<host>/www/resources
Options
| Option | Description |
|---|---|
-f, --format=console|json |
Output format (default: console). |
-i, --ignore=<path> |
Additional dot-notation field path to ignore. Supports wildcards (see below). Repeatable. |
--ignore-config=<file.php> |
PHP file returning a list of dot-notation field paths to ignore. |
--no-media |
Skip the binary media comparison. |
--strict-null |
Treat a null field and a missing field as different. By default, a field that is null in one channel and absent in the other is considered equal. |
--strict-empty-string |
Treat an empty-string field and a missing field as different. By default, a field that is "" in one channel and absent in the other is considered equal. |
--strict-empty-array |
Treat an empty-array field and a missing field as different. By default, a field that is an empty array in one channel and absent in the other is considered equal. Emptiness is recursive: an array whose (nested) values are all empty counts as empty (e.g. ['features' => ['primary' => []]]). |
--strict-uuid-keys |
Treat UUID array keys as significant. By default, an array whose keys are all UUIDs is matched by the content of its values instead of by the (volatile, regenerated) keys, and inner values equal to the key (e.g. a mirrored id) are neutralized so entries pair up. |
Ignore path wildcards
Ignore paths may contain wildcard segments:
*matches exactly one key segment. A*in the middle of a path is a field ignore, e.g.a.b.*.idignores theidfield of every child ofa.b.**matches any number of segments (including none), so a structure that occurs at many depths can be addressed once, e.g.**.geo.features.*.- A terminal
*, e.g.**.geo.features.*, is a key-normalization marker: the matched array is compared by the content of its values, not by their keys (useful for arrays keyed by volatile values, when UUID auto-detection does not apply).
Exit codes
| Code | Meaning |
|---|---|
0 |
Channels are identical. |
1 |
Differences were found. |
2 |
An error occurred (e.g. an invalid path). |
Examples
Compare two channels with human-readable output:
php bin/console channel:diff \
~/ies-environments/site/data/publications/host/www/resources \
~/ies-environments/site/data/publications/host/preview/resources
Produce a machine-readable report for CI:
php bin/console channel:diff /path/to/A /path/to/B --format=json > diff.json
Ignore additional fields, both inline and via a config file:
php bin/console channel:diff /path/to/A /path/to/B \
--ignore=ies \
--ignore=base.germanCourse.venue.link \
--ignore-config=ignore.php
An ignore.php config file simply returns a list of field paths:
<?php return [ 'ies', 'base.germanCourse.venue.addressData.geo', ];
Notes on comparison
- Nested arrays are compared recursively; differences are reported with a
dot-notation path (e.g.
base.metadata.headline). - A field that is
nullin one channel and missing in the other is treated as equal by default; use--strict-nullto report it as a difference. The same applies to an empty string ("") versus a missing field (--strict-empty-string) and an empty array ([]) versus a missing field (--strict-empty-array). - Arrays keyed by UUIDs (which IES regenerates on every publish) are matched by
the content of their values by default, so the changing keys do not show up as
differences; use
--strict-uuid-keysto compare them by key instead. - PHP objects (e.g.
stdClass) are compared structurally, not by instance. - Closures (e.g. from "code" content sections) are compared by their source code, not by instance.
- In the JSON output, field values are rendered as bounded single-line strings;
closures and objects are shown as
<closure>/<object:Class>.
Development
composer test # run the PHPUnit test suite (with coverage) composer analyse # phplint, PHPStan, PHP-CS-Fixer (check), PHP compatibility composer fix # apply PHP-CS-Fixer fixes composer test:infection # run mutation testing (Infection)