From 5f6df2bfdd5b132848178eb899862fc07f731449 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 12 Dec 2024 13:40:09 +1100 Subject: [PATCH] Add before hook support to features with names --- src/Drivers/Decorator.php | 35 +++++++++++++-- tests/Feature/DatabaseDriverTest.php | 66 +++++++++++++++++++++++++++- tests/Feature/FeatureHelperTest.php | 6 +-- tests/IntersectionTypeTest.php | 2 +- 4 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/Drivers/Decorator.php b/src/Drivers/Decorator.php index 363c9dd..0377594 100644 --- a/src/Drivers/Decorator.php +++ b/src/Drivers/Decorator.php @@ -330,13 +330,13 @@ public function getAll($features): array $resolvedBefore = $features->reduce(function ($resolved, $scopes, $feature) use (&$hasUnresolvedFeatures) { $resolved[$feature] = []; - if (! method_exists($feature, 'before')) { + if (! $this->hasBeforeHook($feature)) { $hasUnresolvedFeatures = true; return $resolved; } - $before = $this->container->make($feature)->before(...); + $before = $this->container->make($this->implementationClass($feature))->before(...); foreach ($scopes as $index => $scope) { $value = $this->resolveBeforeHook($feature, $scope, $before); @@ -430,8 +430,8 @@ public function get($feature, $scope): mixed return $item['value']; } - $before = method_exists($feature, 'before') - ? $this->container->make($feature)->before(...) + $before = $this->hasBeforeHook($feature) + ? $this->container->make($this->implementationClass($feature))->before(...) : fn () => null; $value = $this->resolveBeforeHook($feature, $scope, $before) ?? $this->driver->get($feature, $scope); @@ -682,6 +682,33 @@ protected function ensureDynamicFeatureIsDefined($feature) }); } + /** + * Determine if the given feature has a before hook. + * + * @param string $feature + * @return bool + */ + protected function hasBeforeHook($feature) + { + $implementation = $this->implementationClass($feature); + + return is_string($implementation) && class_exists($implementation) && method_exists($implementation, 'before'); + } + + /** + * Retrieve the implementation feature class for the given feature name. + * + * @return ?string + */ + protected function implementationClass($feature) + { + $class = $this->nameMap[$feature] ?? $feature; + + if (is_string($class) && class_exists($class)) { + return $class; + } + } + /** * Resolve the scope. * diff --git a/tests/Feature/DatabaseDriverTest.php b/tests/Feature/DatabaseDriverTest.php index 41f2cf9..1268f12 100644 --- a/tests/Feature/DatabaseDriverTest.php +++ b/tests/Feature/DatabaseDriverTest.php @@ -1460,11 +1460,22 @@ public function test_it_can_get_features_with_before_hook() $queries++; }); FeatureWithBeforeHook::$before = fn ($scope) => ['before' => 'value']; + FeatureWithBeforeHookAndCustomName::$before = fn ($scope) => ['before' => 'value 2']; $value = Feature::get(FeatureWithBeforeHook::class, null); $this->assertSame(['before' => 'value'], $value); $this->assertSame(0, $queries); + + $value = Feature::get(FeatureWithBeforeHookAndCustomName::class, null); + + $this->assertSame(['before' => 'value 2'], $value); + $this->assertSame(0, $queries); + + $value = Feature::get('feature-with-before-hook-and-custom-name', null); + + $this->assertSame(['before' => 'value 2'], $value); + $this->assertSame(0, $queries); } public function test_it_handles_null_scope_for_before_hook() @@ -1491,6 +1502,40 @@ public function test_it_handles_null_scope_for_before_hook() Event::assertDispatchedTimes(UnexpectedNullScopeEncountered::class, 2); } + public function test_it_can_use_before_hook_when_using_feature_name_property() + { + $queries = 0; + FeatureWithBeforeHookAndCustomName::$before = fn ($scope) => 'before'; + Feature::define(FeatureWithBeforeHookAndCustomName::class); + Feature::activate(FeatureWithBeforeHookAndCustomName::class, 'stored-value'); + Feature::flushCache(); + DB::listen(function (QueryExecuted $event) use (&$queries) { + $queries++; + }); + + $value = Feature::for(null)->value(FeatureWithBeforeHookAndCustomName::class); + + $this->assertSame('before', $value); + $this->assertSame(0, $queries); + } + + public function test_it_can_use_before_hook_when_using_feature_name_property_on_a_dynamically_registered_feature() + { + $queries = 0; + FeatureWithBeforeHookAndCustomName::$before = fn ($scope) => 'before'; + Feature::activate(FeatureWithBeforeHookAndCustomName::class, 'stored-value'); + Feature::activate('blah', 'stored-value'); + Feature::flushCache(); + DB::listen(function (QueryExecuted $event) use (&$queries) { + $queries++; + }); + + $value = Feature::for(null)->value(FeatureWithBeforeHookAndCustomName::class); + + $this->assertSame('before', $value); + $this->assertSame(0, $queries); + } + public function test_it_maintains_scope_feature_keys() { $count = 0; @@ -1571,7 +1616,7 @@ public function test_it_keys_by_feature_name() ])); } - public function testItCanLoadAllFeaturesForScope() + public function test_it_can_load_all_features_for_scope() { Feature::define('bar', fn ($scope) => $scope === 'taylor'); Feature::define('foo', fn ($scope) => $scope === 'tim'); @@ -1602,7 +1647,7 @@ public function testItCanLoadAllFeaturesForScope() ], $records[3]); } - public function testCanRetrieveAllFeaturesForDifferingScopeTypes(): void + public function test_can_retrieve_all_features_for_differing_scope_types(): void { Feature::define('user', fn (User $user) => 1); Feature::define('nullable-user', fn (?User $user) => 2); @@ -1688,6 +1733,23 @@ public function before() } } +class FeatureWithBeforeHookAndCustomName +{ + public string $name = 'feature-with-before-hook-and-custom-name'; + + public static $before; + + public function resolve() + { + return 'feature-value'; + } + + public function before() + { + return (static::$before)(...func_get_args()); + } +} + class FeatureWithTypedBeforeHook { public static $before; diff --git a/tests/Feature/FeatureHelperTest.php b/tests/Feature/FeatureHelperTest.php index 3fe39c7..fa788d4 100644 --- a/tests/Feature/FeatureHelperTest.php +++ b/tests/Feature/FeatureHelperTest.php @@ -15,20 +15,20 @@ protected function setUp(): void Config::set('pennant.default', 'array'); } - public function testItReturnsFeatureManager() + public function test_it_returns_feature_manager() { $this->assertNotNull(feature()); $this->assertSame(Feature::getFacadeRoot(), feature()); } - public function testItReturnsTheFeatureValue() + public function test_it_returns_the_feature_value() { Feature::activate('foo', 'bar'); $this->assertSame('bar', feature('foo')); } - public function testItConditionallyExecutesCodeBlocks() + public function test_it_conditionally_executes_code_blocks() { Feature::activate('foo'); $inactive = $active = null; diff --git a/tests/IntersectionTypeTest.php b/tests/IntersectionTypeTest.php index 1c2816f..1f20e29 100644 --- a/tests/IntersectionTypeTest.php +++ b/tests/IntersectionTypeTest.php @@ -24,7 +24,7 @@ protected function setUp(): void DB::enableQueryLog(); } - public function testCanRetrieveAllFeaturesForDifferingScopeTypes(): void + public function test_can_retrieve_all_features_for_differing_scope_types(): void { Feature::define('user', fn (User $user) => 1); Feature::define('nullable-user', fn (?User $user) => 2);