v1.6.0
What's Changed
Added
- Drupal coding standards via
drupal/coder(phpcs) — newphpcs.xml.distrunning theDrupal+DrupalPracticerulesets oversrc/,tests/and the module files;drupal/coderpromoted to an explicitrequire-deventry;composer phpcs/composer phpcbfaliases; CI'scomposer hygienejob gains a phpcs step. The initial sweep fixed all 172 pre-existing violations at the source — 82 viaphpcbf, 90 by hand (property@vardocblocks, empty/short doc comments filled with real descriptions, comment rewraps to ≤80 chars, missing@paramdefinitions, three genuinely-unused variable assignments dropped while keeping their side-effectful calls) — zero suppressions, nophpcs:ignoreanywhere. Full suite (347 tests) and PHPStan level 8 stay green. - Release automation:
release-stamp.yml+release.yml— the two-workflow pattern fromparisek/timber-kit. Stamp (manualworkflow_dispatchwith a semver input) validates the version + non-empty[Unreleased], runs the full test + PHPStan suite as a release gate, stamps the CHANGELOG, commits, pushes an annotated tag and cross-dispatches Release. Release (tag push / manual re-run / cross-dispatch) builds GitHub Release notes from the tag's CHANGELOG section + the(#N)squash-merge PR list between tags, and marks Latest only for the highest semver. Adapted for this repo:checkout@v4, SHA-pinnedsetup-php, the kernel-test prerequisites (gd/pdo_sqlite+scripts/dev-link-module.sh) inside the stamp gate, and no registry-sync step (consumers install viavcs— the pushed tag is immediately consumable, per RELEASING.md). Nothing fires without a manual dispatch or tag push. - AGENTS.md: TDD-non-negotiable + feature-flag doctrine — two sections ported from
parisek/timber-kitand adapted: (1) test-first discipline stated as doctrine (failing test first, bug fixes reproduce as regression tests, lowest-tier-first with the decision tree in CONTRIBUTING.md, pristine output under the existingfailOnRisky/failOnWarningPHPUnit flags); (2) behavior-changing features ship opt-in default-off —protected boolflags on the consumer-subclassedComponentBase/DisplayBase,$params-key opt-ins on container services, breaking changes allowed only behind such opt-ins, opinionated defaults expressed downstream indrupal-base/site projects rather than in library defaults. docs/adr/— Architecture Decision Records — Nygard-triad ADRs (Context / Decision / Consequences), numbered permanently, written sparingly (hard-to-reverse + surprising-without-context + real-trade-off, all three).docs/is git-ignored repo-wide with only theadr/subtree tracked, so scratch planning docs never enter history. Ships with ADR 0001 recording the deliberate no-composer.lockpolicy (drift-detection over reproducibility, contained by theplatform.phppin and the CI PHP matrix + hygiene job). Doctrine ported fromparisek/timber-kit.RELEASING.md— release doctrine — tag-driven flow adapted to this package's no-Packagist distribution (consumers install via avcsrepository entry, so a pushed annotated tag is immediately consumable): semver procedure, Conventional Commits → bump mapping table, a Public API surface definition specific to this package (service IDs + public methods,ComponentBase/DisplayBaseoverridables, the Twig function/filter surface, documented data shapes; container-wired constructor signatures explicitly excluded), and a Deprecation lifecycle (docblock-only@deprecated, no runtime notices in request-serving paths, ≥ one MINOR grace period, live deprecations table — currently empty). README gains a short## Releasingsection pointing at it. Ported fromparisek/timber-kitand adapted.- Status-report warning when
menu.language_tree_manipulatoris missing on a multilingual site (#90) — since the MenuTreeBuilder extraction, the language manipulator (shipped by the Drupal core patch from #2466553) is an optional dependency and menu language filtering silently no-ops when the service is absent. Newhook_requirements()runtime check incustom_components.installmakes the gap visible on/admin/reports/status: on a multilingual site it reports Available (REQUIREMENT_OK) when the service exists and aREQUIREMENT_WARNINGwith a link to the core issue when it doesn't; monolingual sites get no entry (filtering is irrelevant there). Three kernel tests pin the contract: non-runtime phases report nothing, monolingual sites report nothing, multilingual sites without the service warn with the service name in the description. - Auto-typography translation helpers
_xt/__t/_nt/_nxt(#87) — typography-aware twins of the existing_x/__/_n/_nxTwig functions. Each translates first, then pipes the result through thetypographyfilter, so editors get curly quotes / non-breaking spaces / dewidowing on translated UI strings with a one-character opt-in (_x(…)|typography→_xt(…)). Registered onTwigExtensionwithneeds_environment(thetypographyfilter is resolved from the environment at call time, so this extension stays decoupled from the siblingTypographyExtensionthat provides it) andis_safe: ['html'](mirrors the filter's own safety flag — no double-escaping of the markup it returns). Signatures match the WordPress originals 1:1 so the same templates render acrossparisek/styleguide(#21),parisek/timber-kit(#42) and Drupal:_xt($text, $context, $domain),__t($text, $domain),_nt($single, $plural, $number, $domain),_nxt($single, $plural, $number, $context, $domain). The$domainargument has no Drupal analogue (translations are keyed by langcode, not text domain), so it is accepted for cross-CMS parity and otherwise ignored;__t/_ntcarry no context (matching WP), while_xt/_nxtforward it viat()/formatPlural()options. If thetypographyfilter is absent the helpers degrade to a plain translation rather than throwing. Six unit tests assert translate-then-typography compose order, context forwarding, plural selection, the no-filter fallback, and end-to-endis_safe(no double-escaping) through a real Twig render. Drupal side of parisek/styleguide#21.
Changed
- PHPStan raised to level 8 (was 5) — the max-rigor level
parisek/timber-kitruns. The 264 pre-existing findings are grandfathered in a regeneratedphpstan-baseline.neon(dominant categories: missing param/return typehints ~137, missing iterable value types ~57 — routine follow-up, entry by entry); new code baselines nothing and is held to level 8. The 8call to undefined method object::…findings were fixed, not baselined —TaxonomyTreeBuildernow narrowsloadTree(..., TRUE)results with aninstanceof TermInterfaceguard andMenuActiveTrailResolverguardscreateInstance()results withinstanceof MenuLinkInterface— so that whole error class stays live for future typos instead of hiding in the baseline.mglaman/phpstan-drupalwas already active viaphpstan/extension-installerauto-discovery (no wiring change needed). - CI: PHP 8.3/8.4 matrix + composer-hygiene job — the test job now runs on a
fail-fast: falsematrix of PHP 8.3 and 8.4 (coverage + the ratchet threshold stay on the 8.3 floor leg only; the 8.4 leg proves the suite passes on the newer runtime before consumers hit it). A separatecomposer hygienejob runscomposer validate --strict,composer audit --abandoned=report(security advisories fail the job, abandoned transitive packages only report) andcomposer normalize --dry-run;ergebnis/composer-normalizejoinsrequire-dev+allow-pluginsandcomposer.jsonis normalized once to establish the canonical shape. Pattern ported fromparisek/timber-kittests.yml. - CI: Conventional-Commits lint on PR titles — new
commitlint.ymlworkflow (amannn/action-semantic-pull-request, SHA-pinned to the v5 line) gates every PR title against thefeat/fix/docs/chore/refactor/perf/test/ci/build/reverttaxonomy that AGENTS.md documents but nothing previously enforced. PRs squash-merge with the title as the commit subject, so the title is what the future release bump-mapping reads. Scope optional. Pattern ported fromparisek/timber-kit. composer.json:scriptsaliases +config.platform.phppin —composer test/test:unit/test:kernel/phpstanaliases so contributors and docs stop spellingvendor/bin/...paths, andconfig.platform.php: 8.3.0so dependency resolution targets the package's PHP floor even on newer dev machines (the repo deliberately ships nocomposer.lock, so everycomposer install/updatere-resolves — the pin keeps that resolution honest against the>=8.3requirement). Pattern ported fromparisek/timber-kit.- Distribution trimmed via
.gitattributesexport-ignore—composer require parisek/custom-componentspreviously shipped the full tracked tree (tests/, .github/, .ddev/, scripts/, CHANGELOG, AGENTS/CLAUDE/CONTRIBUTING, phpstan/phpunit configs) into consumers'vendor/because the repo had no tracked.gitattributes— the local one is generated bydrupal/core-composer-scaffoldand was.gitignored. Now a tracked.gitattributesexport-ignores everything development-only, so the dist archive carries just the module files (custom_components.*),src/,templates/,composer.json,LICENSE,README.md. Scaffold generation of the file is disabled viaextra.drupal-scaffold.file-mappingso it no longer collides with the tracked copy. Pattern ported fromparisek/timber-kit. - Issue references stripped from source comments — the builder docblocks (
MenuTreeBuilder,TaxonomyTreeBuilder,MediaArrayBuilder),TwigExtension::getResizer()and thecustom_components.services.ymlresizer note referenced repo issue numbers (#6,#44), violating the AGENTS.md comment doctrine ("Don't reference PRs, issues, or call sites in code; those belong in commit messages / CHANGELOG"). The WHY content stays; only the issue-number pointers are gone. Version references (removed in v1.4.0) remain — they resolve against this CHANGELOG, not the issue tracker. - CI:
symfony/runtimeadded toallow-plugins,claude-code-reviewworkflow removed — the repo intentionally ships nocomposer.lock, so CI resolves fresh dependencies every run; the current resolution (Drupal core 11.4.x / Symfony 7.4.x) transitively pullssymfony/runtime, whose Composer plugin was not inconfig.allow-plugins, abortingcomposer installbefore any test ran. Allow-listed (official Symfony package). Theclaude-code-review.ymlworkflow is removed — it failed on missing auth secrets and review runs on demand instead; the interactiveclaude.ymlworkflow stays.
Fixed
MediaArrayBuilder::buildRemoteVideo()no-resolver fallback resolves the wrong field (#89) — the fallback calledbuildImage($media), which reads the media's own source field viagetSourceFieldValue(); for an oembed remote video that is the video URL string, not a file ID, soFile::load()failed and theimagekey came back empty instead of thefield_media_imagethumbnail. The fallback now readsfield_media_image's File references directly via a new protectedbuildImageField()helper that mirrorsEntityHelper::getImageField()'s return shape (single item unwraps, multiple items return a list) minus translation handling — which is exactly what the docblock always claimed the fallback did. Two new kernel tests pin the behavior with real entities (afile-source media type whose source file deliberately differs fromfield_media_image): the fallback returns the thumbnail (not the source file), and its output is identical to theEntityHelper::getImageFieldresolver path. Consumers going through theEntityHelperfacade were never affected (it always passes the resolver); only directcustom_components.media_array_builderconsumers hit the bug.
Pull Requests
- #85 — docs(readme): drop Packagist + PHP-version badges
- #86 — docs: drop Packagist-dependent install instructions + URL
- #88 — feat(twig): auto-typography translation helpers _xt/__t/_nt/_nxt (#87)
- #91 — fix(media): resolve field_media_image in buildRemoteVideo no-resolver fallback (#89)
- #92 — feat(install): status-report warning when language tree manipulator is missing (#90)
- #93 — chore: strip issue references from source comments
- #94 — chore(ci): allow symfony/runtime composer plugin, drop claude-code-review workflow
- #95 — chore(dist): trim composer dist archive via tracked .gitattributes export-ignore
- #96 — chore(composer): add scripts aliases and platform.php pin
- #97 — ci: PHP 8.3/8.4 matrix + composer hygiene job (audit, normalize)
- #98 — ci: enforce Conventional Commits on PR titles
- #99 — docs: add RELEASING.md — semver, public API surface, deprecation lifecycle
- #100 — docs: add ADR directory with doctrine and ADR 0001 (no composer.lock)
- #101 — docs(agents): add TDD-non-negotiable and feature-flag doctrine
- #102 — chore(phpstan): raise level 5 -> 8 with regenerated baseline
- #103 — ci: add release-stamp and release workflows
- #104 — chore(phpcs): adopt drupal/coder with zero-suppression initial sweep
Full Changelog: v1.5.0...v1.6.0