-
-
Save bbrothers/8f7a05ec53eed9d83b6163272ec0a54c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Vehikl\Feature\Tests; | |
use PHPUnit\Framework\TestCase; | |
class FeatureToggleTest extends TestCase | |
{ | |
public function test_it_executes_the_on_method_when_the_feature_is_on() | |
{ | |
global $enabled; | |
$enabled = true; | |
$this->assertTrue((new SomeClass)->go()); | |
} | |
public function test_it_executes_the_off_method_when_the_feature_is_off() | |
{ | |
global $enabled; | |
$enabled = false; | |
$this->assertFalse((new SomeClass)->go()); | |
} | |
public function test_it_can_figure_out_which_feature_to_use_dynamically() | |
{ | |
global $enabled; | |
$enabled = true; | |
$this->assertTrue((new SomeClass)->goAnotherWay()); | |
} | |
public function test_it_can_figure_out_which_feature_to_use_dynamically_when_there_are_multiple_traits() | |
{ | |
global $enabled; | |
$enabled = false; | |
$this->assertFalse((new SomeClass)->yetAnotherWayToGo()); | |
} | |
} | |
class WillThisWork extends Feature | |
{ | |
public function features() : array | |
{ | |
return [ | |
'foobar' => ['on' => 'foo', 'off' => 'bar'], | |
]; | |
} | |
public function enabled() : bool | |
{ | |
global $enabled; | |
return $enabled; | |
} | |
protected function foo() | |
{ | |
return $this->returnTrue(); | |
} | |
protected function bar() | |
{ | |
return $this->returnFalse(); | |
} | |
} | |
class AnotherFeature extends Feature | |
{ | |
public function features() : array | |
{ | |
return [ | |
'quxqiz' => ['on' => 'qux', 'off' => 'qiz'], | |
]; | |
} | |
public function enabled() : bool | |
{ | |
global $enabled; | |
return $enabled; | |
} | |
public function qux() | |
{ | |
return true; | |
} | |
public function qiz() | |
{ | |
return false; | |
} | |
} | |
trait Featurable | |
{ | |
protected function flip() | |
{ | |
return new Flip($this, $this->features); | |
} | |
} | |
class SomeClass | |
{ | |
use Featurable; | |
// Why use traits instead of a list of classes? | |
protected $features = [ | |
WillThisWork::class, | |
AnotherFeature::class, | |
]; | |
public function go() | |
{ | |
return $this->flip()->foobar(); | |
} | |
public function goAnotherWay() | |
{ | |
// return WillThisWork::foobar(); | |
// This API wouldn't require the debug_backtrace | |
return WillThisWork::new($this)->foobar(); | |
} | |
public function yetAnotherWayToGo() | |
{ | |
return AnotherFeature::quxqiz(); | |
} | |
protected function returnTrue() | |
{ | |
return true; | |
} | |
protected function returnFalse() | |
{ | |
return false; | |
} | |
} | |
abstract class Feature | |
{ | |
protected $caller; | |
// Maybe it's worth requiring an interface be applied? | |
public function __construct(object $caller) | |
{ | |
$this->caller = $caller; | |
} | |
public static function new(object $caller) : Feature | |
{ | |
return new static($caller); | |
} | |
abstract public function features() : array; | |
abstract public function enabled() : bool; | |
public function appliesToMethod(string $method) : bool | |
{ | |
return array_key_exists($method, $this->features()); | |
} | |
public function __call($method, $arguments) | |
{ | |
$methodToCall = $this->methodToCall($method); | |
if (method_exists($this, $methodToCall)) { | |
return $this->{$methodToCall}(); | |
} | |
// Probably easier to just expect a public method. | |
$method = (new \ReflectionClass($this->caller()))->getMethod($methodToCall); | |
$method->setAccessible(true); | |
return $method->invoke($this->caller(), $arguments); | |
} | |
protected function caller() | |
{ | |
return $this->caller; | |
} | |
public static function __callStatic($method, $arguments) | |
{ | |
// Could be extracted, but I wonder how reliable this would be? | |
// Does it really improve the API that much? | |
$caller = function () { | |
foreach (debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5) as $trace) { | |
if ( | |
! isset($trace['object']) | |
or in_array(get_class($trace['object']), [Feature::class, static::class]) | |
) { | |
continue; | |
} | |
return $trace['object']; | |
} | |
throw new \LogicException('No caller found.'); | |
}; | |
$instance = new static($caller()); | |
return $instance->{$method}($arguments); | |
} | |
private function methodToCall(string $method) : string | |
{ | |
$features = $this->features(); | |
if (array_key_exists($method, $features)) { | |
// if $features was a class, it'd be a lot less error prone. | |
return $this->enabled() ? $features[$method]['on'] : $features[$method]['off']; | |
} | |
return $method; | |
} | |
} | |
class Flip | |
{ | |
private $class; | |
private $features; | |
public function __construct(object $class, array $features = []) | |
{ | |
$this->class = $class; | |
$this->features = $features; | |
} | |
public function __call($method, $arguments) | |
{ | |
// What happens if a method applies to multiple features? | |
$first = $this->applicableFeature($method) ?: $this->class; | |
return $first->{$method}($arguments); | |
} | |
private function applicableFeature(string $method) : ?Feature | |
{ | |
// Probably want a factory class to resolve any dependencies and cache | |
foreach ($this->buildFeatures() as $feature) { | |
if ($feature->appliesToMethod($method)) { | |
return $feature; | |
} | |
} | |
return null; | |
} | |
/** | |
* @return \Generator|Feature[] | |
*/ | |
private function buildFeatures() : \Generator | |
{ | |
foreach ($this->features as $feature) { | |
yield new $feature($this->class); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@colindecarlo not going to claim this is really any better, because I still don't think I've got a clear picture of what you're going for, but I my thought process was a bit different on how to make the tests pass. Excuse the naming, I was trying to pay attention to a conference call at the same time, so not a lot of thought went into names.