Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
pollora-rest-api
pollora-rest-api
pollora/nectar
claude-code
wordpress
rest-api
attributes
route-parameters
permissions
pollora

Build WordPress REST API endpoints and AJAX handlers using Pollora WpRestRoute and Ajax attributes with automatic discovery and permission management.

Install
php artisan boost:add-skill pollora/nectar

Save this content to: .claude/skills/pollora-rest-api/SKILL.md

# Pollora REST API Development

## When to use this skill
Use this skill when creating WordPress REST API endpoints using Pollora's attribute-based approach or theme API routes.

## Attribute-Based REST API (WordPress REST)

### Basic Endpoint

```php
<?php

namespace Theme\MyTheme\Cms\Api;

use Pollora\Attributes\WpRestRoute;
use Pollora\Attributes\WpRestRoute\Method;
use Pollora\WpRest\Permissions\Everyone;
use Pollora\WpRest\Permissions\IsLoggedIn;
use WP_REST_Response;

#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'items',
    permissionCallback: Everyone::class
)]
class ItemsAPI
{
    #[Method('GET')]
    public function index(): WP_REST_Response
    {
        return new WP_REST_Response([
            'items' => ['Item 1', 'Item 2'],
        ]);
    }

#[Method('POST', permissionCallback: IsLoggedIn::class)]
    public function store(\WP_REST_Request $request): WP_REST_Response
    {
        $name = $request->get_param('name');

        return new WP_REST_Response([
            'success' => true,
            'item' => $name,
        ], 201);
    }
}
```

Endpoint: `GET /wp-json/my-theme/v1/items`

### Route Parameters

```php
#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'documents/(?P<documentId>\d+)',
    permissionCallback: Everyone::class
)]
class DocumentAPI
{
    #[Method('GET')]
    public function get(int $documentId): WP_REST_Response
    {
        return new WP_REST_Response([
            'documentId' => $documentId,
        ]);
    }

#[Method(['PUT', 'PATCH'], permissionCallback: IsLoggedIn::class)]
    public function update(\WP_REST_Request $request, int $documentId): WP_REST_Response
    {
        return new WP_REST_Response([
            'updated' => true,
            'documentId' => $documentId,
        ]);
    }

    #[Method('DELETE', permissionCallback: IsAdmin::class)]
    public function delete(int $documentId): WP_REST_Response
    {
        return new WP_REST_Response(null, 204);
    }
}
```

Endpoint: `GET /wp-json/my-theme/v1/documents/18`

### Permissions

Always declare a permission. A route without one is public, and `pollora/wp-rest` 0.2 warns about it (`_doing_it_wrong`); the next major version refuses it. Use `Everyone::class` for a route that is public on purpose, a method's permission replaces its route's, or create a custom class:

```php
use Pollora\WpRest\Permissions\Can;
use Pollora\WpRest\Permissions\Everyone;
use Pollora\WpRest\Permissions\IsAdmin;
use Pollora\WpRest\Permissions\IsLoggedIn;

// Built-in permissions
#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'admin/settings',
    permissionCallback: IsAdmin::class
)]
class SettingsAPI {}

// A capability (v13.34.5+): a string or a #[CapabilitySet] enum case
#[WpRestRoute(namespace: 'my-theme/v1', route: 'reports', permissionCallback: new Can('export_attendees'))]
class ReportsAPI {}

// A meta capability on the object in the request: edit_post with the `id` parameter
#[Method('PUT', permissionCallback: new Can('edit_post', parameter: 'id'))]
public function update(int $id): array {}

// Custom permission
use Pollora\Attributes\WpRestRoute\Permission;

class CanManageBooks implements Permission
{
    public function allow(\WP_REST_Request $request): bool|\WP_Error
    {
        return current_user_can('edit_books')
            ? true
            : new \WP_Error('forbidden', __('You cannot manage books.'), ['status' => 403]);
    }
}

#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'books',
    permissionCallback: CanManageBooks::class
)]
class BooksAPI {}
```

A `Can` refusal answers 401 to a guest and 403 to a logged-in user. Prefer `Can` to a custom class that only calls `current_user_can()` (see the `pollora-roles` skill).

## Theme API Routes (Lightweight Mode)

For performance-critical API endpoints, themes can define routes that load WordPress **without plugins** (~100ms vs ~1.3s):

```php
// themes/my-theme/routes/api.php
use Illuminate\Support\Facades\Route;

Route::get('/products/search', [ProductSearchController::class, 'search']);
Route::get('/products/{id}', [ProductSearchController::class, 'show']);
```

Endpoint: `GET /api/products/search`

### Plugin Loading Control

```php
// config/wordpress.php
'api_plugins' => [],                           // No plugins (fastest, default)
'api_plugins' => ['woocommerce', 'acf-pro'],   // Selective loading
'api_plugins' => ['*'],                        // All plugins
```

## AJAX Handlers (admin-ajax.php)

For `admin-ajax.php` actions, put `#[Ajax]` on a public method. The class is discovered and built through the container. Handlers are **logged-in users only** unless `access` says otherwise.

```php
use Pollora\Attributes\Ajax;
use Pollora\Ajax\Domain\Model\AjaxAccess;

class NewsletterHandler
{
    #[Ajax('subscribe')]                               // wp_ajax_subscribe (logged in)
    public function subscribe(): void
    {
        check_ajax_referer('newsletter');
        wp_send_json_success(['message' => 'Subscribed!']);
    }

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

`AjaxAccess::LOGGED` (default), `AjaxAccess::ALL`, `AjaxAccess::GUEST`. The imperative form is `Ajax::listen('action', $handler)->forAllUsers()` / `->forGuestUsers()`.

## Choosing Between Approaches

| Feature | WpRestRoute Attributes | Theme API Routes |
|---------|----------------------|------------------|
| WordPress REST API | Yes (`/wp-json/...`) | No (`/api/...`) |
| Plugin access | Full WordPress context | Configurable (none/selective/all) |
| Performance | Standard (~1.3s) | Fast (~100ms without plugins) |
| Authentication | WordPress nonces/cookies | Laravel middleware |
| Discovery | Auto-discovered | File-based (`routes/api.php`) |

## Important Notes

- REST API classes are auto-discovered — no manual `register_rest_route()` needed
- Route parameters use WordPress regex syntax: `(?P<paramName>\d+)`
- Method parameters are auto-injected from route parameters by name
- The `WP_REST_Request` object is available via type-hint
- Run `php artisan discovery:clear` after adding new REST API classes
- Use theme API routes when you need maximum performance and don't need full WordPress context

Pollora REST API Development

When to use this skill

Use this skill when creating WordPress REST API endpoints using Pollora's attribute-based approach or theme API routes.

Attribute-Based REST API (WordPress REST)

Basic Endpoint

<?php

namespace Theme\MyTheme\Cms\Api;

use Pollora\Attributes\WpRestRoute;
use Pollora\Attributes\WpRestRoute\Method;
use Pollora\WpRest\Permissions\Everyone;
use Pollora\WpRest\Permissions\IsLoggedIn;
use WP_REST_Response;

#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'items',
    permissionCallback: Everyone::class
)]
class ItemsAPI
{
    #[Method('GET')]
    public function index(): WP_REST_Response
    {
        return new WP_REST_Response([
            'items' => ['Item 1', 'Item 2'],
        ]);
    }

#[Method('POST', permissionCallback: IsLoggedIn::class)]
    public function store(\WP_REST_Request $request): WP_REST_Response
    {
        $name = $request->get_param('name');

        return new WP_REST_Response([
            'success' => true,
            'item' => $name,
        ], 201);
    }
}

Endpoint: GET /wp-json/my-theme/v1/items

Route Parameters

#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'documents/(?P<documentId>\d+)',
    permissionCallback: Everyone::class
)]
class DocumentAPI
{
    #[Method('GET')]
    public function get(int $documentId): WP_REST_Response
    {
        return new WP_REST_Response([
            'documentId' => $documentId,
        ]);
    }

#[Method(['PUT', 'PATCH'], permissionCallback: IsLoggedIn::class)]
    public function update(\WP_REST_Request $request, int $documentId): WP_REST_Response
    {
        return new WP_REST_Response([
            'updated' => true,
            'documentId' => $documentId,
        ]);
    }

    #[Method('DELETE', permissionCallback: IsAdmin::class)]
    public function delete(int $documentId): WP_REST_Response
    {
        return new WP_REST_Response(null, 204);
    }
}

Endpoint: GET /wp-json/my-theme/v1/documents/18

Permissions

Always declare a permission. A route without one is public, and pollora/wp-rest 0.2 warns about it (_doing_it_wrong); the next major version refuses it. Use Everyone::class for a route that is public on purpose, a method's permission replaces its route's, or create a custom class:

use Pollora\WpRest\Permissions\Can;
use Pollora\WpRest\Permissions\Everyone;
use Pollora\WpRest\Permissions\IsAdmin;
use Pollora\WpRest\Permissions\IsLoggedIn;

// Built-in permissions
#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'admin/settings',
    permissionCallback: IsAdmin::class
)]
class SettingsAPI {}

// A capability (v13.34.5+): a string or a #[CapabilitySet] enum case
#[WpRestRoute(namespace: 'my-theme/v1', route: 'reports', permissionCallback: new Can('export_attendees'))]
class ReportsAPI {}

// A meta capability on the object in the request: edit_post with the `id` parameter
#[Method('PUT', permissionCallback: new Can('edit_post', parameter: 'id'))]
public function update(int $id): array {}

// Custom permission
use Pollora\Attributes\WpRestRoute\Permission;

class CanManageBooks implements Permission
{
    public function allow(\WP_REST_Request $request): bool|\WP_Error
    {
        return current_user_can('edit_books')
            ? true
            : new \WP_Error('forbidden', __('You cannot manage books.'), ['status' => 403]);
    }
}

#[WpRestRoute(
    namespace: 'my-theme/v1',
    route: 'books',
    permissionCallback: CanManageBooks::class
)]
class BooksAPI {}

A Can refusal answers 401 to a guest and 403 to a logged-in user. Prefer Can to a custom class that only calls current_user_can() (see the pollora-roles skill).

Theme API Routes (Lightweight Mode)

For performance-critical API endpoints, themes can define routes that load WordPress without plugins (~100ms vs ~1.3s):

// themes/my-theme/routes/api.php
use Illuminate\Support\Facades\Route;

Route::get('/products/search', [ProductSearchController::class, 'search']);
Route::get('/products/{id}', [ProductSearchController::class, 'show']);

Endpoint: GET /api/products/search

Plugin Loading Control

// config/wordpress.php
'api_plugins' => [],                           // No plugins (fastest, default)
'api_plugins' => ['woocommerce', 'acf-pro'],   // Selective loading
'api_plugins' => ['*'],                        // All plugins

AJAX Handlers (admin-ajax.php)

For admin-ajax.php actions, put #[Ajax] on a public method. The class is discovered and built through the container. Handlers are logged-in users only unless access says otherwise.

use Pollora\Attributes\Ajax;
use Pollora\Ajax\Domain\Model\AjaxAccess;

class NewsletterHandler
{
    #[Ajax('subscribe')]                               // wp_ajax_subscribe (logged in)
    public function subscribe(): void
    {
        check_ajax_referer('newsletter');
        wp_send_json_success(['message' => 'Subscribed!']);
    }

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

AjaxAccess::LOGGED (default), AjaxAccess::ALL, AjaxAccess::GUEST. The imperative form is Ajax::listen('action', $handler)->forAllUsers() / ->forGuestUsers().

Choosing Between Approaches

Feature WpRestRoute Attributes Theme API Routes
WordPress REST API Yes (/wp-json/...) No (/api/...)
Plugin access Full WordPress context Configurable (none/selective/all)
Performance Standard (~1.3s) Fast (~100ms without plugins)
Authentication WordPress nonces/cookies Laravel middleware
Discovery Auto-discovered File-based (routes/api.php)

Important Notes

  • REST API classes are auto-discovered — no manual register_rest_route() needed
  • Route parameters use WordPress regex syntax: (?P<paramName>\d+)
  • Method parameters are auto-injected from route parameters by name
  • The WP_REST_Request object is available via type-hint
  • Run php artisan discovery:clear after adding new REST API classes
  • Use theme API routes when you need maximum performance and don't need full WordPress context
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment