feat(db,rest,ui)!: per-consumer table + slug-scoped REST routes (v2.0.0 — BREAKING) - #16
Merged
Merged
Conversation
BREAKING: each consumer plugin now owns its own database table, object-cache
group, and REST route prefix. AccessControlManager requires a second
constructor argument $table_slug (^[a-z0-9_]{1,32}$). Existing v1.x
consumers that pass only the providers filter tag will fail with
ArgumentCountError on upgrade.
Previously every consumer wrote into one shared {prefix}wpb_access_control
table and registered the same /wpb-ac/v1/... routes — two plugins on the
same WordPress install would collide on rules, on cached payloads (single
wpb_access_control cache group), and on REST handlers (last-loaded-wins).
Migration:
// PHP bootstrap
$manager = new AccessControlManager(
'my_plugin_access_control_providers',
'my_plugin' // slug; pick one per plugin
);
// wpbAcConfig / React props
wp_localize_script( 'wpb-ac-ui', 'wpbAcConfig', [
'pluginSlug' => 'my_plugin', // NEW
// …existing fields…
] );
// REST URLs: /wpb-ac/v1/rules/... → /wpb-ac/v1/{slug}/rules/...
Optional one-off SQL copy from {prefix}wpb_access_control to the new
per-consumer table; see README's "Upgrading from 1.x" section. The
library does NOT auto-migrate — it doesn't know which namespaces each
consumer owns.
Files:
- src/Slug.php — new helper, Slug::sanitize() validates ^[a-z0-9_]{1,32}$
and throws \InvalidArgumentException on invalid input.
- src/Database/Rule/RuleTable.php — constructor accepts $table_slug;
derives $name and $db_version_key per instance; removes hardcoded
defaults.
- src/Database/Rule/RuleQuery.php — constructor accepts $table_slug;
derives $table_name and $cache_group; replaces single static
$table_setup flag with per-slug registry; folds slug into the
transient_key hash so cache cannot collide across consumers.
- src/AccessControlManager.php — second required constructor arg
$table_slug; new get_table_slug() getter; passes the slug into
RuleQuery + RulesController.
- src/RestApi/RulesController.php — accepts slug; every register_rest_route
call now uses '/{slug}/rules/…', '/{slug}/providers', '/{slug}/users',
'/{slug}/namespaces/{namespace}'.
- js/AccessControl.js + js/components/UserSearchPanel.js + js/index.js —
new required pluginSlug prop / wpbAcConfig field; every apiFetch URL
carries the slug segment. Auto-render path bails with a clear console
error when pluginSlug is missing.
- tests: new SlugTest (17 cases covering accept/reject + error message
shape), new RuleQueryConstructorTest (8 cases asserting slug-driven
table_name / cache_group / two-instance isolation + slug validation
propagation through the ctor), 2 new register_routes tests in
RulesControllerTest (slug prefix on all four route groups + distinct
prefix per slug). Existing tests untouched — Brain Monkey suites use
newInstanceWithoutConstructor / disableOriginalConstructor / partial
mocks, so the new required ctor arg is transparent to them.
PHPUnit: 190 tests / 267 assertions all green (was 160/218). JS assets
rebuilt via wp-scripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AccessControlManagernow requires a second constructor argument — a per-consumer table slug. Every embedding plugin gets its own database table, object-cache group, and REST route prefix.Existing v1.x consumers that pass only the providers filter tag will fail with
ArgumentCountErrorimmediately on upgrade. Slugs that don't match the pattern throw\InvalidArgumentExceptionat construction time.Why
Previously every consumer wrote into the single shared
{prefix}wpb_access_controltable and registered the same/wpb-ac/v1/...routes. Two plugins embedding the library on the same WordPress install collided on:wpb_access_controlobject-cache group meantwp_cache_set()writes from Plugin A clobbered Plugin B's reads./wpb-ac/v1/rules/....v2.0.0 fixes all three at the source: each consumer's slug becomes the table name, the cache group, the transient prefix, and the REST route prefix.
Migration
wpbAcConfig— add'pluginSlug' => 'my_plugin'./wpb-ac/v1/rules/...→/wpb-ac/v1/{slug}/rules/...(and same for/providers,/users,/namespaces).{prefix}wpb_access_controlto the new per-slug table — seeREADME.md§ "Upgrading from 1.x". The library does NOT auto-migrate.Changes
src/Slug.php(NEW) —Slug::sanitize()validates^[a-z0-9_]{1,32}$and throws on invalid input. Used consistently byRuleTable,RuleQuery, andAccessControlManager.src/Database/Rule/RuleTable.php— ctor accepts$table_slug; sets$nameand$db_version_keyper instance.src/Database/Rule/RuleQuery.php— ctor accepts$table_slug; sets$table_nameand$cache_group; replaces single static$table_setupflag with per-slug registry; folds slug into thetransient_keyhash so cached payloads cannot collide.src/AccessControlManager.php— second required ctor arg$table_slug; newget_table_slug()getter; threads slug intoRuleQuery+RulesController.src/RestApi/RulesController.php— accepts the slug; everyregister_rest_routecall now uses'/{slug}/...'.js/AccessControl.js+js/components/UserSearchPanel.js+js/index.js— new requiredpluginSlugprop /wpbAcConfigfield; everyapiFetchURL carries the slug segment. The auto-render path bails with a clearconsole.errorwhenpluginSlugis missing.SlugTest(17 cases), newRuleQueryConstructorTest(8 cases including two-instance isolation), 2 newregister_routestests inRulesControllerTestverifying the slug prefix appears on every route group and distinct slugs produce distinct paths.Test plan
composer test— 190 tests / 267 assertions, all green (was 160/218)npm run build— wp-scripts compiles cleanly with the newpluginSlugprop wiringnew AccessControlManager('x', '')/'Has-Caps'/'../etc/passwd'all throw with a readable message🤖 Generated with Claude Code