Skip to content

Improve Permission authorization and cache performance - #54

Merged
binaryfire merged 19 commits into
0.4from
upstream-sync-permission-reconciliation
Oct 5, 2026
Merged

binaryfire merged 19 commits into
0.4from
upstream-sync-permission-reconciliation

Conversation

@binaryfire

@binaryfire binaryfire commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

This fixes authorization and caching bugs in the Permission package and makes reading permissions from the cache several times cheaper.

The main fixes: with wildcard permissions enabled, a denied pattern like posts.* did not block an allowed posts.create. hasRole() matched integer- and UUID-backed enums against role keys instead of role names. And after a migration recreated the permission tables, assignments cached on a shared store could apply to new models that reused old keys. Reading the role catalog from the cache now takes about 3 ms instead of 18 ms at 1,000 role-permission links.

One shared class changes too: the model cache coordinator, which Auth, Sanctum and Permission use, now rechecks the real cache store after taking its fill lock. Auth and Sanctum behave as before.

Denied permissions and wildcards

  • With wildcard permissions enabled, a deny only matched the exact name being checked. A denied posts.* did not block an allowed posts.create, and a denied articles.edit did not block articles.edit.123 granted by articles.*. Denies now go through the same wildcard matching as allows. The Wildcard contract gains getDeniedIndex() beside getIndex(), and models and roles gain getDeniedPermissions().
  • WildcardPermission::buildIndex() built each segment without subparts twice, so the work doubled with every segment: 31 calls instead of 5 for a four-segment name. Spatie has the same code. Each segment is now built once.
  • permission:show showed a role's denied permissions as allowed. It now shows allowed, denied and unassigned cells, with no extra queries.
  • Allowing or denying an existing permission on a relation that uses withTimestamps() did not set updated_at. It now does, like updateExistingPivot().

Role and permission checks

  • hasRole() treated integer- and UUID-backed enums as role keys, so a user holding the role with key 7 passed a check for an enum whose value was 7. Enums now compare with role names, as in Spatie, using a strict comparison. hasAnyRole() and hasAllRoles() follow.
  • Roles and permissions from the cached catalog had no connection name, while models loaded from the database do. Model::is() compares connection names, so $user->roles->contains(Role::findByName('admin')) and Role::findById($id)->is(Role::find($id)) returned false for the same row. Spatie has the same problem. Catalog models now carry the connection name Eloquent would give them, without taking a database connection.
  • Cached direct-permission pivots used the user's database connection, so saving or deleting one went to the wrong database when permissions live on a separate connection. They now use the permission connection, like pivots loaded through the relation.
  • setPermissionClass(), setRoleClass() and setTeamClass() now write the config like Spatie's. Reinitializing the cache had reverted the class while the container binding kept the new one.
  • The registrar resolves the cache manager each time it initializes the cache, so a rebound cache manager takes effect, as in Spatie.
  • The teams migration's "config not loaded" check could never run, because the migration read the config with typed getters first. It now runs first, like the create migration's.
  • The about command always listed denied permissions as an enabled feature, so it never showed Default. It now uses Spatie's feature labels.

Caching and performance

  • After taking its fill lock, the model cache coordinator rechecked through the request's memoized cache, which returned the miss it had remembered before the lock. A fill another request had just finished was loaded and stored again, and later fills in the same request locked and read the store again. The recheck now reads the store directly, as Repository::flexible() does, and its result replaces the request's memoized entry. Auth and Sanctum pass plain repositories, so nothing changes for them.
  • Package relations built a Permission model and resolved a database connection for every loaded pivot. The connection is now resolved once per relation, so loading package relations costs the same as plain Eloquent.
  • The catalog no longer builds a pivot for every role-permission link, and its pivots no longer point back at the models that hold them. Those references made every request's catalog a reference cycle, which only PHP's cycle collector could free. The cached payload now stores role keys instead of pivot rows, which shrinks it from 129 KB to 41 KB at 1,000 links. Measured with Postgres and Redis, reading the catalog from the cache takes about 3 ms instead of 18 ms at 1,000 links, and 26 ms instead of 186 ms at 10,000.
  • Saving a model with queued assignments cleared both its role and permission caches, even when only one kind of assignment was queued. It now clears only what changed, which saves three cache statements per save on the database store. Assigning a role to models from the role side had the same waste.
  • The create and teams migrations cleared only the role catalog, so cached per-model assignments outlived recreated tables on shared cache stores. A model whose key was reused read the old model's roles, and after enabling teams, a check without a team read the assignments cached before teams. Both migrations now reset the assignment cache too.
  • The unused public forgetModel*Cache* methods are removed. They cleared the cache immediately, even inside a transaction, so a concurrent request could refill it from rows that were not committed yet. forgetCachedPermissions() is still the reset for raw writes.

Partitioning

Row partitioning works as before, with less code around it. Eager-loaded partition relations are marked current once, in match(), instead of twice. A resolver that returns a non-scalar partition fails with PHP's own TypeError from PermissionPartition instead of a wrapped exception. New tests check that a sync's detached events leave out the user's assignments in other partitions, and that an eager-loaded relation with no results is still marked current. The guide now says to reset each affected partition's cache when a custom migration recreates partitioned tables.

Simplification and API consistency

  • Queued permission assignments are keyed like queued role assignments, so a later queued allow or deny replaces an earlier one. The separate merge pass and its five helpers are gone.
  • Duplicated sync, cache invalidation and assignment-context code is merged into single helpers. Checks that native types or Eloquent already enforce, and wrappers that only existed to satisfy static analysis, are removed.
  • Assignment, scope and role inputs have native union types. The checks Spatie's tests call with null, objects or arrays keep mixed, so they still throw Spatie's exceptions.
  • PermissionRegistrar is no longer bound with a closure, since the container already shares it. DefaultTeamResolver is no longer final, so it can be extended as in Spatie.
  • UnauthorizedException::missingTraitHasRoles() takes the user types the middleware actually pass instead of object.

Tests

Spatie's current test suite now runs as part of the Permission tests, converted from Pest to PHPUnit, and most of the bugs above came up while merging it in. Spatie's tests keep their upstream names, order, datasets and file placement. Duplicate Hypervel cases are folded into them, keeping the stronger Hypervel assertions, and Hypervel-only coverage for denies, partitions, coroutine isolation and query counts stays. Cases that only tested Eloquent itself, states that only raw SQL can create, or the test schema are removed. Spatie's Octane listener tests are not ported, because the current team and the loaded catalog are already per coroutine.

The suite picks its cache store from CACHE_STORE, like Spatie's CACHE_DRIVER, and gives each parallel worker its own Redis database. Bugs where one request reads another's cached data only show up on a shared store, so CI now also runs the Permission suite with the database cache store in tests.yml and with Redis in redis.yml.

Intentional differences

The package now follows the current spatie/laravel-permission except where noted here. The package README lists the remaining differences from Spatie and why: denied permissions and the wildcard contract's getDeniedIndex(), unit enum inputs, row partitioning, the cache configuration and its lock requirement, the worker-wide cache, and the missing Octane listener. Two smaller adaptations are also kept. An undefined cache store throws instead of quietly falling back to the array store, and permission:setup-teams fails when it cannot write the migration.

Documentation

The Permission guide now covers Spatie's documentation where it applies: guards, direct and role permissions, super-admins, enums, middleware, Blade directives, commands, teams, wildcards, custom models, seeding and testing. Stale guidance is corrected, including sync query counts, UUID migrations, global roles with teams and separate database connections. docs/upstream-sync/sync.yaml records the checked Spatie revision and how Hypervel's cache and relation code maps onto Spatie's.

Verification

The Permission suite passes with the array, database and Redis cache stores; it runs on SQLite. The Postgres Permission tests pass, and the partition database test passes on SQLite and Postgres. PHPStan, formatting and the full parallel suite pass.


Summary by cubic

Aligns Hypervel's Permission package with spatie/laravel-permission at 6615eefac655 (8.x), merging Spatie's Pest test suite into the existing PHPUnit coverage with regression tests for every defect exposed.

  • Denied permissions now use the same wildcard matching as allows, so a denied posts.* blocks an allowed posts.create.
  • WildcardPermission::buildIndex() builds each segment once; permission checks use a single shared wildcard path.
  • hasRole() compares enums with role names instead of treating integer- and UUID-backed enum values as role keys.
  • Cached catalog models carry Eloquent's connection name, and cached direct-permission pivots use the permission connection.
  • setPermissionClass(), setRoleClass() and setTeamClass() write the config like Spatie's, and the registrar resolves the cache manager each time it initializes the cache.
  • The teams migration's "config not loaded" check now runs before the typed getters.
  • permission:show displays allowed, denied and unassigned cells without extra queries, and the about command uses Spatie's feature labels.

Caching and performance

  • The model cache coordinator rechecks the real cache store after taking its fill lock, so a fill finished by another request is not loaded and stored again.
  • Relations resolve the pivot connection once instead of per pivot; roles-related permission catalog loads now cost the same as plain Eloquent.
  • The cached payload stores role keys instead of pivot rows, shrinking it from 129 KB to 41 KB at 1,000 links and cutting catalog reads from cache from ~18 ms to ~3 ms.
  • Saving with queued assignments clears only the changed cache, and the migrations reset the assignment cache when tables are recreated.
  • The unused public forgetModel*Cache* methods are removed.

Partitioning and simplification

  • Eager-loaded partition relations are marked current once in match() instead of twice, and a non-scalar partition fails with PHP's own TypeError.
  • Queued permission assignments are keyed like role assignments, so a later queued allow or deny replaces an earlier one.
  • Duplicated sync, cache invalidation and assignment-context code is merged into single helpers.
  • PermissionRegistrar is no longer bound with a closure, and DefaultTeamResolver is no longer final.

CI now runs the Permission suite with the database and Redis cache stores to surface shared-store cache bugs. The Permission guide covers Spatie's applicable docs, including the teams setup comment and per-partition cache resets, and the README lists the remaining intentional differences.

Tests

  • Spatie's current suite is ported from Pest to PHPUnit and folds in the existing Hypervel cases, keeping stronger assertions.
  • The sync event test now orders Event::fake() after the initial grant so only the sync's event satisfies the assertion.
  • PackageMetadataTest asserts the three required non-Hypervel dependencies, and PolicyTest comments read correctly.

Written for commit 25cac72. Summary will update on new commits.

Review in cubic

Note

Improve permission authorization and cache performance with denied-permission and wildcard-deny support

  • Reworks PermissionRegistrar and the HasPermissions/HasRoles traits to store role keys plus denied-role key lists in the cached catalog, instead of full pivot rows. Hydrated pivots are cloned from a shared prototype and use the permission model's connection.
  • Adds a denied wildcard index. WildcardPermission::getDeniedIndex() is checked before the allowed index, so denied wildcard permissions suppress matching direct and role-derived permissions. A new getDeniedPermissions() accessor returns direct and role-derived denies, de-duplicated.
  • Speeds up cache work in ModelCacheCoordinator.php: fill now locks on the underlying Store, re-reads authoritative storage past a memoized miss, and only memoizes successful primary-cache writes.
  • Queued role/permission assignments are now keyed by context identity, with one effect per permission; pivots are built at flush time inside a transaction. Middleware formatters use the enum_value callable, and permission/role APIs gain native union types.
  • Rewrites the permission guide in permission.md and reworks the test suite for coroutine setup, Passport clients, Redis and database cache stores.
  • Behavioral Change: denied permissions now win over allowed wildcard grants; PermissionRegistrar removes the public model-assignment cache invalidation methods, and DefaultTeamResolver::flushState() and TEAM_ID_CONTEXT_KEY are no longer public — external callers of these APIs must update.

Macroscope summarized 25cac72.

Bring the Permission command, guard, provider and integration tests in
line with spatie/laravel-permission main at 6615eefac655 (8.x): upstream
case names, assertions, comments and file placement, with Gate and
CustomGate tests moved under Integration/. Stronger Hypervel coverage is
kept, including the zero team id, guard named "0" and non-HasRoles model
cases. The test base no longer forces the teams, cache and model
settings, so method-level environment attributes apply, and CACHE_STORE
selects the cache store like upstream's CACHE_DRIVER. A database store
migrates its cache tables first, and lock pruning is disabled because it
adds a random query to counted tests.

Fixes found along the way:

- set{Permission,Role,Team}Class() now write the config like upstream.
  initializeCache() re-reads it, so reinitializing reverted the class
  while the container binding kept the new one.
- The registrar resolves the cache manager when initializing the cache
  instead of keeping the one it was built with, so a rebound cache
  manager takes effect (upstream #2973).
- Required settings that always ship in the config lose their code
  fallbacks; the optional cache settings keep one documented default,
  with a constant for the expiration.
- The about command drops an always-present "Denied Permissions" entry
  that made the "Default" label unreachable.
- permission:setup-teams uses now(), and two Closure::fromCallable()
  static-analysis workarounds become direct calls.

Hypervel adaptations: an undefined permission cache store throws instead
of silently falling back to the array store; setup-teams returns failure
when the migration cannot be written; upstream's chmod-based failure
case uses an unwritable destination because CI runs as root; Octane
listener tests are not ported because team ids and loaded catalogs are
coroutine-local.

Validation: the Permission suite passes on the array store, these files
pass on the database store, the Postgres Permission tests pass, and
formatting and static analysis are clean.
The model cache coordinator rechecked the cache after acquiring its fill
lock with a plain get(). Through a memoized repository, that read returned
the miss remembered before the lock, so a fill finished by another request
in the meantime was loaded and published again. Later fills in the same
coroutine also locked and read the store again, because memoized writes
forget their key.

The recheck now uses getAuthoritativeRaw() when the repository supports it,
matching Repository::flexible(). An envelope found by the recheck, or one
published through the fill repository, replaces the coroutine's memo entry
for a plain MemoizedStore. Failed writes, lost leases and lazy writer
repositories are not remembered. Tagged keys never reach that memo.

On the database cache store, a cold Permission catalog fill is now 9
statements and a cold authorization 24; the roles re-read after filling is
gone.

Validation: tests/Cache, tests/Auth, tests/Integration/Auth (with Redis),
tests/Sanctum, tests/Permission on the array and database stores (only
known later-slice failures), cs-fixer and composer analyse.
…upstream

Bring the middleware, model and reverse-assignment tests in line with
spatie/laravel-permission main at 6615eefac655 (8.x): upstream case
names, order and assertions, including every client case. The separate
Passport client middleware test is folded into the upstream client cases,
keeping its disabled-credentials case and its same-permission precheck in
the via-role case. Stronger Hypervel assertions stay inside the
upstream-named cases: a non-matching wildcard permission is denied, and an
admin-guard user is denied web-guard roles and permissions. Hypervel-only
cases for JSON responses, users without Authorizable, the empty guard and
integer-backed enums are kept. Wildcard and model tests move config to
defineEnvironment() and seeding into the test coroutine, the teams
assigned-model case uses a method-level environment, and its duplicate in
the team variant is removed. Deprecated expectExceptionMessage() calls
become expectExceptionMessageIsOrContains().

Source cleanups:

- RoleMiddleware, RoleOrPermissionMiddleware and WildcardPermission call
  hasAnyRole() and getAllPermissions() directly like upstream instead of
  through Closure::fromCallable() static-analysis workarounds.
- The middleware and the provider's route macros convert listed enum
  names with array_map(enum_value(...)) instead of untyped wrappers; the
  route macros keep mixed input and declare their Route return type.

The middleware keep Hypervel's Authorizable check and per-name can()
loop because the Authorizable contract does not include canAny().

Validation: these files pass on the array and database cache stores;
the Permission suite shows only known later-slice failures; formatting
and static analysis are clean.
…validation

Bring HasPermissionsTest in line with spatie/laravel-permission main at
6615eefac655 (8.x): every upstream case under its upstream name and order,
the restored third user and comment, and the ported detach-event case for
syncPermissions(). The custom-pivot case changes the auth provider model
at runtime, so it calls Guard::flushState() after the change because
Guard caches provider models for the worker lifetime. Exception-only
catch blocks drop their no-op assertTrue(true), and deprecated
expectExceptionMessage() becomes expectExceptionMessageIsOrContains().
The custom-models variant renames its skipped integer-scope override to
match. Count cases add the database cache store's statements through a
new TestCase::usesDatabaseCacheStore() helper. PermissionRegistrarTest's
final missing-name cases expect their exceptions directly.

Source fix: saving a model with queued assignments invalidated both its
role and direct-permission caches for every queued context, even when
only permissions were queued. On the database cache store that cost three
extra statements per save (8 instead of 5). The flush now invalidates the
role cache only for queued role contexts and the permission cache through
the existing per-context permission path. Reverse role assignment had the
same waste for each affected model and now invalidates only role caches.
The combined registrar invalidation methods lost their callers and are
removed. A root CacheTest case covers the reverse-assignment path keeping
the warm direct-permission memo.

Validation: HasPermissionsTest and PermissionRegistrarTest pass on the
array and database cache stores; the Permission suite passes on the
array store, with only known later-slice failures on the database store;
formatting and static analysis are clean.
Bring HasPermissionsWithCustomModelsTest and TeamHasPermissionsTest in
line with spatie/laravel-permission main at 6615eefac655 (8.x). The
custom-models variant follows upstream's case order and restores its
skip comment for the integer-scope override. TeamHasPermissionsTest
gains upstream's five custom-pivot team cases and their fixtures, and
three cases take their upstream names.

Several upstream cases change the auth provider model at runtime. The
new TestCase::useAuthUserModel() sets it and flushes Guard's provider
model cache, which lasts for the worker lifetime; the HasPermissionsTest
custom-pivot case now uses it too.

The custom-models variant's database-store failures came from an
always-zero query-count offset. The counts now name the database cache
store's real statements: soft deletes invalidate the permission catalog
when deleting and when deleted, force deletes invalidate it once in the
delete transaction and write a new assignment token, and the team warm
reuse case pays for two assignment cache fills.

Validation: HasPermissionsTest, HasPermissionsWithCustomModelsTest and
TeamHasPermissionsTest pass on the array and database cache stores; the
Permission suite passes on the array store, with only known later-slice
failures on the database store; formatting is clean.
Bring HasRolesTest in line with spatie/laravel-permission main at
6615eefac655 (8.x): every upstream case under its upstream name and
order, upstream's pipe-conversion and custom-pivot cases, restored
comments, the string scope's second user role, the object scope's
statement order and Admin::all() in the guard withoutscope case. The
pipe-conversion case replaces Hypervel's malformed-quote case, which
covered one of its inputs. The cross-guard sync catch drops its no-op
assertTrue(true), and deprecated expectExceptionMessage() becomes
expectExceptionMessageIsOrContains().

Upstream's teams branch in the unnecessary-SQL case is dropped: the sync
reads the current team's pivot rows directly instead of reloading the
relation. TeamHasRolesTest therefore inherits the base case, and its
duplicate override is removed. Count cases add the database cache
store's statements.

Test setup fix: setUpBaseTestPermissions() created the test user and
admin, so setUpCustomModels() added a second pair. The custom-models
variant's Admin::all() then returned two models, which enables lazy-load
prevention for the admin's touched relations on delete; the base case
had worked around it with an eager load. The user and admin are now
created once after the fixture tables, like upstream's setUpDatabase().

Validation: HasRolesTest and TeamHasRolesTest pass on the array and
database cache stores; the custom-model variants, DeniedPermissionTest
and RoleWithNestingTest pass on the array store; the Permission suite
passes on the array store, with only known later-slice failures on the
database store; formatting is clean.
… upstream

Bring HasRolesWithCustomModelsTest, TeamHasRolesTest and TeamScopeTest
in line with spatie/laravel-permission main at 6615eefac655 (8.x).

HasRolesWithCustomModelsTest takes upstream's case names and order. Its
always-zero query-count offset is replaced by the database cache store's
real statements: soft deletes invalidate the role catalog when deleting
and when deleted, and force deletes invalidate it once in the delete
transaction and write a new assignment token.

TeamHasRolesTest runs the team pivot-deletion body under the base name
and the base body as ...FromHasRolesTest, as upstream names them. It
gains upstream's five custom-pivot team cases and their fixtures,
restores the multi-team case's comments, takes the upstream sync-or-
remove name and drops a no-op assertTrue(true).

TeamScopeTest restores upstream's two multi-expectation exception cases
from five split cases and the introspection case's full name. The team
class config in defineEnvironment() is removed because setTeamClass()
in setup already sets it; permission.teams stays there because the
migration adds team columns only when teams are enabled. Deprecated
expectExceptionMessage() becomes expectExceptionMessageIsOrContains().

TestCase types getPackageProviders()'s $app like its parent and imports
the package role and permission models for its property types.

Validation: the three files pass on the array and database cache
stores; the Permission suite passes on the array store, with only known
later-slice failures on the database store; formatting is clean.
…ions

Bring WildcardHasPermissionsTest in line with spatie/laravel-permission
main at 6615eefac655 (8.x): all 19 upstream cases under their upstream
names, order, variables and comments, plus upstream's two wildcard
index cases for assigning and removing a role. Wildcards are enabled in
defineEnvironment() instead of setUp(), which mutated config and
flushed the cache outside the test coroutine and failed on Redis. A
Hypervel case covers syncModels() rotating the assignment token that
every wildcard index key includes.

The Hypervel-only WildcardPermissionTest is removed: five of its cases
duplicated upstream cases, and its token case built its state with a
raw pivot delete and a direct token rotation. UnitEnumTest now covers
the signatures Hypervel widens from upstream's BackedEnum: findOrCreate()
and findByName() on roles and permissions, and the three middleware
using() methods.

Catalog models were cloned from a new model prototype, as upstream
does, so their connection name stayed null while database-loaded models
carry the resolved name. Model::is() compares connection names, so
$user->roles->contains(Role::findByName(...)) and findById()->is(find())
returned false for the same rows. The registrar now names the prototype
like Eloquent hydration's Connection::getWritableName(): the model's
connection, or the model resolver's default when null or empty, with a
read alias reduced to its base name and a write alias kept. It parses
the name instead of resolving a connection, which would take a pooled
connection on warm checks.

The wildcard docs said a permission must exist before it can be
checked; only the assigned wildcard permission needs a record. GuardTest
gains its data provider's title docblock.

Validation: the changed test files pass on the array, database and
Redis cache stores; the Permission suite passes on the array store,
with only known later-slice failures on the database store; the
Postgres Permission tests pass; formatting and static analysis are
clean.
With wildcard permissions enabled, denies matched only the exact checked
name: a denied posts.* did not block an allowed posts.create, and a
denied articles.edit did not block articles.edit.123 granted by
articles.*. Denies are a Hypervel addition to spatie/laravel-permission
(main at 6615eefac655, 8.x).

The Wildcard contract adds getDeniedIndex() beside getIndex().
WildcardPermission indexes getAllPermissions() and the new public
getDeniedPermissions() through one shared loop. getDeniedPermissions()
reads the same cached direct and via-role collections and joins them
with concat(): a loaded permissions relation or custom pivot supplies
an Eloquent collection, whose merge() would replace a direct deny with
a role allow of the same key.

HasPermissions::hasPermissionTo() and Role::hasPermissionTo() now share
hasWildcardPermission(), as upstream's wildcard branches do. It
normalizes the checked value once, keeps the partition check for
permission objects, returns false when the configured wildcard class
matches the denied index, then checks the allowed index.
hasDeniedPermission() and hasDeniedPermissionViaRoles() stay exact. The
registrar stores both indexes in the existing context entry, so every
wildcard index invalidation clears them together.

WildcardPermission::buildIndex() also built each segment without
subparts twice, in a separate branch and again in the subpart loop, so
work doubled with each segment (31 calls instead of 5 for a
four-segment name). Upstream has the same code. Every segment now goes
through the loop once; its blank-subpart check covers the removed
blank-segment check.

The README records the contract addition and getDeniedPermissions();
the user guide documents getDeniedPermissions(), wildcard deny matching
and custom wildcard classes.

Validation: the changed test files pass on the array, database and
Redis cache stores; the Permission suite passes on the array store,
with only known later-slice failures on the database store; the
Postgres Permission tests pass; formatting and static analysis are
clean.
Assess the configuration, contracts, exceptions, helpers, Guard,
migration and stubs, events and commands against
spatie/laravel-permission main at 6615eefac655.

- permission:show rendered a role's denied permissions as allowed,
  because it plucked permission ids only (upstream has no denies). It
  now reads each eager-loaded pivot's is_denied flag and shows allowed,
  denied and unassigned cells, with no extra queries. The user guide
  explains the symbols.
- The show-by-teams test uses non-sequential team ids, so upstream's
  positional team headers would fail it.
- UnauthorizedException::missingTraitHasRoles() takes the user types
  the middleware pass instead of object.
- DefaultTeamResolver is no longer final, and its flushState() and
  subscriber call are removed: the subscriber's CoroutineContext flush
  already clears the team.
- SchemaConfigTest's omitted-pivot-key case uses the array store for
  the permission cache. It switches the default connection to a fresh
  database, which a database cache store followed when the migration
  cleared the role cache.
- Remove PublicApiTest and SchemaConfigTest cases covered elsewhere or
  that only asserted removed names; BladeTest covers the hasexactroles
  directive. PackageMetadataTest compares every external requirement
  with the root package.
- Method title docblocks, restored upstream comments (the AssignRole
  one now names what it checks), the teams config comment, and README
  and user-guide corrections.

Validation: Permission suite on the array store; database-store run
with only the known cache and partition query-count failures; affected
files on Redis; composer lint and composer analyse.
Assess the registrar's cache and catalog machinery against
spatie/laravel-permission main at 6615eefac655 (upstream has no
equivalent; its CacheTest was reconciled earlier).

- Package relations built a Permission model and resolved a connection
  for every hydrated pivot, because newPivot() asks for the pivot
  connection per pivot. The connection is memoized per relation, so
  package relation loads cost the same as plain Eloquent.
- Catalog hydration clones one prepared pivot instead of building one
  per role-permission link, and builds its indexes in one pass.
- Catalog and via-role pivots no longer point back at the models that
  hold them. Those references made every request's catalog cyclic, so
  only the garbage collector could free it, about half the per-request
  cost.
- The cached payload stores each permission's role keys and denied
  role keys instead of pivot rows (129 to 41 KB at 1,000 links).
- Per-request catalog lookup after a cache hit drops from about 18 to
  3 ms at 1,000 links and from 186 to 26 ms at 10,000.
- Remove the unused forgetModel*Cache* invalidators. They invalidated
  immediately, so a concurrent request could refill from pre-commit
  rows inside a transaction; forgetCachedPermissions() is the
  documented reset.
- The Permission TestCase isolates a Redis permission cache per
  ParaTest worker before the migrations clear it, and redis.yml runs
  tests/Permission with the Redis store.
- CacheTest covers denied role keys in the payload, a shared-store hit
  from a new coroutine, freeing catalog models without the cycle
  collector, and the database store's forget() result. The Postgres
  create-race test covers Role::create() too.

Validation: Permission suite under ParaTest on the array, file and
Redis stores; database store with only the known partition query-count
failures; Postgres integration tests; composer lint and composer
analyse.
Assess the HasPermissions and HasRoles internals against
spatie/laravel-permission main at 6615eefac655 (upstream has no
equivalent for the queued assignments, provenance or deny machinery;
its event cases were reconciled earlier).

- Cached direct-permission pivots took the subject's connection, so
  saving or deleting one wrote through the subject database while live
  relation pivots use permission storage. They now take the
  permission's connection name, and they no longer point back at their
  permission, which made the memoized collection cyclic garbage.
- hasRole() looked up integer- and UUID-valued enums as role keys, so
  a user holding the role with key 7 passed a check for an enum valued
  7. Enums compare with role names, as upstream's enum branch does,
  using a strict string comparison. hasAllRoles() no longer converts a
  single enum before passing it to hasRole(), which sent a UUID value
  to the key lookup.
- The permission queue is keyed by context and permission like the
  role queue, so a later queued effect replaces an earlier one; the
  collapse pass, five helpers and the flush-time team recheck go.
- syncPermissions() calls syncPermissionEffects(); assignment cache
  invalidation and the assignment-context builders each have one
  helper instead of several copies.
- hasDirectPermission() and Role::hasPermissionTo() look up the single
  assignment for a permission (the pivot primary keys allow one row per
  permission and subject), and Role follows upstream's check order.
- Remove requireDeletionModelKey() (Eloquent rejects a keyless delete
  before any query), value checks the native types already enforce,
  and instanceof checks that only narrowed types; two of them silently
  skipped partition checks.
- Type assignment, scope and role inputs with their documented unions;
  the checks upstream tests call with null, objects or arrays keep
  mixed.
- Tests: regressions for the pivot connection, enum role names and
  direct-permission lifetime; remove cases for impossible duplicate
  effects, Eloquent's key check and upstream-covered events; assert
  single dispatches and complete sync results.

Validation: Permission suite under ParaTest on the array, file and
Redis stores; database store with only the known partition query-count
failures; Postgres integration tests; php-cs-fixer and composer
analyse.
…base store

Assess the partition machinery and its tests against
spatie/laravel-permission main at 6615eefac655 (upstream has no
partition support or tests).

- Remove the provider's PermissionRegistrar singleton closure; the
  class is autowirable and auto-singletoned. flushState() no longer
  forgets the container instance: the test subscriber has already
  replaced the container, so the lookup only created an empty one.
- resolvePartition() no longer wraps a non-scalar resolver result in
  an UnexpectedValueException; PermissionPartition's native types
  reject it. partitionFromRecord() no longer rejects an empty stored
  partition, which only raw SQL can write.
- Partition relations marked eager-loaded collections in both
  initRelation() and match(). Builder::eagerLoadRelation() always
  passes the first's result to the second, so match() alone marks
  every model, including those without results.
- Replace type-only instanceof checks missed in the previous slice
  with null checks in the registrar's settlement tokens, coroutine
  memos and key index, and in hasDeniedPermissionViaRoles().
- Tests: a sync's detached event payloads exclude the subject's
  assignments in another partition; an empty eager-loaded relation is
  marked current; exact role-assignment and removal query counts.
  Remove absence checks, cases built from states only raw SQL creates,
  cases reading back the tests' own schema, constructor reflection and
  duplicates. Use non-deprecated exception message expectations.
- The partition query-count cases use the array permission cache,
  since a database store logs its statements between the counted
  queries. CI runs the Permission suite with the database cache store.

Validation: Permission suite under ParaTest on the array, file,
database and Redis stores; Postgres integration tests; the partition
database test on SQLite and Postgres; php-cs-fixer and composer
analyse.
Compare every spatie/laravel-permission docs page at main 6615eefac655
with the user guide, checking each claim against current source.

- Port the applicable upstream coverage: authorizable user models and
  reserved trait names, default guards and guard resolution,
  permission-side role methods, ID and enum lookups, super-admin
  options, the can and package middleware with aliases, guards, pipes,
  priority and controller middleware, Blade directives, command
  arguments, team middleware and team roles, wildcard syntax, model
  extension, pivot timestamps, seeding, test seeding with the Seeder
  attribute, best practices and a policy example that always returns.
- Correct stale or wrong guidance: sync query counts, UUID migration
  changes, global role uniqueness, reloading relations after switching
  teams, separate-connection limits and the default_model fallback.
- Not ported: upstream's example app, PhpStorm, UI and upgrade pages,
  MySQL key-length notes and an attribute example with a static call,
  which is invalid PHP. The guide's differences list moves to the
  README, which gains the cache store's lock requirement and the
  worker-wide cache with partitioning for tenants.
- Bulk allow/deny updates skipped updated_at on permission relations
  using withTimestamps(); they now set it like updateExistingPivot().
- The create migration and the teams stub forgot only the catalog key,
  while upstream's single key is its whole cache. Per-model assignment
  caches therefore survived recreated tables on shared stores: a model
  whose key was reused read the old role keys, and after enabling teams
  a check without a team read pre-teams assignments. Both also forget
  the assignment token now, so the guide's opening seeder reset goes.
- The sync note maps the catalog and assignment caches, the relation
  builders and the diff-based syncs. A " #" in it had started a YAML
  comment that cut off the rest of the note.

Validation: Permission suite under ParaTest on the array, database and
Redis stores; Postgres integration tests; the migration regressions
fail on the database and Redis stores without the fix; php-cs-fixer
and composer analyse.
Set spatie/laravel-permission main 6615eefac655 as the checked-through
revision after the full-package reconciliation of its tests, source
and documentation. The assessment examined no pull requests, so its
last reviewed PR stays unset.
The teams stub read permission.teams and permission.table_names through
typed getters before its "config not loaded" check, so missing config
failed with a generic typed-getter error, and the check itself only
caught an empty array. Upstream's stub and our create migration show
the message that tells users to clear the config cache. The stub now
checks table_names first, like the create migration.

Validation: PermissionServiceProviderTest runs the missing-config case
for both migrations (the stub case failed before the fix); SchemaConfig,
Cache (array and database stores) and command tests; php-cs-fixer.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: hypervel/components-backup/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1d5606b3-27e0-437f-a6d8-df4ae9d23e7b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Reconcile Permission with Spatie and fix authorization and cache defects

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Align Permission behavior and tests with Spatie while preserving Hypervel-specific denies and
 partitioning.
• Fix wildcard denies, role checks, pivot writes, and shared-cache consistency while reducing
 catalog hydration costs.
• Expand documentation and test shared database and Redis cache stores in CI.
Diagram

graph TD
  A["Subject models"] --> B["Assignment traits"] --> C["Permission registrar"] --> D["Catalog hydration"] --> E["Shared cache"]
  C --> F["Cache coordinator"] --> E
  B --> G["Wildcard matching"]
  G --> C
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Depend directly on Spatie Permission
  • ➕ Reduces ongoing manual reconciliation of upstream behavior and tests.
  • ➖ Does not provide Hypervel's denies, partitioning, coroutine-local teams, or transaction-aware shared cache.
  • ➖ Requires a substantial compatibility layer for Laravel-specific behavior.

Recommendation: Keep Hypervel's implementation and use upstream tests and the recorded revision to guide reconciliation. A direct dependency would sacrifice core Hypervel semantics; compact catalogs and targeted invalidation address the implementation's distinct cache costs.

Files changed (92) +4291 / -3305

Enhancement (3) +199 / -402
Wildcard.phpExpose a denied wildcard index +9/-0

Expose a denied wildcard index

• Requires custom wildcard implementations to provide getDeniedIndex() alongside their allowed index.

src/permission/src/Contracts/Wildcard.php

PermissionRegistrar.phpCompact and reconcile Permission cache management +176/-365

Compact and reconcile Permission cache management

• Stores role keys in the catalog; hydrates connection-correct models and cycle-free pivots; maintains allow and deny wildcard indexes. Resolves rebound cache managers, persists model classes, and removes unsafe immediate invalidation APIs.

src/permission/src/PermissionRegistrar.php

EnforcesPermissionPartition.phpResolve pivot connection once per relation +14/-37

Resolve pivot connection once per relation

• Memoizes the pivot connection and marks eager-loaded relations current in match(), including empty collections.

src/permission/src/Traits/EnforcesPermissionPartition.php

Bug fix (9) +404 / -725
ModelCacheCoordinator.phpRecheck authoritative cache after locking +41/-8

Recheck authoritative cache after locking

• Bypasses memoized misses on the fill double-check and updates the coroutine memo after finding or publishing a value.

src/cache/src/ModelCacheCoordinator.php

2025_07_02_000000_create_permission_tables.phpReset assignments on table creation +5/-3

Reset assignments on table creation

• Clears the assignment token alongside the catalog so reused model keys cannot see assignments from old tables.

src/permission/database/migrations/2025_07_02_000000_create_permission_tables.php

add_teams_fields.php.stubValidate config and reset team assignments +9/-5

Validate config and reset team assignments

• Checks for missing configuration before typed reads and clears the assignment token when adding team columns.

src/permission/database/migrations/add_teams_fields.php.stub

ShowCommand.phpDisplay denied role permissions +9/-2

Display denied role permissions

• Distinguishes allowed, denied, and unassigned cells using loaded pivot effects.

src/permission/src/Commands/ShowCommand.php

Role.phpUse deny-aware wildcard checks for roles +5/-12

Use deny-aware wildcard checks for roles

• Routes wildcard checks through common deny matching and simplifies exact effect lookup.

src/permission/src/Models/Role.php

HasAssignedModels.phpInvalidate only reverse-assigned role caches +2/-2

Invalidate only reverse-assigned role caches

• Preserves unrelated direct-permission caches after reverse role assignments.

src/permission/src/Traits/HasAssignedModels.php

HasPermissions.phpUnify assignments and enforce wildcard denies +233/-491

Unify assignments and enforce wildcard denies

• Adds denied-permission retrieval and checks denied wildcard patterns first. Simplifies queued and synchronized assignments, fixes pivot timestamps and connections, and consolidates invalidation.

src/permission/src/Traits/HasPermissions.php

HasRoles.phpCompare enum roles by name +74/-185

Compare enum roles by name

• Prevents enum values being mistaken for role keys; consolidates assignment context and queued writes and narrows invalidation.

src/permission/src/Traits/HasRoles.php

WildcardPermission.phpIndex denied patterns efficiently +26/-17

Index denied patterns efficiently

• Builds a denied-permission index and processes each plain permission segment once.

src/permission/src/WildcardPermission.php

Refactor (10) +35 / -50
AssignRoleCommand.phpSimplify command role assignment +2/-3

Simplify command role assignment

• Removes an indirect callable wrapper around the validated user's assignRole call.

src/permission/src/Commands/AssignRoleCommand.php

UpgradeForTeamsCommand.phpUse framework time for migration names +1/-1

Use framework time for migration names

• Generates the teams migration timestamp with the framework clock.

src/permission/src/Commands/UpgradeForTeamsCommand.php

DefaultTeamResolver.phpMake default team resolver extensible +2/-10

Make default team resolver extensible

• Removes final and obsolete static teardown; makes the context-key constant protected.

src/permission/src/DefaultTeamResolver.php

UnauthorizedException.phpType missing-trait exception users +3/-1

Type missing-trait exception users

• Accepts the authenticatable or authorizable contracts passed by middleware.

src/permission/src/Exceptions/UnauthorizedException.php

PermissionMiddleware.phpSimplify enum permission parsing +1/-4

Simplify enum permission parsing

• Uses shared enum conversion for array-valued middleware inputs.

src/permission/src/Middleware/PermissionMiddleware.php

RoleMiddleware.phpSimplify role middleware checks +2/-3

Simplify role middleware checks

• Calls hasAnyRole directly and reuses enum conversion for role arrays.

src/permission/src/Middleware/RoleMiddleware.php

RoleOrPermissionMiddleware.phpSimplify combined middleware checks +2/-3

Simplify combined middleware checks

• Calls hasAnyRole directly and reuses enum conversion for combined inputs.

src/permission/src/Middleware/RoleOrPermissionMiddleware.php

PermissionServiceProvider.phpUse container sharing and upstream feature labels +9/-19

Use container sharing and upstream feature labels

• Removes the registrar factory binding, simplifies route macros, and reports applicable features in about output.

src/permission/src/PermissionServiceProvider.php

Config.phpUse required configuration directly +13/-5

Use required configuration directly

• Drops redundant defaults for package-provided settings and improves annotations.

src/permission/src/Support/Config.php

AfterEachTestSubscriber.phpRemove obsolete resolver teardown +0/-1

Remove obsolete resolver teardown

• Stops calling the removed DefaultTeamResolver static flush method.

src/testing/src/PHPUnit/AfterEachTestSubscriber.php

Tests (58) +2920 / -1865
ModelCacheCoordinatorTest.phpCover authoritative rechecks and memo publication +106/-0

Cover authoritative rechecks and memo publication

• Tests concurrent fills, memo reuse, failed writes, and lazy-writer behavior.

tests/Cache/ModelCacheCoordinatorTest.php

PermissionPartitionTest.phpFocus database partition tests +1/-24

Focus database partition tests

• Retains composite foreign-key coverage while removing redundant assertions.

tests/Integration/Database/PermissionPartitionTest.php

PermissionCreateTransactionTest.phpCheck PostgreSQL create races +35/-16

Check PostgreSQL create races

• Reworks role and permission create-race tests to verify transaction recovery.

tests/Integration/Permission/Database/Postgres/PermissionCreateTransactionTest.php

CacheTest.phpRegress catalog shape and cache resets +115/-35

Regress catalog shape and cache resets

• Tests compact role keys, cycle-free hydration, migration resets, and targeted reverse-assignment invalidation.

tests/Permission/CacheTest.php

CommandTest.phpExpand command coverage +234/-65

Expand command coverage

• Checks denied matrix cells, about output, teams migration outcomes, and guarded assignment.

tests/Permission/Commands/CommandTest.php

PartitionCommandTest.phpTrim redundant partition command cases +3/-17

Trim redundant partition command cases

• Removes command assertions covered elsewhere in the reconciled suite.

tests/Permission/Commands/PartitionCommandTest.php

TeamCommandTest.phpAlign team command cases +25/-119

Align team command cases

• Consolidates overlapping tests and retains cross-team assignment behavior.

tests/Permission/Commands/TeamCommandTest.php

CoroutineIsolationTest.phpMaintain coroutine isolation coverage +3/-0

Maintain coroutine isolation coverage

• Adjusts coroutine-specific assertions for the reconciled suite.

tests/Permission/CoroutineIsolationTest.php

CustomPivotTest.phpRegress timestamped effect pivots +52/-0

Regress timestamped effect pivots

• Checks updated_at changes on allow/deny updates without replacing created_at.

tests/Permission/CustomPivotTest.php

CustomSchemaConfigTest.phpAlign custom-schema assertions +1/-1

Align custom-schema assertions

• Adapts schema configuration coverage to reconciled defaults.

tests/Permission/CustomSchemaConfigTest.php

DeletionTest.phpRemove redundant deletion cases +0/-97

Remove redundant deletion cases

• Drops artificial missing-key cases while retaining normal deletion coverage.

tests/Permission/DeletionTest.php

DeniedPermissionTest.phpConsolidate denial coverage +28/-82

Consolidate denial coverage

• Keeps deny behavior while folding overlapping cases into upstream-aligned tests.

tests/Permission/DeniedPermissionTest.php

EventTest.phpConsolidate assignment-event coverage +6/-96

Consolidate assignment-event coverage

• Removes scenarios covered by upstream-aligned role and permission tests.

tests/Permission/Events/EventTest.php

PartitionEventTest.phpRegress partition-scoped event payloads +13/-91

Regress partition-scoped event payloads

• Checks that detached sync events exclude assignments from other partitions.

tests/Permission/Events/PartitionEventTest.php

GuardTest.phpAlign guard resolution tests +15/-16

Align guard resolution tests

• Adds missing-provider and LDAP-provider cases while retaining Hypervel edge cases.

tests/Permission/GuardTest.php

BladeTest.phpExpand Blade directive cases +34/-26

Expand Blade directive cases

• Tests guest, guard, role, and permission directives with upstream-style cases.

tests/Permission/Integration/BladeTest.php

CacheTest.phpCount cache queries across stores +42/-16

Count cache queries across stores

• Accounts for database-store initialization and locked cold fills.

tests/Permission/Integration/CacheTest.php

CustomGateTest.phpAlign custom Gate tests +5/-4

Align custom Gate tests

• Checks disabled registration and custom permission-check authorization.

tests/Permission/Integration/CustomGateTest.php

GateTest.phpReconcile Gate authorization coverage +30/-22

Reconcile Gate authorization coverage

• Aligns Gate checks with upstream while retaining Hypervel behavior.

tests/Permission/Integration/GateTest.php

MultipleGuardsTest.phpExpand multiple-guard checks +24/-3

Expand multiple-guard checks

• Tests roles and permissions across configured guards.

tests/Permission/Integration/MultipleGuardsTest.php

PartitionQueryCountTest.phpKeep focused partition query budgets +27/-89

Keep focused partition query budgets

• Consolidates counted SQL cases for partitioned role attachment and removal.

tests/Permission/Integration/PartitionQueryCountTest.php

PermissionRegistrarTest.phpRegress registrar and catalog identity +195/-19

Regress registrar and catalog identity

• Tests cache-manager rebinding, class persistence, missing stores, and catalog identity across connection names.

tests/Permission/Integration/PermissionRegistrarTest.php

PolicyTest.phpAlign policy authorization test +2/-1

Align policy authorization test

• Adjusts the policy integration assertion to its reconciled upstream case.

tests/Permission/Integration/PolicyTest.php

WildcardRouteTest.phpAlign wildcard route checks +4/-4

Align wildcard route checks

• Reconciles wildcard-protected route naming and assertions.

tests/Permission/Integration/WildcardRouteTest.php

PermissionMiddlewareTest.phpExpand permission middleware scenarios +186/-97

Expand permission middleware scenarios

• Covers guests, clients, guards, enums, exceptions, and authorization.

tests/Permission/Middleware/PermissionMiddlewareTest.php

RoleMiddlewareTest.phpExpand role middleware scenarios +152/-84

Expand role middleware scenarios

• Covers role, client, guard, enum, exception, and missing-trait behavior.

tests/Permission/Middleware/RoleMiddlewareTest.php

RoleOrPermissionMiddlewareTest.phpExpand combined middleware scenarios +97/-46

Expand combined middleware scenarios

• Covers guest, client, guard, exception, and combined authorization cases.

tests/Permission/Middleware/RoleOrPermissionMiddlewareTest.php

WildcardMiddlewareTest.phpAlign wildcard middleware cases +25/-17

Align wildcard middleware cases

• Reconciles route authorization and exception assertions.

tests/Permission/Middleware/WildcardMiddlewareTest.php

PermissionTest.phpAlign permission model cases +5/-2

Align permission model cases

• Reconciles upstream permission model factory assertions.

tests/Permission/Models/PermissionTest.php

RoleTest.phpExpand role model checks +45/-20

Expand role model checks

• Aligns role lookup and permission checks while retaining deny behavior.

tests/Permission/Models/RoleTest.php

WildcardRoleTest.phpCover wildcard role denials +22/-4

Cover wildcard role denials

• Expands role-level wildcard checks for deny precedence.

tests/Permission/Models/WildcardRoleTest.php

PackageMetadataTest.phpUpdate package metadata assertions +7/-4

Update package metadata assertions

• Aligns metadata checks with upstream reconciliation.

tests/Permission/PackageMetadataTest.php

PartitionAuthorizationTest.phpRegress partition authorization +13/-0

Regress partition authorization

• Adds permission-check coverage for partitioned assignments.

tests/Permission/PartitionAuthorizationTest.php

PartitionCacheTest.phpRemove obsolete partition cache-reset case +0/-18

Remove obsolete partition cache-reset case

• Drops a case relying on removed immediate reset behavior.

tests/Permission/PartitionCacheTest.php

PartitionCustomPivotTest.phpRetain partitioned custom-pivot coverage +4/-0

Retain partitioned custom-pivot coverage

• Adjusts assertions for custom pivot mutations under partitioning.

tests/Permission/PartitionCustomPivotTest.php

PartitionDeletionTest.phpClarify partition deletion cases +2/-2

Clarify partition deletion cases

• Renames cases to emphasize removal only within the affected partition.

tests/Permission/PartitionDeletionTest.php

PartitionModelTest.phpRemove artificial partition model cases +0/-65

Remove artificial partition model cases

• Trims missing-attribute and unsupported model-state assertions.

tests/Permission/PartitionModelTest.php

PartitionRegistrationTest.phpFocus partition registration tests +37/-62

Focus partition registration tests

• Removes redundant resolver cases and aligns expected native type errors.

tests/Permission/PartitionRegistrationTest.php

PartitionRelationProvenanceTest.phpClarify relation provenance assertion +1/-0

Clarify relation provenance assertion

• Clarifies recognition of a current partition relation context.

tests/Permission/PartitionRelationProvenanceTest.php

PartitionRelationsTest.phpCheck empty eager-loaded relations +6/-25

Check empty eager-loaded relations

• Verifies an eager-loaded partition relation with no results is marked current.

tests/Permission/PartitionRelationsTest.php

PartitionTeamsTest.phpTrim redundant partition/team cases +0/-12

Trim redundant partition/team cases

• Removes overlapping schema and partition/team assertions.

tests/Permission/PartitionTeamsTest.php

PermissionCacheTransactionTest.phpCover transactional cache behavior +17/-0

Cover transactional cache behavior

• Adds assertions around cache behavior when assignments change in transactions.

tests/Permission/PermissionCacheTransactionTest.php

PermissionServiceProviderTest.phpRegress missing migration configuration +19/-6

Regress missing migration configuration

• Checks both migration variants report unloaded configuration before typed reads.

tests/Permission/PermissionServiceProviderTest.php

PublicApiTest.phpRemove obsolete cache API cases +0/-86

Remove obsolete cache API cases

• Removes tests for deleted immediate invalidation methods and overlapping checks.

tests/Permission/PublicApiTest.php

SchemaConfigTest.phpReduce schema-test duplication +3/-35

Reduce schema-test duplication

• Removes schema assertions already covered by migration and configuration tests.

tests/Permission/SchemaConfigTest.php

ConfigTest.phpAlign configuration default cases +2/-14

Align configuration default cases

• Retains an optional wildcard-class fallback check and removes obsolete fallback cases.

tests/Permission/Support/ConfigTest.php

TestCase.phpSupport upstream tests and shared stores +129/-34

Support upstream tests and shared stores

• Stops overriding package defaults and sets up database or worker-isolated Redis caching before migrations.

tests/Permission/TestCase.php

HasAssignedModelsTest.phpExpand reverse-assignment tests +46/-23

Expand reverse-assignment tests

• Covers unsaved roles, team IDs, and default model resolution.

tests/Permission/Traits/HasAssignedModelsTest.php

HasPermissionsTest.phpExpand direct-permission trait cases +108/-16

Expand direct-permission trait cases

• Reconciles assignment, sync, scope, and check cases while retaining deny assertions.

tests/Permission/Traits/HasPermissionsTest.php

HasPermissionsWithCustomModelsTest.phpExercise custom permission models +54/-49

Exercise custom permission models

• Adds custom-field, soft-delete, scope, and timestamped assignment cases.

tests/Permission/Traits/HasPermissionsWithCustomModelsTest.php

HasRolesTest.phpRegress enum and role assignment semantics +181/-62

Regress enum and role assignment semantics

• Checks enum name matching, unsaved-model queues, scopes, custom pivots, and SQL budgets.

tests/Permission/Traits/HasRolesTest.php

HasRolesWithCustomModelsTest.phpExercise custom role models +31/-27

Exercise custom role models

• Adds soft-delete restoration and assignment-touch cases.

tests/Permission/Traits/HasRolesWithCustomModelsTest.php

TeamHasAssignedModelsTest.phpConsolidate team reverse assignments +3/-13

Consolidate team reverse assignments

• Removes overlapping cases while preserving team-scoped behavior.

tests/Permission/Traits/TeamHasAssignedModelsTest.php

TeamHasPermissionsTest.phpExpand team permission and deny cases +173/-5

Expand team permission and deny cases

• Checks team-isolated wildcard denies, queued assignments, and custom-pivot syncs.

tests/Permission/Traits/TeamHasPermissionsTest.php

TeamHasRolesTest.phpExpand team-scoped role cases +171/-27

Expand team-scoped role cases

• Ports upstream team assignment and sync cases while retaining Hypervel behavior.

tests/Permission/Traits/TeamHasRolesTest.php

TeamScopeTest.phpAlign team scope edge cases +23/-30

Align team scope edge cases

• Checks disabled teams and missing team-model behavior.

tests/Permission/Traits/TeamScopeTest.php

WildcardHasPermissionsTest.phpRegress wildcard deny precedence +332/-133

Regress wildcard deny precedence

• Covers role, guard, warm-cache, and custom-parser denies and checks index construction work.

tests/Permission/Traits/WildcardHasPermissionsTest.php

UnitEnumTest.phpExpand unit enum coverage +26/-4

Expand unit enum coverage

• Aligns enum assignment and check cases while retaining Hypervel's unit enum support.

tests/Permission/UnitEnumTest.php

Documentation (10) +729 / -263
sync.yamlRecord reviewed Spatie revision +3/-2

Record reviewed Spatie revision

• Records the upstream commit and maps Spatie tests, caching, relations, and documentation to Hypervel.

docs/upstream-sync/sync.yaml

permission.mdExpand the Permission guide +697/-253

Expand the Permission guide

• Adds applicable Spatie guidance and corrects Hypervel-specific cache, migration, partition, and connection instructions.

src/docs/permission.md

README.mdClarify differences from Spatie +5/-1

Clarify differences from Spatie

• Documents denied wildcard indexes, cache locking and scope, coroutine-local state, and the absent Octane listener.

src/permission/README.md

permission.phpClarify configuration defaults +9/-7

Clarify configuration defaults

• Corrects default-model and teams guidance and documents omitted cache settings.

src/permission/config/permission.php

PermissionAttachedEvent.phpClarify attached-permission event +2/-0

Clarify attached-permission event

• Improves event payload documentation.

src/permission/src/Events/PermissionAttachedEvent.php

PermissionDetachedEvent.phpClarify detached-permission event +2/-0

Clarify detached-permission event

• Improves event payload documentation.

src/permission/src/Events/PermissionDetachedEvent.php

RoleAttachedEvent.phpClarify attached-role event +2/-0

Clarify attached-role event

• Improves event payload documentation.

src/permission/src/Events/RoleAttachedEvent.php

RoleDetachedEvent.phpClarify detached-role event +2/-0

Clarify detached-role event

• Improves event payload documentation.

src/permission/src/Events/RoleDetachedEvent.php

Guard.phpClarify guard resolution +5/-0

Clarify guard resolution

• Adds explanatory provider and guard lookup comments and a model-class return annotation.

src/permission/src/Guard.php

Permission.phpClarify permission creation contract +2/-0

Clarify permission creation contract

• Documents the permission factory method.

src/permission/src/Models/Permission.php

Other (2) +4 / -0
redis.ymlTest Permission with Redis +1/-0

Test Permission with Redis

• Adds a Redis-backed Permission suite run to CI.

.github/workflows/redis.yml

tests.ymlTest Permission with database caching +3/-0

Test Permission with database caching

• Adds a database-cache Permission suite run to CI.

.github/workflows/tests.yml

@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Rewrites permission authorization and cache coordination.

The PR is not yet safe to merge because deployments retaining an older permission catalog can lose role grants.

Fix All in Claude CodeFindings

  1. P1 Cached catalogs break after deployment ▶

Summary

The PR revises Permission authorization, catalog caching, assignment invalidation, migrations, tests, and documentation.

  • Denied wildcard permissions take precedence over grants, and role checks handle enum names consistently.
  • Catalog serialization and cache coordination reduce repeated work while the expanded suite exercises shared cache stores.

Reviews (2) · Last reviewed commit: "Tighten Permission sync event, metadata ..."

Comment on lines 1942 to +1945
$roles = $this->getHydratedPermissionRoleCollection(
(array) $item['roles'],
$permission,
(array) $item['roles'],
(array) $item['denied_roles'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cached catalogs break after deployment

If a deployment keeps entries in a persistent cache store, an existing catalog still has pivot-row arrays in roles and no denied_roles field. This code reads it under the unchanged cache key as though roles contained IDs. Permission checks can then warn and lose role grants until the entry expires or is cleared. Version the catalog key or rebuild entries in the old format.

Knowledge Base Used: Cache, filesystem, and Redis storage

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hypervel 0.4 hasn't been released, and we don't keep compatibility with earlier 0.4 builds, so no persistent store holds a catalog in the old format. Versioning the key or rebuilding old entries would add code that only serves pre-release builds. No change here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s fair. Given Hypervel 0.4 is unreleased and compatibility with earlier 0.4 builds is explicitly out of scope, a persistent cache cannot contain the old catalog format under the supported upgrade path. I’m withdrawing this finding; no key versioning or legacy rebuild is needed.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Permission checks repeatedly rebuild caches 🐞 Bug ➹ Performance
Description
Removing the singleton binding makes each resolution of PermissionRegistrar construct a new
instance, whose initializeCache() clears all permission runtime state. Permission operations
resolve the registrar independently, so checks in the same request repeatedly discard their hydrated
catalog and wildcard indexes and reload them.
Code

src/permission/src/PermissionServiceProvider.php[L42-47]

-        $this->app->singleton(PermissionRegistrar::class, fn ($app) => new PermissionRegistrar(
-            $app->make(CacheManager::class),
-            $app->make('config'),
-            $app,
-            $app->make(ModelCacheCoordinator::class),
-        ));
Evidence
The deleted provider binding was the registrar’s shared lifetime. Its constructor initializes the
cache, which forgets the coroutine-local catalog and other permission caches; multiple consumers
independently resolve the registrar.

src/permission/src/PermissionServiceProvider.php[37-40]
src/permission/src/PermissionRegistrar.php[132-142]
src/permission/src/PermissionRegistrar.php[316-334]
src/permission/src/PermissionRegistrar.php[1167-1174]
src/permission/src/Traits/HasPermissions.php[53-57]
src/permission/src/Models/Role.php[95-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Removing the registrar singleton causes each independent resolution to initialize a new registrar and clear coroutine-local permission caches.
## Fix Focus Areas
- src/permission/src/PermissionServiceProvider.php[37-40]
- src/permission/src/PermissionRegistrar.php[132-142]
## Recommended Fix
Restore a singleton binding for PermissionRegistrar using its current constructor dependencies. Keep cache-manager resolution inside initializeCache() so reinitialization can pick up a rebound manager.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Permission checks break on catalogs cached before the upgrade 🐞 Bug ☼ Reliability
Description
getSerializedPermissionsForCache() now stores scalar role keys and a denied_roles field under
the unchanged, unversioned catalog cache key, but getHydratedPermissionCollection() assumes every
cached entry has that new shape. A pre-upgrade entry can remain in the shared cache for up to 24
hours and cause warnings or lost role-granted permissions when a new worker reads it; during a
rolling deploy, an old worker reading a new entry can instead throw a TypeError.
Code

src/permission/src/PermissionRegistrar.php[R1883-1887]

+                    // Role keys instead of pivot rows keep the payload small; hydration rebuilds the pivots.
                  return [
                      'attributes' => Arr::except($permission->getAttributes(), $except),
-                        'roles' => $roles,
+                        'roles' => $roleKeys,
+                        'denied_roles' => $deniedRoleKeys,
Evidence
The previous serializer stored pivot-bearing arrays in roles, while the new serializer stores
scalar keys and adds denied_roles. The catalog is read through rememberSharedOrDirtyValue()
using the existing permission.cache.keys.roles key, whose default is
hypervel.permission.cache.roles and which has no format version; hydration then reads
$item['denied_roles'] and casts each role key to a string without checking the payload shape. No
migration or upgrade note requires a reset, the migrations forget only the unpartitioned roles key,
and the docs say permission:cache-reset clears only the current partition, so incompatible entries
can remain available to either worker version.

src/permission/src/PermissionRegistrar.php[1869-1888]
src/permission/src/PermissionRegistrar.php[1939-1950]
src/permission/src/PermissionRegistrar.php[2049-2080]
src/permission/src/PermissionRegistrar.php[1323-1331]
src/permission/src/PermissionRegistrar.php[60-60]
src/permission/src/PermissionRegistrar.php[292-292]
src/permission/src/PermissionRegistrar.php[1869-1887]
src/permission/src/PermissionRegistrar.php[1940-1948]
src/permission/src/PermissionRegistrar.php[2054-2066]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The catalog payload changed from pivot-bearing role arrays to scalar role keys plus `denied_roles`, but it remains under the same unversioned shared cache key. Pre-upgrade entries and entries written by old workers during a rolling deploy can therefore reach an incompatible hydrator, causing warnings, TypeErrors, or lost role-granted permissions until the entry expires or is cleared.
## Fix Focus Areas
- src/permission/src/PermissionRegistrar.php[1323-1331]
- src/permission/src/PermissionRegistrar.php[1869-1888]
- src/permission/src/PermissionRegistrar.php[1939-1950]
## Recommended Fix
Version the serialized catalog format, for example with a `version` field or a format suffix such as `:v2` on the catalog key in `getCacheKey()`. Before hydration in `permissionCatalog()`, treat an incompatible version or shape—including missing `denied_roles` or non-scalar role entries—as a cache miss and rebuild and overwrite the entry using `getSerializedPermissionsForCache()`; alternatively, detect and convert old-format role links. Add a regression test that reads an old-format cached payload and an upgrade note to the Permission docs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +1883 to +1887
// Role keys instead of pivot rows keep the payload small; hydration rebuilds the pivots.
return [
'attributes' => Arr::except($permission->getAttributes(), $except),
'roles' => $roles,
'roles' => $roleKeys,
'denied_roles' => $deniedRoleKeys,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Permission checks break on catalogs cached before the upgrade 🐞 Bug ☼ Reliability

getSerializedPermissionsForCache() now stores scalar role keys and a denied_roles field under
the unchanged, unversioned catalog cache key, but getHydratedPermissionCollection() assumes every
cached entry has that new shape. A pre-upgrade entry can remain in the shared cache for up to 24
hours and cause warnings or lost role-granted permissions when a new worker reads it; during a
rolling deploy, an old worker reading a new entry can instead throw a TypeError.
Agent Prompt
## Issue description
The catalog payload changed from pivot-bearing role arrays to scalar role keys plus `denied_roles`, but it remains under the same unversioned shared cache key. Pre-upgrade entries and entries written by old workers during a rolling deploy can therefore reach an incompatible hydrator, causing warnings, TypeErrors, or lost role-granted permissions until the entry expires or is cleared.

## Fix Focus Areas
- src/permission/src/PermissionRegistrar.php[1323-1331]
- src/permission/src/PermissionRegistrar.php[1869-1888]
- src/permission/src/PermissionRegistrar.php[1939-1950]

## Recommended Fix
Version the serialized catalog format, for example with a `version` field or a format suffix such as `:v2` on the catalog key in `getCacheKey()`. Before hydration in `permissionCatalog()`, treat an incompatible version or shape—including missing `denied_roles` or non-scalar role entries—as a cache miss and rebuild and overwrite the entry using `getSerializedPermissionsForCache()`; alternatively, detect and convert old-format role links. Add a regression test that reads an old-format cached payload and an upgrade note to the Permission docs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hypervel 0.4 hasn't been released, and we don't keep compatibility with earlier 0.4 builds, so there's no older cached format or older worker to handle during a deploy. No change here.

{
$this->mergeConfigFrom(__DIR__ . '/../config/permission.php', 'permission');

$this->app->singleton(PermissionRegistrar::class, fn ($app) => new PermissionRegistrar(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Permission checks repeatedly rebuild caches 🐞 Bug ➹ Performance

Removing the singleton binding makes each resolution of PermissionRegistrar construct a new
instance, whose initializeCache() clears all permission runtime state. Permission operations
resolve the registrar independently, so checks in the same request repeatedly discard their hydrated
catalog and wildcard indexes and reload them.
Agent Prompt
## Issue description
Removing the registrar singleton causes each independent resolution to initialize a new registrar and clear coroutine-local permission caches.

## Fix Focus Areas
- src/permission/src/PermissionServiceProvider.php[37-40]
- src/permission/src/PermissionRegistrar.php[132-142]

## Recommended Fix
Restore a singleton binding for PermissionRegistrar using its current constructor dependencies. Keep cache-manager resolution inside initializeCache() so reinitialization can pick up a rebound manager.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hypervel's container shares unbound concrete classes for the worker's lifetime: the first make() caches the instance, and later resolutions return it. PermissionRegistrar is still built once per worker, so the closure binding only repeated what the container already does. No change here.

@binaryfire binaryfire changed the title Align Permission with Spatie and fix deny, wildcard and cache defects Improve Permission authorization and cache performance Oct 5, 2026
The teams config comment said to enable teams before migrating or to
run permission:setup-teams, which read as if the command works without
enabling teams. It refuses to run until teams are enabled, so the
comment now says to enable teams first in both cases.

The partitioning guide explains how to reset every partition but not
when that matters. The create migration clears only the unpartitioned
cache keys, so a custom migration that recreates partitioned tables
must reset each affected partition. Otherwise cached assignments from
the old tables apply to new records that reuse their keys. A
per-partition reset rotates that partition's assignment token, so it
clears those entries.

The enum example now imports the Role and Permission models it uses.
testItFiresDetachEventWhenSyncingPermissions faked events before the
initial grant, so the grant's own attached event satisfied the attached
assertion, which checks the model's permissions only when it runs. The
test passed even if the sync dispatched no attached event. The grant now
runs before Event::fake(), so only the sync's event can match. Spatie's
test has the same order; a comment keeps the change from being undone.

PackageMetadataTest compared every non-Hypervel requirement with the
root package, but dropping composer-runtime-api, nesbot/carbon or
symfony/http-kernel from the Permission package would still have
passed. It now asserts those three requirements are present.

PolicyTest's comments said "view" where the assertions check "update".

Validation: HasPermissionsTest and its two inherited variants
(custom models, teams), PackageMetadataTest and PolicyTest;
php-cs-fixer.
@binaryfire
binaryfire merged commit 80ee26a into 0.4 Oct 5, 2026
51 checks passed
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