Laravel + WordPress

v13.32.0-beta.2

Modern PHP,
zero compromise.

Build WordPress sites with Laravel's elegance. PHP 8 attributes, Blade templates, auto-discovery, and a full ecosystem of packages.

Book.php
use Pollora\Attributes\PostType;
use Pollora\Attributes\ShowInRest;

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

// That's it. Auto-discovered, registered, ready.

Up and running in one command

Install the CLI, scaffold a project, start building. WordPress + Laravel, ready in under two minutes.

Terminal

Built for the modern PHP developer

Everything you need to build WordPress sites with the tooling you already know and love.

PHP 8 Attributes

Declare, don't register

Post types, taxonomies, hooks, REST endpoints, AJAX handlers, WP-CLI commands, schedules, AI abilities — all defined with PHP 8 attributes. No more register_post_type() calls.

#[PostType('product')]
#[HasArchive]
#[ShowInRest]
class Product {}
Auto-Discovery

Write it, it works

No manual registration. Pollora scans your codebase and registers everything automatically. Convention over configuration.

Blade Templates

WordPress data, Laravel syntax

Replace PHP template files with Blade. Use directives like @posts, @title, @content for clean, readable templates.

Hybrid Routing

Laravel routes + WordPress fallback

Route::wp() maps WordPress conditions to controllers. Custom routes take priority; the template hierarchy handles the rest.

Route::wp('single', [PostController::class, 'show']);
Route::wp('page', 'contact', [ContactController::class, 'index']);
Vite + Tailwind

Modern asset pipeline

Vite with HMR, Tailwind CSS v4, and the Asset facade for script and style registration. No more enqueue headaches.

Type Safety

PHPStan, types everywhere

Constructor property promotion, explicit return types, type-hinted parameters. PHPStan level 5 enforced across the framework.

The difference is visible

Register a custom post type.

WordPress — 25+ lines
function register_book_post_type() {
    register_post_type('book', [
        'labels' => [
            'name'          => 'Books',
            'singular_name' => 'Book',
            'add_new'       => 'Add New Book',
            'edit_item'     => 'Edit Book',
            'view_item'     => 'View Book',
        ],
        'public'       => true,
        'has_archive'  => true,
        'show_in_rest' => true,
        'supports'     => ['title', 'editor', 'thumbnail'],
    ]);
}
add_action('init', 'register_book_post_type');
Pollora — 6 lines
use Pollora\Attributes\PostType;
use Pollora\Attributes\ShowInRest;

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

// Auto-discovered. Auto-registered.
// Labels generated from the class name.
WordPress — scattered across files
// functions.php — action
add_action('init', 'setup_custom_rewrites', 20);

function setup_custom_rewrites() {
    add_rewrite_rule(/* ... */);
}

// functions.php — filter
add_filter('the_content', 'add_cta_to_posts');

function add_cta_to_posts($content) {
    if (is_single()) {
        $content .= '<div class="cta">...</div>';
    }
    return $content;
}
Pollora — one class, typed methods
use Pollora\Attributes\Action;
use Pollora\Attributes\Filter;

class ContentHooks
{
    #[Action('init', priority: 20)]
    public function setupRewrites(): void
    {
        // Auto-discovered, auto-registered
    }

    #[Filter('the_content')]
    public function addCta(string $content): string
    {
        return $content . '<div class="cta">...</div>';
    }
}
WordPress — 30+ lines, fragile
// 1. Register custom 6h interval
add_filter('cron_schedules', function($s) {
    $s['every_6h'] = [
        'interval' => 21600,
        'display'  => 'Every 6 hours',
    ];
    return $s;
});

// 2. Schedule daily cleanup
if (!wp_next_scheduled('daily_cleanup')) {
    wp_schedule_event(time(), 'daily', 'daily_cleanup');
}
add_action('daily_cleanup', 'do_daily_cleanup');

function do_daily_cleanup() {
    // Clean up old data
}

// 3. Schedule feed check every 6h
if (!wp_next_scheduled('check_feeds')) {
    wp_schedule_event(time(), 'every_6h', 'check_feeds');
}
add_action('check_feeds', 'do_check_feeds');

function do_check_feeds() {
    // Check RSS feeds
}
Pollora — one attribute, done
use Pollora\Attributes\Schedule;
use Pollora\Schedule\Every;
use Pollora\Schedule\Interval;

class Maintenance
{
    #[Schedule(Every::DAY)]
    public function dailyCleanup(): void
    {
        // Runs daily. Auto-registered.
    }

    #[Schedule(new Interval(hours: 6))]
    public function checkFeeds(): void
    {
        // Custom interval. No cron_schedules filter.
    }
}
WordPress — one array, loose callbacks
add_action('rest_api_init', function () {
    register_rest_route('app/v2', '/document/(?P<documentId>\\d+)', [
        [
            'methods'             => 'GET',
            'callback'            => 'app_get_document',
            'permission_callback' => 'app_is_admin',
            'args' => [
                'documentId' => ['validate_callback' => 'is_numeric'],
            ],
        ],
        [
            'methods'             => ['DELETE', 'POST'],
            'callback'            => 'app_delete_document',
            'permission_callback' => 'app_is_admin',
        ],
    ]);
});

function app_get_document(WP_REST_Request $request) {
    $id = (int) $request->get_param('documentId');
    return new WP_REST_Response(['documentId' => $id]);
}

function app_delete_document(WP_REST_Request $request) {
    $id = (int) $request->get_param('documentId');
    return new WP_REST_Response(['deleted' => $id]);
}

function app_is_admin() {
    return current_user_can('manage_options');
}
Pollora — a class, typed methods
use Pollora\Attributes\WpRestRoute;
use Pollora\Attributes\WpRestRoute\Method;
use Pollora\Attributes\WpRestRoute\Permissions\IsAdmin;

#[WpRestRoute(
    namespace: 'app/v2',
    route: 'document/(?P<documentId>\\d+)',
    permissionCallback: IsAdmin::class,
)]
class DocumentAPI
{
    #[Method('GET')]
    public function get(int $documentId): WP_REST_Response
    {
        return new WP_REST_Response(['documentId' => $documentId]);
    }

    #[Method(['DELETE', 'POST'])]
    public function delete(int $documentId): WP_REST_Response
    {
        return new WP_REST_Response(['deleted' => $documentId]);
    }
}

// Route params injected as typed arguments.
WordPress — one hook per audience
// Logged-in users only
add_action('wp_ajax_subscribe', 'app_subscribe');

// Everyone — two hooks to keep in sync
add_action('wp_ajax_load_more', 'app_load_more');
add_action('wp_ajax_nopriv_load_more', 'app_load_more');

// Guests only
add_action('wp_ajax_nopriv_track_visit', 'app_track_visit');

function app_subscribe() {
    wp_send_json_success(['message' => 'Subscribed!']);
}

function app_load_more() {
    wp_send_json_success([/* ... */]);
}

function app_track_visit() {
    wp_send_json_success([/* ... */]);
}

// Forget a nopriv_ hook: silently broken for guests.
// Add one by mistake: open to the whole internet.
Pollora — secure by default
use Pollora\Attributes\Ajax;
use Pollora\Ajax\Domain\Model\AjaxAccess;

class NewsletterHandler
{
    #[Ajax('subscribe')]
    public function subscribe(): void
    {
        wp_send_json_success(['message' => 'Subscribed!']);
    }

    #[Ajax('load_more', access: AjaxAccess::ALL)]
    public function loadMore(): void
    {
        wp_send_json_success([/* ... */]);
    }

    #[Ajax('track_visit', access: AjaxAccess::GUEST)]
    public function trackVisit(): void
    {
        wp_send_json_success([/* ... */]);
    }
}

// Logged-in only by default. Public is opt-in.
WordPress — manual require, synopsis array
// commands/class-greet-command.php
class Greet_Command {
    public function __invoke($args, $assoc_args) {
        $greeting = $assoc_args['greeting'] ?? 'Hello';
        WP_CLI::success("{$greeting}, {$args[0]}!");
    }
}

// functions.php
if (defined('WP_CLI') && WP_CLI) {
    require_once __DIR__ . '/commands/class-greet-command.php';

    WP_CLI::add_command('greet', 'Greet_Command', [
        'shortdesc' => 'Greets someone.',
        'synopsis'  => [
            [
                'type' => 'positional',
                'name' => 'name',
            ],
            [
                'type'     => 'assoc',
                'name'     => 'greeting',
                'optional' => true,
            ],
        ],
    ]);
}
Pollora — one attribute, auto-discovered
use Pollora\Attributes\WpCli;
use Pollora\Attributes\WpCli\Synopsis;

#[WpCli]
#[Synopsis('<name> [--greeting=<greeting>]')]
class GreetCommand
{
    public function __invoke(array $arguments, array $options): void
    {
        $greeting = $options['greeting'] ?? 'Hello';
        WP_CLI::success("{$greeting}, {$arguments[0]}!");
    }
}

// $ wp greet John --greeting=Hi
// Slug from the class name. No require, no guard.
WordPress 6.9 — two hooks, raw JSON Schema
// 1. Register the category
add_action('wp_abilities_api_categories_init', function () {
    wp_register_ability_category('acme-content', [
        'label'       => 'Editorial',
        'description' => 'Posts and pages.',
    ]);
});

// 2. Register the ability — raw JSON Schema
add_action('wp_abilities_api_init', function () {
    wp_register_ability('acme/create-post', [
        'label'        => 'Create Post',
        'description'  => 'Creates a post from a title and a status.',
        'category'     => 'acme-content',
        'input_schema' => [
            'type'       => 'object',
            'properties' => [
                'title'  => ['type' => 'string', /* ... */],
                'status' => ['type' => 'string', 'enum' => [/* ... */]],
            ],
            'required' => ['title'],
        ],
        'permission_callback' => fn () => current_user_can('edit_posts'),
        'execute_callback'    => function ($input) {
            return ['id' => wp_insert_post([
                'post_title'  => $input['title'] ?? '',
                'post_status' => $input['status'] ?? 'draft',
            ])];
        },
        'meta' => ['annotations' => [
            'readonly' => false, 'destructive' => false, 'idempotent' => false,
        ]],
    ]);
});
Pollora — one attribute, a testable class
use Pollora\Attributes\Ability;
use Pollora\Abilities\Domain\Model\Behaviour;

#[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 schema(SchemaBuilder $schema): void
    {
        $schema->string('title', 'Title of the post.', required: true);
        $schema->enum('status', 'Publication status.', ['draft', 'publish']);
    }

    public function authorize(Input $input): mixed
    {
        return current_user_can('edit_posts');
    }

    public function handle(Input $input): mixed
    {
        return ['id' => wp_insert_post([
            'post_title'  => $input->string('title'),
            'post_status' => $input->string('status', 'draft'),
        ])];
    }
}

// Category auto-declared. Ready for MCP clients.

A complete ecosystem

Modular packages that work independently or together. Each one solves one problem well.

Core packages

Power the framework — and work in any WordPress project.

Tools & plugins

CLI, AI tooling and ready-to-install WordPress plugins.

Two themes, two purposes

Start with a clean default or a conversion-ready WooCommerce storefront. Both use Blade, Vite, and Tailwind CSS.

pollora:make-theme starter

Clean, minimal, ready to customize

Default Theme

Blade templates, Vite with HMR, Tailwind CSS v4, theme-specific service providers and configuration. The right starting point for any project.

--template=apiary

WooCommerce, conversion-optimized

Apiary WooCommerce

Sticky add-to-cart, search autocomplete, Alpine.js interactivity, responsive product grids, and full checkout override. Built for conversion.

pollora/nectar

AI-native development

Nectar gives AI coding agents deep context about your Pollora project. Built on Laravel Boost, it provides live introspection via MCP and domain-specific skills for every framework feature.

10

MCP tools for live introspection of WordPress, routes, hooks, and components

8

Agent skills for post types, theming, hooks, blocks, REST API, and more

Compatible with
Claude Code
Cursor
Windsurf
Any MCP-compatible agent
php artisan nectar:mcp
Pollora documentation mascot

Documentation

Everything you need, explained

From installation to advanced plugin development — comprehensive guides, API references, and real code examples. Searchable, and AI-ready with llms.txt.

Start building with Pollora

One command to install. Laravel conventions from day one. WordPress power underneath.

Found a bug? Have an idea?

Open an issue on GitHub. Every report, question and feature request helps shape Pollora.