Search by

Provides an abstraction layer for WordPress API

Package info

github.com/merkushin/WPAL

pkg:composer/merkushin/wpal

Statistics

Installs: 329

Dependents: 1

Suggesters: 0

Stars: 3

Open Issues: 0

0.8.0 2026-10-09 09:33 UTC

This package is not auto-updated.

Last update: 2026-10-09 09:39:46 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 can remove().
  • options()->get() returns your default for a missing option, not false; int(), bool(), string() and array() convert WordPress's stored strings.
  • Posts come back as Post value objects. find() returns null, get() throws PostNotFound, and failed writes throw WordPressError.
  • Scripts are built fluently; data() passes JSON-encoded data safely instead of wp_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.

Shipping only what you use

WPAL is about 2 MB, nearly all of it services a plugin doesn't call. In your release build, after composer install --no-dev, remove the rest:

vendor/bin/wpal-prune --scan=src

--scan finds the services your code uses: WPAL classes it refers to, ServiceFactory::create_*() calls, and Wpal accessors such as ->posts(). It fails if the code refers to something WPAL doesn't have. You can also name services instead: Hooks keeps a Service, api:Hooks an Api service along with the Services it's built on. A plugin that only uses the Service layer ships no PHP 8.4 Api code. Run it before scoping tools such as PHP-Scoper or wp-scoper; --dry-run --format=json prints the plan instead.

Calling a ServiceFactory or Wpal method for a removed service fails with "class not found", so test the built plugin, not only the sources.

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's createMock()) 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