در مواردی ممکن است پروژه شما نیاز داشته باشد قبل از کاری یک سری موارد را اصلاح کند و تغییراتی انجام دهد. این تغییرات ممکن است گاهی با منطق خاصی و طبق روال و ترتیب خاصی انجام شود. در این مقاله در مورد خط لوله (Pipeline) در لاراول صحبت خواهیم کرد و به شما نشان میدهیم که چگونه میتوان یک سری اصلاحات و عملیاتی قبل از انجام عملیات اصلی در لاراول انجام دهید.
Pipeline در لاراول چیست؟
برای روشن شدن مفهموم Pipeline میتوان به این شکل تعریف کرد.
خط لوله (Pipeline) یک شی قابل عبور (Passable) را قبول میکند، مقداری توسط توسعه دهنده ارسال (send) میشود، از طریق (through) یک کلاس منطقهایی را اعمال میکند که Pipeها نامیده میشود، زمانی که آخرین pipe اجرا و تمام شود، فقط در این زمان (then) به شما نتیجه نهایی را بر میگرداند.
کلاس کامل Pipeline به صورت زیر است.
<?php
namespace Illuminate\Pipeline;
use Closure;
use RuntimeException;
use Illuminate\Http\Request;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract;
class Pipeline implements PipelineContract
{
/**
* The container implementation.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The object being passed through the pipeline.
*
* @var mixed
*/
protected $passable;
/**
* The array of class pipes.
*
* @var array
*/
protected $pipes = [];
/**
* The method to call on each pipe.
*
* @var string
*/
protected $method = 'handle';
/**
* Create a new class instance.
*
* @param \Illuminate\Contracts\Container\Container|null $container
* @return void
*/
public function __construct(Container $container = null)
{
$this->container = $container;
}
/**
* Set the object being sent through the pipeline.
*
* @param mixed $passable
* @return $this
*/
public function send($passable)
{
$this->passable = $passable;
return $this;
}
/**
* Set the array of pipes.
*
* @param array|mixed $pipes
* @return $this
*/
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
/**
* Set the method to call on the pipes.
*
* @param string $method
* @return $this
*/
public function via($method)
{
$this->method = $method;
return $this;
}
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
/**
* Run the pipeline and return the result.
*
* @return mixed
*/
public function thenReturn()
{
return $this->then(function ($passable) {
return $passable;
});
}
/**
* Get the final piece of the Closure onion.
*
* @param \Closure $destination
* @return \Closure
*/
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if (is_callable($pipe)) {
// If the pipe is an instance of a Closure, we will just call it directly but
// otherwise we'll resolve the pipes out of the container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
[$name, $parameters] = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $response instanceof Responsable
? $response->toResponse($this->getContainer()->make(Request::class))
: $response;
};
};
}
/**
* Parse full pipe string to get name and parameters.
*
* @param string $pipe
* @return array
*/
protected function parsePipeString($pipe)
{
[$name, $parameters] = array_pad(explode(':', $pipe, 2), 2, []);
if (is_string($parameters)) {
$parameters = explode(',', $parameters);
}
return [$name, $parameters];
}
/**
* Get the container instance.
*
* @return \Illuminate\Contracts\Container\Container
*
* @throws \RuntimeException
*/
protected function getContainer()
{
if (! $this->container) {
throw new RuntimeException('A container instance has not been passed to the Pipeline.');
}
return $this->container;
}
}
در کلاس Pipeline متدهایی به صورت عمومی ارائه شده است که بتوانیم با آنها کار کنیم.
- متد send که مقادیر ارسالی به کلاس Pipe خواهد بود.
- و متد through که کلاس یا کلاسهای Pipe را میپذیرد.
- متد via که ورودی آن یک متد از کلاس Pipe خواهد بود و کدهای موجود در این متد اجرا خواهد شد و اختیاری است.
- در نهایت متد then که یک تابع میپذیرد و بعد از انجام Pipeها اجرا خواهد شد.
Pipeline به Service Container نیاز دارد ( برای درک بیشتر مفهوم مقالهی Service Container را مطالعه نمایید. ) چرا که Pipeline در زمانی که یک شی به آن ارسال (pass) میشود از Service Container برای حل این موضوع استفاده میکند.
یکی از شایعترین مثالهای عملیات خط لوله (Pipeline) عملیات Middleware است که در انجام فیلتر سازی درخواستهای ارسالی کاربران، دخیل است. برای مثال Middlewareها نیاز یا عدم نیاز به احراز هویت را در Routeها بررسی میکنند و قبل از انجام هر عملیاتی زمانی که کاربر درخواستی به Route مورد نظر ارسال میکند، نیاز یا عدم نیاز به ورود و احراز هویت را بررسی میکند.
اگر نگاه کوچکی به کلاس Illuminate\Foundation\Http\Kernel بیاندازید با نحوهی اجرای Middewareها آشنا خواهید شد.
متد زیر نحوه اجرای Middlewareها است.
<?php
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
شروع با Pipeline در لاراول
در اولین مرحله ایجاد Pipeline نیاز است از آن یک نمونه (Instance) بسازیم. برای این کار میتوانیم از تابع کمک کننده app استفاده کنیم و کلاس Pipeline را به آن ارسال کنیم.
$result = app(\Illuminate\Pipeline\Pipeline::class);
حال میتوانیم متدهای Pipeline که در بالا ذکر کردیم را اعمال کنیم که در ادامه توضیح خواهم داد.
ارسال شی به Pipeline در لاراول
همانطور که در لیستی از متدهای عمومی Pipeline ذکر شد، متد send میتواند یک شی قابل عبور (Passable Object) را بپذیرد و آن را به کلاس Pipe مورد نظر ارسال کند. ورودی متد send میتواند هر نمونه ای از شی باشد برای مثال میتواند یک آرایه، یک رشته، یک Collection و … باشد.
$result = app(\Illuminate\Pipeline\Pipeline::class)
->send('this should be correctly formatted');
به یاد داشته باشید که شی مورد نظر با آدرس یا به اصطلاح reference ارسال میشود و نیازی به ذخیره در متغیر جدیدی ندارید. این نکته در مورد مدل ها، مجموعه ها(Collection) و هرچیزی که قابل نمونه سازی باشد صدق میکند.
کلاس Pipe
به کمک متد through در کلاس Pipeline میتوانیم مشخص کنیم که این شی مورد نظر ما به کدام کلاس ارسال شود تا در آنجا بر روی آن عملیات مورد نظر انجام شود. این متد به صورت زیر پیاده سازی میشود.
$result = app(\Illuminate\Pipeline\Pipeline::class)
->send(’this should be correctly formatted’)
->through(
function ($passable, $next) {
return $next(ucfirst($passable));
},
AddPunctuation::class,
new RemoveDoubleSpacing(),
InvokableClassToRemoveDuplicatedWords::class
);
متد through مهمترین بخش Pipeline است. در ورودی آن میتوانیم آرایهای از موارد قابل قبول این متد را ارسال کرد یا حتی در ورودی یک تابع تعریف کرد. به اصطلاح ورودی این تابع Pipe نامیده میشود.
Pipeها میتوانند به این شکلها باشند:
- یک کلاس که از طریق Service Containerها ایجاد شده است.
- یک بسته (Closure) یا کلاس غیرقابل پذیرش (invokable)
- یک نمونه از شی
در کلاس Pipe باید یک متد با نام handle باشد که کدهای این متد اجرا خواهند شد. ولی میتوانیم در صورت لزوم این متد را تغییر دهیم. در ادامه این موضوع را مورد بررسی قرار میدهیم.
متد سفارشی کلاس Pipe
در کلاس Pipeline یک متد با نام via قرار دارد که در بالا نیز ذکر کردیم. این متد اجباری نیست و عملیات اجرای Pipeline در متد handle در کلاس Pipe به صورت پیشفرض انجام میپذیرد.
ما میتوانیم نام این متد را عوض کنیم و این کار را به یک متد دیگر در کلاس Pipe واگذار کنیم.
برای مثال ممکن است شما برای انجام عملیات دیگری از متد handle استفاده کرده باشید و حالا میخواهید یک عملیات دیگر را همان کلاس بر عهده بگیرد.
$result = app(\Illuminate\Pipeline\Pipeline::class)
->send('this should be correctly formatted')
->through([
...
])
->via('modifyString');
نتایج Pipeline در لاراول
آخرین مرحله، اجرای Pipeline است که از طریق متد then یا thenReturn اجرا میشود. خود Pipeline تا اجرای این متد در حالت Sleep است.
متد then یک بسته (Closure) دریافت میکند و ورودی تابع در این متد، نتیجهی آخرین Pipe اجرا شده است. برای مثال زمانی که یک متن را در دو Pipe برای حذف کلمات زشت و حذف تگهای اسکریپت قرار میدهیم، ورودی تابعی که در متد then قرار دارد، همان نتیجهی آخرین Pipe یعنی متن بدون کلمات زشت و بدون تگهای اسکریپت خواهد بود.
شما میتوانید از متد thenReturn نیز استفاده کنید. این متد فقط آخرین نتیجهی Pipeهای Pipeline را برمیگرداند و Pipeline را تمام میکند. این متد ورودی نمیگیرد.
$result = app(\Illuminate\Pipeline\Pipeline::class)
->send('this should be correctly formatted')
->through(...)
->via('modifyString')
->thenReturn();
<?php
app(Pipeline::class)
->send($content)
->through($pipes)
->then(function ($content) {
return Post::create(['content' => $content]);
});
پیاده سازی Pipeline در لاراول با یک مثال ساده
فرض کنیم در حال راه اندازی یک انجمن (Forum) هستیم و میخواهیم که در موقع ارسال یک مطلب سه مورد زیر انجام شود.
- تگهای لینک به صورت عادی نوشته شود
- واژههای بد به صورت * نوشته شود.
- تگ script به صورت کامل از متن پاک شود.
در انجام این کار به سه کلاس برای انجام هر کدام از عملیاتهای بالا نیاز داریم یک کلاس تعریف کنیم. پس Pipeهای ما به صورت زیر خواهد بود.
$pipes = [
RemoveBadWords::class
ReplaceLinkTags::clas
RemoveScriptTags::class
];
هرکدام از کلاسهای بالا برای ما یکی از عملیاتهایی که ذکر شد را انجام خواهد داد.
در ادامه متدی که قرار است کار ثبت و ذخیره مطلب موردنظر را انجام دهد باید قبل از هر چیزی یک Pipeline را فراخوانی کند تا عملیاتهای بالا بر روی متن مطلب اعمال شود و بعد از آن میتوان مطلب را ذخیره کرد. برای مثال کد ثبت مطلب به صورت زیر خواهد بود.
<?php
public function create(Request $request)
{
$pipes = [
RemoveBadWords::class,
ReplaceLinkTags::clas,
RemoveScriptTags::class
];
$post = app(Pipeline::class)
->send($request->content)
->through($pipes)
->then(function ($content) {
return Post::create(['content' => 'content']);
});
// return any type of response
}
در متغیر post همیشه مقدار برگشتی تابع فراخوانی شده در متد then خواهد بود.
به کلاس Pipeline و متد through میتوان یک آرایه از Pipeها را در ورودی ارسال کرد.
کلاس Pipeline در Pipe هایی که در ورودی متد through هستند، از طریق یک متد به اسم handle عملیات را انجام میدهد. یعنی نیاز است تا هر کلاس Pipe یک متد به اسم handle داشته باشد تا کدهای آن در Pipeline اجرا شود.
ایدهی خوبی خواهد بود، اگر یک Interface برای کلاسهای Pipe ایجاد کنیم که نیاز بودن متد handle را اجباری میکند.
<?php
namespace App;
use Closure;
interface Pipe
{
public function handle($content, Closure $next);
}
بعد از آن نیاز است تا کلاسهایی که تعریف میکنیم از این Interface پیروی کنند و حتما implements فراموش نشود.
برای مثال کلاس RemoveBadWord به صورت زیر خواهد بود.
<?php
namespace App;
use Closure;
class RemoveBadWords implements Pipe
{
public function handle($content, Closure $next)
{
// Here you perform the task and return the updated $content
// to the next pipe
return $next($content);
}
}
متد handle دو ورودی خواهد داشت. اولین ورودی باید یک شی قابل عبور (Passable Object) باشد و دومی یک بسته (Closure) است که شی مورد نظر بعد از انجام Pipe به آن فرستاده میشود.
حال میتوانید تمامی Pipeهای خود را به صورت دلخواه بنویسید.
همچنین شما میتوانید به غیر از متد handle در کلاس Pipe از یک متد دیگر نیز استفاده کنید.
<?php
app(Pipeline::class)
->send($content)
->through($pipes)
->via(‘customMethodName’) // <---- This one :)
->then(function ($content) {
return Post::create(['content' => $content]);
});
در متد via میتوانید نام متد مورد نظر خود در کلاس Pipe را وارد کنید تا عملیات از طریق آن انجام شود.
جمعبندی
در این مقاله در مورد قابلیتی در لاراول به اسم Pipeline صحبت کردیم، قابلیتی که کمتر کسی از آن خبر دارد و حتی مستنداتی برای آن نوشته نشده است. با استفاده از Pipeline شما میتوانید اگر جایی نیاز به اصلاح سازی مواردی دارید. برای مثال نیاز دارید قبل از ذخیره یک متن یک سری اصلاحاتی بر روی آن انجام دهید و سپس ذخیره کنید، Pipeline این امکان را به شما میدهد. تمامی متدهای کلاس Pipeline را مورد بررسی قرار دادیم و گفتیم شما با ایجاد یک کلاس Pipeline و چندین کلاس Pipe میتوانید یک Pipeline راه اندازی کنید و بعد از آن نیز با ذکر یک مثال کاربرد Pipeline را توضیح دادیم.