alleyinteractive/wp-caper
WP Caper is a WordPress library from Alley Interactive for defining and managing custom capabilities and roles in code. It offers a clean API for registering capabilities and assigning them to roles, keeping permission logic consistent, version-controlled, and easier to maintain across environments.
WP Caper is Alley Interactive's library for defining WordPress roles and capabilities in code instead of clicking them into the database via a plugin UI. In a Laravel shop you typically meet it in a Bedrock-style repo where WordPress lives alongside the Laravel app as a Composer dependency — the value is that roles/caps become reviewable, diffable, deployable artifacts rather than untracked rows in the wp_user_roles option.
Minimal steps:
composer require alleyinteractive/wp-caper in the WordPress side of the repo (not the Laravel app, unless you are running WP inside Laravel).// web/app/mu-plugins/roles.php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
Mu-plugins are not autoloaded by Composer, so the explicit require is mandatory and is the single most common reason "the package does nothing."init. Run the CLI command with a dry-run/no-op flag first if one is exposed, and diff the output against the live site before committing.Where to look first: the CHANGELOG entry for 3.0.0 (#300) and the README's upgrade notes. This is a major version bump with a link-only release note, which by convention means breaking changes — class names, constructor signatures, and CLI command names are all fair game. Grep your codebase for the bootstrap call and every CLI invocation before you bump the constraint. Do not assume the 2.x API surface survived.
First use case: take the pile of add_role() / add_cap() calls scattered across functions.php and a permissions plugin export, collapse them into one declarative definition, and apply it through your deploy pipeline. That alone usually justifies adoption.
Definition-as-code, applied by CLI, not by request. The tempting pattern is to hook definitions onto init so they "always apply." Resist it: that turns every page load into a potential DB write and makes production state depend on traffic. Instead treat cap application like a migration — run it in the deploy script after composer install and before the new code takes traffic:
deploy: composer install --no-dev → wp caper apply → reload php-fpm
Idempotency and reconciliation. The package's model is declarative; your job is to decide the reconciliation policy. Adding caps is safe and repeatable. Removing them requires an explicit apply that prunes — confirm whether v3 prunes by default or only adds. Assume "add-only" until proven otherwise; silent non-removal is the bug that bites six months later when a deprecated manage_secrets cap is still granted to every editor.
One file per role, keyed by slug. Long single arrays rot. Keep a directory of roles/editor.php, roles/shop_manager.php, plus a capabilities.php of shared custom caps with a prefix (acme_manage_orders) to avoid colliding with core or third-party plugins.
Laravel as the source of truth for app permissions. The clean split is: Laravel owns application-level authorization (Gates/Policies backed by an enum of slugs), WordPress owns the enforcement surface. Share the slugs via a config file or generated enum, and have Laravel publish cap changes to WP through a queued job or a deploy hook — never call WP functions from Laravel directly. If the Laravel app needs to read current caps, go through WP-CLI JSON output or a small REST endpoint rather than reaching into the options table.
Multi-site. Definitions are per-site, so loop the sites explicitly in the deploy command. Do not assume a network-wide apply exists or is what you want; a per-site loop is easier to reason about and to re-run partially.
Testing. Assert the resulting capability state, not the definition array. A test that checks "editor has edit_others_posts" against a booted WP test install catches the reconciliation bugs that unit-testing your array never will. Also test with a real non-admin user — see the super-admin gotcha below.
CHANGELOG.md#300. That is a deliberate signal: read it before touching composer.json. Check PHP minimum version, namespace/class renames, whether the bootstrap became a singleton or a plain instance, and whether CLI commands were renamed or regrouped. Pin ^2.0 until you have read it.wp_user_roles option forever. Always ship a rollback path, and keep a "reset to definitions" command for local/staging.administrator. Removing a cap from administrator can lock every admin out of the very screen you'd use to fix it. Rehearse on staging with a disposable admin account, and keep a WP-CLI escape hatch (wp cap add administrator ...) documented in the runbook.current_user_can() returns true for a super admin regardless of role caps. Your manual testing on a super admin will pass while real editors are broken. Always verify with a plain subscriber/editor account.user_roles option cache may still be warm. Flush the object cache as part of the deploy step, or you will "verify" a change that hasn't landed.class_exists() and fail soft in non-production.is_admin() branch).map_meta_cap. A raw custom cap is all-or-nothing; if you need "can edit this order," you're in map_meta_cap territory and Caper won't do that part for you.How can I help you explore Laravel packages today?