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 11 commits into
base: main
Choose a base branch
from
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
{
//
}
27 changes: 27 additions & 0 deletions src/Decorators/PipelineDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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
{
$passable = array_shift($arguments);
$closure = array_pop($arguments);

$method = $this->hasMethod('asPipeline') ? 'asPipeline' : 'handle';

return $closure($this->callMethod($method, [$passable]) ?? $passable) ?? $passable;
}
}
28 changes: 28 additions & 0 deletions src/DesignPatterns/PipelineDesignPattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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}')
|| $frame->matches(Pipeline::class, '{closure:{closure:Illuminate\Pipeline\Pipeline::carry():184}:185}');
}

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

namespace Lorisleiva\Actions\Tests;

use Illuminate\Support\Facades\Pipeline;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Tests\Stubs\PipelinePassable;

class AsPipelineTest
{
use AsAction;

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

public function asPipeline(PipelinePassable $passable): void
{
$this->handle($passable);
}
}

it('can run as a pipe in a pipeline, with an explicit asPipeline method', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineTest::class,
AsPipelineTest::class,
AsPipelineTest::class,
AsPipelineTest::class,
])
->thenReturn();

expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(4);
});

it('can run with an arbitrary via method configured on Pipeline', function () {
$passable = Pipeline::send(new PipelinePassable)
->via('arbitraryMethodThatDoesNotExistOnTheAction')
->through([
AsPipelineTest::class,
app()->make(AsPipelineTest::class),
])
->thenReturn();

expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(2);
});

it('can run as a pipe in a pipeline with only one explicit container resolved instance at the bottom of the stack', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineTest::class, // implicit container resolved instance
app()->make(AsPipelineTest::class), // explicit container resolved instance
])
->thenReturn();

expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(2);
});

it('cannot run as a pipe in a pipeline with an explicit container resolved instance in the middle of the stack', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineTest::class, // implicit container resolved instance
app()->make(AsPipelineTest::class), // explicit container resolved instance
AsPipelineTest::class, // implicit container resolved instance
AsPipelineTest::class, // implicit container resolved instance
])
->thenReturn();

expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(2);
});

it('cannot run as a pipe in a pipeline as an standalone instance', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
new AsPipelineTest, // standalone instance
AsPipelineTest::class, // implicit container resolved instance
app()->make(AsPipelineTest::class), // explicit container resolved instance
])
->thenReturn();

expect(is_null($passable))->toBe(true);
});
29 changes: 29 additions & 0 deletions tests/AsPipelineWithExplicitTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Lorisleiva\Actions\Tests;

use Illuminate\Support\Facades\Pipeline;
use Lorisleiva\Actions\Concerns\AsPipeline;
use Lorisleiva\Actions\Tests\Stubs\PipelinePassable;

class AsPipelineWithExplicitTraitTest
{
use AsPipeline;

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

it('can run as a pipe in a pipeline, with explicit trait, without asPipeline method', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineWithExplicitTraitTest::class,
app()->make(AsPipelineWithExplicitTraitTest::class),
])
->thenReturn();

expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(2);
});
29 changes: 29 additions & 0 deletions tests/AsPipelineWithImplicitTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Lorisleiva\Actions\Tests;

use Illuminate\Support\Facades\Pipeline;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Tests\Stubs\PipelinePassable;

class AsPipelineWithImplicitTraitTest
{
use AsAction;

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

it('can run as a pipe in a pipeline, with implicit trait, without asPipeline method', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineWithImplicitTraitTest::class,
app()->make(AsPipelineWithImplicitTraitTest::class),
])
->thenReturn();

expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(2);
});
44 changes: 44 additions & 0 deletions tests/AsPipelineWithMultipleNonOptionalParametersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Lorisleiva\Actions\Tests;

use ArgumentCountError;
use Illuminate\Support\Facades\Pipeline;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Tests\Stubs\PipelinePassable;
use TypeError;

class AsPipelineWithMultipleNonOptionalParametersTest
{
use AsAction;

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

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

return $passable;
}
}

it('cannot run as a pipe in a pipeline expecting multiple non-optional parameters', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineWithMultipleNonOptionalParametersTest::class,
app()->make(AsPipelineWithMultipleNonOptionalParametersTest::class),
])
->thenReturn();
})->throws(ArgumentCountError::class);

it('cannot run as a pipe in a pipeline as an explicit container resolved instance preceding an implicit container resolved instance', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
app()->make(AsPipelineWithMultipleNonOptionalParametersTest::class),
AsPipelineWithMultipleNonOptionalParametersTest::class,
])
->thenReturn();
})->throws(TypeError::class);
19 changes: 19 additions & 0 deletions tests/Stubs/PipelinePassable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Lorisleiva\Actions\Tests\Stubs;

/**
* Test fixture used in all of the AsPipeline{*} tests.
*/
class PipelinePassable
{
public function __construct(public int $count = 0)
{
//
}

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