From 0bfb333ca04fd4bc05431fce5bf3cb7258078f29 Mon Sep 17 00:00:00 2001 From: Luke Watts Date: Thu, 7 Mar 2024 12:30:15 +0000 Subject: [PATCH] Adding Conditionable trait, and HigherOrderWhenProxy class --- README.md | 90 ++++++++- src/Support/HigherOrderWhenProxy.php | 109 +++++++++++ src/Support/Traits/Conditionable.php | 73 +++++++ tests/Support/Traits/ConditionableTest.php | 210 +++++++++++++++++++++ 4 files changed, 479 insertions(+), 3 deletions(-) create mode 100644 src/Support/HigherOrderWhenProxy.php create mode 100644 src/Support/Traits/Conditionable.php create mode 100644 tests/Support/Traits/ConditionableTest.php diff --git a/README.md b/README.md index 568a8cb..6c9773d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SlimPHP Facades -Add Laravel style facades and helper functions to any SlimPHP app. +Add Laravel style facades, traits and helper functions to any SlimPHP app ## Installation @@ -100,7 +100,7 @@ return tap(new Psr7Response(), function ($response) { ## Traits -### SlimFacades\Traits\Tappable +### Tappable ```php use SlimFacades\Support\Traits\Tappable; @@ -135,7 +135,7 @@ $name = TappableClass::make()->tap(function ($tappable) { $name = TappableClass::make()->tap()->setName('MyName')->getName() ``` -### Support\Traits\Macroable +### Macroable Macros allow you to add methods to classes dynamically (without having to modify their code). @@ -180,3 +180,87 @@ App::get('/', function () { return Container::get('response')->write('Macro!'); }); ``` + +### Conditionable + +Allows to conditionally chain functionality. + +For example, let's imagine we have a standard PSR-11 Container, which has a the bare minimum PSR-11 compliant methods, `set`, `get` and `has`. The `set` method adds a service to the container, `get` returns the service and `has` checks an service is in the container. + +We have a `Logger` we want to add to the container, but it requires a `FileDriver` to be in the container already, or else we need to also add the `FileDriver` class to the container first. + +We might then have some bootstrapping logic like so: + +```php +$container = new Container; + +if (!$container->has('FileDriver')) { + $container->set('FileDriver', fn() => new FileDriver); +} + +if (!$container->has('Logger')) { + $container->set('Logger', function ($container) { + $logger = new Logger; + $logger->setDriver($container->get('FileDriver')); + return $logger; + }); +} +``` + +However, if we extends our `Container` class and add the `Conditionable` trait, we can instead use the `unless` method to do this check with a fluent interface: + +__NOTE: To check the opposite, there is also `when`.__ + +```php +class ConditionableContainer extends Container +{ + use Conditionable; +} + +$container = new ConditionableContainer; +$container + ->unless( + fn($container) => $container->has('FileDriver'), + function ($container) { + $container->set('FileDriver', fn() => new FileDriver); + } + )->unless( + fn($container) => $container->has('Logger'), + function ($container) { + $container->set('Logger', function ($container) { + $logger = new Logger; + $logger->setDriver($container->get('FileDriver')); + return $logger; + }); + } + ); +``` + +You're probably thinking this is still quite bit verbose, so to clean this up you could create `invokable` ServiceFactory classes for all of your `$container->set` logic.__ + +```php +class FileDriverServiceFactory +{ + public function __invoke($container) + { + $container->set('FileDriver', fn() => new FileDriver); + } +} + +class LoggerServiceFactory +{ + public function __invoke($cotnainer) + { + $logger = new Logger; + $logger->setDriver($container->get('FileDriver')); + return $logger; + } +} + +$container = new ConditionableContainer; + +// or, using unless, instead of when +$container + ->unless(fn($container) => $container->has('FileDriver'), FileDriverServiceFactory($container)) + ->unless(fn($container) => $container->has('Logger'), LoggerServiceFactory($container)); +``` diff --git a/src/Support/HigherOrderWhenProxy.php b/src/Support/HigherOrderWhenProxy.php new file mode 100644 index 0000000..24574ae --- /dev/null +++ b/src/Support/HigherOrderWhenProxy.php @@ -0,0 +1,109 @@ +target = $target; + } + + /** + * Set the condition on the proxy. + * + * @param bool $condition + * @return $this + */ + public function condition($condition) + { + [$this->condition, $this->hasCondition] = [$condition, true]; + + return $this; + } + + /** + * Indicate that the condition should be negated. + * + * @return $this + */ + public function negateConditionOnCapture() + { + $this->negateConditionOnCapture = true; + + return $this; + } + + /** + * Proxy accessing an attribute onto the target. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + if (! $this->hasCondition) { + $condition = $this->target->{$key}; + + return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition); + } + + return $this->condition + ? $this->target->{$key} + : $this->target; + } + + /** + * Proxy a method call on the target. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if (! $this->hasCondition) { + $condition = $this->target->{$method}(...$parameters); + + return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition); + } + + return $this->condition + ? $this->target->{$method}(...$parameters) + : $this->target; + } +} diff --git a/src/Support/Traits/Conditionable.php b/src/Support/Traits/Conditionable.php new file mode 100644 index 0000000..037effc --- /dev/null +++ b/src/Support/Traits/Conditionable.php @@ -0,0 +1,73 @@ +condition($value); + } + + if ($value) { + return $callback($this, $value) ?? $this; + } elseif ($default) { + return $default($this, $value) ?? $this; + } + + return $this; + } + + /** + * Apply the callback if the given "value" is (or resolves to) falsy. + * + * @template TUnlessParameter + * @template TUnlessReturnType + * + * @param (\Closure($this): TUnlessParameter)|TUnlessParameter|null $value + * @param (callable($this, TUnlessParameter): TUnlessReturnType)|null $callback + * @param (callable($this, TUnlessParameter): TUnlessReturnType)|null $default + * @return $this|TUnlessReturnType + */ + public function unless($value = null, callable $callback = null, callable $default = null) + { + $value = $value instanceof Closure ? $value($this) : $value; + + if (func_num_args() === 0) { + return (new HigherOrderWhenProxy($this))->negateConditionOnCapture(); + } + + if (func_num_args() === 1) { + return (new HigherOrderWhenProxy($this))->condition(! $value); + } + + if (! $value) { + return $callback($this, $value) ?? $this; + } elseif ($default) { + return $default($this, $value) ?? $this; + } + + return $this; + } +} diff --git a/tests/Support/Traits/ConditionableTest.php b/tests/Support/Traits/ConditionableTest.php new file mode 100644 index 0000000..e570ee9 --- /dev/null +++ b/tests/Support/Traits/ConditionableTest.php @@ -0,0 +1,210 @@ +assertInstanceOf(HigherOrderWhenProxy::class, (new ConditionableLogger)->when(true)); + $this->assertInstanceOf(HigherOrderWhenProxy::class, (new ConditionableLogger)->when(false)); + $this->assertInstanceOf(HigherOrderWhenProxy::class, (new ConditionableLogger)->when()); + $this->assertInstanceOf(ConditionableLogger::class, (new ConditionableLogger)->when(false, null)); + $this->assertInstanceOf(ConditionableLogger::class, (new ConditionableLogger)->when(true, function () {})); + } + + public function testUnless(): void + { + $this->assertInstanceOf(HigherOrderWhenProxy::class, (new ConditionableLogger)->unless(true)); + $this->assertInstanceOf(HigherOrderWhenProxy::class, (new ConditionableLogger)->unless(false)); + $this->assertInstanceOf(HigherOrderWhenProxy::class, (new ConditionableLogger)->unless()); + $this->assertInstanceOf(ConditionableLogger::class, (new ConditionableLogger)->unless(true, null)); + $this->assertInstanceOf(ConditionableLogger::class, (new ConditionableLogger)->unless(false, function () {})); + } + + public function testWhenConditionCallback() + { + // With static condition + $logger = (new ConditionableLogger()) + ->when(2, function ($logger, $condition) { + $logger->log('when', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['when', 2], $logger->values); + + // With callback condition + $logger = (new ConditionableLogger())->log('init') + ->when(function ($logger) { + return $logger->has('init'); + }, function ($logger, $condition) { + $logger->log('when', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['init', 'when', true], $logger->values); + } + + public function testWhenDefaultCallback() + { + // With static condition + $logger = (new ConditionableLogger()) + ->when(null, function ($logger, $condition) { + $logger->log('when', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['default', null], $logger->values); + + // With callback condition + $logger = (new ConditionableLogger()) + ->when(function ($logger) { + return $logger->has('missing'); + }, function ($logger, $condition) { + $logger->log('when', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['default', false], $logger->values); + } + + public function testUnlessConditionCallback() + { + // With static condition + $logger = (new ConditionableLogger()) + ->unless(null, function ($logger, $condition) { + $logger->log('unless', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['unless', null], $logger->values); + + // With callback condition + $logger = (new ConditionableLogger()) + ->unless(function ($logger) { + return $logger->has('missing'); + }, function ($logger, $condition) { + $logger->log('unless', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['unless', false], $logger->values); + } + + public function testUnlessDefaultCallback() + { + // With static condition + $logger = (new ConditionableLogger()) + ->unless(2, function ($logger, $condition) { + $logger->log('unless', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['default', 2], $logger->values); + + // With callback condition + $logger = (new ConditionableLogger())->log('init') + ->unless(function ($logger) { + return $logger->has('init'); + }, function ($logger, $condition) { + $logger->log('unless', $condition); + }, function ($logger, $condition) { + $logger->log('default', $condition); + }); + + $this->assertSame(['init', 'default', true], $logger->values); + } + + public function testWhenProxy() + { + // With static condition + $logger = (new ConditionableLogger()) + ->when(true)->log('one') + ->when(false)->log('two'); + + $this->assertSame(['one'], $logger->values); + + // With callback condition + $logger = (new ConditionableLogger())->log('init') + ->when(function ($logger) { + return $logger->has('init'); + }) + ->log('one') + ->when(function ($logger) { + return $logger->has('missing'); + }) + ->log('two') + ->when()->has('init')->log('three') + ->when()->has('missing')->log('four') + ->when()->toggle->log('five') + ->toggle() + ->when()->toggle->log('six'); + + $this->assertSame(['init', 'one', 'three', 'six'], $logger->values); + } + + public function testUnlessProxy() + { + // With static condition + $logger = (new ConditionableLogger()) + ->unless(true)->log('one') + ->unless(false)->log('two'); + + $this->assertSame(['two'], $logger->values); + + // With callback condition + $logger = (new ConditionableLogger())->log('init') + ->unless(function ($logger) { + return $logger->has('init'); + }) + ->log('one') + ->unless(function ($logger) { + return $logger->has('missing'); + }) + ->log('two') + ->unless()->has('init')->log('three') + ->unless()->has('missing')->log('four') + ->unless()->toggle->log('five') + ->toggle() + ->unless()->toggle->log('six'); + + $this->assertSame(['init', 'two', 'four', 'five'], $logger->values); + } +} + +class ConditionableLogger +{ + use Conditionable; + + public $values = []; + + public $toggle = false; + + public function log(...$values) + { + array_push($this->values, ...$values); + + return $this; + } + + public function has($value) + { + return in_array($value, $this->values); + } + + public function toggle() + { + $this->toggle = ! $this->toggle; + + return $this; + } +}