pollora / wp-rest
WordPress REST API routes declared with PHP attributes: a controller class, #[WpRestRoute] and #[Method], permissions as classes. Runs with or without Laravel.
Requires
- php: ^8.3
- pollora/attributes: ^0.1 || ^1.0
- pollora/discovery: ^0.1 || ^1.0
- psr/container: ^2.0
- psr/log: ^3.0
Requires (Dev)
- antecedent/patchwork: ^2.2
- brain/monkey: ^2.7
- laravel/pint: ^1.22.1
- mockery/mockery: ^1.6.12
- pestphp/pest: ^3.8.2
- phpstan/phpstan: ^2.1.16
- pollora/harness: ^0.1.9
- szepeviktor/phpstan-wordpress: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 2.0.0
- v0.2.1
- v0.2.0
- v0.1.0
- dev-chore/harness-0-1-20
- dev-fix/arch-rules
- dev-chore/harness-0-1-19
- dev-chore/harness-0-1-15
- dev-chore/pause-downstream
- dev-feat/private-by-default
- dev-chore/harness-0.1.6
- dev-chore/harness-0.1.4
- dev-chore/harness-enforce
- dev-chore/release-0.2.1
- dev-fix/filter-name-convention
- dev-chore/adopt-harness
- dev-chore/release-0.2.0
- dev-fix/doing-it-wrong-outside-rest
- dev-feat/explicit-public-routes
This package is auto-updated.
Last update: 2026-11-04 15:31:46 UTC
README
WordPress REST API routes written as a controller class: #[WpRestRoute] on the class gives the namespace and route, #[Method] on a method gives its HTTP verbs, and a permission class decides who may call it. No register_rest_route() call, no rest_api_init callback to write.
The controller is built with its constructor dependencies, its methods receive the route parameters they name and the WP_REST_Request when they ask for it.
Part of Pollora, the Laravel framework for WordPress, where it is installed and wired for you: see REST API. It runs without Laravel too, in a WordPress plugin, a Bedrock site or any PHP project.
Installation
composer require pollora/wp-rest
Requires PHP 8.3+. It builds on pollora/attributes and pollora/discovery.
Declaring routes
use Pollora\Attributes\WpRestRoute; use Pollora\Attributes\WpRestRoute\Method; use Pollora\WpRest\Permissions\Can; use Pollora\WpRest\Permissions\IsLoggedIn; #[WpRestRoute('app/v1', 'documents/(?P<documentId>\\d+)', permissionCallback: IsLoggedIn::class)] final class DocumentApi { public function __construct(private readonly DocumentRepository $documents) {} #[Method('GET')] public function show(int $documentId): array { return $this->documents->find($documentId); } #[Method('DELETE', permissionCallback: new Can('delete_post', parameter: 'documentId'))] public function destroy(WP_REST_Request $request, int $documentId): array { return ['deleted' => $this->documents->delete($documentId)]; } }
GET wp-json/app/v1/documents/18callsshow(18). A parameter typedWP_REST_Requestreceives the request; the others are read from the route by name.#[Method]acceptsGET,POST,PUT,DELETEandPATCH, alone or as a list; with any other verb the route is not registered, and the logger says why.- A method's permission replaces the route's. Without either, the route is refused (401 or 403,
rest_forbidden). Say so withEveryone::classwhen a route is public on purpose.
Permissions
| Class | Allows |
|---|---|
Everyone |
anyone, logged in or not: a route that is public on purpose |
IsLoggedIn |
any logged-in user |
IsAdmin |
users who can manage_options |
new Can('edit_posts') |
users with a capability; parameter: passes a request parameter for a meta capability such as edit_post |
A permission class of your own implements Pollora\Attributes\WpRestRoute\Permission: allow(WP_REST_Request $request): bool|WP_Error.
While a site migrates from 1.x, the routes that declare no permission can be kept public for a time. WordPress is then told which routes to fix (shown with WP_DEBUG, on the pages that load the routes, never inside a REST response):
add_filter('pollora/wp-rest/private_by_default', '__return_false');
Wiring it outside Pollora
Point the discovery engine at your code and add WpRestDiscovery; routes are registered on rest_api_init.
use Pollora\Discovery\Domain\Models\DiscoveryLocation; use Pollora\Discovery\Infrastructure\Services\DiscoveryEngine; use Pollora\WpRest\Infrastructure\Services\WpRestDiscovery; (new DiscoveryEngine) ->addLocation(new DiscoveryLocation('MyPlugin\\', __DIR__.'/src')) ->addDiscovery('wp_rest_routes', new WpRestDiscovery) ->run();
Controllers are built by the engine's instance resolver: by reflection by default (constructor parameters typed with a class are built in turn), by your container if you give the engine a ContainerInstanceResolver (PSR-11). WpRestDiscovery takes a PSR-3 logger, which records the routes it could not register.
Testing
composer test # unit tests, PHPStan, Pint
License
MIT. See LICENSE.