feat(entity-extension): add entity extension system#59
Conversation
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>
|
@markshust This adds a basic Entity extensibility and could later be updated with more complex extensions (using joins, etc.). |
|
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 #[Table(extends: WebhookAttempt::class)]
class WebhookAttemptAnalytics extends Entity { ... }That reuses 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. |
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>
Summary
src/EntityExtension/directories across vendor/modules/app at boot$entity->extension(MyExtension::class)with full IDE type inference via@templateWhat was built
EntityExtensionabstract base class +#[ExtensionOf]attribute to declare which entity a class extendsEntityExtensionRegistrysingleton +EntityExtensionDiscoveryscanner — registered and populated inmodule.phpbootExtensionMetadata+EntityExtensionMetadataFactory— parses extension classes; validates no#[Table], no primary key, no relationshipsEntityMetadata::$extensionsfield (backward-compatible last param) —EntityMetadataFactorymerges extensions and detects column/property name conflicts loud at bootEntity::extension()/setExtension()— typed accessor with@template T of EntityExtensionfor IDE inferenceEntityHydrator— 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 timeSchemaBuilder— extension columns included in table schema for migration diffspackages/database/README.mdupdated with full developer workflowTest plan
🤖 Generated with Claude Code