Skip to content

Commit

Permalink
Merge pull request #4 from vehikl/always-on
Browse files Browse the repository at this point in the history
Always on
  • Loading branch information
colindecarlo committed Sep 13, 2018
2 parents 8316fb6 + 80a7fa6 commit 35e855d
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
build
composer.lock
vendor
Expand Down
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ $ composer require vehikl/flip

## Usage

A Flip feature is just a regular PHP class with a few required methods.

`enabled` - This method returns a boolean value indicating if the feature is enabled or not

`toggles` - This method returns an array of available feature toggles. The array is keyed by the name of the method which
is called to run the feature. The value of each key is an associative array with keys `on` and `off`, each key
is mapped to the appropriate method to call depending on if the feature is "on" or "off".

``` php
class SomeFeature extends \Vehikl\Flip\Feature
{
Expand Down Expand Up @@ -70,6 +78,72 @@ class SomeClass
}
```

### Forcing Features to be On or Off

You can force a feature to be "on" or "off" by calling the `alwaysOn` or `alwaysOff` static methods respectively. This
will force all features of that class to be either "on" or "off" regardless of how their `enabled` methods evaluate.

```php
class SomeFeature extends \Vehikl\Flip\Feature
{
// include the $forcedState static variable if you want to enable forcing state
protected static $forcedState;

/**
* Decides under which conditions this Feature is enabled
*/
public function enabled()
{
return random_int(0, 1) == 1;
}

/**
* Returns an array of available toggles for this feature
*/
public function toggles()
{
return [
'someToggle' => [
'on' => 'whenOn',
'off' => 'whenOff'
]
];
}

public function whenOn()
{
return "I'm on!";
}

public function whenOff()
{
return "I'm off!";
}
}

class SomeClass
{
use Vehikl\Flip\Featurable;

protected $features = [SomeFeature::class];

public function someBehaviour()
{
// no need for if/else blocks, just call the toggle using the
// `flip` helper
return $this->flip()->someToggle();
}
}

// force the SomeFeature feature to be always on
SomeFeature::alwaysOn()

// anytime `someToggle` is called on instances of SomeClass,
// the `on` version of `someToggle` will be run
$someObject = new SomeClass;
$someObject->someBehaviour(); // always returns "I'm on!"
```

## Change log

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
Expand Down
10 changes: 1 addition & 9 deletions src/DefaultResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@

namespace Vehikl\Flip;

class DefaultResolver implements Resolver
class DefaultResolver extends Resolver
{
public function resolve($object, string $method)
{
try {
return $object->{$method}();
} catch (\ArgumentCountError $e) {
throw new UnresolvableDependencies();
}
}
}
24 changes: 24 additions & 0 deletions src/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
*/
abstract class Feature
{
const ENABLED = 'enabled';
const DISABLED = 'disabled';

private static $resolver;
protected $caller;
protected static $forceState;

// Maybe it's worth requiring an interface be applied?
public function __construct($caller)
Expand Down Expand Up @@ -84,4 +88,24 @@ public static function __callStatic($method, $arguments)

return $instance->{$method}($arguments);
}

public static function alwaysOn() : void
{
static::$forceState = self::ENABLED;
}

public static function alwaysOff() : void
{
static::$forceState = self::DISABLED;
}

public function hasForcedState() : bool
{
return static::$forceState !== null;
}

public function isAlwaysOn() : bool
{
return static::$forceState === self::ENABLED;
}
}
20 changes: 18 additions & 2 deletions src/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@

namespace Vehikl\Flip;

interface Resolver
abstract class Resolver
{
public function resolve($object, string $method);
public function resolve(Feature $feature, string $method)
{
if ($feature->hasForcedState()) {
return $feature->isAlwaysOn();
}

return $this->call($feature, $method);
}

protected function call($feature, string $method)
{
try {
return $feature->{$method}();
} catch (\ArgumentCountError $e) {
throw new UnresolvableDependencies();
}
}
}
14 changes: 13 additions & 1 deletion tests/SomeFeature.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ public function toggles(): array
'someParameterAcceptingToggle' => [
'on' => 'whenSomeParameterAcceptingToggleIsOn',
'off' => 'whenSomeParameterAcceptingToggleIsOff'
]
],
'someRequiredParameterAcceptingToggle' => [
'on' => 'whenSomeRequiredParameterAcceptingToggleIsOn',
'off' => 'whenSomeRequiredParameterAcceptingToggleIsOff'
],
];
}

Expand Down Expand Up @@ -114,4 +118,12 @@ public function whenSomeParameterAcceptingToggleIsOff(...$params): void
{
$this->parameters = $params;
}

public function whenSomeRequiredParameterAcceptingToggleIsOn($someParameter) : void
{
}

public function whenSomeRequiredParameterAcceptingToggleIsOff($someParameter) : void
{
}
}
30 changes: 25 additions & 5 deletions tests/SomeOtherFeature.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

class SomeOtherFeature extends Feature
{
protected static $forceState;

private $invokedMethod;
private $enabled = true;

public function toggles(): array
{
return [
Expand All @@ -16,18 +21,33 @@ public function toggles(): array
];
}

public function turnOff() : void
{
$this->enabled = false;
}

public function turnOn() : void
{
$this->enabled = true;
}

public function enabled(): bool
{
return true;
return $this->enabled;
}

public function whenSomeOtherToggleIsOn() : string
{
return $this->invokedMethod = 'whenSomeOtherToggleIsOn';
}

public function whenSomeOtherToggleIsOn()
public function whenSomeOtherToggleIsOff() : string
{
return 'whenSomeOtherToggleIsOn';
return $this->invokedMethod = 'whenSomeOtherToggleIsOff';
}

public function whenSomeOtherToggleIsOff()
public function invokedMethod() : string
{
return 'whenSomeOtherToggleIsOff';
return $this->invokedMethod;
}
}
24 changes: 7 additions & 17 deletions tests/Unit/DefaultResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,27 @@

namespace Vehikl\Flip\Tests\Unit;

use Vehikl\Flip\DefaultResolver;
use PHPUnit\Framework\TestCase;
use Vehikl\Flip\Tests\SomeDependency;
use Vehikl\Flip\DefaultResolver;
use Vehikl\Flip\Tests\SomeFeature;
use Vehikl\Flip\UnresolvableDependencies;

class DefaultResolverTest extends TestCase
{
public function test_it_calls_methods_which_do_not_have_dependencies()
{
$class = new class() {
public function someMethod()
{
return true;
}
};
$someFeature = new SomeFeature($this);
$someFeature->turnOn();

$this->assertTrue((new DefaultResolver)->resolve($class, 'someMethod'));
$this->assertEquals('whenOn', (new DefaultResolver)->resolve($someFeature, 'someToggle'));
}

public function test_it_throws_an_exception_when_the_resolved_method_does_have_dependencies()
{
$class = new class() {
public function someMethod(SomeDependency $someDependency)
{
return false;
}
};

$this->expectException(UnresolvableDependencies::class);
$this->expectExceptionMessage("The Flip DefaultResolver is unable to resolve method dependencies.");

(new DefaultResolver)->resolve($class, 'someMethod');
$someFeature = new SomeFeature($this);
(new DefaultResolver)->resolve($someFeature, 'someRequiredParameterAcceptingToggle');
}
}
69 changes: 67 additions & 2 deletions tests/Unit/FeatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Vehikl\Flip\Feature;
use Vehikl\Flip\Resolver;
use Vehikl\Flip\Tests\SomeFeature;
use Vehikl\Flip\Tests\SomeOtherFeature;

/**
* @backupStaticAttributes enabled
Expand Down Expand Up @@ -84,8 +85,8 @@ public function test_it_blows_up_when_a_toggle_method_calls_a_method_that_doesnt

public function test_a_resolver_can_be_registered_with_the_feature()
{
$resolver = new class() implements Resolver {
public function resolve($object, string $method)
$resolver = new class() extends Resolver {
public function resolve(Feature $feature, string $method)
{
}
};
Expand All @@ -101,4 +102,68 @@ public function test_the_default_flip_resolver_is_registered_by_default()

$this->assertInstanceOf(DefaultResolver::class, $someFeature->resolver());
}

public function test_features_can_be_forced_to_on()
{
$someFeature = new SomeFeature($this);
$someFeature->alwaysOn();
$someFeature->turnOff();

$someFeature->someToggle();

$this->assertEquals('whenOn', $someFeature->invokedMethod());
}

public function test_features_can_be_forced_to_off()
{
$someFeature = new SomeFeature($this);
$someFeature->alwaysOff();
$someFeature->turnOn();

$someFeature->someToggle();

$this->assertEquals('whenOff', $someFeature->invokedMethod());
}

public function test_features_forced_into_an_enabled_state_will_apply_to_all_features_of_the_same_type()
{
SomeFeature::alwaysOn();

$someFeature = new SomeFeature($this);
$anotherSomeFeature = new SomeFeature($this);
$someOtherFeature = new SomeOtherFeature($this);

$someFeature->turnOff();
$anotherSomeFeature->turnOff();
$someOtherFeature->turnOff();

$someFeature->someToggle();
$anotherSomeFeature->someToggle();
$someOtherFeature->someOtherToggle();

$this->assertEquals('whenOn', $someFeature->invokedMethod());
$this->assertEquals('whenOn', $someFeature->invokedMethod());
$this->assertEquals('whenSomeOtherToggleIsOff', $someOtherFeature->invokedMethod());
}

public function test_features_forced_into_a_disabled_state_will_apply_to_all_features_of_the_same_type()
{
SomeFeature::alwaysOff();

$someFeature = new SomeFeature($this);
$anotherSomeFeature = new SomeFeature($this);
$someOtherFeature = new SomeOtherFeature($this);

$someFeature->turnOn();
$anotherSomeFeature->turnOn();
$someOtherFeature->turnOn();

$someFeature->someToggle();
$anotherSomeFeature->someToggle();
$someOtherFeature->someOtherToggle();

$this->assertEquals('whenOff', $someFeature->invokedMethod());
$this->assertEquals('whenOff', $someFeature->invokedMethod());
$this->assertEquals('whenSomeOtherToggleIsOn', $someOtherFeature->invokedMethod());
}
}

0 comments on commit 35e855d

Please sign in to comment.