Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
8 changes: 8 additions & 0 deletions src/Concerns/AsPipeline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Lorisleiva\Actions\Concerns;

trait AsPipeline
{
//
}
60 changes: 60 additions & 0 deletions src/Decorators/PipelineDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?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);
}

/**
* 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.
*/
protected function handleFromAnyMethod(mixed ...$arguments): mixed
{
$passable = array_shift($arguments);
$closure = array_pop($arguments);
$returned = null;

if ($this->hasMethod('asPipeline')) {
$returned = $this->callMethod('asPipeline', [$passable]);
} elseif ($this->hasMethod('handle')) {
$returned = $this->callMethod('handle', [$passable]);
}

return $closure($returned ?? $passable);
}
}
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]);
}
}
210 changes: 210 additions & 0 deletions tests/AsPipelineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php

namespace Lorisleiva\Actions\Tests;

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

class AsPipelinePassable
{
public function __construct(public int $count = 0)
{
//
}

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

class AsPipelineExplicitTest
{
use AsPipeline;

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

public function asPipeline(AsPipelinePassable $passable): AsPipelinePassable
{
$this->handle($passable);

return $passable;
}
}

class AsPipelineImplicitTest
{
use AsAction;

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

public function asPipeline(AsPipelinePassable $passable): AsPipelinePassable
{
$this->handle($passable);

return $passable;
}
}

class AsPipelineMultipleParamTest
{
use AsAction;

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

public function asPipeline(AsPipelinePassable $passable, int $foo): AsPipelinePassable
{
$this->handle($passable);

return $passable;
}
}

class AsPipelineSingleParamHandleOnlyTest
{
use AsAction;

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

class AsPipelineMultipleParamHandleOnlyTest
{
use AsAction;

public function handle(AsPipelinePassable $passable, int $foo): void
{
$passable->increment();
}
}

class AsPipelineWithoutHandleOrAsPipeline
{
use AsAction;
}

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

return $next($p);
};
}

function getPassable() {
return new AsPipelinePassable;
}

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

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

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

it('can run as a noop/passthrough pipe in a pipeline, without a handle or asPipeline method', function () {
$anonymous = getAnonymous();
$passable = Pipeline::send(getPassable())
->through([
AsPipelineWithoutHandleOrAsPipeline::class,
$anonymous,
AsPipelineWithoutHandleOrAsPipeline::class,
$anonymous,
])
->thenReturn();

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

it('can run with an arbitrary via method configured on Pipeline', function () {
$anonymous = getAnonymous();
$passable = Pipeline::send(getPassable())
->via('foobar')
->through([
AsPipelineImplicitTest::class,
$anonymous,
AsPipelineImplicitTest::class,
$anonymous,
])
->thenReturn();

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

it('cannot run as a pipe in a pipeline, with an explicit asPipeline method expecting multiple non-optional params', function () {
$anonymous = getAnonymous();
$passable = Pipeline::send(getPassable())
->through([
AsPipelineMultipleParamTest::class,
$anonymous,
AsPipelineMultipleParamTest::class,
$anonymous,
])
->thenReturn();
})->throws(ArgumentCountError::class, 'Too few arguments to function Lorisleiva\Actions\Tests\AsPipelineMultipleParamTest::asPipeline(), 1 passed and exactly 2 expected');

it('cannot run as a pipe in a pipeline, without an explicit asPipeline method and multiple non-optional handle params', function () {
$anonymous = getAnonymous();
$passable = Pipeline::send(getPassable())
->through([
AsPipelineMultipleParamHandleOnlyTest::class,
$anonymous,
AsPipelineMultipleParamHandleOnlyTest::class,
$anonymous,
])
->thenReturn();
})->throws(ArgumentCountError::class, 'Too few arguments to function Lorisleiva\Actions\Tests\AsPipelineMultipleParamHandleOnlyTest::handle(), 1 passed and exactly 2 expected');
Loading