Repository navigation
v0.14.0
What's Changed
Added
-
fields-migratemaps the ACFrelationship,rangeandmessage
field types. Measured across the downstream fleet: 9relationship
fields, 2message, 1range— every one of them refused the WHOLE
component before, not just the one field.relationshipmaps to
type: referencewithmultiple: true, anof: post:<type>[,...]target
andwp.acf_type: relationship(it is always multi-value by field
design, unlikepost_object's opt-inmultiple).rangemaps to
type: numberwithwp.acf_type: range— the same min/max/step
constraint-lifting as a plainnumber.messageholds no author-set
value, so it is captured the same way asaccordion(identity + "which
field did it precede") and replayed on generation, rather than invented
as a data field with no abstract home. Tests: mapper and reverse-mapper
unit tests for all three types, a completeness audit, and a generation
round trip on a newrelated-contentcorpus fixture covering a
fully-baseline relationship, a relationship deviating on
filters/elements/taxonomy/min/max, a range with every
constraint authored, and both a leading and a trailing message (one with
a residualesc_htmldeviation, one with the real corpus's empty
name).composer check: 906 tests OK, PHPStan clean.End to end on four real downstream components that failed before
(aleszejdl/career-list, loyaltiq/estimate-list, anyever/testimonial-feature,
umbili/image-promo): all four now migrate and regenerate with every
relationship/range/message field intact, including image-promo's
message-then-accordion stacking ahead of its first real field.
Regeneration on career-list and estimate-list shows the same class of
pre-existing, documented residual already tolerated elsewhere in this
tool (legacy ACF-export sentinel conventions —post_typeas a bare
string vs. an array, missingallow_in_bindings, string vs.0
dimension sentinels) — unrelated to this change, not a regression.
Closes #77.
Fixed
fields-migrateno longer discards a twigfields:annotation on a
component with noacf.json.element/part/section/utility
components take their values from the calling template, not from ACF, so
the CLI synthesised an empty ACF document for them and previously wrote
fields: {}— losing the annotation entirely once ADR 0007 strips the
front-comment. Measured on one downstream project: 16 of 22 non-block
components carried such an annotation.AcfJsonReadernow falls back to
the newTwigMetadataReader::readFields()/TwigFieldTypeMapperwhen
acf.jsonhas no fields but the twig annotation does, translating the
twig type vocabulary (text/textarea/url/link/select/image/…)
into the abstract schema. Whenacf.jsongenuinely has fields, behaviour
is unchanged. A twig field
annotatedtype: array(with or without nestedfields:) is refused with
a\DomainExceptionnaming the field, rather than guessed — see the
linked issue for the decision and its rejected alternatives. A twig
annotation prop with no mapping to the abstract schema (e.g. a typo, or a
genuinely unhandled key) is likewise refused by field path rather than
silently dropped, andplaceholder:is now carried across. Aselect
with no (or empty)options:/choices:, or a non-string option label,
is refused locally instead of migrating to schema-invalid YAML. A
select'soptions:written as a YAML list ([a, b, c]) is read like
the comma-string shorthand instead of collapsing to a single bogus
Array: Arrayoption;choices:andoptions:given together, a
malformedchoices:, and a non-canonicalrequired:value (e.g.
required: yes) are refused by field path instead of one silently
winning over the other or the constraint silently vanishing. Afields:
annotation whose value is a scalar rather than a map is likewise refused,
distinct from the "explicitly no fields" bare/nullfields:shape. The
twig fallback now only fires whenacf.jsonis genuinely absent from
disk (AcfJsonReader::read()'s new$acfJsonExistsparameter) — a real
acf.json with an intentionally empty field group is no longer
overridable by a stale twig annotation. Aselect'soptions:list
entry that is a boolean or number, or two entries (list or comma-string)
that collide once trimmed, is refused instead of silently coercing or
overwriting. An associativeoptions:map (its own keys, e.g.
draft: Draft) is refused rather than silently re-keyed by value — use
choices:for that shape.title:,description:andplaceholder:
now require an actual string; a YAML list or map there used to be cast
straight to the literal string"Array"via PHP's array-to-string
coercion, producing schema-valid but corrupted output. Provenance
(role:) is no longer inferred from missingacf.jsonalone — absence
ofacf.jsonproves only that a field is not editor-authored, not that
it isparent(it could bequery, e.g. a PHP sidecar's own database
read, orglobal). A field's own twigrole:annotation always wins
(validated against the full schema role enum;role: derivedrequires
from:); absent that,fields-migratenow requires an explicit
--assume-role=parent|query|globalfor the whole component and refuses
the component by field path when neither is given. The flag is restricted
to those three roles (field/inherited/derivedmake no sense as a
blanket assumption) and is ignored entirely for anacf.json-backed
component.AcfJsonReader::readFields()'s twig-annotation parse now also
only runs whenacf.jsonis genuinely absent, so a stale/malformed twig
fields:block next to a validacf.jsonno longer aborts migration for
fields that were never going to be used. An un-annotated child field now
inherits its container's own resolvedrole:(e.g.role: queryon a
repeater) instead of falling straight back to--assume-role, matching
the schema's "a descendant inherits its ancestor's role" contract; this
also means an explicit containerrole:lets its un-annotated children
through even without--assume-roleat all. Aselect'schoices:/
options:keyed with the sequential integers0, 1, 2, …from zero
(e.g.choices: {0: None, 1: One}) is refused by field name — PHP
cannot keep such a key as a string, so it is otherwise indistinguishable
from a plain list once written, and used to fail later at YAML-write
time with a message naming neither the field nor the cause.role: derivedis no longer inherited by an un-annotated child — it always
carries its ownfrom:naming a specific sibling, which is meaningless
to copy onto a child with different siblings; inheriting it produced a
schema-invalidrole: derivedwith nofrom:and rejected the whole
component for an annotation that looked completely valid. Such a child
now falls back to--assume-role(or ambiguous-provenance) instead,
same as if the container had no role at all. Closes #75.- A nested
accordion/messageinside a group/repeater'ssub_fields
or a flexible_content layout is now rejected loudly, not silently
dropped. Both pseudo-field kinds are captured and replayed ONLY at a
component's root — the reader used tocontinuepast a nested one with
no diagnostic at all, generation never reproduced it, and a container
whose only child was a pseudo-field threw a misleading "zero sub-fields"
error with no hint why.AcfJsonReader::rejectNestedPseudoField()now
throws immediately, naming the field, its container and its ACF type.
MigrationCompletenessAuditornow flags a nested pseudo-field it
encounters directly as silent data loss too, instead of skipping it the
same way it correctly skips a ROOT-level one. Found in review of #78. - An unrestricted
relationship/post_object(post_type: []or
missing — "every post type", a normal and common ACF shape) used to
migrate to the schema-invalidof: "". Empty/missingpost_typenow
normalizes to WordPress's own reserved "no restriction" sentinel,
of: post:any(mirroringWP_Query'spost_type: 'any'), which
reverses back to ACF's genuinely-unrestrictedpost_type: []— not a
literal, nonexistentanypost type. Found in review of #78. - A message immediately followed by an accordion (or vice versa), both
anchored on the same real field, now reproduces its true authored
order on regeneration. It used to always emit accordions first
regardless of which was actually authored first.Migration\AcfJsonReader
now attaches aseqordering hint, but ONLY on an anchor that mixes
both kinds — a single-kind anchor (the fleet's overwhelming majority)
never gets one, sowp.accordionsstays byte-for-byte unchanged for
every definition with no messages. Found in review of #78.
Pull Requests
- #76 — fix(migration): preserve twig fields: annotation without acf.json
- #78 — feat(migration): map ACF relationship, range and message fields
Full Changelog: v0.13.0...v0.14.0