Skip to content

Module 2 · Typed content: post types, meta and roles

Duration: 60 min · Prerequisites: module 1, and knowing what a WordPress meta and a role are.

By the end of this module you declare a content type, its fields and its rights once, in PHP classes, and Pollora derives the WordPress registration.

app/Cms/PostTypes/Event.php
use Pollora\Attributes\PostType;
use Pollora\Attributes\PostType\HasArchive;
use Pollora\Attributes\PostType\ShowInRest;
use Pollora\Attributes\PostType\Supports;
#[PostType]
#[HasArchive]
#[ShowInRest]
#[Supports(['title', 'editor'])]
class Event {}

The slug and the singular and plural names come from the class name: Event gives event, BookReview gives book-review. Auto-discovery finds the class: no register_post_type() call.

In development, after adding a class: php artisan discovery:clear. To generate one: php artisan pollora:make:post-type Event.

A WordPress meta is a string: its key is repeated everywhere and every read casts it again. With Pollora the meta is a typed PHP property of the same class.

use Pollora\Attributes\Meta;
#[PostType]
#[ShowInRest]
class Event
{
#[Meta(showInRest: true, label: 'Capacity', rules: ['min:0', 'max:5000'])]
public int $capacity = 0; // key: capacity
#[Meta(showInRest: true)]
public ?CarbonImmutable $startsAt = null; // key: starts_at
}
  • The property type gives the meta type, its initial value gives the default (what an absent meta reads as), its name gives the key in snake_case.
  • Pollora derives register_meta(), sanitization and the REST schema.
  • A property with neither a default nor a nullable type is refused at discovery.
use Pollora\Support\Facades\Meta;
$event = Meta::of(Event::class, $postId);
$event->capacity; // int
$event->capacity = 250;
$event->save(); // written through update_metadata()

WordPress still owns the storage: get_post_meta($id, 'capacity', true) still returns the string '250'. But a write through Meta::of() is checked at once: $event->capacity = 6000 throws a MetaValidationException before anything is written.

Two commands to check: php artisan pollora:meta:list (every meta, by class) and php artisan pollora:meta:audit (stored values that cannot be read as their type).

app/Cms/Roles/EventManager.php
use Pollora\Attributes\Role;
use Pollora\Attributes\Role\Grants;
use Pollora\Attributes\Role\Without;
#[Role('event_manager', label: 'Event manager', inherits: 'author')]
#[Grants('moderate_comments')]
#[Without('publish_posts')]
final class EventManager {}

This role starts from an author’s capabilities, can moderate comments and cannot publish posts.

The code is the only source of truth. Pollora does not write these roles to the database: it injects them in memory on every request. Change the class and deploy, the role follows everywhere, with nothing to migrate. Revert with Git and the previous rights are back.

Safety rules refuse, at discovery, what would grant the wrong rights:

  • a sensitive capability (manage_options, edit_users…) without allowSensitive: true;
  • redeclaring a core role (editor, author…) with #[Role]: use #[ModifyRole];
  • a capability both granted and removed by the same class.

#[Without] removes a capability, it never sets it to false: WordPress merges a user’s roles and an explicit denial would depend on their order.

To see the result: php artisan pollora:roles:list and php artisan pollora:roles:show event_manager.

A post type slug

Which slug does Pollora generate for #[PostType] class BookReview {}?

  • bookreview
  • book-review
  • book_review

    WordPress accepts underscores, but Pollora generates the slug in kebab-case from the class name.

The slug, singular and plural are derived from the class name.

A meta key

Under which key is public ?CarbonImmutable $startsAt = null; marked #[Meta] stored?

  • startsAt
  • starts_at
  • _starts_at

    A key starting with _ is protected: WordPress hides it. It requires an explicit capability.

The property name gives the key in snake_case, unless the key parameter picks another.

Writing an invalid value

A capacity meta has rules: ['max:5000']. What happens with $event->capacity = 6000;, then with update_post_meta($id, 'capacity', 6000);?

  • The first throws a MetaValidationException, the second writes the value
  • Both throw an exception

    WordPress’s functions cannot refuse a value: they only apply the type’s sanitization.

  • Both write the value, the error only shows on read

rules apply to writes from PHP (Meta::of(), models) and from REST.

Where a declared role lives

You delete the EventManager class and deploy. What happens to the event_manager role?

  • It stays in the database until removed by hand
  • It disappears: the code is the only source of truth

    Pollora injects roles in memory on every request. Users who had it keep the slug but get no capability from it.

  • It becomes a subscriber role

Pollora also undoes what a plugin may have written back to the database for a role removed from the code.

Sensitive rights

Which declarations are refused at discovery?

  • #[Grants('manage_options')] without allowSensitive: true
  • #[Role('editor', …)] to redeclare a core role
  • #[Without('publish_posts')] on a role inheriting from author

    That is the normal use of #[Without]: removing an inherited capability.

  • #[Role('event_manager', inherits: 'author')]

A core role is modified with #[ModifyRole].

Quiz Progress

0 / 0 questions answered (0%)

0 correct

lab/bin/lab reset 02, then edit app/Cms/PostTypes/Event.php.

Goal. Declare capacity as a typed meta: an integer, 0 by default, exposed in the REST API, never above 5000 (and not negative).

lab/bin/lab check 02 checks that the meta is registered as integer with show_in_rest, that it reads and writes with its PHP type, and that a value of 6000 is refused.

Exercise 03 · An “event manager” role

Section titled “Exercise 03 · An “event manager” role”

lab/bin/lab reset 03, then edit app/Cms/Roles/EventManager.php.

Goal. Turn this class into the event_manager role, labelled “Event manager”, that starts from an author’s capabilities, can moderate comments and cannot publish posts.

lab/bin/lab check 03 checks that the role exists with its label, the inherited capabilities, and the presence or absence of the two capabilities.