Skip to content

feat(db,rest,ui)!: per-consumer table + slug-scoped REST routes (v2.0.0 — BREAKING) - #16

Merged
raftaar1191 merged 1 commit into
mainfrom
release/v2.0.0
Jun 24, 2026
Merged

raftaar1191 merged 1 commit into
mainfrom
release/v2.0.0

Conversation

@raftaar1191

Copy link
Copy Markdown
Contributor

⚠️ Breaking change

AccessControlManager now 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.

// Before (v1.x)
$manager = new AccessControlManager( 'my_plugin_access_control_providers' );

// After (v2.0.0)
$manager = new AccessControlManager(
    'my_plugin_access_control_providers',
    'my_plugin'  // slug: ^[a-z0-9_]{1,32}$
);

Existing v1.x consumers that pass only the providers filter tag will fail with ArgumentCountError immediately on upgrade. Slugs that don't match the pattern throw \InvalidArgumentException at construction time.

Why

Previously every consumer wrote into the single shared {prefix}wpb_access_control table and registered the same /wpb-ac/v1/... routes. Two plugins embedding the library on the same WordPress install collided on:

  • Storage — both wrote rows into one table; namespace conflicts were the only thing keeping them apart.
  • Cache — single wpb_access_control object-cache group meant wp_cache_set() writes from Plugin A clobbered Plugin B's reads.
  • REST routes — last-loaded-wins; only one manager's handler actually served /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

  1. Manager constructor — add the slug (see snippet above).
  2. wpbAcConfig — add 'pluginSlug' => 'my_plugin'.
  3. REST URLs — /wpb-ac/v1/rules/... → /wpb-ac/v1/{slug}/rules/... (and same for /providers, /users, /namespaces).
  4. Optional one-off SQL copy from {prefix}wpb_access_control to the new per-slug table — see README.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 by RuleTable, RuleQuery, and AccessControlManager.
  • src/Database/Rule/RuleTable.php — ctor accepts $table_slug; sets $name and $db_version_key per instance.
  • src/Database/Rule/RuleQuery.php — ctor accepts $table_slug; sets $table_name and $cache_group; replaces single static $table_setup flag with per-slug registry; folds slug into the transient_key hash so cached payloads cannot collide.
  • src/AccessControlManager.php — second required ctor arg $table_slug; new get_table_slug() getter; threads slug into RuleQuery + RulesController.
  • src/RestApi/RulesController.php — accepts the slug; every register_rest_route call now uses '/{slug}/...'.
  • js/AccessControl.js + js/components/UserSearchPanel.js + js/index.js — new required pluginSlug prop / wpbAcConfig field; every apiFetch URL carries the slug segment. The auto-render path bails with a clear console.error when pluginSlug is missing.
  • Tests — new SlugTest (17 cases), new RuleQueryConstructorTest (8 cases including two-instance isolation), 2 new register_routes tests in RulesControllerTest verifying 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 new pluginSlug prop wiring
  • Two-plugin smoke test: install two consumers with different slugs; confirm two tables exist, rules don't bleed across them, and REST GETs return empty for the other plugin's namespace
  • Cache-isolation check: PUT a rule via Plugin A, GET it via Plugin B's slug — expect empty (not A's cached payload)
  • Verify new AccessControlManager('x', '') / 'Has-Caps' / '../etc/passwd' all throw with a readable message
  • CI matrix passes on PHP 8.1 – 8.5

🤖 Generated with Claude Code

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>
@raftaar1191
raftaar1191 merged commit d75a53c into main Jun 24, 2026
5 checks passed
@raftaar1191
raftaar1191 deleted the release/v2.0.0 branch June 24, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant