zaengle / pipeline
Niceties on top of Laravel Pipelines
Installs: 4 732
Dependents: 0
Suggesters: 0
Security: 0
Stars: 99
Watchers: 4
Forks: 5
Open Issues: 0
Requires
- php: ^8.1|^8.2|^8.3
- illuminate/console: 8.*|9.*|10.*|11.*
- illuminate/support: 8.*|9.*|10.*|11.*
Requires (Dev)
- mockery/mockery: >=0.9.9
- orchestra/testbench: ~8.0
- phpunit/phpunit: >=4.1
README
Zaengle Pipeline
After using Laravel Pipelines to handle complex data flows in our projects we saw a few patterns emerge:
- Database transactions
- Pipe interface
- Responses and exception handling
This package adds niceties on top of the Laravel Pipeline and consolidates them into a single reusable location.
FYI - See the "Example" directory for a more thorough example.
Installation
composer require zaengle/pipeline
Testing
phpunit
Basic Class Example
A pipeline is a common pattern for breaking data, logic, and response/exceptions into three distinct elements. Zaengle Pipeline abstracts these parts into helpful classes and gives some structure to the underlying pattern. For example, let's explore at what a pipeline might look like for a ficticious user registration:
<?php use App\RegisterTraveler; use App\Pipes\CreateUser; use App\Pipes\HandleMailingList; use Zaengle\Pipeline\Pipeline; class FicticiousRegisterController { public function __invoke() { $traveler = (new RegisterTraveler())->setRequest(request()->all()); $pipes = [ CreateUser::class, HandleMailingList::class, // any other items that need to happen during registration... ]; $response = app(Pipeline::class)->pipe($traveler, $pipes, $useTransactions = true); if ($response->passed()) { return response()->json('Welcome!'); } else { return response()->json('Your registration failed with the following error: ' . $response->getMessage()); } } }
Breaking it Down
Create a Traveler
The first step in using the Zaengle Pipeline is to create a Data Traveler class. Note: The setRequest()
method is contrived for this example.
$traveler = (new RegisterTraveler())->setRequest(request()->all());
Zaengle\Pipeline\Contracts\AbstractTraveler
provides additional methods utilized in the Zaengle\Pipeline\Pipeline
class.
<?php use Zaengle\Pipeline\Contracts\AbstractTraveler; class RegisterTraveler extends AbstractTraveler { }
Within the $traveler
you may set any data required and it will be available within any of the pipes.
<?php use Zaengle\Pipeline\Contracts\AbstractTraveler; class RegisterTraveler extends AbstractTraveler { private $request; private $user; // custom methods public function setRequest($request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function setUser($user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } }
Pipes
Separate your business logic into appropriate "pipes," each of which should implement the Zaengle\Pipeline\Contracts\PipeInterface
.
<?php use App\User; use Zaengle\Pipeline\Contracts\PipeInterface; class CreateUser implements PipeInterface { public function handle(RegisterTraveler|AbstractTraveler $traveler, \Closure $next): RegisterTraveler { $traveler->setUser( User::create([ 'email' => $traveler->getRequest()->email, 'password' => $traveler()->getRequest()->password, ]) ); return $next($traveler); } }
<?php use App\MailingService; use Zaengle\Pipeline\Contracts\PipeInterface; class HandleMailingList implements PipeInterface { public function handle(RegisterTraveler|AbstractTraveler $traveler, \Closure $next): RegisterTraveler { if ($traveler->getRequest()->subscribe) { MailingService::subscribe($traveler->getUser()->email); $traveler->getUser()->update([ 'subscriber' => true, ]); } return $next($traveler); } }
Primary Pipeline
Once you have your data and pipes established, send them through the Zaengle\Pipeline\Pipeline
->pipe()
method.
pipe()
accepts three parameters, two of which are required. The first parameter should be your $traveler
, the second is your array of pipes, and the third, optional parameter tells Pipeline
whether to use transactions or not.
// use Zaengle\Pipeline\Pipeline; $response = app(Pipeline::class)->pipe($traveler, $pipes, $useTransactions = true);
Results
After sending the $traveler
through the data pipes you will have access to a ->passed()
method which indicates whether the pipeline completed successfully or not.
$response = app(Pipeline::class)->pipe($traveler, $pipes, $useTransactions = true); if ($response->passed()) { // Handle pass dump($response->getMessage()); } else { // Handle fail // $response->getException(); // $response->getMessage(); // $response->getStatus(); }
AbstractTraveler
grants you access to the following convenience methods:
$response->passed()
A boolean to indicate whether the traveler made it all the way through the pipes without any exceptions.
$response->getStatus()
A string you can set with ->setStatus()
or will automatically be either 'ok' or 'fail'.
$response->getException()
To abort the process you may throw an exception which Pipeline will capture on the response. It will also set the status and message giving you access to $response->getMessage()
.
$response->setMessage()
A $message
property will automatically be set in the case of an exception. Otherwise, you can set it at any point during the pipeline execution.
$response->getMessage()
A string that is available upon the completion of the pipeline.
Testing Strategy
When testing a pipeline you should test the overall pipeline to make sure the given input matches the expected output.
<?php use TestCase; class FicticiousRegistrationTest extends TestCase { /** @test */ public function it_registers_a_user() { $userStub = factory(User::class)->make(); $response = $this->postJson('ficticious-endpoint', [ 'email' => $userStub->email, 'password' => 'password', 'subscribe' => true, ]); $response->assertJsonFragment('Welcome!'); $this->assertDatabaseHas('users', [ 'email' => $userStub->email, 'subscribed' => true, ]); } }
You may also want to test individual pipes like this:
<?php use TestCase; use App\User; use App\RegisterTraveler; use App\Pipes\CreateUser; class CreateUserTest extends TestCase { /** @test */ public function it_creates_a_user() { $traveler = (new RegisterTraveler)->setRequest(['email' => 'test', 'password' => 'password']); (new CreateUser)->handle($traveler, function () {}); $this->assertInstanceOf(User::class, $traveler->getUser()); } }
License
The MIT License (MIT). Please see License File for more information.