Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.x] Gracefully handles unexpected null scope #31

Merged
merged 4 commits into from
Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
68 changes: 57 additions & 11 deletions src/Drivers/Decorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Laravel\Pennant\Drivers;

use Closure;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Collection;
use Illuminate\Support\Lottery;
Expand All @@ -12,7 +13,10 @@
use Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass;
use Laravel\Pennant\Events\FeatureResolved;
use Laravel\Pennant\Events\FeatureRetrieved;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
use Laravel\Pennant\LazilyResolvedFeature;
use Laravel\Pennant\PendingScopedFeatureInteraction;
use ReflectionFunction;
use Symfony\Component\Finder\Finder;

/**
Expand Down Expand Up @@ -107,27 +111,69 @@ public function define($feature, $resolver = null): void
if (func_num_args() === 1) {
[$feature, $resolver] = [
$this->container->make($feature)->name ?? $feature,
fn ($scope) => with($this->container[$feature], fn ($featureClass) => method_exists($featureClass, 'resolve')
? $featureClass->resolve($scope)
: $featureClass($scope)),
new LazilyResolvedFeature($feature),
];
}

if (is_string($resolver) || ! is_callable($resolver)) {
$resolver = fn () => $resolver;
}

$this->driver->define($feature, function ($scope) use ($feature, $resolver) {
$value = $resolver($scope);
if ($resolver instanceof LazilyResolvedFeature) {
$resolver = with($this->container[$resolver->feature], fn ($instance) => method_exists($instance, 'resolve')
? $instance->resolve(...)
: $instance(...));
}

if (! $resolver instanceof Closure) {
return $this->resolve($feature, fn () => $resolver, $scope);
}

if ($scope !== null) {
return $this->resolve($feature, $resolver, $scope);
}

$value = $value instanceof Lottery ? $value() : $value;
if ($this->canHandleNullScope($resolver)) {
return $this->resolve($feature, $resolver, $scope);
}

$this->container['events']->dispatch(new FeatureResolved($feature, $scope, $value));
$this->container['events']->dispatch(new UnexpectedNullScopeEncountered($feature));

return $value;
return $this->resolve($feature, fn () => false, $scope);
});
}

/**
* Resolve the feature value.
*
* @param string $feature
* @param callable $resolver
* @param mixed $scope
* @return mixed
*/
protected function resolve($feature, $resolver, $scope)
{
$value = $resolver($scope);

$value = $value instanceof Lottery ? $value() : $value;

$this->container['events']->dispatch(new FeatureResolved($feature, $scope, $value));

return $value;
}

/**
* Determine if the resolver accepts null scope.
*
* @param callable $resolver
* @return bool
*/
protected function canHandleNullScope($resolver)
{
$function = new ReflectionFunction(Closure::fromCallable($resolver));

return $function->getNumberOfParameters() === 0 ||
! $function->getParameters()[0]->hasType() ||
$function->getParameters()[0]->getType()->allowsNull();
}

/**
* Retrieve the names of all defined features.
*
Expand Down
23 changes: 23 additions & 0 deletions src/Events/UnexpectedNullScopeEncountered.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Laravel\Pennant\Events;

class UnexpectedNullScopeEncountered
{
/**
* The feature name.
*
* @var string
*/
public $feature;

/**
* Create a new event instance.
*
* @param string $feature
*/
public function __construct($feature)
{
$this->feature = $feature;
}
}
2 changes: 1 addition & 1 deletion src/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use RuntimeException;

/**
* @method static mixed store(string|null $store = null)
* @method static \Laravel\Pennant\Drivers\Decorator store(string|null $store = null)
* @method static \Laravel\Pennant\Drivers\ArrayDriver createArrayDriver()
* @method static \Laravel\Pennant\Drivers\DatabaseDriver createDatabaseDriver()
* @method static void flushCache()
Expand Down
2 changes: 1 addition & 1 deletion src/FeatureManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class FeatureManager extends Manager
* Get a Pennant store instance.
*
* @param string|null $store
* @return mixed
* @return \Laravel\Pennant\Drivers\Decorator
*
* @throws \InvalidArgumentException
*/
Expand Down
23 changes: 23 additions & 0 deletions src/LazilyResolvedFeature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Laravel\Pennant;

class LazilyResolvedFeature
{
/**
* The feature class.
*
* @var class-string
*/
public $feature;

/**
* Create a new lazy feature instance.
*
* @param class-string $feature
*/
public function __construct($feature)
{
$this->feature = $feature;
}
}
Loading