Skip to content

fix: support union-typed entity columns via explicit Column type - #145

Merged
markshust merged 2 commits into
developfrom
fix/media-attachable-id-union-type
Jul 26, 2026
Merged

fix: support union-typed entity columns via explicit Column type#145
markshust merged 2 commits into
developfrom
fix/media-attachable-id-union-type

Conversation

@TuVanDev

@TuVanDev TuVanDev commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

EntityMetadataFactory threw Property '...' must have a type declaration for any entity property with a PHP union type (ReflectionUnionType), with no escape hatch. This broke db:migrate/db:diff outright for any project with marko/media installed, since Marko\Media\Entity\MediaAttachment::$attachableId is declared int|string (a polymorphic foreign key — an attachment can belong to entity types whose primary keys are int or string).

Fix

EntityMetadataFactory now accepts a ReflectionUnionType property when it carries an explicit #[Column(type: ...)] attribute (already a supported mechanism for named-type columns, just not consulted before the union-type guard threw). MediaAttachment::$attachableId is now declared #[Column(type: 'varchar', length: 255)], preserving its int|string PHP type while giving the factory an unambiguous DB column type. Union properties without an explicit #[Column(type:...)] still throw, now with a clearer EntityException::unionTypeRequiresColumnType() message instead of the previous misleading "must have a type declaration".

The ReflectionNamedType code path is unchanged — this is a strictly additive branch.

Notes for reviewers

  • Hydrated attachableId always comes back as a PHP string regardless of the original id's type (varchar columns hydrate to string, and int|string accepts that with no TypeError) — code comparing it against a real int PK should use loose comparison or cast explicitly. No existing usage in packages/media does a strict comparison.
  • This escape hatch only casts safely end-to-end for unions that include string in their type list (a varchar column round-trips fine). A hypothetical future union like int|bool would still need casting support at hydration time — out of scope here since no such entity currently exists.
  • packages/media ships no migration files — schema is derived entirely from entity metadata via SchemaRegistry, so this fix directly unblocks db:diff/db:migrate for any consumer.

Test plan

  • ./vendor/bin/pest packages/database/tests — 883 passed
  • ./vendor/bin/pest packages/media/tests — 38 passed
  • Two new regression tests confirmed to fail on develop (pre-fix) with the original misleading error, and pass on this branch
  • ./vendor/bin/phpcs --standard=phpcs.xml clean on all changed files
  • ./vendor/bin/phpstan — zero new errors vs develop baseline

Additional changes (maintainer)

  • Documented the new rule in packages/docs-markdown/docs/packages/database.md: a union-type row in the Type Inference Rules table, a Union-Typed Columns section using the polymorphic-foreign-key case, and a note that a varchar-backed union always hydrates to string (so app code should not strict-compare it against an integer primary key).

… type

EntityMetadataFactory rejected any entity property whose reflection type is
not a single named type, throwing "must have a type declaration" for union
types such as `int|string`. Because db:migrate/db:diff parse metadata for
every discovered entity, marko/media's MediaAttachment (a polymorphic foreign
key declared `int|string $attachableId`) broke both commands for any project
with marko/media installed, regardless of whether the entity was used.

Handle ReflectionUnionType: a union has no single reflection type to infer a
column type from, so require an explicit #[Column(type: ...)] and use it as
the database type. Declare the media_attachments.attachable_id column as
varchar, which holds both int and string primary keys while preserving the
polymorphic union on the PHP side.
@github-actions github-actions Bot added the bug Something isn't working label Jul 20, 2026
@markshust markshust self-assigned this Jul 26, 2026
The database docs page listed inference rules for every named type but said
nothing about unions, so the new "explicit #[Column(type: ...)] required"
behavior was undiscoverable. Add the inference-table row, a Union-Typed
Columns section using the polymorphic-foreign-key case, and a note that a
varchar-backed union always hydrates to string.

Co-Authored-By: Tu Van <vandinhtuit@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@markshust markshust changed the title fix(database): support union-typed entity columns via explicit Column type fix: support union-typed entity columns via explicit Column type Jul 26, 2026
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Jul 26, 2026
@markshust

Copy link
Copy Markdown
Collaborator

Thanks @TuVanDev — confirmed and merging.

Reproduced on develop: EntityMetadataFactory threw must have a type declaration
for MediaAttachment::$attachableId, which broke db:migrate/db:diff for any project
with marko/media installed. Since packages/media derives its schema entirely from
entity metadata, there was no workaround.

Requiring an explicit #[Column(type: ...)] rather than auto-widening the union is the
right call for this codebase — it matches "explicit over implicit," and the new
EntityException::unionTypeRequiresColumnType() carries proper context/suggestion per
the loud-errors principle. varchar(255) also matches what docs/packages/media.md
already documented for attachable_id.

Verified locally: full suite 6861 passed, phpcs and php-cs-fixer clean on all touched
files, and phpstan shows 12 errors on both this branch and develop (pre-existing, in
packages/core/Plugin).

I pushed one docs commit to the branch so the new rule is discoverable: the database docs
page had inference rules for every named type but nothing about unions. It now has a
union row in the Type Inference Rules table, a Union-Typed Columns section, and a note
that a varchar-backed union always hydrates to string — so app code should compare
loosely rather than strict-comparing against an integer primary key.

@markshust
markshust merged commit 6dc637b into develop Jul 26, 2026
1 check passed
@markshust
markshust deleted the fix/media-attachable-id-union-type branch July 26, 2026 14:29
markshust added a commit that referenced this pull request Jul 26, 2026
Step 5 told the user a failed run "leaves nothing to clean up". That is wrong:
bin/release.sh checks out main and merges develop at line 25, well before the
test suite runs at line 61. A test failure leaves the repo on main with the merge
already made — nothing pushed and no tag, but not a no-op, and a retry from there
trips the skill's own on-develop precondition with a misleading error.

State the real failure state and the `git checkout develop` recovery. Add a test
that asserts the merge still precedes the test run in bin/release.sh, so the
claim cannot silently rot if the script is reordered.

Also narrow tier 2: internals added in service of a fix (a new exception factory,
say) are thrown at consumers rather than built against, so they do not promote a
bug fix to a feature. Without that, PR #145 in the calibration table reads as
tier 3 on a strict pass and would wrongly escalate a patch to a minor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants