diff --git a/src/PendingScopedFeatureInteraction.php b/src/PendingScopedFeatureInteraction.php index ab59b5d..506f2cf 100644 --- a/src/PendingScopedFeatureInteraction.php +++ b/src/PendingScopedFeatureInteraction.php @@ -3,7 +3,6 @@ namespace Laravel\Pennant; use Illuminate\Support\Collection; -use Laravel\Pennant\Events\FeatureUnavailableForScope; use RuntimeException; class PendingScopedFeatureInteraction @@ -74,7 +73,6 @@ public function loadMissing($features) /** * Load all defined features into memory. * - * @param string|array $features * @return array> */ public function loadAll() @@ -102,10 +100,7 @@ public function value($feature) public function values($features) { return Collection::make($this->rawValues($features)) - ->mapWithKeys(function($value, $key) { - $value = $value instanceof FeatureDoesNotMatchScope ? false : $value; - return [$key => $value]; - }) + ->mapWithKeys(fn($value, $key) => [$key => $this->fromRaw($value)]) ->all(); } @@ -180,7 +175,7 @@ public function someAreActive($features) return Collection::make($this->scope()) ->every(fn ($scope) => Collection::make($features) - ->some(fn ($feature) => $this->driver->get($feature, $scope) !== false)); + ->some(fn ($feature) => $this->fromRaw($this->driver->get($feature, $scope)) !== false)); } /** @@ -206,7 +201,7 @@ public function allAreInactive($features) return Collection::make($features) ->crossJoin($this->scope()) - ->every(fn ($bits) => $this->driver->get(...$bits) === false); + ->every(fn ($bits) => $this->fromRaw($this->driver->get(...$bits)) === false); } /** @@ -221,7 +216,7 @@ public function someAreInactive($features) return Collection::make($this->scope()) ->every(fn ($scope) => Collection::make($features) - ->some(fn ($feature) => $this->driver->get($feature, $scope) === false)); + ->some(fn ($feature) => $this->fromRaw($this->driver->get($feature, $scope)) === false)); } /** @@ -305,4 +300,19 @@ protected function scope() { return $this->scope ?: [null]; } + + /** + * Replace FeatureDoesNotMatchScope with false. + * + * @param mixed $value + * @return false|mixed + */ + protected function fromRaw($value) + { + if ($value instanceof FeatureDoesNotMatchScope) { + return false; + } + + return $value; + } } diff --git a/tests/Feature/DatabaseDriverTest.php b/tests/Feature/DatabaseDriverTest.php index a6c997e..15301d6 100644 --- a/tests/Feature/DatabaseDriverTest.php +++ b/tests/Feature/DatabaseDriverTest.php @@ -1573,7 +1573,7 @@ public function testItCanLoadAllFeaturesForScope() public function testCanGetAllWhenFeaturesAreDefinedForDifferentScopes(): void { - // Given features + // Given features of varying scopes Feature::define('for-teams', fn(Team $team) => true); Feature::define('for-users', fn(User $user) => true); Feature::define('for-nullable-users', fn(?User $user) => false); @@ -1598,6 +1598,7 @@ public function testCanGetAllWhenFeaturesAreDefinedForDifferentScopes(): void $features ); + // And an event was dispatched indicating that we tried to retrieve a feature not matched to scope Event::assertDispatchedTimes(FeatureUnavailableForScope::class, 1); Event::assertDispatched(function (FeatureUnavailableForScope $event) use ($user) { return $event->feature === 'for-teams' @@ -1607,10 +1608,10 @@ public function testCanGetAllWhenFeaturesAreDefinedForDifferentScopes(): void public function testInvalidScopedFeatureReturnsFalse(): void { - // Given + // Given scope belonging to a Team scope Feature::define('yooo', fn(Team $team) => true); - // When + // When attempting to fetch that feature for a User scope $result = Feature::for(new User)->active('yooo'); // Then @@ -1619,7 +1620,7 @@ public function testInvalidScopedFeatureReturnsFalse(): void public function testValuesReturnsFalseForFeaturesWhichDoNotBelongToScope(): void { - // Given + // Given features with varying scopes Feature::define('foo', fn(User $user) => true); Feature::define('bar', fn(Team $team) => true); Feature::define('zed', fn(mixed $v) => true); @@ -1640,6 +1641,76 @@ public function testValuesReturnsFalseForFeaturesWhichDoNotBelongToScope(): void 'woof' => false, ], $features); } + + public function testSomeAreActiveWithMismatchedScopeTreatsAsFalse(): void + { + // Given features with varying scopes + Feature::define('for-teams', fn(Team $team) => true); + Feature::define('for-nullable', fn() => false); + + // When + $result = Feature::for(new User)->someAreActive(['for-teams', 'for-nullable']); + + // Then + $this->assertFalse($result); + } + + public function testAllAreActiveTreatsMismatchedScopeAsFalse(): void + { + // Given features with varying scopes + Feature::define('for-team', fn(Team $team) => true); + Feature::define('for-user', fn(User $user) => true); + + // When + $result = Feature::for(new User)->allAreActive(['for-team', 'for-user']); + + // Then + $this->assertFalse($result); + } + + public function testSomeAreInactiveWithMismatchedScopeTreatsAsFalse(): void + { + // Given features with varying scopes + Feature::define('for-teams', fn(Team $team) => true); + Feature::define('for-user', fn(User $user) => true); + Feature::define('for-null-scope', fn() => true); + + // When + $result = Feature::for(new User)->someAreInactive([ + 'for-teams', 'for-user', 'for-null-scope' + ]); + + // Then + $this->assertTrue($result); + } + + public function testAllAreInactiveWithMismatchedScope(): void + { + // Given features with varying scopes + Feature::define('for-teams', fn(Team $team) => true); + Feature::define('for-user', fn(User $user) => false); + Feature::define('for-null-scope', fn() => false); + + // When + $result = Feature::for(new User)->allAreInactive(['for-teams', 'for-user', 'for-null-scope']); + + // Then + $this->assertTrue($result); + } + + public function test_mismatchedScopes_load(): void + { + // Given + Feature::define('for-teams', fn(Team $team) => true); + Feature::define('for-user', fn(User $user) => false); + Feature::define('for-null-scope', fn() => false); + + // When + $result = Feature::for(new User)->load(['for-teams']); + + // Then + $this->assertFalse($result['for-teams'][0]); + } } class UnregisteredFeature