Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for AsPipeline concern #304

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions src/ActionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public function getDesignPatterns(): array
public function registerDesignPattern(DesignPattern $designPattern): ActionManager
{
$this->designPatterns[] = $designPattern;

return $this;
}

Expand Down Expand Up @@ -136,7 +136,7 @@ public function identifyFromBacktrace($usedTraits, ?BacktraceFrame &$frame = nul
$designPatterns = $this->getDesignPatternsMatching($usedTraits);
$backtraceOptions = DEBUG_BACKTRACE_PROVIDE_OBJECT
| DEBUG_BACKTRACE_IGNORE_ARGS;

$ownNumberOfFrames = 2;
$frames = array_slice(
debug_backtrace($backtraceOptions, $ownNumberOfFrames + $this->backtraceLimit),
Expand Down
2 changes: 2 additions & 0 deletions src/ActionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Lorisleiva\Actions\DesignPatterns\CommandDesignPattern;
use Lorisleiva\Actions\DesignPatterns\ControllerDesignPattern;
use Lorisleiva\Actions\DesignPatterns\ListenerDesignPattern;
use Lorisleiva\Actions\DesignPatterns\PipelineDesignPattern;

class ActionServiceProvider extends ServiceProvider
{
Expand All @@ -21,6 +22,7 @@ public function register(): void
new ControllerDesignPattern(),
new ListenerDesignPattern(),
new CommandDesignPattern(),
new PipelineDesignPattern(),
]);
});

Expand Down
1 change: 1 addition & 0 deletions src/Concerns/AsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ trait AsAction
use AsJob;
use AsCommand;
use AsFake;
use AsPipeline;
}
42 changes: 42 additions & 0 deletions src/Concerns/AsPipeline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Lorisleiva\Actions\Concerns;

trait AsPipeline
{
/**
* Typical pipeline behavior expects two things:
*
* 1) The pipe class to expect a single incoming parameter (along with
* a closure) and single return value.
* 2) The pipe class to be aware of the next closure and determine what
* should be passed into the next pipe.
*
* Because of these expectations, this behavior is asserting two opinions:
*
* 1) Regardless of the number of parameters provided to the asPipeline
* method implemented here, only the first will be supplied to the
* invoked Action.
* 2) If the invoked Action does not return anything, then the next
* closure will be supplied the same parameter. However, if the
* invoked action does return a non-null value, that value will
* be supplied to the next closure.
*
* Also, this logic is implemented in the trait rather than the decorator
* to afford some flexibility to consuming projects, should the wish to
* implement their own logic in their Action classes directly.
*/
public function asPipeline(mixed ...$arguments): mixed
{
$passable = array_shift($arguments);
$closure = array_pop($arguments);

$returned = $this->handle($passable);

if (! is_null($returned)) {
return $closure($returned);
stevenmaguire marked this conversation as resolved.
Show resolved Hide resolved
}

return $closure($passable);
stevenmaguire marked this conversation as resolved.
Show resolved Hide resolved
}
}
38 changes: 38 additions & 0 deletions src/Decorators/PipelineDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Lorisleiva\Actions\Decorators;

use Closure;
use Exception;
use Lorisleiva\Actions\Concerns\DecorateActions;

class PipelineDecorator
{
use DecorateActions;

public function __construct($action)
{
$this->setAction($action);
}

public function __invoke(mixed ...$arguments): mixed
{
return $this->handleFromAnyMethod(...$arguments);
}

public function handle(mixed ...$arguments): mixed
{
return $this->handleFromAnyMethod(...$arguments);
}

protected function handleFromAnyMethod(mixed ...$arguments): mixed
{
if ($this->hasMethod('asPipeline')) {
return $this->resolveAndCallMethod('asPipeline', $arguments);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should resolve the passable, closure, etc. and then pass it to the asPipeline function or fallback to passing it to the handle function.


if ($this->hasMethod('handle')) {
return $this->resolveFromArgumentsAndCall('handle', $arguments);
}
}
}
27 changes: 27 additions & 0 deletions src/DesignPatterns/PipelineDesignPattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Lorisleiva\Actions\DesignPatterns;

use Illuminate\Pipeline\Pipeline;
use Lorisleiva\Actions\BacktraceFrame;
use Lorisleiva\Actions\Concerns\AsPipeline;
use Lorisleiva\Actions\Decorators\PipelineDecorator;
use Lorisleiva\Actions\DesignPatterns\DesignPattern;

class PipelineDesignPattern extends DesignPattern
{
public function getTrait(): string
{
return AsPipeline::class;
}

public function recognizeFrame(BacktraceFrame $frame): bool
{
return $frame->matches(Pipeline::class, 'Illuminate\Pipeline\{closure}');
}

public function decorate($instance, BacktraceFrame $frame)
{
return app(PipelineDecorator::class, ['action' => $instance]);
}
}
79 changes: 79 additions & 0 deletions tests/AsPipelineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Lorisleiva\Actions\Tests;

use Illuminate\Support\Facades\Pipeline;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Concerns\AsPipeline;

class AsPipelineExplicitTest
{
use AsPipeline;

public function handle($passable): void
{
$passable->increment();
}
}

class AsPipelineImplicitTest
{
use AsAction;

public function handle($passable): void
{
$passable->increment();
}
}

function getAnonymous() {
return function ($p, $next) {
$p->increment();

return $next($p);
};
}

function getPassable() {
return new class {
public function __construct(public int $count = 0)
{
//
}

public function increment()
{
$this->count++;
}
};
}

it('can run as a pipe in a pipeline, with explicit trait', function () {
$anonymous = getAnonymous();
$passable = Pipeline::send(getPassable())
->through([
AsPipelineExplicitTest::class,
$anonymous,
AsPipelineExplicitTest::class,
$anonymous,
])
->thenReturn();

expect(is_object($passable))->toBe(true);
expect($passable->count)->toBe(4);
});

it('can run as a pipe in a pipeline, with implicit trait', function () {
$anonymous = getAnonymous();
$passable = Pipeline::send(getPassable())
->through([
AsPipelineImplicitTest::class,
$anonymous,
AsPipelineImplicitTest::class,
$anonymous,
])
->thenReturn();

expect(is_object($passable))->toBe(true);
expect($passable->count)->toBe(4);
});