Skip to content

Post Types

Pollora uses PHP 8 attributes to declare custom post types, providing a clean, declarative syntax with automatic discovery and registration.

Create a class with the #[PostType] attribute anywhere in your application:

<?php
namespace App\Cms\PostTypes;
use Pollora\Attributes\PostType;
use Pollora\Attributes\PostType\PubliclyQueryable;
use Pollora\Attributes\PostType\HasArchive;
use Pollora\Attributes\PostType\Supports;
use Pollora\Attributes\PostType\MenuIcon;
#[PostType]
#[PubliclyQueryable]
#[HasArchive]
#[Supports(['title', 'editor', 'thumbnail'])]
#[MenuIcon('dashicons-calendar')]
class Event
{
}

You can also generate a post type class using Artisan:

Terminal window
php artisan pollora:make:post-type Event
#[PostType] // Auto-generated slug, singular, and plural
#[PostType('book')] // Custom slug, auto-generated names
#[PostType('product', singular: 'Product', plural: 'Products')] // All custom

Parameters:

  • $slug (optional): Post type slug. Auto-generated from class name in kebab-case if omitted
  • $singular (optional): Singular name. Auto-generated from class name if omitted
  • $plural (optional): Plural name. Auto-pluralized from singular if omitted
  • $textDomain (optional): Text domain for label translations. Defaults to 'pollora'

Pollora generates slug, singular, and plural names from your class name:

#[PostType] class Event {} // slug="event", singular="Event", plural="Events"
#[PostType] class EventCategory {} // slug="event-category", singular="Event Category", plural="Event Categories"
#[PostType] class BookReview {} // slug="book-review", singular="Book Review", plural="Book Reviews"

All labels (menu names, descriptions, etc.) are automatically generated from the singular and plural names.

AttributePurposeUsage
PubliclyQueryableFront-end queryable#[PubliclyQueryable] or #[PubliclyQueryable(false)]
PublicPostTypeVisible in admin + front end#[PublicPostType] or #[PublicPostType(false)]
HasArchiveEnable archive page#[HasArchive] or #[HasArchive('custom-slug')]
HierarchicalParent/child like pages#[Hierarchical] or #[Hierarchical(false)]
RewriteURL rewrite rules#[Rewrite(['slug' => 'events', 'with_front' => false])]
ShowInRestREST API + Gutenberg#[ShowInRest] or #[ShowInRest(false)]
AttributePurposeUsage
SupportsSupported features#[Supports(['title', 'editor', 'thumbnail'])]
TaxonomiesAssociated taxonomies#[Taxonomies(['category', 'post_tag'])]
TemplateDefault block template#[Template([['core/heading', ['level' => 2]]])]
BlockEditorGutenberg on/off#[BlockEditor] or #[BlockEditor(false)]
AttributePurposeUsage
ShowUIAdmin management UI#[ShowUI] or #[ShowUI(false)]
ShowInMenuAdmin menu placement#[ShowInMenu], #[ShowInMenu(false)], or #[ShowInMenu('tools.php')]
MenuIconAdmin menu icon#[MenuIcon('dashicons-calendar')]
LabelsOverride specific labels#[Labels(addNew: 'New Project', notFound: 'None found.')]

Note: #[Labels] only accepts constant expressions — __() is not allowed. For translatable labels, use withArgs() or configuring() (see Internationalization).

Support options for #[Supports]: title, editor, author, thumbnail, excerpt, comments, revisions, custom-fields, page-attributes.

Post types with #[PostType] are automatically discovered and registered — no manual registration or configuration needed.

Provide additional WordPress post type arguments merged with attribute-generated ones:

#[PostType('book')]
#[PubliclyQueryable]
#[HasArchive]
class Book
{
public function withArgs(): array
{
return [
'description' => 'A custom book post type for our library',
'show_in_rest' => true,
];
}
}

Programmatically configure the post type before registration using the Entity API:

#[PostType('book')]
#[PubliclyQueryable]
#[HasArchive]
class Book
{
public function configuring(\Pollora\Entity\Domain\Model\PostType $postType): void
{
$postType->labels([
'name' => 'Library Books',
'singular_name' => 'Library Book',
'add_new_item' => 'Add New Book to Library',
]);
$postType->supports(['title', 'editor', 'thumbnail', 'custom-fields']);
$postType->capabilityType('book');
$postType->showInRest();
$postType->restBase('library-books');
$postType->rewrite(['slug' => 'library/books', 'with_front' => false]);
}
}

Key features:

  • Receives a \Pollora\Entity\Domain\Model\PostType instance with the full Entity API
  • Configurations in configuring() take priority over attributes; attributes fill remaining gaps
  • Labels are smart-merged: your custom labels override, missing labels auto-generate

Available Entity methods:

public function configuring(\Pollora\Entity\Domain\Model\PostType $postType): void
{
// Labels and descriptions
$postType->label('Custom Label');
$postType->labels(['name' => 'Books', 'singular_name' => 'Book']);
$postType->description('A custom book post type');
// Visibility (parameterless = true, use inverse methods for false)
$postType->public(); // or ->private()
$postType->publiclyQueryable(); // or ->notPubliclyQueryable()
$postType->showUi(); // or ->hideUi()
$postType->showInMenu(); // or ->hideFromMenu()
$postType->showInNavMenus(); // or ->hideFromNavMenus()
$postType->showInAdminBar(); // or ->hideFromAdminBar()
// Features and capabilities
$postType->supports(['title', 'editor', 'thumbnail']);
$postType->capabilityType('post');
$postType->mapMetaCap(); // or ->withoutMetaCap()
// Archive and hierarchical
$postType->hasArchive(true); // accepts bool|string
$postType->nonHierarchical(); // or ->hierarchical()
// REST API
$postType->showInRest(); // or ->hideFromRest()
$postType->restBase('books');
$postType->restNamespace('wp/v2');
// URL rewriting
$postType->rewrite(['slug' => 'books', 'with_front' => false]);
$postType->queryVar(true); // accepts bool|string
// Menu configuration
$postType->menuPosition(5);
$postType->menuIcon('dashicons-book');
// Search and export
$postType->includeInSearch(); // or ->excludeFromSearch()
$postType->canExport(); // or ->cannotExport()
$postType->keepOnUserDelete(); // or ->deleteWithUser()
}

Pollora provides three approaches for translatable post type labels:

Pollora auto-generates labels using sprintf(__('Edit %s', 'pollora'), $singular). The textDomain parameter lets you use your own domain:

#[PostType('project', textDomain: 'my-theme')]

Partial label overrides using named parameters (no __() — constant expressions only):

#[PostType]
#[Labels(addNew: 'New Project', notFound: 'No projects found.')]
class Project {}

For fully translatable labels with __() calls evaluated at runtime:

#[PostType]
#[HasArchive]
class Service
{
public function withArgs(): array
{
return [
'labels' => [
'name' => __('Services', 'my-theme'),
'singular_name' => __('Service', 'my-theme'),
'add_new_item' => __('Add New Service', 'my-theme'),
],
];
}
}
#[PostType]
class Event
{
public function configuring(\Pollora\Entity\Domain\Model\PostType $postType): void
{
$postType->labels([
'name' => __('Events', 'my-theme'),
'singular_name' => __('Event', 'my-theme'),
'add_new_item' => __('Add New Event', 'my-theme'),
]);
}
}
ApproachTranslatableExtractible by make-potUse case
Auto-generatedYes (via framework .pot)YesDefault behavior, no code needed
#[Labels]NoNoQuick static overrides
withArgs()YesYesFull i18n support
configuring()YesYesDynamic + i18n support

For a complete list of all available attributes, see Post Type Attributes Reference.