Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3263e3b
Reconcile Permission command, guard and integration tests with upstream
binaryfire Oct 5, 2026
bc09bd4
Recheck model cache fills against the backing store
binaryfire Oct 5, 2026
de6ac34
Reconcile Permission middleware, model and assigned-model tests with …
binaryfire Oct 5, 2026
64cbc56
Reconcile HasPermissionsTest with upstream and narrow queued-flush in…
binaryfire Oct 5, 2026
e530601
Reconcile custom-model and team HasPermissions tests with upstream
binaryfire Oct 5, 2026
6a2be28
Reconcile HasRolesTest with upstream and create test users once
binaryfire Oct 5, 2026
2aae0be
Reconcile custom-model and team HasRoles tests and TeamScopeTest with…
binaryfire Oct 5, 2026
4be57ab
Reconcile wildcard and unit-enum tests and name catalog model connect…
binaryfire Oct 5, 2026
4109945
Apply denies through wildcard matching
binaryfire Oct 5, 2026
ce35d0c
Reconcile the Permission public surface
binaryfire Oct 5, 2026
8dbd9fb
Cut Permission catalog hydration costs and isolate Redis test runs
binaryfire Oct 5, 2026
7bb786a
Simplify Permission assignment internals and fix role checks
binaryfire Oct 5, 2026
be761cb
Simplify Permission partition machinery and run its suite on the data…
binaryfire Oct 5, 2026
89dfafb
Reconcile the Permission guide with upstream docs and fix cache resets
binaryfire Oct 5, 2026
52bc27f
Record the Permission sync checkpoint
binaryfire Oct 5, 2026
62b9e65
Report missing Permission config from the teams migration
binaryfire Oct 5, 2026
838247a
Merge branch '0.4' into upstream-sync-permission-reconciliation
binaryfire Oct 5, 2026
7a9db22
Clarify Permission teams setup and partition cache resets
binaryfire Oct 5, 2026
25cac72
Tighten Permission sync event, metadata and policy tests
binaryfire Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/redis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:
vendor/bin/paratest --max-processes=15 tests/Integration/Horizon
vendor/bin/paratest --max-processes=15 tests/Integration/Http/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/OpenTelemetry/Redis
CACHE_STORE=redis vendor/bin/paratest --max-processes=15 tests/Permission
QUEUE_CONNECTION=redis vendor/bin/paratest --max-processes=15 tests/Integration/Queue
vendor/bin/paratest --max-processes=15 tests/Integration/RateLimiter/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Redis
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
- name: Run framework test suite
run: vendor/bin/paratest -c phpunit.xml.dist

- name: Run Permission suite with the database cache store
run: CACHE_STORE=database vendor/bin/paratest -c phpunit.xml.dist tests/Permission

- name: Run Testbench package-mode suite
run: php src/testbench/bin/testbench package:test --parallel tests/Testbench

Expand Down
5 changes: 3 additions & 2 deletions docs/upstream-sync/sync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,10 @@ spatie/laravel-data:

spatie/laravel-permission:
branch: main
checked_through: null
checked_through: 6615eefac655efcd652fe394a20cbdf4f72c609f
last_reviewed_pr: null
sync_date: null
sync_date: '2026-10-05'
notes: Upstream Pest files map to PHPUnit classes at the same paths under tests/Permission, with each it()/test() description as the method name. Upstream's permission.testing migration flag, which adds the roles team column while teams are off so cases can enable teams mid-test, is not ported; those cases enable teams before migrating through the DefineEnvironment attribute's usesTeams method. The CACHE_DRIVER cache-driver test runs map to CACHE_STORE. Passport::actingAsClient() maps to TestCase::actingAsClient() with the PassportGuard fixture because Hypervel has no Passport package. Upstream's single cached permission collection maps to PermissionRegistrar's catalog payload (getSerializedPermissionsForCache() stores role keys per permission; permissionCatalog() and modelClassCatalog() hydrate it) plus the per-model assignment caches (rememberModel*()), which are filled through Cache\ModelCacheCoordinator and invalidated after commit; apply upstream cache or loading changes to both. Upstream relation definitions map to the Role and Permission relation methods built through BuildsPermissionRelations, whose Partitioned* relations add partition and team constraints and loaded-relation tracking. Upstream's detach-then-attach syncs map to HasRoles::syncRoles() and HasPermissions::synchronizePermissionAssignments(), which read the current pivots and write only the differences, keeping each permission's is_denied effect. Upstream docs map to sections of src/docs/permission.md; the example app, PhpStorm, UI options, schema diagram, upgrade and project pages are not ported.

aimeos/laravel-nestedset:
branch: master
Expand Down
49 changes: 41 additions & 8 deletions src/cache/src/ModelCacheCoordinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use Closure;
use Hypervel\Cache\Exceptions\UnsupportedModelCacheStoreException;
use Hypervel\Contracts\Cache\AuthoritativeRawReadable;
use Hypervel\Contracts\Cache\LockProvider;
use Hypervel\Contracts\Cache\RefreshableLock;
use Hypervel\Contracts\Cache\Repository as CacheRepository;
use Hypervel\Contracts\Cache\Store;

/**
* Coordinate shared model cache fills and exact invalidations.
Expand Down Expand Up @@ -65,14 +67,22 @@ public function fill(
return $cached[self::ENVELOPE_VALUE_KEY];
}

$lock = $this->lock($cache, $key);
$store = $cache->getStore();
$lock = $this->lock($store, $key);
$acquired = false;
$result = $lock
->get(function () use ($cache, $key, $ttl, $read, $cacheNull, $writeCache, $lock, &$acquired): mixed {
->get(function () use ($cache, $store, $key, $ttl, $read, $cacheNull, $writeCache, $lock, &$acquired): mixed {
$acquired = true;
$cached = $cache->get($key);

// A memoized read would repeat the miss above and hide a fill that
// finished before this lock was acquired.
$cached = $cache instanceof AuthoritativeRawReadable
? $cache->getAuthoritativeRaw($key)
: $cache->get($key);

if ($this->isEnvelope($cached)) {
$this->rememberEnvelope($store, $key, $cached);

return $cached[self::ENVELOPE_VALUE_KEY];
}

Expand All @@ -88,8 +98,13 @@ public function fill(
return $value;
}

($writeCache === null ? $cache : $writeCache())
->put($key, $this->envelope($value), $ttl);
$envelope = $this->envelope($value);
$published = ($writeCache === null ? $cache : $writeCache())
->put($key, $envelope, $ttl);

if ($published && $writeCache === null) {
$this->rememberEnvelope($store, $key, $envelope);
}

return $value;
});
Expand All @@ -102,7 +117,7 @@ public function fill(
*/
public function invalidate(CacheRepository $cache, string $key): bool
{
return (bool) $this->lock($cache, $key)
return (bool) $this->lock($cache->getStore(), $key)
->betweenBlockedAttemptsSleepFor(self::INVALIDATION_RETRY_MILLISECONDS)
->block(
self::INVALIDATION_WAIT_SECONDS,
Expand All @@ -123,6 +138,25 @@ private function envelope(mixed $value): array
];
}

/**
* Remember a shared envelope in a plain memoized store for the current coroutine.
*
* Memoized writes forget their key, and a memoized miss survives a store read
* that bypasses it, so later fills would otherwise lock and read the store again.
*
* @param array{__hypervel_model_cache: 'present', value: mixed} $envelope
*/
private function rememberEnvelope(Store $store, string $key, array $envelope): void
{
// Memoized tagged caches expose their backing store, so tagged keys never reach this memo.
if (! $store instanceof MemoizedStore) {
return;
}

$store->forgetMemoized($key);
$store->memoize($key, fn (): array => $envelope);
}

/**
* Determine whether the value is a cache presence envelope.
*/
Expand All @@ -139,9 +173,8 @@ private function isEnvelope(mixed $value): bool
*
* @throws UnsupportedModelCacheStoreException
*/
private function lock(CacheRepository $cache, string $key): RefreshableLock
private function lock(Store $store, string $key): RefreshableLock
{
$store = $cache->getStore();
$validatedStore = $store instanceof MemoizedStore
? $store->getInnerStore()
: $store;
Expand Down
Loading
Loading