Skip to content

Instantly share code, notes, and snippets.

@bbrothers
Forked from colindecarlo/CrazyTown.php
Last active September 3, 2018 14:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bbrothers/8f7a05ec53eed9d83b6163272ec0a54c to your computer and use it in GitHub Desktop.
Save bbrothers/8f7a05ec53eed9d83b6163272ec0a54c to your computer and use it in GitHub Desktop.
<?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);
}
}
}
@bbrothers
Copy link
Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment