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.
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.
Built for the modern PHP developer
Everything you need to build WordPress sites with the tooling you already know and love.
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 {} Write it, it works
No manual registration. Pollora scans your codebase and registers everything automatically. Convention over configuration.
WordPress data, Laravel syntax
Replace PHP template files with Blade. Use directives like @posts, @title, @content for clean, readable templates.
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']); Modern asset pipeline
Vite with HMR, Tailwind CSS v4, and the Asset facade for script and style registration. No more enqueue headaches.
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. Left: WordPress traditional. Right: Pollora.
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'); 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. // 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;
} 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>';
}
} // 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
} 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.
}
} 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');
} 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. // 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. 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. // 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,
],
],
]);
} 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. // 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,
]],
]);
}); 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.
Entity
Fluent interface for post types and taxonomies, built on Extended CPTs. Hexagonal architecture with full type safety.
Query
Fluent WP_Query wrapper. Type-safe method chaining with IDE autocompletion for readable post queries.
Colt
Eloquent ORM models for WordPress. Access posts, users, taxonomies, and options with Laravel's query builder.
Hook
Actions and filters as objects, with callback resolution and reflection caching behind a clean API.
Ajax
AJAX actions with a fluent API. Logged-in users only by default — public access is an explicit opt-in.
Option
WordPress options with validation and immutable value objects. Typed reads, smart upserts.
Abilities
The WordPress 6.9 Abilities API, declared fluently or with #[Ability]. Typed input schemas and deny-by-default permissions, ready for AI agents and MCP.
Tools & plugins
CLI, AI tooling and ready-to-install WordPress plugins.
CLI
Scaffold projects in one command. Interactive setup with optional DDEV integration. Global binary.
Nectar
AI-powered development context. 10 MCP tools for live introspection, 8 agent skills for domain tasks.
MCP Connector
Connect Claude and other MCP clients to WordPress by URL. 27 content-management tools with a built-in OAuth 2.1 provider.
AI Visibility
Make your site legible to AI engines: /llms.txt, a Markdown twin of every page, AI crawler rules and discovery files.
Portcullis
Secure the WordPress login: a secret URL, genuine 404s on wp-login.php and wp-admin, and brute-force lockouts.
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.
MCP tools for live introspection of WordPress, routes, hooks, and components
Agent skills for post types, theming, hooks, blocks, REST API, and more
php artisan nectar:mcp 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.