merkushin / wpal
Provides an abstraction layer for WordPress API
Requires
- php: >=7.4
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- nikic/php-parser: ^5.6
- php-stubs/wordpress-stubs: ^7.1
- phpcompatibility/php-compatibility: ^10.0@alpha
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6
- squizlabs/php_codesniffer: ^3.13
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-28 15:07:42 UTC
README
WPAL gives WordPress an object-oriented API that is pleasant to use and easy to unit-test without WordPress.
It has two layers:
- Api (PHP 8.4+): a designed API with real types, value objects, exceptions instead of
WP_Error, and in-memory fakes for tests. Start here. - Service (PHP 7.4+): every WordPress function as a method, generated from WordPress itself and kept in step with each release. Use it for anything the Api doesn't cover yet, or on older PHP.
Api
use Merkushin\Wpal\Api\Posts\SortBy; use Merkushin\Wpal\Wpal; $wp = new Wpal(); $wp->hooks()->onAction( 'wp_enqueue_scripts', function () use ( $wp ): void { $wp->assets()->script( 'my-plugin' ) ->src( plugins_url( 'build/app.js', __FILE__ ) ) ->deps( 'wp-element' ) ->data( 'myPlugin', [ 'limit' => $wp->options()->int( 'my_plugin_limit', 10 ) ] ) ->defer() ->enqueue(); } ); $recent = $wp->posts()->query()->type( 'page' )->sortBy( SortBy::Modified )->limit( 5 )->get(); $post = $wp->posts()->create( title: 'Hello', status: 'publish' ); // throws WordPressError on failure
What changes compared to WordPress:
- Hook callbacks receive as many arguments as they declare: no
$accepted_args.onAction()returns a subscription you canremove(). options()->get()returns your default for a missing option, notfalse;int(),bool(),string()andarray()convert WordPress's stored strings.- Posts come back as
Postvalue objects.find()returnsnull,get()throwsPostNotFound, and failed writes throwWordPressError. - Scripts are built fluently;
data()passes JSON-encoded data safely instead ofwp_localize_script()'s strings.
Abilities and AI
Describe what your plugin can do as an ability, and AI agents, the REST API and other plugins can discover and run
it. WPAL registers it on the right hook, whenever you call register():
$wp->abilities()->define( 'my-plugin/summarize-post' ) ->label( 'Summarize post' ) ->description( 'Returns a one-paragraph summary of a post.' ) ->category( 'content' ) ->input( [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer' ] ], 'required' => [ 'id' ] ] ) ->readonly() ->public() ->requireCapability( 'read' ) ->execute( fn ( array $input ): string => $wp->ai() ->prompt( $wp->posts()->get( $input['id'] )->content ) ->system( 'Summarize in one paragraph.' ) ->generateText() ) ->register(); $summary = $wp->abilities()->execute( 'my-plugin/summarize-post', [ 'id' => 42 ] );
ai() uses the site's configured provider through WordPress's AI Client; generateText() and generateJson() throw
WordPressError instead of returning WP_Error.
Testing
Pass in-memory fakes for the services your code uses; they behave like WordPress without it:
use Merkushin\Wpal\Api\Testing\FakeAi; use Merkushin\Wpal\Api\Testing\FakeHooks; use Merkushin\Wpal\Api\Testing\FakeOptions; use Merkushin\Wpal\Wpal; $wp = new Wpal( hooks: new FakeHooks(), options: new FakeOptions( [ 'my_plugin_limit' => '3' ] ), ai: new FakeAi( [ 'A short summary.' ] ), // scripted responses; prompts are recorded ); ( new MyPlugin( $wp ) )->boot(); $wp->hooks()->doAction( 'init' );
The full reference is in docs/api.md. Coding agents: start with llms.txt.
Service
Every WordPress function, grouped into services such as Posts, Hooks or Assets:
use Merkushin\Wpal\ServiceFactory; $assets = ServiceFactory::create_assets(); $assets->wp_enqueue_script( 'my-plugin', plugins_url( 'build/app.js', __FILE__ ), [], '1.0.0', [ 'in_footer' => true ] );
In tests, swap a service for a mock with ServiceFactory::set_custom_assets( $mock ); the Api's default services pick
it up too. docs/services.md lists which service wraps which function.
Requirements
- PHP 7.4 or later; the Api layer needs PHP 8.4. On older PHP,
new Wpal()throws a clear error and the Service layer works as before. - The latest WordPress version. Each WPAL release targets the WordPress version that was current when it shipped; on an older WordPress, use an older WPAL release.
Compatibility
- Api follows semantic versioning strictly.
- Service mirrors WordPress functions, so it follows WordPress:
- Code that calls a service keeps working across releases unless WordPress itself breaks the same call.
- Implementing service interfaces yourself is not supported: they gain methods and parameters whenever WordPress
does. Use
ServiceFactory::set_custom_*()with mocks (e.g. PHPUnit'screateMock()) for tests. - Deprecated WordPress functions stay available and are marked
@deprecated.
Contributing
See AGENTS.md for the layout, conventions and checks. Run composer check before opening a pull request.
merkushin/wpplugin uses WPAL: https://github.com/merkushin/wpplugin/blob/main/src/Wpplugin.php