Skip to content

Commit

Permalink
Introduce before hook (#111)
Browse files Browse the repository at this point in the history
Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
timacdonald and taylorotwell authored Jul 8, 2024
1 parent a091b1f commit e031b6f
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/vendor
composer.lock
/composer.lock
/phpunit.xml
.phpunit.result.cache
/.phpunit.cache
10 changes: 7 additions & 3 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
colors="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
</source>
<php>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
Expand Down
4 changes: 2 additions & 2 deletions src/Drivers/DatabaseDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public function getAll($features): array
{
$query = $this->newQuery();

$features = Collection::make($features)
$resolved = Collection::make($features)
->map(fn ($scopes, $feature) => Collection::make($scopes)
->each(fn ($scope) => $query->orWhere(
fn ($q) => $q->where('name', $feature)->where('scope', Feature::serializeScope($scope))
Expand All @@ -154,7 +154,7 @@ public function getAll($features): array

$inserts = new Collection;

$results = $features->map(fn ($scopes, $feature) => $scopes->map(function ($scope) use ($feature, $records, $inserts) {
$results = $resolved->map(fn ($scopes, $feature) => $scopes->map(function ($scope) use ($feature, $records, $inserts) {
$filtered = $records->where('name', $feature)->where('scope', Feature::serializeScope($scope));

if ($filtered->isNotEmpty()) {
Expand Down
79 changes: 69 additions & 10 deletions src/Drivers/Decorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,46 @@ public function getAll($features): array
return [];
}

return tap($this->driver->getAll($features->all()), function ($results) use ($features) {
$features->flatMap(fn ($scopes, $key) => Collection::make($scopes)
->zip($results[$key])
->map(fn ($scopes) => $scopes->push($key)))
->each(fn ($value) => $this->putInCache($value[2], $value[0], $value[1]));
});
$hasUnresolvedFeatures = false;

$resolvedBefore = $features->reduce(function ($resolved, $scopes, $feature) use (&$hasUnresolvedFeatures) {
$resolved[$feature] = [];

if (! method_exists($feature, 'before')) {
$hasUnresolvedFeatures = true;

return $resolved;
}

$before = $this->container->make($feature)->before(...);

foreach ($scopes as $index => $scope) {
$value = $this->resolveBeforeHook($feature, $scope, $before);

if ($value !== null) {
$resolved[$feature][$index] = $value;
} else {
$hasUnresolvedFeatures = true;
}
}

return $resolved;
}, []);

$results = array_replace_recursive(
$features->all(),
$resolvedBefore,
$hasUnresolvedFeatures ? $this->driver->getAll($features->map(function ($scopes, $feature) use ($resolvedBefore) {
return array_diff_key($scopes, $resolvedBefore[$feature]);
})->all()) : [],
);

$features->flatMap(fn ($scopes, $key) => Collection::make($scopes)
->zip($results[$key])
->map(fn ($scopes) => $scopes->push($key)))
->each(fn ($value) => $this->putInCache($value[2], $value[0], $value[1]));

return $results;
}

/**
Expand Down Expand Up @@ -294,11 +328,36 @@ public function get($feature, $scope): mixed
return $item['value'];
}

return tap($this->driver->get($feature, $scope), function ($value) use ($feature, $scope) {
$this->putInCache($feature, $scope, $value);
$before = method_exists($feature, 'before')
? $this->container->make($feature)->before(...)
: fn () => null;

Event::dispatch(new FeatureRetrieved($feature, $scope, $value));
});
$value = $this->resolveBeforeHook($feature, $scope, $before) ?? $this->driver->get($feature, $scope);

$this->putInCache($feature, $scope, $value);

Event::dispatch(new FeatureRetrieved($feature, $scope, $value));

return $value;
}

/**
* Resolve the before hook value.
*
* @param string $feature
* @param mixed $scope
* @param callable $hook
* @return mixed
*/
protected function resolveBeforeHook($feature, $scope, $hook)
{
if ($scope === null && ! $this->canHandleNullScope($hook)) {
Event::dispatch(new UnexpectedNullScopeEncountered($feature));

return null;
}

return $hook($scope);
}

/**
Expand Down
128 changes: 128 additions & 0 deletions tests/Feature/DatabaseDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Laravel\Pennant\Events\FeaturesPurged;
use Laravel\Pennant\Events\FeatureUpdated;
use Laravel\Pennant\Events\FeatureUpdatedForAllScopes;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
use Laravel\Pennant\Events\UnknownFeatureResolved;
use Laravel\Pennant\Feature;
use RuntimeException;
Expand Down Expand Up @@ -1383,6 +1384,103 @@ public function test_it_only_retries_on_conflicts_for_individual_queries()
$this->assertSame(1, $insertAttempts);
}

public function test_it_can_intercept_feature_values_using_before_hook()
{
$queries = 0;
FeatureWithBeforeHook::$before = fn ($scope) => 'before';
Feature::define(FeatureWithBeforeHook::class);
Feature::activate(FeatureWithBeforeHook::class, 'stored-value');
Feature::flushCache();
DB::listen(function (QueryExecuted $event) use (&$queries) {
$queries++;
});

$value = Feature::for(null)->value(FeatureWithBeforeHook::class);

$this->assertSame('before', $value);
$this->assertSame(0, $queries);
}

public function test_it_can_merges_before_with_resolved_features()
{
$queries = 0;
DB::listen(function (QueryExecuted $event) use (&$queries) {
$queries++;
});
FeatureWithBeforeHook::$before = fn ($scope) => ['before' => 'value'];
Feature::define('foo', 'bar');
Feature::define(FeatureWithBeforeHook::class);

$values = Feature::for('user:1')->all();

$this->assertSame([
'foo' => 'bar',
'Tests\Feature\FeatureWithBeforeHook' => ['before' => 'value'],
], $values);
$this->assertSame(2, $queries);
}

public function test_it_can_get_features_with_before_hook()
{
$queries = 0;
DB::listen(function (QueryExecuted $event) use (&$queries) {
$queries++;
});
FeatureWithBeforeHook::$before = fn ($scope) => ['before' => 'value'];

$value = Feature::get(FeatureWithBeforeHook::class, null);

$this->assertSame(['before' => 'value'], $value);
$this->assertSame(0, $queries);
}

public function test_it_handles_null_scope_for_before_hook()
{
Event::fake(UnexpectedNullScopeEncountered::class);
$queries = 0;
DB::listen(function (QueryExecuted $event) use (&$queries) {
$queries++;
});
FeatureWithTypedBeforeHook::$before = fn ($scope) => 'before-value';
Feature::define(FeatureWithTypedBeforeHook::class);

$values = Feature::for(null)->value(FeatureWithTypedBeforeHook::class);

$this->assertSame('feature-value', $values);
$this->assertSame(2, $queries);

Feature::flushCache();

$value = Feature::get(FeatureWithTypedBeforeHook::class, null);

$this->assertSame('feature-value', $values);
$this->assertSame(3, $queries);
Event::assertDispatchedTimes(UnexpectedNullScopeEncountered::class, 2);
}

public function test_it_maintains_scope_feature_keys()
{
$count = 0;
FeatureWithBeforeHook::$before = function ($scope) use (&$count) {
if ($scope === 'user:2') {
return null;
}

$count++;

return "before-value-{$count}";
};
Feature::define(FeatureWithBeforeHook::class);

$values = Feature::getAll([
FeatureWithBeforeHook::class => ['user:1', 'user:2', 'user:3'],
]);

$this->assertSame([
'Tests\Feature\FeatureWithBeforeHook' => ['before-value-1', 'feature-value', 'before-value-2'],
], $values);
}

public function test_it_keys_by_feature_name()
{
Feature::define(FeatureWithName::class);
Expand Down Expand Up @@ -1467,6 +1565,36 @@ public function __invoke()
}
}

class FeatureWithBeforeHook
{
public static $before;

public function resolve()
{
return 'feature-value';
}

public function before()
{
return (static::$before)(...func_get_args());
}
}

class FeatureWithTypedBeforeHook
{
public static $before;

public function resolve()
{
return 'feature-value';
}

public function before(string $scope)
{
return (static::$before)(...func_get_args());
}
}

class FeatureWithoutName
{
public function __invoke()
Expand Down

0 comments on commit e031b6f

Please sign in to comment.