feat(scope): scoped entity attributes with multi-axis hierarchical fallback#76
Open
michalbiarda wants to merge 14 commits into
Open
feat(scope): scoped entity attributes with multi-axis hierarchical fallback#76michalbiarda wants to merge 14 commits into
michalbiarda wants to merge 14 commits into
Conversation
…llback Introduces marko/scope, marko/scope-mysql, and marko/scope-pgsql — a three-package family that lets entities declare #[Scoped] properties whose overrides are stored in a JSON/JSONB `scopes` column via the existing extender mechanism. Key capabilities: - Multi-axis declared-priority resolution (ScopeWalker) - ScopeContext mutable singleton for request-scoped axis values - ScopeResolver service for read/write/clear of per-scope overrides - ScopedOrderBy QuerySpecification for COALESCE ORDER BY via driver renderers - ScopeRegistryInterface + PhpScopeRegistry (swappable for DB-driven impl) - ScopedEntityValidator for boot-time integrity checks - orderByRaw() added to QueryBuilderInterface and both driver builders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or extender merge Single-pass buildEntitySchema loop called schemaBuilder->build() per entity independently, so extender columns (Table(extends:)) were never merged into the parent table schema. Commands saw extender columns as missing from the entity definition — reporting existing ones as destructive drops and never generating ADD COLUMN for new ones. Replaced the loop with SchemaRegistry::registerEntities(), which already implements the correct two-pass merge. Removed the now-unused EntityMetadataFactory and SchemaBuilder constructor dependencies from both commands; updated test helpers accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PgSqlGenerator maps entity type 'json' to JSONB in DDL, which is correct (JSONB is preferred in PostgreSQL for indexing and performance). However, the introspector returned 'jsonb' for those columns, while the entity schema still held 'json'. With no alias in Column::typeEquals(), the diff calculator reported a spurious Modify on every json column after the initial migration. Mapping 'jsonb' → 'json' in PgSqlIntrospector::TYPE_MAP normalises the round-trip so introspected JSONB columns compare equal to entity-declared json columns. The fix is intentionally scoped to the PgSQL driver — MySQL is unaffected (it stores and introspects JSON under the same name). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or extender merge Single-pass buildEntitySchema loop called schemaBuilder->build() per entity independently, so extender columns (Table(extends:)) were never merged into the parent table schema. Commands saw extender columns as missing from the entity definition — reporting existing ones as destructive drops and never generating ADD COLUMN for new ones. Replaced the loop with SchemaRegistry::registerEntities(), which already implements the correct two-pass merge. Removed the now-unused EntityMetadataFactory and SchemaBuilder constructor dependencies from both commands; updated test helpers accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PgSqlGenerator maps entity type 'json' to JSONB in DDL, which is correct (JSONB is preferred in PostgreSQL for indexing and performance). However, the introspector returned 'jsonb' for those columns, while the entity schema still held 'json'. With no alias in Column::typeEquals(), the diff calculator reported a spurious Modify on every json column after the initial migration. Mapping 'jsonb' → 'json' in PgSqlIntrospector::TYPE_MAP normalises the round-trip so introspected JSONB columns compare equal to entity-declared json columns. The fix is intentionally scoped to the PgSQL driver — MySQL is unaffected (it stores and introspects JSON under the same name). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… scope storage Introduces HasScopesInterface as the polymorphic storage contract and HasScopes trait as the primary (simpler) storage approach. ScopeWalker, ScopeResolver, and ScopedEntityValidator are updated to accept either trait-based entities or the existing ScopedOverridesEntity companion approach, with no breaking changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidates scope storage on HasScopes trait + HasScopesInterface. Removes the ScopedOverridesEntity companion base class, ScopeResolver::createCompanion(), and the two-approach framing throughout. ScopedEntityValidator and ScopeResolver now accept any HasScopesInterface implementor (entity-self or manually-declared companion). Driver auto-migration tests updated to use the manual companion pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PDO silently casts PHP arrays to the string "Array" when bound as query parameters. For json/jsonb columns this causes an "invalid input syntax for type json" error at the database level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both PgSqlScopeSortRenderer and MySqlScopeSortRenderer were appending direction to the rendered expression. ScopedOrderBy passes direction separately to orderByRaw(), so the query builder was emitting COALESCE(...) ASC ASC — a syntax error at runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PDO silently casts PHP arrays to the string "Array" when bound as query parameters. For json/jsonb columns this causes an "invalid input syntax for type json" error at the database level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ring HTTP requests EntityMetadataFactory is now a singleton and a boot callback discovers all entity classes and calls linkExtendersFrom() so extender metadata is populated before any repository hydration occurs. Previously linkExtenders() was only called from CLI migration commands, leaving companions unattached at runtime. Closes marko-php#73 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…binding-json-encode', 'fix/db-commands-extender-merge' and 'feature/entity-extender-boot' into feature/scope
…are-noun convention Renames HasScopesInterface accessor methods to align with the rest of the framework (Entity::companion/companions, CursorInterface::parameter/parameters): getOverride() → override() allOverrides() → overrides() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract a shared findFirstMatch() helper in ScopeWalker so walk() and walkAt() no longer repeat the hierarchy walk + override scan. Extract assertScopedAndFindStorage() in ScopeResolver to share the prologue of setOverride() and clearOverride(). Inline the trivial getRegistry() wrapper and use Scope::toString() in place of inline "axis:path" concatenation so the format lives in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds scoped entity attributes to Marko as three new packages:
marko/scope— driver-agnostic core:#[Scoped]attribute,ScopeContext,ScopeResolver,ScopeWalker,ScopeRegistry,ScopeHierarchy,HasScopestrait +HasScopesInterface,ScopedDataSerializer,ScopedOrderByFactory,ScopedEntityValidator.marko/scope-mysql— MySQL / MariaDB driver: scopedORDER BYrendered withJSON_EXTRACT/JSON_UNQUOTEover ajsoncolumn, plus a migration helper.marko/scope-pgsql— PostgreSQL driver: scopedORDER BYrendered withjsonboperators (->,->>), plus a migration helper.Properties are marked
#[Scoped(axes: ['locale', 'store'])]. Axes are declared inscope.axesconfig with a dotted-path hierarchy (e.g.['en', 'en.gb', 'en.us']).ScopeContext::in($axis, $path)sets the ambient scope per request.ScopeResolver::resolved($entity, $property)returns the property with walk-up fallback (en.gb→en→ default). Overrides are JSON-serialized into a singlescopescolumn on the entity (or on a companion class).ScopedOrderByFactoryproduces aQuerySpecificationthat emits the same walk in SQL, so sorts honor the resolved value.The branch also rolls up a handful of small
marko/databaseandmarko/database-pgsqlfixes that surfaced while wiring this up — extender boot, jsonb type normalisation, json array binding encode, schema-registry use in db commands. Each of these is also being submitted as a standalone PR for narrower review: #67, #69, #72, #74.Related Issues
Closes #75