atoolo/channel-diff

CLI tool to compare two IES publication channels.

Maintainers

Package info

github.com/sitepark/atoolo-channel-diff

Type:project

pkg:composer/atoolo/channel-diff

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-08-03 07:22 UTC

This package is auto-updated.

Last update: 2026-08-03 07:22:11 UTC


README

codecov Verify phpstan php php php php

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-bundle into 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/ and media/public/ trees.
    • RESOURCE, ID-based — same as above, with an ID-bucketed objects/ tree.
  • 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 intl and json extensions
  • Composer 2.x
  • phive (for the QA tools; only needed for composer test and composer 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> [subPath] [options]

Arguments

Argument Description
<channelA>, <channelB> Base directories of the two publication channels (required).
[subPath] Restrict the comparison to this sub directory of both channels (optional; default: the whole channel).

The channel base directories are, for example:

  • DOCUMENT_ROOT: .../publications/<host>/www
  • RESOURCE: .../publications/<host>/www/resources

Restricting the comparison to a sub directory

[subPath] is resolved against the channel base directory, so one path expression addresses the resource tree and the media tree alike — even though those are separate sub trees in the RESOURCE layout. A sub path that lies outside one of the two trees simply excludes that tree from the comparison.

For a RESOURCE-layout channel (resourceDir = <base>/objects, mediaDir = <base>/media/public):

subPath Resources compared Media compared
(omitted) all all
objects all none
objects/de/produkte objects/de/produkte only none
media/public none all
media/public/img none media/public/img only

In the DOCUMENT_ROOT layout, resources and media share one directory, so a sub path narrows both at once.

Only the given sub tree is walked, which also makes a scoped run considerably faster than a full channel comparison.

A sub path that exists in neither channel is an error (exit code 2) — it is almost always a typo, and reporting it as "identical" would be misleading. A sub path that exists in only one of the two channels is a real difference and is reported as such. Sub paths must stay inside the channel; .. segments are rejected.

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.
--rules=<file.yaml> Rule file with accepted differences. Overrides the automatic lookup (see below).
--no-rules Ignore any rule file found next to the channels.
--float-precision=<n> Number of decimal places at which two floats still count as equal. Overrides floatPrecision from the rule file.
--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.

Rule file: recording accepted differences

Differences that have been reviewed and are known to be acceptable can be recorded in a rule file, so they stop showing up in every subsequent run. Place a channel-diff.yaml next to the channels:

# .../publications/<host>/www/channel-diff.yaml

excludes:
  # The new publisher writes an explicit "static: false" per source.
  - '**.sources.*.static'

# Focalpoints are stored with 8 decimals, but the 8th digit is not always a
# correct rounding of the previous value.
floatPrecision: 7

The file is looked up from each channel's base directory upwards, so a single file above both channels covers both — for example one www/channel-diff.yaml for www/resources and www/resources.old. The nearest file wins; .yml is accepted as well. The report lists which rule file was applied, so it is always visible why a difference is missing.

Use --rules=<file> to point at a specific file instead, or --no-rules to ignore a discovered one for a single run.

Key Meaning
excludes List of dot-notation field paths to exclude, with the same wildcards as --ignore (see below).
floatPrecision Number of decimal places at which two floats still count as equal. Omit it to compare floats strictly.

floatPrecision: n is applied as a tolerance of 0.5 × 10⁻ⁿ rather than by rounding both sides, so two values that happen to straddle a rounding boundary are not reported as a difference. Note that this makes the effective precision one place coarser than the number of digits you see: values written with 8 decimals whose last digit is not a faithful rounding of the original need floatPrecision: 7. Only float-to-float pairs use the tolerance — a float against an int or a numeric string stays a difference, since that is a type change rather than a precision issue.

Rule files are parsed as data and never executed, unlike --ignore-config.

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.*.id ignores the id field of every child of a.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 channel path, or a subPath that exists in neither channel).

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

Compare only one sub directory — here the German product pages, which also skips the media comparison entirely:

php bin/console channel:diff /path/to/A /path/to/B objects/de/produkte

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

Accept the known differences of a re-publish via a rule file, and check what is left:

cat > /path/to/www/channel-diff.yaml <<'YAML'
excludes:
  - '**.sources.*.static'
floatPrecision: 7
YAML

php bin/console channel:diff \
    /path/to/www/resources.old \
    /path/to/www/resources \
    objects/testseiten

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 null in one channel and missing in the other is treated as equal by default; use --strict-null to 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-keys to compare them by key instead.
  • Floats are compared strictly unless a floatPrecision is configured; see Rule file.
  • 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)