অধ্যায় ৬

The Pipeline Pattern

"Pass a value through a sequence of steps."

জটিল লজিককে ছোট ছোট স্বাধীন ধাপে ভাগ করা — HTTP middleware থেকে শুরু করে shell pipe পর্যন্ত।

~১৩ মিনিট১২টি স্লাইড

আপনি আগেই ব্যবহার করেছেন

একটি কারখানায় গাড়ি তৈরি হচ্ছে। প্রথমে চেসিস বসে, তারপর ইঞ্জিন, তারপর রঙ। প্রতিটি ধাপ আলাদা এবং পর্যায়ক্রমে হয়। প্রোগ্রামিংয়েও প্রায়ই একটি ডেটাকে ধাপে ধাপে ফিল্টার বা মডিফাই করতে হয়।

লারাভেলে প্রতিটি HTTP রিকোয়েস্ট একটি middleware স্ট্যাক পার হয়: authentication, CSRF protection, rate limiting, string trimming। প্রতিটি নিজের কাজ করে রিকোয়েস্ট সামনে পাঠায়। Controller-এ পৌঁছানোর আগেই রিকোয়েস্টটি একের পর এক স্বাধীন ধাপে আকার পেয়েছে।

Request Auth CSRF Throttle TrimStrings Controller

এটাই pipeline।

একই আইডিয়া আছে job middleware, outgoing HTTP client middleware, process piping এবং নিজের কোডে ব্যবহারের জন্য একটি general-purpose class-এ।

Pipeline কীভাবে কাজ করে

  1. একটি input দিয়ে শুরু (request, DTO, model, যেকোনো object)।
  2. সেটা একের পর এক ধাপ পার হয়।
  3. প্রতিটি ধাপ input বদলাতে পারে, অথবা সামনে পাঠাবে কি না ঠিক করতে পারে।
  4. শেষ মাথায় চূড়ান্ত ফলাফল বের হয়।

প্রতিটি ধাপ স্বাধীন — অন্যগুলো না ছুঁয়েই যোগ, বাদ, ক্রম বদল বা পুনঃব্যবহার করা যায়।

ভেতরে যা ঘটে

লারাভেল প্রতিটি “pipe” (যার handle($passable, Closure $next) মেথড আছে) array_reduce দিয়ে পরেরটার চারপাশে মুড়ে closure-এর একটি নেস্টেড চেইন বানায়। সরলীকৃত রূপ:

$destination = fn ($passable) => $passable;

$pipeline = array_reduce(
    array_reverse($pipes),
    fn ($next, $pipe) => fn ($passable) => (new $pipe)->handle($passable, $next),
    $destination
);

$result = $pipeline($input); // সবচেয়ে বাইরের pipe থেকে শুরু

সবচেয়ে বাইরের pipe পরেরটাকে কল করে, সে তার পরেরটাকে… শেষে destination callback চলে। শেষ pipe যা রিটার্ন করে, সেটাই পুরো pipeline-এর output।

একটি সহজ উদাহরণ

use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send($input)
    ->through([
        CleanInput::class,
        ValidateData::class,
        TransformPayload::class,
    ])
    ->thenReturn();

প্রতিটি pipe দুটি আর্গুমেন্ট পায় — payload আর $next:

class CleanInput
{
    public function handle($payload, Closure $next)
    {
        $payload['name'] = trim($payload['name']); // payload নিয়ে কাজ

        return $next($payload); // পরের ধাপে পাঠানো
    }
}

Isolated

প্রতিটি ক্লাস একটাই কাজ করে।

Testable

প্রতিটি pipe আলাদাভাবে টেস্ট করা যায়।

Reusable

একই pipe অন্য workflow-এ।

$next() কল না করলে pipeline সেখানেই থেমে যায় — এভাবেই auth middleware অননুমোদিত রিকোয়েস্ট আটকায়।

HTTP Middleware

public function handle(Request $request, Closure $next)
{
    // ← রিকোয়েস্ট সামনে যাওয়ার আগের কাজ

    $response = $next($request);

    // ← রেসপন্স ফেরার পথের কাজ

    return $response;
}

HTTP kernel middleware লিস্ট রিজলভ করে রিকোয়েস্টকে ক্রমানুসারে প্রতিটি স্তর দিয়ে পাঠায়। রেসপন্স উল্টো ক্রমে একই স্ট্যাক দিয়ে ফেরে, কারণ প্রতিটি স্তর পরেরটাকে মুড়ে রাখে — পেঁয়াজের খোসার মতো।

A (আগে) B (আগে) Controller B (পরে) A (পরে)

লারাভেলের ভেতরে (সরলীকৃত)

use Illuminate\Pipeline\Pipeline;

$response = (new Pipeline($app))
    ->send($request)
    ->through($middleware)
    ->then(function ($request) use ($route) {
        return $route->run($request); // শেষে Controller
    });

প্রতিটি middleware-এ যে $next($request) কল করি, সেটা আসলে pipeline-এর পরের ধাপ।

Docs: Middleware

Job Middleware

Queue job-এও middleware আছে, আর গঠন HTTP middleware-এর প্রায় হুবহু:

public function middleware(): array
{
    return [
        new RateLimited('api'),                    // অধ্যায় ৫
        new WithoutOverlapping($this->modelId),    // অধ্যায় ৪
    ];
}

প্রতিটি middleware job পায় এবং সেটাকে চলতে না দেওয়া, throttle করা, lock নেওয়া, retry যোগ করা বা logging-এ মুড়ে দিতে পারে।

নিজের Job Middleware

class LogJobDuration
{
    public function handle(object $job, Closure $next): void
    {
        $start = microtime(true);

        $next($job); // job-এর handle() এখানে চলে

        Log::info(get_class($job) . ' took ' . round(microtime(true) - $start, 2) . 's');
    }
}

লাভ

লারাভেল HTTP-এর মতো একই pipeline ইঞ্জিনে job middleware চালায়। ফলে cross-cutting concern-গুলো job-এর handle()-এর ভেতরে চাপা পড়ে না।

Docs: Job Middleware

HTTP Client Middleware

লারাভেলের HTTP client Guzzle-চালিত, তাই outgoing রিকোয়েস্টের pipeline Guzzle middleware দিয়ে হয়।

Outgoing রিকোয়েস্ট বদলানো

use Illuminate\Support\Facades\Http;
use Psr\Http\Message\RequestInterface;

$response = Http::withRequestMiddleware(
    function (RequestInterface $request) {
        return $request->withHeader('X-Example', 'Value');
    }
)->get('http://example.com');

Incoming রেসপন্স দেখা

use Psr\Http\Message\ResponseInterface;

$response = Http::withResponseMiddleware(
    function (ResponseInterface $response) {
        $header = $response->getHeader('X-Example');
        // ...
        return $response;
    }
)->get('http://example.com');

ব্যবহার: request/response লগ, হেডার বদলানো, টাইমিং বা ট্রেসিং, failover লজিক, circuit breaker। গঠন server-side middleware-এর মতোই — শুধু incoming-এর বদলে external কলে প্রয়োগ।

সব রিকোয়েস্টে লাগাতে চাইলে AppServiceProvider-এ Http::globalRequestMiddleware(...)।

Docs: Guzzle Middleware

Process Pipelines

একাধিক external command ক্রমানুসারে চালানো, যেখানে একটির output পরেরটির input — ঠিক shell-এর | এর মতো।

use Illuminate\Process\Pipe;
use Illuminate\Support\Facades\Process;

$result = Process::pipe(function (Pipe $pipe) {
    $pipe->command('cat file.txt');
    $pipe->command('grep "Laravel"');
});

echo $result->output();

এটা shell-এ cat file.txt | grep "Laravel" এর সমান। প্রথম command-এর stdout পরেরটায় যায়।

রিয়েল-টাইম output

দ্বিতীয় আর্গুমেন্টে callback দিন।

ধাপের নাম

ডিবাগিং-এর জন্য $pipe->as('name')।

কখন? grep, awk, sed চেইন করা, এক্সটার্নাল টুল দিয়ে ছোট ETL, অথবা Laravel কোড থেকে shell pipeline-এর composability। একই pipeline কনসেপ্ট — শুধু PHP ডেটার বদলে OS লেভেলে।

Docs: Process Pipelines

Pipeline Class — নিজের কোডে

HTTP বা queue-এর বাইরে pipeline লাগতে পারে — import প্রসেসিং, ডেটা ট্রান্সফর্ম, business rule-এর সিরিজ। ধরুন একটি Order-এ Discount, Inventory, Total — সব কন্ট্রোলারে না লিখে আলাদা ক্লাসে:

use Illuminate\Support\Facades\Pipeline;

$result = Pipeline::send($order)
    ->through([
        ApplyDiscounts::class,
        ValidateInventory::class,
        RecalculateTotals::class,
    ])
    ->thenReturn();
class ApplyDiscounts
{
    public function handle(Order $order, Closure $next)
    {
        if ($order->coupon) {
            $order->total -= $order->coupon->amount;
        }

        return $next($order);
    }
}
মেথডকাজ
send($value)যে ভ্যালু pipeline দিয়ে যাবে
through([...])pipe-এর লিস্ট (class নাম বা closure)
via('process')handle-এর বদলে অন্য মেথড নাম
then(fn)শেষে একটি destination closure চালাও
thenReturn()শেষ ভ্যালুটাই রিটার্ন করো
Docs: Pipeline

কখন Pipeline ব্যবহার করবেন

ভালো মানায় যখন

  • লজিক নির্দিষ্ট ক্রমে প্রয়োগ করতে হবে
  • প্রতিটি ধাপ আলাদা ও testable
  • পরে ধাপ যোগ/বাদ দেওয়ার সম্ভাবনা
  • একাধিক workflow-এ ধাপ পুনঃব্যবহার

দরকার নেই যখন

  • মাত্র ২–৩ লাইনের লজিক — সাধারণ loop বা মেথড কলই যথেষ্ট
  • ধাপগুলো একে অপরের উপর জটিলভাবে নির্ভরশীল

বাস্তব প্রজেক্টের উদাহরণ

CSV Import

sanitize → validate → column map → store

Business Rules

domain object-এ নিয়মের সিরিজ

Uploaded File

resize → watermark → optimize

User Content

strip HTML → mention → link → spam check

Pipeline সবকিছু করা একটি বিশাল service class-এর বদলে ছোট, উদ্দেশ্য-নির্দিষ্ট ক্লাসে কাজ ঠেলে দেয়। প্রতিটি ধাপের একটাই কাজ আর পরিষ্কার interface থাকলে workflow টেস্ট, বদলানো ও পড়া সহজ হয়।

পুরো লারাভেলে Pipeline

কোথায়কী পাস হয়API
HTTP MiddlewareIncoming Requesthandle($request, $next)
Job MiddlewareQueue Jobmiddleware(): array
HTTP ClientOutgoing Request / ResponsewithRequestMiddleware()
Processstdout → stdinProcess::pipe()
Pipeline Classযেকোনো ভ্যালুPipeline::send()->through()

মূল কথা

Incoming request, outgoing HTTP call, background job, shell command — সব একই কাঠামোগত আইডিয়া ব্যবহার করে। HTTP middleware কীভাবে কাজ করে বুঝলে job ও outgoing request-এরটাও বুঝে গেছেন।

সারসংক্ষেপ ও যাচাই

  • Pipeline = একটি ভ্যালু একের পর এক স্বাধীন ধাপে পাস করা।
  • প্রতিটি pipe: handle($passable, Closure $next) এবং return $next($passable)।
  • ভেতরে array_reduce দিয়ে closure-এর নেস্টেড চেইন — তাই রেসপন্স উল্টো ক্রমে ফেরে।
  • HTTP, Job, HTTP Client, Process আর Pipeline class — সব একই আকার।

নিজেকে যাচাই করুন

Middleware-এ $next($request) কল না করে সরাসরি response রিটার্ন করলে কী হবে?

Pipeline সেখানেই থামবে — পরের middleware বা controller চলবে না। Auth middleware এভাবেই redirect করে।

A, B, C middleware — রেসপন্সের “পরের কাজ” কোন ক্রমে চলবে?

C → B → A। প্রতিটি স্তর পরেরটাকে মুড়ে রাখে, তাই ফেরার পথ উল্টো।

then() আর thenReturn()-এর পার্থক্য?

then() শেষে একটি destination closure চালায় (যেমন controller); thenReturn() শুধু শেষ ভ্যালুটা ফেরত দেয়।