Skip to content

feat(meta): typed meta as attributes of the Pollora models - #400

Merged
ogorzalka merged 3 commits into
developfrom
feature/meta-models
Oct 5, 2026
Merged

ogorzalka merged 3 commits into
developfrom
feature/meta-models

Conversation

@ogorzalka

@ogorzalka ogorzalka commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Second half of phase 2 of « Pollora — Meta typées : document d'implémentation ». Stacked on #399 (base feature/meta-owners; GitHub retargets it to develop once #399 is merged and its branch deleted).

class Event extends \Pollora\Models\Post
{
    protected $postType = 'event';
}

$event = Post::find($id);              // an Event: bound to 'event' at discovery
$event->capacity;                      // int (or by key: $event->sold_out)
$event->capacity = 'many';             // InvalidMetaValueException, now
$event->capacity = 250;
$event->save();                        // update_metadata(): sanitize, hooks, cache
Event::whereMeta('capacity', '>=', 100)->get();

User::find(1)->newsletterOptIn;        // #[UserMeta] on the user model

Design

  • HasTypedMeta is on Pollora\Models\Post, Page, Term, User and Comment (as the document puts it, so $user->newsletter_opt_in works on the authenticated user). getAttribute()/setAttribute() intercept only names a schema of the object type declares (MetaSchemaRepository::declares(), an O(1) index), then find the schema for the object's subtype: post_type column or $postType for posts, the taxonomy relation for terms, none for users and comments. Everything else goes to Colt/Eloquent untouched.
  • Writes are checked by MetaValueCaster::toStorage() immediately, kept out of the model's attributes, and written by the saved event through a MetaRecord (so a new model gets its ID first).
  • No behaviour change without typed meta. A class is "known to carry typed meta" when the repository has schemas for its object type and static subtype ($postType; users/comments: any #[UserMeta]/#[CommentMeta]). Only then does it drop meta from $with; each model it loads joins a batch on retrieved, and the first typed read primes the whole batch with one update_meta_cache() call. (A first version overrode newCollection(), whose generic return type PHPStan could not reconcile with Colt's models in CI.) The generic Post keeps eager loading. Undeclared keys keep Colt's fallback (lazy relation).
  • whereMeta(): CAST(meta_value AS SIGNED) / DECIMAL(65, 10) for numbers, stored-form comparison otherwise (dates are stored in UTC with a fixed format, so they sort as strings); null means the meta is absent. Operators are whitelisted (they are concatenated into the raw SQL).
  • Post::registerPostType(): MetaDiscovery also keeps classes extending a Colt post model and binds their $postType default.
  • Rector's ScalarParamTypeByMethodCallTypeRector is skipped for the trait: typing $key breaks compatibility with Eloquent's untyped getAttribute($key) (fatal).

Verified on pollora-test, temporary classes and values since removed

#[PostMeta('post')] (int $tmpViews, ?string $tmpSubtitle), class Article extends Post { $postType = 'post' }, #[UserMeta]:

  • Post::find() returns App\Cms\TmpMeta\Article.
  • A collection read with ->tmpViews on each: 1 Eloquent query + 1 wpdb query (meta cache), integers; measured again after the batch rework: 20 posts, 1 wpdb query.
  • $article->tmpViews = 999; ->tmpSubtitle = 'Typé'; ->save() → get_post_meta() returns them; 'many' throws.
  • whereMeta('tmpViews', '>=', 300) and whereMeta('tmpSubtitle', null) return the right IDs on MySQL.
  • User::find(1)->tmpNewsletterOptIn: false, then true after save().
  • An undeclared key (_edit_lock) still reads through Colt on a typed model.

Tests

HasTypedMetaTest (reads by property and key, generic post model, untouched attributes, checked writes and save, defaults before save, user meta, $with, cache priming, whereMeta() SQL), repository forObject()/declares(), discovery binding. Full suite 1648 passed; Pint, PHPStan, Rector clean.

HasTypedMeta on Post, Page, Term, User and Comment: typed reads, checked
writes stored on save, whereMeta() and one meta cache query per
collection. Post models are bound to their $postType at discovery.
…erriding newCollection()

Overriding newCollection() could not keep Eloquent's generic return type
(PHPStan in CI). Models loaded together now join a batch on 'retrieved',
primed in one update_meta_cache() call on the first typed read.
@ogorzalka
ogorzalka deleted the branch develop October 5, 2026 15:53
@ogorzalka ogorzalka closed this Oct 5, 2026
@ogorzalka ogorzalka reopened this Oct 5, 2026
@ogorzalka
ogorzalka changed the base branch from feature/meta-owners to develop October 5, 2026 15:54
@ogorzalka
ogorzalka merged commit 1c30e49 into develop Oct 5, 2026
20 checks passed
@ogorzalka
ogorzalka deleted the feature/meta-models branch October 5, 2026 16:04
@ogorzalka ogorzalka mentioned this pull request Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant