Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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;
}
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);
}

return $closure($passable);
}
}
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);
}

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