Skip to content

feat(entity-extension): add entity extension system#59

Closed
michalbiarda wants to merge 1 commit into
marko-php:developfrom
michalbiarda:feature/entity-extension
Closed

feat(entity-extension): add entity extension system#59
michalbiarda wants to merge 1 commit into
marko-php:developfrom
michalbiarda:feature/entity-extension

Conversation

@michalbiarda
Copy link
Copy Markdown

Summary

  • Any module can now add columns to an existing entity's table without touching the original entity class
  • Extensions are auto-discovered from src/EntityExtension/ directories across vendor/modules/app at boot
  • Hydrated transparently from the same DB row; accessed via $entity->extension(MyExtension::class) with full IDE type inference via @template

What was built

  • EntityExtension abstract base class + #[ExtensionOf] attribute to declare which entity a class extends
  • EntityExtensionRegistry singleton + EntityExtensionDiscovery scanner — registered and populated in module.php boot
  • ExtensionMetadata + EntityExtensionMetadataFactory — parses extension classes; validates no #[Table], no primary key, no relationships
  • EntityMetadata::$extensions field (backward-compatible last param) — EntityMetadataFactory merges extensions and detects column/property name conflicts loud at boot
  • Entity::extension() / setExtension() — typed accessor with @template T of EntityExtension for IDE inference
  • EntityHydrator — hydrates extension objects from the same DB row; silently skips extensions whose columns are absent (safe during rolling deploys)
  • Repository — INSERT/UPDATE/batch include extension columns; null/default/loud-error policy enforced at save time
  • SchemaBuilder — extension columns included in table schema for migration diffs
  • packages/database/README.md updated with full developer workflow

Test plan

  • 47 new tests added across 10 new/modified test files
  • Full suite: 4956 passing

🤖 Generated with Claude Code

Any module can now add columns to an existing entity's table without
modifying the entity class. Extensions are auto-discovered from
src/EntityExtension/ directories, merged into the entity schema at boot,
and hydrated transparently from the same DB row. Access via the typed
extension() accessor which IDEs resolve through @template.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label May 11, 2026
@michalbiarda
Copy link
Copy Markdown
Author

@markshust This adds a basic Entity extensibility and could later be updated with more complex extensions (using joins, etc.).

@markshust
Copy link
Copy Markdown
Collaborator

Thanks for putting this together, @michalbiarda — the problem is real and your implementation handles a lot of important edge cases (rolling-deploy column absence, conflict detection at boot, batch INSERT merging, the constraint validation on extension classes). Those are exactly the things the next iteration needs to get right.

After tracing the surface area, I want to take a different shape on it. Instead of a parallel EntityExtension hierarchy (new base class, new attribute, new discovery, new metadata factory, new registry, new accessor), I'd like extenders to be plain Entity subclasses that declare their target via a new param on the existing #[Table]:

#[Table(extends: WebhookAttempt::class)]
class WebhookAttemptAnalytics extends Entity { ... }

That reuses EntityDiscovery, EntityMetadataFactory, EntityHydrator, and Repository instead of duplicating them, drops the "no PK / no relationships" restriction (extenders become first-class entities that can have their own indexes, relationships, and lifecycle hooks), and should land in roughly ~300–500 lines vs the ~3.3k here.

Closing this in favor of #63 — the validation logic, the rolling-deploy handling, and the conflict-detection story you worked out here will all carry over to the new approach. Would love your input on the issue, and very much appreciate the work.

@markshust markshust closed this May 12, 2026
michalbiarda pushed a commit to michalbiarda/marko that referenced this pull request May 13, 2026
Any module can now add columns to another module's entity table without
touching the original entity class. Declare a plain Entity subclass with
#[Table(extends: ParentEntity::class)] — the framework merges its
columns/indexes/foreign-keys into the parent's table schema and hydrates
the extender from the same row as a companion on the parent entity.

Closes marko-php#63
Supersedes marko-php#59

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants