Skip to content

অধ্যায় ৩: সার্ভিস কন্টেইনার এবং প্রোভাইডার আর্কিটেকচার

লারাভেল ফ্রেমওয়ার্কের মোস্ট পাওয়ারফুল ফিচারের একটি হলো সার্ভিস কন্টেইনার (Service Container)। আপনি সার্ভিস কন্টেইনার না বুঝলে আপনার জন্য লারাভেল কোর বা এডভান্স জিনিসপত্র বোঝা কঠিন হয়ে যাবে। তাই শুরুতে এইটা দিয়ে আলোচনা শুরু করতেছি। এই অধ্যায়ে আমরা কন্টেইনারের প্রতিটি মেথড, মেমরি ম্যানেজমেন্ট এবং লাইফসাইকেলকে সম্পর্কে আলোচনা করবো।

৩.১ সার্ভিস কন্টেইনার: আসলে কী?

সহজ কথায়, সার্ভিস কন্টেইনার হলো একটি Dependency Injection (DI) Container। কিন্তু টেকনিক্যালি এটি একটি Key-Value Registry যা PHP Reflection API ব্যবহার করে ডায়নামিকলি অবজেক্ট তৈরি করতে পারে।

লারাভেলের Illuminate\Container\Container ক্লাসটি মূলত দুটি জিনিস ম্যানেজ করে:

  1. Bindings (Instruction): কোন ক্লাস চাইলে কী দিতে হবে সেইটা বোঝা়।
  2. Resolution (Creation): সেই নির্দেশনা অনুযায়ী অবজেক্ট তৈরি করে দেওয়া

৩.১.১ বাইন্ডিং মেকানিজম: bind() বনাম singleton()

লারাভেল কন্টেইনারে ক্লাস রেজিস্টার করার প্রধান দুটি উপায় হলো bind এবং singleton। এদের মেমরি ব্যবহারের পার্থক্য বোঝা প্যাকেজের পারফরম্যান্সের জন্য জরুরি।

১. bind($abstract, $concrete) – ফ্যাক্টরি প্যাটার্ন

যখন আপনি bind মেথড ব্যবহার করেন, তখন কন্টেইনার ক্লাস তৈরির লজিকটি (Closure) শুধু সেভ করে রাখে।

ইন্টারনাল প্রসেস:

  • কন্টেইনার তার $bindings অ্যারেতে key হিসেবে আপনার ক্লাসের নাম এবং value হিসেবে ক্লোজারটি রাখে
  • প্রতিবার যখন ইউজার app($abstract) কল করে, কন্টেইনার ওই ক্লোজারটি এক্সিকিউট করে ফলে প্রতিবার একটি সম্পূর্ণ নতুন অবজেক্ট তৈরি হয় এবং এই অবজেক্টের জন্য মেমরি এলকেট করে।
php
$this->app->bind('InvoiceCalculator', function ($app) {
    // প্রতিবার কল করার সময় এই লাইনগুলো রান হবে
    return new InvoiceCalculator(now()); 
});

২. singleton($abstract, $concrete) – শেয়ার্ড ইনস্ট্যান্স

প্যাকেজ ডেভেলপমেন্টে এটিই সবচেয়ে বেশি ব্যবহৃত হয়।

ইন্টারনাল প্রসেস:

  • প্রথমবার app('InvoiceCalculator') কল করলে কন্টেইনার ক্লোজারটি এক্সিকিউট করে অবজেক্ট তৈরি করে
  • তৈরি হওয়া অবজেক্টটি কন্টেইনার তার $instances নামক প্রটেক্টেড অ্যারেতে সেভ করে রাখে
  • দ্বিতীয়বার কল করলে, সে আর ক্লোজার রান করে না। সরাসরি $instances অ্যারে থেকে পুরনো অবজেক্টটি রিটার্ন করে ফলে পুরো অ্যাপ্লিকেশনে মাত্র একটি অবজেক্ট থাকে (Single Source of Truth)

৩.২ ডিপেন্ডেন্সি রেজল্যুশন: স্টেপ-বাই-স্টেপ অ্যালগরিদম

যখন আপনি app(InvoiceController::class) কল করেন বা কোনো কন্ট্রোলারের কনস্ট্রাক্টরে টাইপ-হিন্ট করেন, তখন লারাভেল বিহাইন্ড দ্যা সিনে যে কাজ গুলো করে,

ধরি, আপনার ক্লাসের স্ট্রাকচার এমন:

  • InvoiceController নির্ভর করে InvoiceService এর ওপর
  • InvoiceService নির্ভর করে TaxCalculator এর ওপর

ধাপ ১: make($abstract) কল করা হলো

ইউজার app(InvoiceController::class) কল করল। কন্টেইনার প্রথমে চেক করে:

  • "আমার $instances অ্যারেতে কি এই নামের কোনো অবজেক্ট আগেই তৈরি করা আছে?" (Singleton Check)
  • যদি থাকে, সেটিই দিয়ে দেয়
  • না থাকলে পরের ধাপে যায়

ধাপ ২: কনক্রিট রেজল্যুশন (Concrete Resolution)

কন্টেইনার চেক করে: "এই ক্লাসের জন্য কি কোনো বাইন্ডিং (bind) সেট করা আছে?"

  • যদি থাকে, সেই বাইন্ডিং লজিক ব্যবহার করে
  • যদি না থাকে (Automatic Resolution), তবে সে ধরে নেয় এটি একটি সাধারণ পিএইচপি ক্লাস এবং সেটিকে তৈরি করার প্রক্রিয়া শুরু করে

ধাপ ৩: রিফলেকশন এপিআই (The Reflection API)

এটিই কন্টেইনারের ম্যাজিক। পিএইচপির ReflectionClass ব্যবহার করে কন্টেইনার ক্লাসটিকে স্ক্যান করে।

php
// লারাভেল ইন্টারনালি যা করে:
$reflector = new ReflectionClass('InvoiceController');

কন্টেইনার চেক করে:

  • Is Instantiable? (এটি কি ইন্টারফেস বা অ্যাবস্ট্রাক্ট ক্লাস? যদি হয় এবং বাইন্ডিং না থাকে, তবে এরর থ্রো করে)
  • Get Constructor: এর কি কনস্ট্রাক্টর আছে? না থাকলে, সরাসরি new InvoiceController রিটার্ন করে

ধাপ ৪: ডিপেন্ডেন্সি এক্সট্রাকশন

যদি কনস্ট্রাক্টর থাকে, কন্টেইনার তার প্যারামিটারগুলো চেক করে।

আমাদের উদাহরণে: __construct(InvoiceService $service)

কন্টেইনার দেখল $service এর টাইপ হলো InvoiceService ক্লাস।

ধাপ ৫: রিকার্সিভ রেজল্যুশন (Recursive Resolution)

এখন কন্টেইনার InvoiceController কে স্কিপ করে InvoiceService তৈরি করে।

  • আবার ধাপ ১-৪ রিপিট করে InvoiceService এর জন্য
  • দেখে InvoiceService এর দরকার TaxCalculator
  • আবার ধাপ ১-৪ রিপিট করে TaxCalculator এর জন্য
  • দেখে TaxCalculator এর কোনো ডিপেন্ডেন্সি নেই

ধাপ ৬: অবজেক্ট অ্যাসেম্বলি

এবার উল্টো পথে কাজ হয়:

  1. TaxCalculator অবজেক্ট তৈরি হলো
  2. সেটি ইনজেক্ট করে InvoiceService তৈরি হলো
  3. সেটি ইনজেক্ট করে InvoiceController তৈরি হলো
  4. ফাইনাল অবজেক্ট ইউজারের কাছে রিটার্ন করা হলো

৩.৩ সার্ভিস প্রভাইডার

সার্ভিস প্রভাইডার হলো কনফিগারেশন ফাইল যেখানে আপনি কন্টেইনারকে বলে দিতে পারবেন কিভাবে কাজ করতে হবে। লারাভেল যখন বুট হয়, সে প্রতিটি প্রভাইডারকে দুটি ধাপে লোড করে। এই ধাপগুলো বোঝা প্যাকেজ ডেভেলপারের জন্য ক্রুশিয়াল।

৩.৩.১ register() মেথড: মেমরি সেটআপ

লারাভেল অ্যাপ রান করার একেবারে শুরুর দিকে এই মেথড কল হয়। এইটার কাজ শুধুমাত্র কন্টেইনারে ক্লাস বাইন্ডিং ($app->bind, $app->singleton) এবং কনফিগ ফাইল মার্জ করা। তবে এই মেথডে কখনো অন্য কোনো সার্ভিস (যেমন: Database, Auth, Router) এক্সেস করা কথা উচিৎ না। কারণ, কারণ এই সার্ভিসগুলো তখনো লোড হয়েছে কিনা নিশ্চিত না। ফলে Class not found বা Target not instantiable এরর পাইতে পারেন।

php
public function register(): void
{
    // ১. কনফিগ ফাইল লোড করা
    $this->mergeConfigFrom(__DIR__.'/../config/invoice.php', 'invoice');

    // ২. ইন্টারফেসের সাথে কনক্রিট ক্লাস বাইন্ড করা
    $this->app->bind(InvoiceInterface::class, function ($app) {
        return new InvoiceManager($app['config']['invoice.currency']);
    });
}

৩.৩.২ boot() মেথড: ইভেন্ট এবং অ্যাকশন

সব প্রভাইডারের register মেথড শেষ হওয়ার পর লারাভেল boot মেথড কল করা শুরু করে। এই পর্যায়ে পুরো অ্যাপ্লিকেশন রেডি। ডাটাবেস, রাউটার, কিউ—সব সার্ভিস এভেইলেবল। এই ধাপে এসে সাধারণত যে সকল কাজ করা হয়ঃ রাউট লোড করা, ইভেন্ট লিসেনার রেজিস্ট্রেশন, ভিউ শেয়ারিং, মিডলওয়্যার পাইপ লাইনে মিডলওয়্যার যুক্ত করা, কনসোল কমান্ড রেজিস্ট্রেশন।

Best Practice Code:

php
public function boot(): void
{
    // রাউট লোড করা
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');

    // মাইগ্রেশন পাবলিশ করা
    if ($this->app->runningInConsole()) {
        $this->publishes([
            __DIR__.'/../database/migrations' => database_path('migrations'),
        ], 'invoice-migrations');
    }
    
    // মডেল অবজার্ভার সেট করা
    Invoice::observe(InvoiceObserver::class);
}

৩.৪ কনটেক্সচুয়াল বাইন্ডিং (Contextual Binding)

মাঝে মাঝে আপনার এমন প্রয়োজন হতে পারে যে, একই ইন্টারফেসের জন্য দুটি ভিন্ন ক্লাস ইনজেক্ট করতে হবে। উদাহরণ:

  • VideoControllerFilesystem ইন্টারফেস চাইলে S3Driver দিতে হবে
  • ImageControllerFilesystem ইন্টারফেস চাইলে LocalDriver দিতে হবে

সাধারণ bind দিয়ে এটি সম্ভব নয়। এর জন্য সার্ভিস প্রভাইডারে Contextual Binding ব্যবহার করতে হয়:

php
$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

$this->app->when(ImageController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

লারাভেল কন্টেইনার যখন রিজল্ভ করবে, সে চেক করবে "কে রিকোয়েস্ট করছে?" (Context) এবং সেই অনুযায়ী অবজেক্ট রিটার্ন করবে।

৩.৫ ট্যাগিং (Tagging)

প্যাকেজ ডেভেলপমেন্টে ট্যাগিং খুব সব সময় কাজে না লাগলেও মাঝেমধ্যে বেশ কাজে লাগে। ধরেন, আপনি একটি "Report Generation" প্যাকেজ বানাচ্ছেন যেখানে একাধিক ফরম্যাটে (PDF, Excel, CSV) রিপোর্ট জেনারেট করা যায়। আপনি চাইলে সব জেনারেটরকে একটি ট্যাগ দিয়ে গ্রুপ করতে পারেন।

php
// সার্ভিস প্রভাইডারে
$this->app->bind('ReportPdf', fn() => new PdfGenerator);
$this->app->bind('ReportCsv', fn() => new CsvGenerator);

// ট্যাগ করা
$this->app->tag(['ReportPdf', 'ReportCsv'], 'reports');

এই ট্যাগ ব্যাবহার করে ট্যাগের আন্ডারে থাকা সকল অবজেক্ট কে এক সাথে পাওয়া যায়।

php
$allGenerators = app()->tagged('reports'); // Returns array of objects

foreach ($allGenerators as $generator) {
    $generator->generate($data);
}

## ৩.৬ Practical Implementation: Service Container in InvoiceBuilder

এখন আমরা Chapter 2 এর minimal service provider কে পূর্ণাঙ্গ করব service container concepts প্রয়োগ করে।

### ৩.৬.১ Core Service Classes তৈরি

প্রথমে আমাদের main service classes তৈরি করি:

**Invoice Service (Core Business Logic):**
```php
// packages/DevMaster/InvoiceBuilder/src/Services/InvoiceService.php
<?php

namespace DevMaster\InvoiceBuilder\Services;

use DevMaster\InvoiceBuilder\Contracts\InvoiceServiceInterface;
use DevMaster\InvoiceBuilder\Services\TaxCalculator;
use DevMaster\InvoiceBuilder\Services\NumberGenerator;

class InvoiceService implements InvoiceServiceInterface
{
    protected array $config;
    protected array $invoiceData = [];
    protected TaxCalculator $taxCalculator;
    protected NumberGenerator $numberGenerator;

    public function __construct(
        array $config,
        TaxCalculator $taxCalculator,
        NumberGenerator $numberGenerator
    ) {
        $this->config = $config;
        $this->taxCalculator = $taxCalculator;
        $this->numberGenerator = $numberGenerator;
    }

    /**
     * Create a new invoice instance
     */
    public function create(): self
    {
        $this->invoiceData = [
            'invoice_number' => $this->numberGenerator->generate(),
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
            'currency' => $this->config['currency'] ?? 'USD',
            'company' => $this->config['company'] ?? [],
            'customer' => [],
            'items' => [],
            'notes' => '',
            'subtotal' => 0,
            'tax_rate' => $this->config['tax']['default_rate'] ?? 0,
            'tax_amount' => 0,
            'total' => 0,
        ];

        return $this;
    }

    /**
     * Set customer information
     */
    public function customer(array $customer): self
    {
        $this->invoiceData['customer'] = $customer;
        return $this;
    }

    /**
     * Add an item to the invoice
     */
    public function addItem(string $description, float $price, int $quantity = 1): self
    {
        $total = $price * $quantity;
        
        $this->invoiceData['items'][] = [
            'description' => $description,
            'price' => $price,
            'quantity' => $quantity,
            'total' => $total,
        ];

        $this->calculateTotals();
        return $this;
    }

    /**
     * Get invoice data
     */
    public function getData(): array
    {
        return $this->invoiceData;
    }

    /**
     * Calculate invoice totals using injected TaxCalculator
     */
    protected function calculateTotals(): void
    {
        $subtotal = array_sum(array_column($this->invoiceData['items'], 'total'));
        $taxAmount = $this->taxCalculator->calculate(
            $subtotal, 
            $this->invoiceData['tax_rate']
        );
        
        $this->invoiceData['subtotal'] = $subtotal;
        $this->invoiceData['tax_amount'] = $taxAmount;
        $this->invoiceData['total'] = $subtotal + $taxAmount;
    }
}

Tax Calculator Service:

php
// packages/DevMaster/InvoiceBuilder/src/Services/TaxCalculator.php
<?php

namespace DevMaster\InvoiceBuilder\Services;

class TaxCalculator
{
    protected array $config;

    public function __construct(array $config = [])
    {
        $this->config = $config;
    }

    /**
     * Calculate tax amount
     */
    public function calculate(float $amount, float $rate): float
    {
        $isInclusive = $this->config['tax']['included'] ?? false;
        
        if ($isInclusive) {
            // Tax is included in price
            return $amount - ($amount / (1 + $rate));
        }
        
        // Tax is added on top
        return $amount * $rate;
    }

    /**
     * Get effective tax rate for a region
     */
    public function getEffectiveRate(string $region = null): float
    {
        if (!$region) {
            return $this->config['tax']['default_rate'] ?? 0.0;
        }

        $regionRates = $this->config['tax']['regions'] ?? [];
        return $regionRates[$region] ?? $this->config['tax']['default_rate'] ?? 0.0;
    }
}

Number Generator Service:

php
// packages/DevMaster/InvoiceBuilder/src/Services/NumberGenerator.php
<?php

namespace DevMaster\InvoiceBuilder\Services;

class NumberGenerator
{
    protected array $config;
    protected static int $counter = 0;

    public function __construct(array $config)
    {
        $this->config = $config;
    }

    /**
     * Generate invoice number
     */
    public function generate(): string
    {
        $prefix = $this->config['invoice_number']['prefix'] ?? 'INV-';
        $length = $this->config['invoice_number']['length'] ?? 6;
        $startFrom = $this->config['invoice_number']['start_from'] ?? 1;
        
        // Simple incremental number (in real app, use database)
        static::$counter++;
        $number = $startFrom + static::$counter;
        
        return $prefix . str_pad($number, $length, '0', STR_PAD_LEFT);
    }
}

৩.৬.২ Interface Definition

Contract/Interface তৈরি:

php
// packages/DevMaster/InvoiceBuilder/src/Contracts/InvoiceServiceInterface.php
<?php

namespace DevMaster\InvoiceBuilder\Contracts;

interface InvoiceServiceInterface
{
    public function create(): self;
    public function customer(array $customer): self;
    public function addItem(string $description, float $price, int $quantity = 1): self;
    public function getData(): array;
}

৩.৬.৩ Service Provider - register() Method Enhancement

এখন Chapter 2 এর service provider কে enhance করি:

php
// packages/DevMaster/InvoiceBuilder/src/InvoiceBuilderServiceProvider.php
<?php

namespace DevMaster\InvoiceBuilder;

use Illuminate\Support\ServiceProvider;
use DevMaster\InvoiceBuilder\Contracts\InvoiceServiceInterface;
use DevMaster\InvoiceBuilder\Services\InvoiceService;
use DevMaster\InvoiceBuilder\Services\TaxCalculator;
use DevMaster\InvoiceBuilder\Services\NumberGenerator;

class InvoiceBuilderServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register()
    {
        // ১. Configuration merge করি
        $this->mergeConfigFrom(
            __DIR__.'/../config/invoice-builder.php',
            'invoice-builder'
        );

        // ২. Dependency services register করি  
        $this->registerDependencies();

        // ৩. Main service register করি
        $this->registerMainService();

        // ৪. Interface binding করি
        $this->registerContracts();
    }

    /**
     * Register dependency services
     */
    protected function registerDependencies()
    {
        // TaxCalculator - singleton কারণ এটি stateless
        $this->app->singleton(TaxCalculator::class, function ($app) {
            return new TaxCalculator($app['config']['invoice-builder']);
        });

        // NumberGenerator - singleton কারণ counter maintain করে
        $this->app->singleton(NumberGenerator::class, function ($app) {
            return new NumberGenerator($app['config']['invoice-builder']);
        });
    }

    /**
     * Register main invoice service
     */
    protected function registerMainService()
    {
        // InvoiceService - bind (not singleton) কারণ প্রতিটি invoice আলাদা
        $this->app->bind(InvoiceService::class, function ($app) {
            return new InvoiceService(
                $app['config']['invoice-builder'],
                $app->make(TaxCalculator::class),
                $app->make(NumberGenerator::class)
            );
        });

        // Facade accessor
        $this->app->singleton('invoice-builder', function ($app) {
            return $app->make(InvoiceService::class);
        });
    }

    /**
     * Register interface contracts
     */
    protected function registerContracts()
    {
        // Interface to concrete binding
        $this->app->bind(InvoiceServiceInterface::class, InvoiceService::class);
    }

    /**
     * Bootstrap any application services.
     */
    public function boot()
    {
        // Boot logic will be added in Chapter 5
    }
}

৩.৬.৪ Contextual Binding Example

বিভিন্ন context এ বিভিন্ন tax calculator দরকার হলে:

php
// Enhanced service provider এ contextual binding যোগ করুন
protected function registerContextualBindings()
{
    // B2B invoices এর জন্য আলাদা tax calculator
    $this->app->when('App\Http\Controllers\B2BInvoiceController')
              ->needs(TaxCalculator::class)
              ->give(function ($app) {
                  $config = $app['config']['invoice-builder'];
                  $config['tax']['default_rate'] = 0.0; // B2B সাধারণত tax-free
                  return new TaxCalculator($config);
              });

    // Consumer invoices এর জন্য standard tax
    $this->app->when('App\Http\Controllers\ConsumerInvoiceController')
              ->needs(TaxCalculator::class)
              ->give(TaxCalculator::class); // Default binding
}

৩.৭ Chapter 3 Checkpoint

এই chapter শেষে আপনার InvoiceBuilder package এ আছে:

  • Service Container Mastery - bind() vs singleton() বাস্তব প্রয়োগ
  • Dependency Injection - Constructor injection with automatic resolution
  • Interface Binding - Contract-driven development
  • Service Provider Architecture - Proper register() method structure
  • Contextual Binding - Different services for different contexts
  • Memory Optimization - সঠিক singleton/bind usage

সৎ ক্রেডিট: লেখক AI, সম্পাদক আবুল হাসান