diff --git a/src/Contracts/FeatureScopeSerializeable.php b/src/Contracts/FeatureScopeSerializeable.php new file mode 100644 index 0000000..3fd1af3 --- /dev/null +++ b/src/Contracts/FeatureScopeSerializeable.php @@ -0,0 +1,11 @@ +resolveScope($scope); $item = $this->cache - ->whereStrict('scope', Feature::serializeScope($scope)) + ->whereStrict('scope', Feature::serializeScope($scope, $this->name)) ->whereStrict('feature', $feature) ->first(); @@ -585,7 +585,7 @@ protected function resolveScope($scope) */ protected function isCached($feature, $scope) { - $scope = Feature::serializeScope($scope); + $scope = Feature::serializeScope($scope, $this->name, $this->name, $this->name, $this->name, $this->name, $this->name, $this->name, $this->name); return $this->cache->search( fn ($item) => $item['feature'] === $feature && $item['scope'] === $scope @@ -602,7 +602,7 @@ protected function isCached($feature, $scope) */ protected function putInCache($feature, $scope, $value) { - $scope = Feature::serializeScope($scope); + $scope = Feature::serializeScope($scope, $this->name); $position = $this->cache->search( fn ($item) => $item['feature'] === $feature && $item['scope'] === $scope @@ -624,7 +624,7 @@ protected function putInCache($feature, $scope, $value) */ protected function removeFromCache($feature, $scope) { - $scope = Feature::serializeScope($scope); + $scope = Feature::serializeScope($scope, $this->name); $position = $this->cache->search( fn ($item) => $item['feature'] === $feature && $item['scope'] === $scope diff --git a/src/FeatureManager.php b/src/FeatureManager.php index 99e6943..153b16a 100644 --- a/src/FeatureManager.php +++ b/src/FeatureManager.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use InvalidArgumentException; +use Laravel\Pennant\Contracts\FeatureScopeSerializeable; use Laravel\Pennant\Drivers\ArrayDriver; use Laravel\Pennant\Drivers\DatabaseDriver; use Laravel\Pennant\Drivers\Decorator; @@ -178,17 +179,19 @@ public function createDatabaseDriver(array $config, string $name) * Serialize the given scope for storage. * * @param mixed $scope + * @param string $driver * @return string|null */ - public function serializeScope($scope) + public function serializeScope($scope, $driver = '') { return match (true) { + $scope instanceof FeatureScopeSerializeable => $scope->featureScopeSerialize($driver), $scope === null => '__laravel_null', is_string($scope) => $scope, is_numeric($scope) => (string) $scope, $scope instanceof Model && $this->useMorphMap => $scope->getMorphClass().'|'.$scope->getKey(), $scope instanceof Model && ! $this->useMorphMap => $scope::class.'|'.$scope->getKey(), - default => throw new RuntimeException('Unable to serialize the feature scope to a string. You should implement the FeatureScopeable contract.') + default => throw new RuntimeException('Unable to serialize the feature scope to a string. You should implement the FeatureScopeSerializeable contract.') }; } diff --git a/tests/Feature/DatabaseDriverTest.php b/tests/Feature/DatabaseDriverTest.php index 8b26110..50c1d70 100644 --- a/tests/Feature/DatabaseDriverTest.php +++ b/tests/Feature/DatabaseDriverTest.php @@ -14,6 +14,7 @@ use Illuminate\Support\Str; use InvalidArgumentException; use Laravel\Pennant\Contracts\FeatureScopeable; +use Laravel\Pennant\Contracts\FeatureScopeSerializeable; use Laravel\Pennant\Events\AllFeaturesPurged; use Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass; use Laravel\Pennant\Events\FeatureDeleted; @@ -356,21 +357,27 @@ public function test_scope_can_be_strings_like_email_addresses() public function test_it_can_handle_feature_scopeable_objects() { - $scopeable = fn () => new class extends User implements FeatureScopeable + Feature::define('foo', function ($scope) { + return $scope === 'tim@laravel.com'; + }); + $scopeable = fn ($email) => new class(['email' => $email]) extends User implements FeatureScopeable { public function toFeatureIdentifier($driver): mixed { - return 'tim@laravel.com'; + return $this->email; } }; - Feature::for($scopeable())->activate('foo'); + $this->assertTrue(Feature::for($scopeable('tim@laravel.com'))->active('foo')); + $this->assertFalse(Feature::for($scopeable('james@laravel.com'))->active('foo')); - $this->assertFalse(Feature::for('james@laravel.com')->active('foo')); - $this->assertTrue(Feature::for('tim@laravel.com')->active('foo')); - $this->assertTrue(Feature::for($scopeable())->active('foo')); + $this->assertCount(4, DB::getQueryLog()); - $this->assertCount(2, DB::getQueryLog()); + $scope = DB::table('features')->get('scope'); + $this->assertSame([ + 'james@laravel.com', + 'tim@laravel.com', + ], $scope->pluck('scope')->all()); } public function test_it_serializes_eloquent_models() @@ -382,6 +389,31 @@ public function test_it_serializes_eloquent_models() $this->assertStringContainsString('Workbench\App\Models\User|1', $scope); } + public function test_it_can_manually_serialize_scope() + { + Feature::define('foo', function ($scope) { + return $scope->email === 'tim@laravel.com'; + }); + $scopeable = fn ($email) => new class(['email' => $email]) extends User implements FeatureScopeSerializeable + { + public function featureScopeSerialize(string $driver): string + { + return $this->email; + } + }; + + $this->assertTrue(Feature::for($scopeable('tim@laravel.com'))->active('foo')); + $this->assertFalse(Feature::for($scopeable('james@laravel.com'))->active('foo')); + + $this->assertCount(4, DB::getQueryLog()); + + $scope = DB::table('features')->get('scope'); + $this->assertSame([ + 'james@laravel.com', + 'tim@laravel.com', + ], $scope->pluck('scope')->all()); + } + public function test_it_can_load_feature_state_into_memory() { $called = ['foo' => 0, 'bar' => 0];