Skip to content

Releases: parisek/drupal-kit

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 11:17

What's Changed

Added

  • The module ships its own interface translations (#123) — every string is wrapped in t() or TranslatableMarkup, so it was translatable in principle. In practice nobody translated it and every site showed English, including the message above an unpublished page on a site whose default language is Czech, and the abbreviated weekday names in EntityHelper::getOfficeHours(), which are user-facing content on a contact page rather than an admin screen.

    drupal_kit.info.yml now declares the module as its own translation project, and translations/ carries cs.po, sk.po, de.po and pl.po covering the 28 strings the module owns. Drupal's locale module picks them up on drush locale:update.

    The server pattern ends %language.po, with no closing percent. %language is the whole placeholder — %language%.po resolves to cs%.po, a file that does not exist, and the import then reports the project as checked while silently importing nothing.

    Seven strings carry ['context' => 'Abbreviated weekday'] and their entries carry the matching msgctxt, so they do not collide with the unqualified Mon that core already translates.

    Three generic words the module emits — Advanced, Available, Not available — are deliberately not translated here. Locale stores a string globally by source and context rather than per project, and core emits the same three untagged, so shipping a translation for them would overwrite core's on every import and flip back on the next core update. Core already translates them.

    The install path is not assumed. drupal_kit_locale_translation_projects_alter() rebuilds the server pattern from the extension list, so a consumer whose installer-paths put the module outside modules/contrib still gets its translations instead of a project reported as checked with nothing imported.

    The scanner that guards the catalogue is held to account by its own test. It unescapes per quote style — a single-quoted PHP literal knows only \' and \\, so running the double-quoted rules over one turns a literal backslash-n into a newline and silently renames the string — and it reads a context key written either way. A shape it cannot read is a false green: the parity check reports a complete catalogue while the string ships untranslated.

    A consumer still overrides any string in Admin → Translate interface. A shipped translation is a default, not a lock.

  • A Scheduler publish or unpublish date is announced on the entity page (#121) — drupal_kit_page_attachments_alter() already says This page has not been published yet, only privileged users can see it. When Scheduler holds that page for a date, the message stopped short: it said the content was invisible, not that a date was set and cron would act on it. An editor could not tell a planned article from a forgotten draft without opening the edit form. Scheduler names the date once, in the message after the entity form is saved, so an editor who opens the page a week later saw nothing.

    The hook now adds Scheduler publishes this content on @Date. and Scheduler unpublishes this content on @Date., after the existing message so the two read as one thought.

    Nothing changes on a site without Scheduler, and nothing changes on content that carries no date — drupal/scheduler is a suggest, never a dependency. The new drupal_kit.schedule_announcer service holds the logic and returns text; the caller decides where it goes and the theme decides how it looks, which is the split the existing message already had.

    The viewer must pass access('update') on the entity. An unpublish_on date leaves the entity published, so an anonymous visitor reaches the page, and the schedule is editorial information. The check is edit access rather than a permission name because Scheduler names its permission per entity type, and this hook serves nodes, taxonomy terms and commerce products alike.

    A date left on a bundle whose scheduling was switched off is not announced. Scheduler's base fields belong to a whole entity type rather than to the bundles that opt in, so such a value sits in the field forever and cron never acts on it.

    Scheduler's own view scheduled <type> permission also opens the message, alongside edit access. That permission exists for a read-only reviewer role, which by definition has no edit rights.

    Found on htdvere, where a future-dated article had been publishing itself immediately.

Pull Requests

  • #122 — feat(messages): announce a Scheduler publish or unpublish date
  • #124 — feat(i18n): ship interface translations for cs, sk, de and pl

Full Changelog: v2.1.2...v2.2.0

v2.1.2

Choose a tag to compare

@github-actions github-actions released this 09 Sep 05:22

What's Changed

Fixed

  • ViteManifest no longer asks for a theme named core (#118) — Drupal runs hook_library_info_alter() for the core pseudo-extension, which is neither a module nor a theme. extensionRoot() decided the type as module-else-theme, so every library-discovery cache rebuild asked the resolver for a theme called core. The resolver reports the miss with trigger_error(), a warning rather than an exception, so the catch (\Throwable) beside it never ran and the NULL it returned reached dirname(). Two log entries per rebuild, and a red error box on a site with error display on.

    extensionRoot() now returns NULL for core before deciding a type. LibraryDiscoveryParser::buildByExtension() carries the same module-else-theme rule and the same single exception, and every other extension name reaches core's own getPath() call before this hook runs, so mirroring core covers the whole class of names that can arrive here.

    Found on htdvere. The entries follow the cache rebuild rather than any route, so warming the cache with one page moves the warning to the next one and it reads as route-specific until you look twice.

Pull Requests

  • #119 — fix(vite): stop asking the path resolver for a theme named core

Full Changelog: v2.1.1...v2.1.2

v2.1.1

Choose a tag to compare

@github-actions github-actions released this 01 Sep 17:15

What's Changed

Fixed

  • |typography no longer fatals on a number or a boolean — the upstream filter's signature is Stringable|string|null, and only arrays were guarded, so an int reached it and raised a TypeError: a 500 on the whole page rather than a filter that declined. Ints, floats and booleans now pass through untouched, alongside the render arrays that already did. Passing through rather than casting is deliberate — typography is for prose, a number gains nothing from a non-breaking space, and the value keeps its type for arithmetic or a chained filter further down the template.

    The list stops there on purpose, and an object still reaches upstream. This filter is registered is_safe => ['html'], so what it returns is printed unescaped; passing an arbitrary object through would extend that promise to a value the extension never inspected, and Drupal prints an object carrying a toString() method raw. An object reaching a typography filter is a template defect and stays loud.

    Found while migrating a site off its local custom_components copy, whose filter cast silently: a branch count piped through |typography rendered there and took two pages down here. Every consumer moving to this package is one such call away from the same page, so the tolerance belongs in the filter rather than in a rule each site has to remember.

Pull Requests

  • #117 — fix(typography): pass non-string values through instead of fataling

Full Changelog: v2.1.0...v2.1.1

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 14:51

What's Changed

Added

  • ViteManifest resolves a content-hashed JS entry through .vite/manifest.json. A library opts in with vite_entry (the manifest key, or true for the default src/js/script.js) and keeps declaring its real dist path; hook_library_info_alter() swaps in the hashed filename when a usable manifest sits beside it. Ported from StarterBase::themeScriptFile() in parisek/timber-kit.

    Why the entry needs a hash: lazy chunks always carried one, the entry did not, because *.libraries.yml names it by a fixed path and cache-busting came from Drupal's ?v= instead. That covers the reference in the HTML but not the one the bundler emits inside a chunk — a module reachable from the entry graph and from a lazy chunk is hoisted into the entry, and the chunk imports it back as ./script.js, unhashed and unqueried. Measured on the WordPress sibling (sloneek, 2026-08-17): 5 of 52 chunks imported the entry, max-age was 31536000, and a form silently stopped rendering with does not provide an export named 'n' — minified export names are positions in a table, so a stale entry can also answer with the wrong binding and no error.

    This closes the correctness defect, not the double fetch. JsCollectionRenderer appends a query to every unaggregated asset unconditionally, so the tag's URL and a chunk's own import remain two module identities — now with identical content. See ADR 0002, which also records why drupal/vite was not taken.

    vite_entry also accepts a map of asset path to manifest key, which a library declaring more than one JS asset needs; a bare key covering several assets is refused with a logged warning instead of rewriting one of them.

    Rewrites keep their declared position (Drupal emits a library's JS in array order, and an unset-and-append moved the rewritten asset last), and a map whose entries resolve to one built filename is refused whole rather than dropping an asset.

    Two constraints worth stating: only the asset whose filename matches the key's is rewritten (the property names one entry, and applying the key to every JS file made the rewrites overwrite each other), and the resolved name is cached with Drupal's library info, so a deploy shipping new assets must run drush cr.

    Backwards compatible by construction: no vite_entry, or no manifest, and the declared path is served unchanged. Four guards on the manifest value — resolvable inside the built directory, free of URL-significant characters, .js suffix, present on disk — each answering a reproduction rather than a hypothesis and each pinned by a mutation-verified test. Every rejection also logs: an opt-in that cannot do its job says so, instead of silently serving a declared path that may 404.

||||||| 682a6f1

Changed

  • |typography now typesets per languageTypographyExtension hands the upstream Parisek\Twig\TypographyExtension a locale resolver, so the languages: tables shipped by parisek/twig-typography ^1.3 (quote style, dash convention, single-character word spacing, …) actually apply. Without a resolver the upstream localeCandidates() returns [] and that whole layer is inert: only the language-neutral house defaults ever ran, so Czech content was typeset with English curled quotes (“ahoj”, not „ahoj“) and lost the non-breaking space after single-letter prepositions that Czech typography requires. Ported from StarterBase::typography_locale_resolver() in parisek/timber-kit, the WordPress-side sibling.

    The resolver reports the content language (LanguageInterface::TYPE_CONTENT), not the interface language. The two diverge exactly where it matters — an editor whose account language is Czech previewing an English node would otherwise get Czech typography applied to English prose. Drupal falls back to the interface language when content language negotiation is not configured, so monolingual sites are unaffected. Same distinction timber-kit documents for get_locale() vs determine_locale().

    FilterTypography now forwards its own $langcode. Drupal hands a text filter the language of the exact text being processed, and process() was discarding it — harmless while no language layer existed, wrong the moment one did: the filter would have typeset with the negotiated content language instead, which differs on mixed-language views, an explicitly rendered translation, mail and cron. applyTypography() takes an optional fourth argument for it; an empty langcode falls back to negotiation. A pinned language gets its own cache entry, so the negotiated path is unaffected.

    Note for direct instantiators: the constructor takes a fourth required argument. Container consumers are unaffected; the release doctrine excludes container-wired constructor signatures from the public API.

    Exposed as the overridable protected TypographyExtension::localeResolver() for sites whose language detection does not go through language_manager. The closure is evaluated per applyTypography() call rather than once at construction, so one cached upstream instance still serves every language in a request and the per-theme cache needs no language component.

    This changes rendered output on multilingual sites and on any monolingual site whose language has a languages: entry — review pages before deploying. Two regression tests pin the contract (Czech low-9 quotes reach the output; one instance typesets Czech and English differently across consecutive calls); both fail against the pre-fix constructor call.

  • Bumped the parisek/twig-typography floor from ^1.2 to ^1.3 — the languages: layer this change depends on does not exist before 1.3, where the resolver argument would be accepted and silently ignored.

  • Docs: Packagist is the distribution channel — the package is now published as parisek/drupal-kit on Packagist with the GitHub auto-sync webhook. README gains Packagist version + downloads badges and an Installation section (composer require parisek/drupal-kit); RELEASING.md drops the vcs repository entry instructions in favour of a Packagist sync-verification step and documents that the auto-created GitHub release must not be duplicated manually. Packagist serves every tag (including 1.x) under the canonical package name, so the vcs route is obsolete for all versions.

Fixed

  • The sitemap no longer lists the front page twicesystem.site points page.front at a node, and simple_sitemap listed that page as both / and the node's own URL. Where the redirect module's route normalizer is enabled the second answers 301, so the file handed crawlers a redirect to a page it already contained. drupal_kit_simple_sitemap_links_alter() drops the duplicate and keeps /, which is what Drupal itself declares canonical on the front page.

    page.front is translatable, so the filter is per language. A site can point each language at its own node; asking system.site once and applying that answer to every link deleted the node matching the generation-time language while leaving the other language's duplicate in place. The hook now builds a langcode → front-page map, judges each link by its own langcode, and prunes alternate_urls per language — a surviving link must not re-advertise the withheld URL through its hreflang block.

    Which entry survives is decided by what the page declares canonical, not by preference. Metatag ships canonical_url: '[site:url]' for the front page as its own default, and a site that disables that group falls back to the global group's [current-page:url-with-query:…], which on / resolves to / as well — so on any consumer running Metatag, keeping / agrees with the page. On a site without Metatag core declares the node's alias instead, and the two disagree; that limitation is recorded in ADR 0003 rather than argued away.

    The hook is unconditional, against AGENTS.md's opt-in default. It only runs where simple_sitemap is installed and only fires on the configured front page, so "always on" means "on exactly where the defect is"; and a flag defaulting to off would not reach the nineteen sites that have the defect and have not noticed. Reasoning, and the narrow precedent it sets, in ADR 0003.

  • merge_resizer() supports optional per-viewport images — ported from timber-kit's StarterBase::twig_merge_resizer() after the OPOP page-header case (optional mobile mascot variant) exposed two defects in the original implementation. (1) Empty groups are now dropped before the last-group detection: an unfilled optional image field makes Resizer return [], and keeping it as the "last" group filtered the remaining (desktop) group down to media-qualified variants — producing a <picture> with no unconditional <img> fallback at all. (2) The non-last-group filter switches isset($image['media'])!empty(...): Resizer sets media to '' for tuples without a breakpoint (and omits the key on the appended original image), so isset() leaked fallback-shaped desktop entries into the merged set ahead of the mobile entries, shadowing the mobile image on every viewport. Net effect: merge_resizer(desktop, mobile) keeps one call shape whether or not the optional image is filled — no {% if %} branching in templates. Two regression unit tests pin the contract (empty-group drop preserves the fallback; empty-media desktop entries are filtered when a mobile group follows). Consumers with a hand-mirrored copy in their theme's static/index.php (styleguide runs without Drupal) must apply the same change — done in opopcz and drupal-base.

  • PHPStan drift: DependencySerializationTrait vs promoted private readonly plugin propertiesFilterLinks and FilterTypography injected their services as constructor-promoted private readonly properties; FilterBase carries DependencySerializationTrait, which supports neit...

Read more

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 08 Jul 10:32

What's Changed

Changed

  • BREAKING: module machine name renamed custom_componentsdrupal_kit — completes the package rename inside Drupal: module files (drupal_kit.info.yml / .module / .install / .services.yml), hook implementations, the PHP namespace (Drupal\custom_componentsDrupal\drupal_kit incl. tests), every service ID (custom_components.entity_helperdrupal_kit.entity_helper, …), extra.installer-name, and the dev symlink target. The module's human-readable name changes from "Component: Global" to "Drupal Kit" with a real description. Consumers moving from a site-local custom_components copy uninstall it and composer require parisek/drupal-kit + drush en drupal_kit; templates and site code referencing the old namespace or service IDs must be updated.
  • BREAKING: package renamed parisek/custom-componentsparisek/drupal-kit — the GitHub repository moved to parisek/drupal-kit (old URLs redirect) and composer.json name follows, mirroring the WordPress-side parisek/timber-kit naming. Existing installs must update their composer.json require entry (and, until the package lands on Packagist, the vcs repository URL). The Drupal module machine name stays custom_components in this change; it is renamed to drupal_kit separately before v2.0.0.
  • BREAKING: Composer type changed drupal-custom-moduledrupal-module — the package is shared infrastructure distributed to multiple projects, not a site-local module, so composer/installers now places it in web/modules/contrib/ instead of web/modules/custom/. Matches where the local dev symlink (scripts/dev-link-module.sh) and the kernel-test bootstrap already expected it.
  • composer.json description rewritten — from the placeholder "Provides functionality for components." to a sentence that describes the package for the Packagist search listing.
  • README refreshed after 1.6.0 — PHPStan badge 5 → 8, CI badge points at the renamed repo, the Services list gains the three builders (media_array_builder, menu_tree_builder, taxonomy_tree_builder) and the _xt / __t / _nt / _nxt translation helpers, the core-patch note mentions the new status-report warning, and Related projects links parisek/timber-kit.

Pull Requests

  • #105 — chore!: rename package to parisek/drupal-kit
  • #106 — refactor!: rename module machine name to drupal_kit

Full Changelog: v1.6.0...v2.0.0

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 07 Jul 20:12

What's Changed

Added

  • Drupal coding standards via drupal/coder (phpcs) — new phpcs.xml.dist running the Drupal + DrupalPractice rulesets over src/, tests/ and the module files; drupal/coder promoted to an explicit require-dev entry; composer phpcs / composer phpcbf aliases; CI's composer hygiene job gains a phpcs step. The initial sweep fixed all 172 pre-existing violations at the source — 82 via phpcbf, 90 by hand (property @var docblocks, empty/short doc comments filled with real descriptions, comment rewraps to ≤80 chars, missing @param definitions, three genuinely-unused variable assignments dropped while keeping their side-effectful calls) — zero suppressions, no phpcs:ignore anywhere. Full suite (347 tests) and PHPStan level 8 stay green.
  • Release automation: release-stamp.yml + release.yml — the two-workflow pattern from parisek/timber-kit. Stamp (manual workflow_dispatch with 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-pinned setup-php, the kernel-test prerequisites (gd/pdo_sqlite + scripts/dev-link-module.sh) inside the stamp gate, and no registry-sync step (consumers install via vcs — 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-kit and 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 existing failOnRisky/failOnWarning PHPUnit flags); (2) behavior-changing features ship opt-in default-off — protected bool flags on the consumer-subclassed ComponentBase/DisplayBase, $params-key opt-ins on container services, breaking changes allowed only behind such opt-ins, opinionated defaults expressed downstream in drupal-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 the adr/ subtree tracked, so scratch planning docs never enter history. Ships with ADR 0001 recording the deliberate no-composer.lock policy (drift-detection over reproducibility, contained by the platform.php pin and the CI PHP matrix + hygiene job). Doctrine ported from parisek/timber-kit.
  • RELEASING.md — release doctrine — tag-driven flow adapted to this package's no-Packagist distribution (consumers install via a vcs repository 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/DisplayBase overridables, 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 ## Releasing section pointing at it. Ported from parisek/timber-kit and adapted.
  • Status-report warning when menu.language_tree_manipulator is 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. New hook_requirements() runtime check in custom_components.install makes the gap visible on /admin/reports/status: on a multilingual site it reports Available (REQUIREMENT_OK) when the service exists and a REQUIREMENT_WARNING with 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 / _nx Twig functions. Each translates first, then pipes the result through the typography filter, so editors get curly quotes / non-breaking spaces / dewidowing on translated UI strings with a one-character opt-in (_x(…)|typography_xt(…)). Registered on TwigExtension with needs_environment (the typography filter is resolved from the environment at call time, so this extension stays decoupled from the sibling TypographyExtension that provides it) and is_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 across parisek/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 $domain argument has no Drupal analogue (translations are keyed by langcode, not text domain), so it is accepted for cross-CMS parity and otherwise ignored; __t / _nt carry no context (matching WP), while _xt / _nxt forward it via t() / formatPlural() options. If the typography filter 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-end is_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-kit runs. The 264 pre-existing findings are grandfathered in a regenerated phpstan-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 8 call to undefined method object::… findings were fixed, not baselinedTaxonomyTreeBuilder now narrows loadTree(..., TRUE) results with an instanceof TermInterface guard and MenuActiveTrailResolver guards createInstance() results with instanceof MenuLinkInterface — so that whole error class stays live for future typos instead of hiding in the baseline. mglaman/phpstan-drupal was already active via phpstan/extension-installer auto-discovery (no wiring change needed).
  • CI: PHP 8.3/8.4 matrix + composer-hygiene job — the test job now runs on a fail-fast: false matrix 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 separate composer hygiene job runs composer validate --strict, composer audit --abandoned=report (security advisories fail the job, abandoned transitive packages only report) and composer normalize --dry-run; ergebnis/composer-normalize joins require-dev + allow-plugins and composer.json is normalized once to establish the canonical shape. Pattern ported from parisek/timber-kit tests.yml.
  • CI: Conventional-Commits lint on PR titles — new commitlint.yml workflow (amannn/action-semantic-pull-request, SHA-pinned to the v5 line) gates every PR title against the feat/fix/docs/chore/refactor/perf/test/ci/build/revert taxonomy 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 from parisek/timber-kit.
  • composer.json: scripts aliases + config.platform.php pincomposer test / test:unit / test:kernel / phpstan aliases so contributors and docs stop spelling vendor/bin/... paths, and config.platform.php: 8.3.0 so dependency resolution targets the package's PHP floor even on newer dev machines (the repo deliberately ships no composer.lock, so every composer install/update re-resolves — the pin keeps that resolution honest against the >=8.3 requirement). Pattern ported from parisek/timber-kit.
  • Distribution trimmed via .gitattributes export-ignorecomposer require parisek/custom-components previously 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 by drupal/core-composer-scaffold and was .gitignored. Now a tracked .gitattributes export-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 via extra.drupal-scaffold.file-mapping so it no longer collides with the tracked copy. Pattern ported from parisek/timber-kit.
  • Issue references stripped from source comments — the builder docblocks (MenuTreeBuilder, TaxonomyTreeBuilder, MediaArrayBuilder), TwigExtension::getResizer() and the custom_components.services.yml resizer note referenced repo issue numbers (#6, #44), v...
Read more

v1.5.0 — Coverage push to 78.42 %

Choose a tag to compare

@parisek parisek released this 28 May 15:40
494db78

Closes the v1.5.0 coverage roadmap (#55). Pushes line coverage from 53.71 % → 78.42 % (+24.71 pp) across 19 merged PRs. No new public-API features; no behaviour changes.

Headline metrics

  • Line coverage: 78.42 % (1141 / 1455 statements, measured under PHP 8.3 / DDEV + xdebug).
  • Tests: 282 → 336 (+54).
  • CI MIN_COVERAGE floor raised 53 → 78.

What changed (high level)

  • Tier 1 (Public API contract, #56#59): MediaArrayBuilder::buildSvg, TwigExtension four missing filters/functions, EntityHelper nine generateMedia* / generateFile* dispatch delegates, four EntityHelper public hot-paths (addCacheTags, getMenuField, getSelectFieldOptions, getSvgViewBoxDimensions).
  • Tier 2 (Branch coverage, #60#63): EntityHelper private dispatch + mapping helpers, Resizer post-#44 audit + effect-builder coverage, MenuTreeBuilder renderLinks deep paths, MenuActiveTrailResolver + TaxonomyTreeBuilder helper coverage.
  • Tier 3 (Activations + cleanups, #64#65): drupal/commerce + drupal/address added to require-dev (activating two contrib-gated tests); TaxonomyTermController + its entity.taxonomy_term.canonical route override deleted (Drupal 6/7 legacy carry-over made obsolete by core's _entity_view: 'taxonomy_term.full' since Drupal 8).
  • Fixed (#70): EntityHelper::getSelectFieldOptions honours the $langcode parameter — a stray $config = $originalConfig->get(); was overwriting the merged-with-language-overrides config. Surfaced during the #69 Copilot review.
  • Docs (#76): New AGENTS.md + CLAUDE.md shared agent-config files (mirrors the parisek/styleguide pattern). Documents the DDEV-first commands, kernel-test module-list gotchas, PR + review-thread workflow.
  • Metric-attribution fixes (#79#82): Two recurring patterns identified and fixed across multiple classes — facade-as-default-class indirection and strict @covers ::publicMethod filtering. About 7 pp of the total move came from these without writing new tests, just adding the missing annotations / direct test files.
  • Final push (#83): Eight EntityHelper::dispatchByFieldType case lines via per-field-type formatField tests.

Per-class final state

Class Lines
ComponentBase, all five Plugin\Filter\*, Routing\RouteSubscriber 100 %
Services\MenuActiveTrailResolver 97.50 %
TwigExtension 96.75 %
Twig\TypographyExtension 94.74 %
Services\Resizer 90.45 %
Services\MediaArrayBuilder 90.30 %
Services\MenuTreeBuilder 89.33 %
Services\TaxonomyTreeBuilder 85.96 %
Services\EntityHelper 62.48 %
DisplayBase 13.33 %

Three contrib-gated tests still skip (office_hours / geofield / webform — modules not in require-dev). Detailed Deferred to v1.6.0+ section in CHANGELOG.md.

Upgrade

composer update parisek/custom-components

Drop-in. No code changes required on the consumer side.

v1.4.0 — quality + forward-compat hardening

Choose a tag to compare

@parisek parisek released this 26 May 13:11

Quality + forward-compat release. No behavioural changes for callers — focus is hardening of the v1.3.0 surface, static-analysis ratcheting, and closing deferred test gaps.

Highlights

  • PHPStan 1.x → 2.x at level 5 with mglaman/phpstan-drupal (#45)
  • menu.language_tree_manipulator made optional — no longer hard-depends on the contrib-or-patched service (#43)
  • Resizer reduced to static utility — service registration dropped, Resizer::resizer() called directly (#44)
  • Deferred kernel coverage closed — remote-video URL extraction, Lottie file path, DisplayBase __call delegation, contrib-gated getters behind markTestSkipped (#47)
  • DDEV pinned as canonical local environment — PHP 8.3 matches CI + production; ddev coverage custom command bundled
  • README polish — 6 badges, PORTA caps, Drupal links (#50)
  • Brittle entity_reference walker tests removed (#46)

Coverage

53.71% (789 / 1469 lines), measured under PHP 8.3 / DDEV with xdebug coverage driver. MIN_COVERAGE floor unchanged at 53 — v1.4.0 is hardening, not a coverage push. See CHANGELOG.md for the per-class breakdown that seeds v1.5.0 planning.

BC

None. No public-API changes. The only consumer-visible change is the documented Resizer call shape, which was already a static-style call in practice (the service had no state).

v1.3.0 — Coverage 45% → 53%

Choose a tag to compare

@parisek parisek released this 26 May 09:57
e8443f8

Closes #30 (meta).

Highlights

  • Resizer kernel test suite (#31): first kernel-level coverage of the image-resizer service.
  • EntityHelper image/file/media field-getter kernel tests (#32): getImageField, getFileField, getMediaField with cache-tag bubble assertion.
  • formatField polymorphic dispatch kernel tests (#34): one test per field-type branch verifies the dispatch wiring.
  • ComponentBase form API kernel tests (#35): real FormState validates the build + submit cycle.

Stats

  • 262 tests (+34), 1379 assertions (+403)
  • Coverage: 53.03% (+7.15% over v1.2.0; floor 53)
  • PHPStan: clean at level 1
  • 5 PRs merged: #37, #38, #39, #40, #41

Honest accounting

v1.3.0 spec called for 65%. We landed at 53.03%. The 1:5 test:coverage ratio we observed in v1.2.0 turned out optimistic for the harder paths — Resizer's effect chains and EntityHelper's polymorphic dispatch each cost more than estimated. The remaining 12% to 65% (and the 27% to the original 80%) move to v1.4.0 + v1.5.0.

Deferred to v1.4.0

  • buildRemoteVideo (oembed) + buildLottie (#33)
  • DisplayBase form API
  • Contrib-gated getters (getOfficeHoursField, getAddressField, getGeoField, getPriceField, getWebformField)
  • MenuActiveTrailResolver remaining 50% paths
  • TypographyExtension::applyTypography variants
  • MenuTreeBuilder::renderLinks deep paths

v1.2.0 — Complete the test pyramid + DI cleanup

Choose a tag to compare

@parisek parisek released this 26 May 08:51
380c414

Closes #17 (meta).

Highlights

  • MediaArrayBuilder kernel safety net (#18): 18 new kernel tests covering buildImage / buildFileImage / buildVideo / buildDocument / buildImageLink / buildFileImageLink / getSvgViewBoxDimensions against real Drupal media + file + image entities.
  • EntityHelper field-getter kernel safety net (#19): 18 new kernel tests covering text/textarea/select/double/boolean/date/daterange/link/term/entity_reference getters.
  • FilterLinks DI (#20): request_stack via ContainerFactoryPluginInterface. \Drupal::request() static call gone.
  • TwigExtension DI (#21): string_translation via constructor. \Drupal::translation() static call gone.
  • scripts/dev-link-module.sh (#22): single source of truth for local + CI module wiring. find -maxdepth 1 picks up any new top-level file automatically.
  • Coverage floor raised to 45% (current observed: 45.88%).

Stats

  • 228 tests, 976 assertions
  • Coverage: 45.88% (+13.55% over v1.1.0)
  • PHPStan: clean at level 1
  • 6 PRs merged: #24, #25, #26, #27, #28, #29

Deferred to v1.3.0

  • buildRemoteVideo kernel coverage (needs oembed bundle + network mocking)
  • buildLottie + getImageField / getFileField / getMediaField kernel coverage
  • Resizer kernel coverage (177-line service with 0% kernel cover)
  • contrib-gated getters (getOfficeHoursField, getAddressField, getGeoField, getPriceField, getWebformField)
  • 80% coverage target — requires the above