Skip to content

Features

Laravel, all the way into WordPress

Pollora runs WordPress inside a Laravel application. Here is what that gives you, from the first post type to the production deploy.

01 · Model your content

Content as classes, not arrays

Post types, taxonomies and meta are PHP classes with attributes. Pollora reads them and does the WordPress registration for you.

PHP 8 attributes

Declare post types and taxonomies, never register them

A class and a few attributes replace the register_post_type() array. Auto-discovery finds the class and registers it: nothing to wire in a provider.

  • Slug, singular and plural come from the class name: BookReview gives book-review and “Book Reviews”.
  • One attribute per WordPress argument, with a full reference.
  • Discovery also covers hooks, REST routes and schedules, cached in production.
app/Cms/PostTypes/Event.php
use Pollora\Attributes\PostType;
use Pollora\Attributes\PostType\HasArchive;
use Pollora\Attributes\PostType\Supports;

#[PostType]
#[HasArchive]
#[Supports(['title', 'editor', 'thumbnail'])]
class Event {}

Typed meta Experimental

Meta as typed properties

Declare a meta once, as a typed property marked #[Meta]. Pollora derives register_meta(), sanitization, the REST schema, and reads and writes that come back with the right type.

  • Strings, numbers, booleans, dates, backed enums, arrays and typed objects.
  • On posts, terms, users and comments, including post types you did not declare, such as WooCommerce products.
  • Laravel validation rules, whereMeta() queries and an audit command.
app/Cms/PostTypes/Event.php
#[PostType]
class Event
{
    #[Meta(showInRest: true)]
    public int $capacity = 0;

    #[Meta(showInRest: true)]
    public EventStatus $status = EventStatus::Draft;
}

$event = Meta::of(Event::class, $postId);
$event->capacity;   // int
$event->status;     // EventStatus

Eloquent models

Query WordPress with Eloquent

Posts, pages, attachments, terms, users, comments and menus are Eloquent models over the WordPress tables, with scopes for type, status and taxonomy.

  • Built on pollora/colt, a fork of Corcel: Corcel-style queries keep working.
  • A model bound to a post type reads its typed meta as attributes.
  • A collection loads its meta in one query, through the WordPress cache.
app/Http/Controllers/BookController.php
use Pollora\Models\Post;

$books = Post::type('book')
    ->published()
    ->taxonomy('genre', 'fantasy')
    ->newest()
    ->take(12)
    ->get();

02 · Route and render

A Laravel front end on WordPress content

Laravel routes and controllers where you need them, the template hierarchy everywhere else, Blade views, blocks and Vite.

Hybrid routing

Laravel routes on WordPress conditional tags

Route::wp() sends a WordPress condition to a controller. Routes you declare win; when none matches, the template hierarchy picks a Blade view.

  • Aliases (single, page) or real function names (is_single).
  • WP_Post and WP_Query injected into the controller.
  • Laravel middleware on WordPress routes, and Route::wpMatch() for HTTP verbs.
routes/web.php
Route::wp('home', [BlogController::class, 'index']);
Route::wp('single', [BlogController::class, 'show']);

class BlogController extends Controller
{
    public function show(\WP_Post $post, \WP_Query $query)
    {
        return view('post', ['post' => $post, 'query' => $query]);
    }
}

Blade

Every template in Blade

WordPress still chooses the template; Pollora looks for a .blade.php file first. Layouts, components and directives for the loop and the post data.

  • @posts, @title, @content, @thumbnail, @menu and more, from Sage Directives.
  • Custom page templates declared in a Blade comment.
  • Output escaped by default, views compiled and cached.
resources/views/single.blade.php
@extends('layouts.app')

@section('content')
    @posts
        <article>
            <h1>@title</h1>
            @content
        </article>
    @endposts
@endsection

Blocks Bindings: experimental

Gutenberg blocks in JSX and Blade

Blocks are built with Vite and JSX, rendered with Blade, and registered automatically in themes, plugins and modules. Block Bindings fill core blocks with values computed in PHP.

  • Dynamic by default, inner blocks included, no register_block_type().
  • Four binding sources built in: post meta, term meta, author meta and options.
  • Generators: pollora:make:block and pollora:make:binding.
app/Bindings/EventBinding.php
#[BlockBinding('acme/event', label: 'Event', postTypes: 'event')]
final class EventBinding
{
    #[BindingField(label: 'Booking link', type: 'url')]
    public function bookingUrl(BindingContext $context): string
    {
        return route('events.book', ['event' => $context->postId]);
    }
}

Vite

A modern asset pipeline

The Asset facade enqueues scripts and styles with Vite and hot reload, per theme, plugin or module. Tailwind values land in theme.json at build time.

  • Targets: front end, admin, editor, login screen and customizer.
  • Vite scripts enqueued as WordPress script modules.
  • Colors, font sizes, fonts and radii from Tailwind’s @theme in the editor.
app/Providers/AssetServiceProvider.php
Asset::add('my-vite-asset', 'path/to/asset.js')
    ->container('theme')
    ->useVite()
    ->toFrontend();

03 · React to WordPress

Hooks, the Laravel way

Actions and filters as attributes, WordPress hooks as typed Laravel events, and slow work moved after the response.

Actions & filters

Hooks on methods, events for listeners

Put #[Action] or #[Filter] on a method and it is hooked. WordPress hooks are also dispatched as typed Laravel events, for standard (and queueable) listeners.

  • Services injected into the class, priorities as named arguments.
  • Events for posts, users, media, comments, menus, options, plugins and more.
  • Facades too: Action::add(), Filter::add().
app/Hooks/ContentFilters.php
use Pollora\Attributes\Filter;

class ContentFilters
{
    #[Filter('the_content', priority: 10)]
    public function handleTheContent(string $content): string
    {
        return str_replace('ugly', 'shiny', $content);
    }
}

Async actions New in 13.35.2

Slow handlers, after the response

Add #[Async] next to #[Action], or ->async() to a registration: the handler is queued, and a call to a CRM no longer slows down saving a post.

  • Runs through a Laravel queue worker, Action Scheduler or WP-Cron, picked automatically.
  • Delay, retries, uniqueness and queue options, like Laravel jobs.
  • pollora:doctor reports a declaration it had to run synchronously.
app/Hooks/SyncEventToCrm.php
use Pollora\Attributes\Action;
use Pollora\Attributes\Async;

class SyncEventToCrm
{
    #[Action('save_post_event', priority: 20)]
    #[Async(delay: 60, tries: 3, unique: true)]
    public function handle(int $postId): void
    {
        // Runs after the request
    }
}

04 · Expose and automate

Endpoints, commands and agents

REST routes, AJAX handlers, WP-CLI commands and cron jobs are attributes too, and your site can describe what it does to AI agents.

REST, AJAX, WP-CLI, cron

One attribute per entry point

A REST route, an AJAX handler, a WP-CLI command or a scheduled task is a class or a method with its attribute. Discovery registers them all.

  • REST permission classes: IsAdmin, IsLoggedIn, Can, or your own.
  • AJAX for logged-in users only, unless you open it explicitly.
  • Cron intervals from Every::HOUR to Every::YEAR, or a custom Interval.
app/Api/AdminDocumentAPI.php
#[WpRestRoute(
    namespace: 'app/v2',
    route: 'document/(?P<documentId>\\d+)',
    permissionCallback: IsAdmin::class
)]
class AdminDocumentAPI {}

#[Ajax('load_more', access: AjaxAccess::ALL)]
public function loadMore(): void

#[Schedule(Every::HOUR)]
public function pingSlack(): void

Abilities API

Abilities for AI agents

Declare what your site can do with #[Ability]: the WordPress Abilities API turns it into a registry that AI agents and MCP clients discover and call.

  • Typed input schemas, and permissions that refuse by default.
  • Reads, creates, updates or deletes, declared as a behaviour.
  • Published over REST, and over MCP with the MCP Adapter plugin.
app/Abilities/CreatePost.php
#[Ability(
    name: 'acme/create-post',
    description: 'Creates a post from a title and a status.',
    category: 'acme-content',
    behaviour: Behaviour::Creates,
)]
final class CreatePost implements AbilityHandler
{
    public function handle(Input $input): mixed

05 · Build and run

From first route to production

Laravel authentication on WordPress users, roles in code, a doctor for silent failures, and modules and plugins to organize the code.

Auth & roles

Laravel Auth on WordPress users

The Auth facade works against WordPress accounts, and Laravel authorization answers with WordPress capabilities. Roles can be declared as classes.

  • can(), the can: middleware, @can and @role in Blade.
  • Meta capabilities resolved through map_meta_cap().
  • Role declarations are experimental; capability checks are stable.
app/Roles/EventManager.php
#[Role('event_manager', label: 'Event manager', inherits: 'author')]
#[GrantsPostType(Event::class, Access::Editor)]
#[Grants(EventCap::ExportAttendees, 'moderate_comments')]
#[Without('publish_posts')]
final class EventManager {}

Route::get('/reports', ReportController::class)->middleware('can:manage_options');

Diagnostics

A doctor for silent failures

pollora:doctor finds what fails without an error, and prints the command that fixes it. Tools › Pollora and pollora:status show what was discovered.

  • Core patch, caches, builds, Vite hot file, async actions and more, also in Site Health.
  • Exits with an error code, to gate CI or a deploy; JSON output available.
  • WordPress notices and deprecations logged to a Laravel channel.
php artisan pollora:doctor
  ✓ WordPress core patch — The core is patched, and __() is Pollora's.
  ✗ Composer patches lock — patches.lock.json is older than the framework's patches: Composer applies the old ones, or none.
      johnpbloch/wordpress-core: Patch __ method in l10n to stop conflicting with Laravel
      → composer patches-relock && composer patches-repatch

  1 error(s), 1 warning(s).

Modules & plugins

Modules for the project, plugins to reuse

Group a project’s features in Laravel Modules, or build reusable WordPress plugins with the same architecture. Both get auto-discovery.

  • Providers, post types, hooks, REST routes, schedules, blocks and abilities discovered.
  • Plugins with their own namespace, views, routes, translations, Vite and Tailwind.
  • module:make and pollora:make:plugin to start one.
plugins/my-awesome-plugin/app/MyPluginClass.php
use Pollora\Attributes\Action;

class MyPluginClass
{
    #[Action('wp_enqueue_scripts', priority: 10)]
    public function enqueueScripts(): void
    {
        // Enqueue scripts and styles
    }
}

Try it in one command

A new project with WordPress, Laravel and a Blade theme, ready to run.

composer global require pollora/cli
pollora new example-app