pollora/entity
Pollora Entity brings a fluent, Eloquent-style entity layer to WordPress development, letting you model posts, terms, and users as expressive PHP objects. Query, relate, and manipulate content with clean, type-safe APIs while retaining full compatibility with WordPress internals.
This is a small, attribute-driven wrapper around register_post_type() and register_taxonomy(). It gives you a class per entity instead of an array of arguments, which is the main thing you get over hand-rolled register_post_type() calls.
Minimal steps:
composer require pollora/entity (requires PHP 8.1+ for attributes; WordPress 5.9+ realistically).init runs — a require in functions.php or a PSR-4 autoloader wired into your plugin bootstrap.init.use Pollora\Entity\Attributes\PostType;
use Pollora\Entity\Attributes\Taxonomy;
#[PostType(
name: 'book',
singular: 'Book',
plural: 'Books',
public: true,
hasArchive: true,
showInRest: true,
supports: ['title', 'editor', 'thumbnail', 'excerpt']
)]
#[Taxonomy(
name: 'genre',
singular: 'Genre',
plural: 'Genres',
postTypes: ['book'],
hierarchical: true,
showInRest: true
)]
final class Book {}
Then register once, typically from your plugin bootstrap:
add_action('init', function () {
\Pollora\Entity\Entity::register(Book::class);
});
Where to look first: src/Attributes/ for the actual attribute signatures (this package is pre-1.0 in spirit — verify parameter names against source rather than trusting blog posts), then tests/ for the canonical registration examples. First real use case is almost always "I need a book CPT with a genre taxonomy and REST/Gutenberg support," which is exactly the two-attribute example above.
One class per entity, grouped by domain. Put Book, Author, Genre in app/Entities/ (or src/ContentTypes/). This keeps the argument array in one place and makes the registration call a one-liner per class.
Prefer attributes for declaration, fluent setters for conditionals. Attributes can't express runtime logic (e.g. "public only if a feature flag is on"). Keep the attribute for the stable shape and adjust at registration time:
$cpt = \Pollora\Entity\PostType::make('book')
->labels(['name' => __('Books', 'textdomain')])
->public(get_option('books_enabled') === 'yes')
->register();
Bulk registration in a service provider. If you're on Pollora/Laravel, register entities in a dedicated provider rather than in functions.php; scan a directory or list classes explicitly. Explicit lists beat directory scanning — scanning adds filesystem I/O on every request and hides ordering.
Taxonomies attached to core post types. The same #[Taxonomy] works with postTypes: ['post']. This is often the highest-value use: giving the default post type a proper taxonomy without writing a 40-line register_taxonomy call.
Keep querying plain WordPress. This package registers entities; it does not query them. Use WP_Query(['post_type' => 'book', 'genre' => 'sci-fi']) or get_posts(). Don't expect an Eloquent-style layer — if you want that, that's Pollora's ORM/Model layer, a separate concern.
Testing. Registration is a side effect on global state. For unit tests, assert the generated args array (build the entity, call the args getter) rather than booting WordPress. For integration tests, run registration inside WP_UnitTestCase and assert post_type_exists('book') and taxonomy_exists('genre').
register_post_type() before init triggers _doing_it_wrong and produces "invalid post type" in the admin. Register on init (default priority is fine; use 9 only if something else must read it earlier).hasArchive/custom slugs 404 until rewrite rules are flushed. Flush once on plugin activation or deploy (flush_rewrite_rules() after registration), never on every request. If you're iterating locally, wp rewrite flush is your friend.init.post, page, attachment, revision, nav_menu_item, action, author, order, theme are taken. Taxonomy names are capped at 32 characters, and the reserved-term list (category, tag, post_tag, etc.) will bite you. Validate names before you ship.showInRest: true (and show_in_rest on the taxonomy) or the entity won't appear in Gutenberg's sidebar or the REST API. This is the single most common "it registered but I can't see it" issue.labels() array with __() calls for anything user-facing. Remember menu_name, all_items, add_new_item, search_items.menuIcon (a Dashicons slug or base64 SVG) and menuPosition; two CPTs defaulting to position 25 will collide and one will silently move.capability_type => 'post' means anyone who can edit posts can edit your CPT. For anything with editorial workflow, set capability_type, map_meta_cap: true, and grant caps on activation.get_post_types(['_builtin' => false], 'objects') and get_taxonomies() show what actually registered. post_type_exists() catches typos. Hook register_post_type_args / register_taxonomy_args to inspect or patch args late.show_in_rest and a standard icon); write a custom attribute if you have a repeated cluster of settings. Avoid forking the package for small tweaks.composer.json, read the source before relying on a documented option, and expect API churn. Wrap registration behind your own thin function so a future swap to plain register_post_type() is a one-file change.unregister_post_type() on deactivation is not automatic — content becomes orphaned and invisible. Decide deliberately whether the CPT belongs to the theme or the plugin (it belongs to the plugin).How can I help you explore Laravel packages today?