Skip to content

Style

In addition to the standard Drupal coding standards, this module has a handful of project-specific rules. Everything below applies to both branches (2.x and 3.x) unless stated otherwise.

American English

All written language — docblocks, inline comments, commit messages, README copy, config descriptions, log messages, exception text — must be American English. The CI pipeline runs cspell, which is configured for American spellings; British spellings (colour, behaviour, organise, customisation, summarise, …) cause pipeline failures.

Watch for:

  • -ise-ize
  • -our-or
  • -re-er
  • -yse-yze
  • doubled-l past tense (travelled) → single-l (traveled)

PHP attributes, not annotations

For all plugin types defined or extended here, use PHP 8 attributes (#[RecordType(...)]) rather than Doctrine annotations (@RecordType(...)). Attribute classes live in src/Attribute/, never in src/Annotation/.

This is why core_version_requirement is ^10.3 || ^11 in dns.info.yml — 10.3 is the first widely-stable release for custom plugin-type attribute discovery.

The same does not apply to entity types, which still use the @ContentEntityType annotation in this module — broader compat surface, no functional gain from switching.

Plugin manager service injection — protected, not private readonly

Drupal forms (and any class composing DependencySerializationTrait via an ancestor — controllers, list builders, plugins of certain types) get serialized and woken from form-state cache between the page load and the submit. The trait's __wakeup re-injects services by writing back to the property.

__wakeup runs in the trait's host class scope (typically a parent of yours), and from that scope it can't write to a private property of a subclass. The write silently fails, the property stays uninitialized, and the next read raises "Typed property … must not be accessed before initialization".

The fix is consistent across this module: declare service properties as protected (no readonly) in the class body, and assign them in the constructor body.

// ❌ Broken: __wakeup can't restore this from an ancestor scope.
public function __construct(
  EntityRepositoryInterface $entity_repository,

  private readonly PluginManagerInterface $recordTypeManager,
) { … }

// ✓ Works: protected, non-readonly, assigned in body.
protected PluginManagerInterface $recordTypeManager;
public function __construct(
  EntityRepositoryInterface $entity_repository,

  PluginManagerInterface $record_type_manager,
) {
  parent::__construct($entity_repository, …);
  $this->recordTypeManager = $record_type_manager;
}

readonly is fine for genuinely-non-cached classes — constraint validators, entities, value objects, controllers that don't reach serialization. If in doubt, default to non-readonly.

Constraint validator naming

Drupal/Symfony resolves a constraint plugin's validator by appending Validator to the constraint's class name. So XxxConstraint looks for XxxConstraintValidator — not XxxValidator. Keep the Constraint suffix in both names.

No Co-Authored-By trailers in commits

Commits in this repo must not include the Co-Authored-By: Claude … trailer that some tooling appends by default. Project history attribution is done at the commit-author level.

Magic values

Promote a literal to a constant when:

  • It's an RFC limit or other domain-meaningful number used in more than one place (see ZoneNameTransformer::MAX_NAME_LENGTH, MAX_LABEL_LENGTH, PUNYCODE_LABEL_PREFIX).
  • It's a contract between two layers (see RecordTypeBase::FORM_KEY_RDATA, the form-state key the form builds and plugin validators read).

Don't promote:

  • Permission strings, service IDs, config keys, route names, field-storage names — Drupal core uses string literals here and the convention is widely understood.
  • Numeric maxlengths used in exactly one field declaration.
  • Single-call-site bitmasks and PHP's own constants (JSON_THROW_ON_ERROR, IDNA_DEFAULT, etc.).

CI pipeline

The CI pipeline is reused verbatim from field_ipaddress's .gitlab-ci.yml — Drupal's central GitLab templates handle linting, unit tests, kernel tests, functional tests, code-quality, and cspell. The only project-specific knob in the file is _CSPELL_WORDS (project-name terms not in the dictionary).