Skip to content

Commit

Permalink
Adding Conditionable trait, and HigherOrderWhenProxy class
Browse files Browse the repository at this point in the history
  • Loading branch information
lukewatts committed Mar 7, 2024
1 parent e61a534 commit 0bfb333
Show file tree
Hide file tree
Showing 4 changed files with 479 additions and 3 deletions.
90 changes: 87 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -100,7 +100,7 @@ return tap(new Psr7Response(), function ($response) {

## Traits

### SlimFacades\Traits\Tappable
### Tappable

```php
use SlimFacades\Support\Traits\Tappable;
Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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));
```
109 changes: 109 additions & 0 deletions src/Support/HigherOrderWhenProxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace SlimFacades\Support;

class HigherOrderWhenProxy
{
/**
* The target being conditionally operated on.
*
* @var mixed
*/
protected $target;

/**
* The condition for proxying.
*
* @var bool
*/
protected $condition;

/**
* Indicates whether the proxy has a condition.
*
* @var bool
*/
protected $hasCondition = false;

/**
* Determine whether the condition should be negated.
*
* @var bool
*/
protected $negateConditionOnCapture;

/**
* Create a new proxy instance.
*
* @param mixed $target
* @return void
*/
public function __construct($target)
{
$this->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;
}
}
73 changes: 73 additions & 0 deletions src/Support/Traits/Conditionable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace SlimFacades\Support\Traits;

use Closure;
use SlimFacades\Support\HigherOrderWhenProxy;

trait Conditionable
{
/**
* Apply the callback if the given "value" is (or resolves to) truthy.
*
* @template TWhenParameter
* @template TWhenReturnType
*
* @param (\Closure($this): TWhenParameter)|TWhenParameter|null $value
* @param (callable($this, TWhenParameter): TWhenReturnType)|null $callback
* @param (callable($this, TWhenParameter): TWhenReturnType)|null $default
* @return $this|TWhenReturnType
*/
public function when($value = null, callable $callback = null, callable $default = null)
{
$value = $value instanceof Closure ? $value($this) : $value;

if (func_num_args() === 0) {
return new HigherOrderWhenProxy($this);
}

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;
}

/**
* 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;
}
}
Loading

0 comments on commit 0bfb333

Please sign in to comment.