থিম
অধ্যায় ৩: সার্ভিস কন্টেইনার এবং প্রোভাইডার আর্কিটেকচার
লারাভেল ফ্রেমওয়ার্কের মোস্ট পাওয়ারফুল ফিচারের একটি হলো সার্ভিস কন্টেইনার (Service Container)। আপনি সার্ভিস কন্টেইনার না বুঝলে আপনার জন্য লারাভেল কোর বা এডভান্স জিনিসপত্র বোঝা কঠিন হয়ে যাবে। তাই শুরুতে এইটা দিয়ে আলোচনা শুরু করতেছি। এই অধ্যায়ে আমরা কন্টেইনারের প্রতিটি মেথড, মেমরি ম্যানেজমেন্ট এবং লাইফসাইকেলকে সম্পর্কে আলোচনা করবো।
৩.১ সার্ভিস কন্টেইনার: আসলে কী?
সহজ কথায়, সার্ভিস কন্টেইনার হলো একটি Dependency Injection (DI) Container। কিন্তু টেকনিক্যালি এটি একটি Key-Value Registry যা PHP Reflection API ব্যবহার করে ডায়নামিকলি অবজেক্ট তৈরি করতে পারে।
লারাভেলের Illuminate\Container\Container ক্লাসটি মূলত দুটি জিনিস ম্যানেজ করে:
- Bindings (Instruction): কোন ক্লাস চাইলে কী দিতে হবে সেইটা বোঝা়।
- 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এর কোনো ডিপেন্ডেন্সি নেই
ধাপ ৬: অবজেক্ট অ্যাসেম্বলি
এবার উল্টো পথে কাজ হয়:
TaxCalculatorঅবজেক্ট তৈরি হলো- সেটি ইনজেক্ট করে
InvoiceServiceতৈরি হলো - সেটি ইনজেক্ট করে
InvoiceControllerতৈরি হলো - ফাইনাল অবজেক্ট ইউজারের কাছে রিটার্ন করা হলো
৩.৩ সার্ভিস প্রভাইডার
সার্ভিস প্রভাইডার হলো কনফিগারেশন ফাইল যেখানে আপনি কন্টেইনারকে বলে দিতে পারবেন কিভাবে কাজ করতে হবে। লারাভেল যখন বুট হয়, সে প্রতিটি প্রভাইডারকে দুটি ধাপে লোড করে। এই ধাপগুলো বোঝা প্যাকেজ ডেভেলপারের জন্য ক্রুশিয়াল।
৩.৩.১ 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)
মাঝে মাঝে আপনার এমন প্রয়োজন হতে পারে যে, একই ইন্টারফেসের জন্য দুটি ভিন্ন ক্লাস ইনজেক্ট করতে হবে। উদাহরণ:
VideoControllerএFilesystemইন্টারফেস চাইলেS3Driverদিতে হবেImageControllerএFilesystemইন্টারফেস চাইলে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()vssingleton()বাস্তব প্রয়োগ - 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