Skip to content

অধ্যায় ৫: কনফিগারেশন ম্যানেজমেন্ট এবং অ্যাসেট পাবলিশিং আর্কিটেকচার

আমারা এই পুরো কোর্সজুড়ে InvoiceLite নামের একটা প্যাজেক বানাবো যেইটা দিয়ে ইনভুয়েজ বানানো যাবে। এখন এই প্যাকেজের মধ্যে অনেক সেটিংস থাকবে যেগুলো ডেভেলপার তার নিজের অ্যাপ্লিকেশনে বসে কাস্টমাইজ করতে চাইবে। যেমন: কারেন্সি কোড, ইনভয়েস প্রিফিক্স, স্টোরেজ ড্রাইভার ইত্যাদি। এই সেটিংসগুলো ম্যানেজ করার জন্য লারাভেল একটি শক্তিশালী কনফিগারেশন সিস্টেম অফার করে। এই অধ্যায়ে আমরা শিখবো কিভাবে প্যাকেজের কনফিগারেশন ফাইল ডিজাইন করতে হয়, ইউজারের অ্যাপ্লিকেশনে কনফিগারেশন পাবলিশ করতে হয় এবং কিভাবে প্যাকেজের অন্যান্য অ্যাসেট (যেমন ভিউ, পাবলিক ফাইল) পাবলিশ করা যায়। এই প্যাকেজের কন্টেক্সটে সামনের অধ্যায়গুলো লেখা হবে ইনশাল্লাহ।

৫.১ কেন কনফিগারেশন ফাইল অপরিহার্য?

ধরি, আপনার InvoiceLite প্যাকেজটি ইনভয়েস জেনারেট করার সময় ডিফল্ট কারেন্সি হিসেবে 'USD' ব্যবহার করে। এখন একজন বাংলাদেশী ডেভেলপার যদি এটি ব্যবহার করতে চান, তিনি চাইবেন 'BDT' ব্যবহার করতে।

Bad Practice

ডেভেলপার সরাসরি vendor/devmaster/src/InvoiceManager.php ফাইল এডিট করে 'USD' কে 'BDT' বানালেন।

সমস্যা: যখন আপনি প্যাকেজের ২.০ ভার্সন রিলিজ করবেন এবং তিনি composer update দেবেন, তার সব পরিবর্তন মুছে যাবে।

Good Practice

প্যাকেজটি একটি কনফিগ ফাইল অফার করবে। ডেভেলপার তার নিজের অ্যাপ্লিকেশনে বসে সেই কনফিগ ফাইলের ভ্যালু পরিবর্তন করবেন। প্যাকেজ আপডেট হলেও এই কনফিগ অক্ষত থাকবে।

৫.২ কনফিগারেশন ফাইল ডিজাইন এবং স্ট্রাকচার

২য় অধ্যাএর সিমলিংকিং ফলো করে একটা ডিরেক্টরি বানান এবং আপনার লারাভেল এপ্লিকেশনের composer.json এ লোকাল রিপো হিসাবে সেট করুন। আপনি আপনার প্যাকেজের ডিরেক্টরিতে গিয়ে pwd কমান্ড রান করুন এবং এইটার আউটপুট url এ ব্যাবহার করুন।

bladehtml
    "repositories": [
        {
            "type": "path",
            "url": "/Users/figlab/Desktop/package-development-masterclass/megamind-learnings-package"
        }
    ],

এইবার প্যাকেজের ভেতরে একটি ডিফল্ট কনফিগ ফাইল তৈরি করি। এটি হবে আমাদের প্যাকেজের "Factory Settings"

ফাইল: /InvoiceLite/config/invoicelite.php

php
<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Invoice Currency Code
    |--------------------------------------------------------------------------
    |
    | This value determines the currency used for generating invoices.
    | It defaults to USD but can be overridden via .env file.
    |
    */
    'currency' => env('INVOICE_CURRENCY', 'USD'),

    /*
    |--------------------------------------------------------------------------
    | Serial Prefix
    |--------------------------------------------------------------------------
    */
    'prefix' => 'INV-',

    /*
    |--------------------------------------------------------------------------
    | Storage Disk
    |--------------------------------------------------------------------------
    |
    | Where the generated PDF files will be stored.
    |
    */
    'disk' => 'public',
    
    // Nested array example for drivers
    'drivers' => [
        'stripe' => [
            'key' => env('STRIPE_KEY'),
            'secret' => env('STRIPE_SECRET'),
        ],
    ],
];

আর্কিটেক্ট নোট: কখনোই কনফিগ ফাইলের বাইরে (যেমন কন্ট্রোলারে বা সার্ভিসে) env() ফাংশন ব্যবহার করবেন না। কারণ প্রোডাকশনে যখন ইউজার php artisan config:cache কমান্ড চালায়, তখন .env ফাইল লোড হয় না, শুধুমাত্র ক্যাশড কনফিগ লোড হয়। ফলে আপনার কোড null রিটার্ন করবে এবং অ্যাপ ক্র্যাশ করবে। সবসময় config('key') ব্যবহার করবেন।

৫.৩ ইন্টারনাল মেকানিজম: mergeConfigFrom

ইউজার যখন প্যাকেজটি ইন্সটল করে, তখন তার config/ ফোল্ডারে invoicelite.php ফাইলটি থাকে না। তাহলে সে ডিফল্ট ভ্যালু পায় কীভাবে? এখানেই সার্ভিস প্রভাইডারের register মেথডের mergeConfigFrom জাদুর মতো কাজ করে। এখন আপনার প্যাকেজের সার্ভিস প্রভাইডারে নিচের কোডটি যোগ করুন: আপনার সার্ভিস প্রভাইডার দেখতে এমন হবেঃ

ফাইল: src/InvoiceLiteServiceProvider.php

php
<?php
namespace DevMaster\InvoiceLite;
use Illuminate\Support\ServiceProvider;
class InvoiceLiteServiceProvider extends ServiceProvider
{
    // InvoiceLiteServiceProvider.php
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/invoicelite.php', 'invoicelite'
        );
    }
}

মার্জিং অ্যালগরিদম (কিভাবে কাজ করে?)

লারাভেলের Illuminate\Support\ServiceProvider ক্লাসের ভেতরে mergeConfig মেথডটি একটি Two-Layer Array Merge প্রক্রিয়া চালায়:

  1. লেভেল ১ (User Config): লারাভেল প্রথমে দেখে ইউজারের মূল অ্যাপ্লিকেশনের config/invoicelite.php ফাইলে কোনো ভ্যালু আছে কি না
  2. লেভেল ২ (Package Config): এরপর সে প্যাকেজের ডিফল্ট কনফিগ ফাইলটি লোড করে
  3. মার্জিং: লারাভেল পিএইচপির array_merge_recursive এর মতো লজিক ব্যবহার করে ভ্যালুগুলো কম্বাইন করে

ফলাফল: যদি ইউজার শুধু 'currency' => 'BDT' সেট করে এবং বাকি সব বাদ দেয়, তবুও অ্যাপ ক্র্যাশ করবে না। কারণ বাকি ভ্যালুগুলো (যেমন prefix, disk) প্যাকেজের ডিফল্ট ফাইল থেকে অটোমেটিক্যালি পূরণ হয়ে যাবে। এটি Fallback Mechanism হিসেবে কাজ করে।

৫.৪ পাবলিশিং আর্কিটেকচার: vendor:publish

ইউজার যদি ডিফল্ট সেটিং পরিবর্তন করতে চায়, তাকে কনফিগ ফাইলটি নিজের অ্যাপ্লিকেশনে কপি করতে হবে। লারাভেলের ভাষায় একে বলা হয় Publishing

এটি boot মেথডে হ্যান্ডেল করতে হয়।

php
<?php
namespace DevMaster\InvoiceLite;
use Illuminate\Support\ServiceProvider;
class InvoiceLiteServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/invoicelite.php', 'invoicelite'
        );
    }
    
    public function boot(): void
    {
        // পারফরম্যান্স অপ্টিমাইজেশন: কনসোল ছাড়া এই কোড রান করার দরকার নেই
        if ($this->app->runningInConsole()) {
            
            $this->publishes([
                // Source (Package) -> Destination (User App)
                __DIR__.'/../config/invoicelite.php' => config_path('invoicelite.php'),
            ], 'invoicelite-config'); // <--- Tag Name
        }
    }
}

৫.৪.১ ট্যাগিং (Tagging) এর গুরুত্ব

invoicelite-config স্ট্রিংটি হলো একটি Tag। একটি প্যাকেজে অনেক ধরণের অ্যাসেট থাকতে পারে (Config, Views, Migrations)। ইউজার হয়তো শুধু কনফিগ চায়, ভিউ চায় না। ট্যাগ ব্যবহার করে ইউজার নির্দিষ্ট অংশ পাবলিশ করতে পারে।

যেমন:

bash
php artisan vendor:publish --tag=invoicelite-config

এই কমান্ড আমাদের কনফিগ ফাইলটাকেই ইউজারের অ্যাপ্লিকেশনে কপি করবে।

৫.৪.২ ইন্টারনাল প্রসেস: VendorPublishCommand

যখন কমান্ডটি রান হয়:

  1. লারাভেল সব প্রভাইডারের pathsToPublish অ্যারে স্ক্যান করে
  2. সে দেখে কোন পাথগুলো রিকোয়েস্ট করা ট্যাগের সাথে মিলছে
  3. File Copy Operation: সে সোর্স থেকে ডেস্টিনেশনে ফাইল কপি করে (Filesystem::copy)
  4. Overwrite Check: যদি ডেস্টিনেশনে ফাইলটি আগেই থাকে, সে ইউজারকে প্রশ্ন করে: "File already exists. Do you want to overwrite?"

৫.৫ অন্যান্য অ্যাসেট পাবলিশিং (Views, Migrations, Public Assets)

শুধু কনফিগ নয়, প্যাকেজ ডেভেলপমেন্টে আরও কিছু জিনিস পাবলিশ করার প্রয়োজন হতে পারে।

৫.৫.১ ভিউ (Views) ওভাররাইডিং

সাধারণত আমরা loadViewsFrom ব্যবহার করি।

php
<?php
namespace DevMaster\InvoiceLite;
use Illuminate\Support\ServiceProvider;
class InvoiceLiteServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/invoicelite.php', 'invoicelite'
        );
        
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'invoicelite');
    }
    
    public function boot(): void
    {
        // পারফরম্যান্স অপ্টিমাইজেশন: কনসোল ছাড়া এই কোড রান করার দরকার নেই
        if ($this->app->runningInConsole()) {
            
            $this->publishes([
                // Source (Package) -> Destination (User App)
                __DIR__.'/../config/invoicelite.php' => config_path('invoicelite.php'),
            ], 'invoicelite-config'); // <--- Tag Name
        }
    }
    
}

ইউজার view('invoicelite::invoice') কল করলে প্যাকেজের ভিউ লোড হয়।

কিন্তু ইউজার যদি ভিউ ডিজাইন পরিবর্তন করতে চায়? লারাভেল অটোমেটিক্যালি resources/views/vendor/invoicelite ফোল্ডার চেক করে। সেখানে ফাইল না পেলে প্যাকেজের ভিউ লোড করে।

ভিউ পাবলিশ করার জন্য:

php
    public function boot(): void
    {
        // পারফরম্যান্স অপ্টিমাইজেশন: কনসোল ছাড়া এই কোড রান করার দরকার নেই
        if ($this->app->runningInConsole()) {
            
            $this->publishes([
                // Source (Package) -> Destination (User App)
                __DIR__.'/../config/invoicelite.php' => config_path('invoicelite.php'),
            ], 'invoicelite-config'); // <--- Tag Name
        }
        
        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/invoicelite'),
        ], 'invoicelite-views');
    }

৫.৫.২ পাবলিক অ্যাসেট (JS/CSS/Images) – সিকিউরিটি ফ্যাক্টর

এটি অত্যন্ত গুরুত্বপূর্ণ। ব্রাউজার সরাসরি vendor/ ফোল্ডারে এক্সেস করতে পারে না (এটি সিকিউরিটি রিস্ক)। তাই আপনার প্যাকেজে যদি কোনো জাভাস্ক্রিপ্ট, সিএসএস বা ইমেজ থাকে, সেগুলোকে অবশ্যই public/ ফোল্ডারে পাবলিশ করতে হবে।

php
    public function boot(): void
    {
        // পারফরম্যান্স অপ্টিমাইজেশন: কনসোল ছাড়া এই কোড রান করার দরকার নেই
        if ($this->app->runningInConsole()) {
            
            $this->publishes([
                // Source (Package) -> Destination (User App)
                __DIR__.'/../config/invoicelite.php' => config_path('invoicelite.php'),
            ], 'invoicelite-config'); // <--- Tag Name
        }
        
        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/invoicelite'),
        ], 'invoicelite-views');
        
        $this->publishes([
            __DIR__.'/../resources/assets' => public_path('vendor/invoicelite'),
        ], 'invoicelite-assets');
    }

এরপর ইউজার ব্লেড ফাইলে asset('vendor/invoicelite/style.css') ব্যবহার করতে পারবে।

৫.৬ কনফিগারেশন এক্সেস করার সঠিক পদ্ধতি

প্যাকেজের ভেতর থেকে কনফিগ ভ্যালু পড়ার জন্য Dot Notation ব্যবহার করা হয়।

php
namespace DevMaster\InvoiceLite;

use Illuminate\Support\Facades\Config;

class InvoiceManager 
{
    public function getCurrency()
    {
        // 1. Helper function (Most common)
        return config('invoicelite.currency', 'USD');
        
        // 2. Facade (Alternative)
        return Config::get('invoicelite.currency');
    }
}

রানটাইম কনফিগারেশন (Runtime Configuration)

মাঝে মাঝে কোড এক্সিকিউশনের মাঝখানে কনফিগ চেঞ্জ করার দরকার হতে পারে (শুধুমাত্র ওই রিকোয়েস্টের জন্য)।

php
// টেম্পোরারিভাবে কারেন্সি পরিবর্তন করা
config(['invoicelite.currency' => 'EUR']);

// এরপর কোড রান করলে EUR ব্যবহার হবে
$invoice->generate();

এটি টেস্টিং-এর সময় খুব কাজে লাগে।

৫.৭ চ্যালেঞ্জ: মাল্টি-ড্রাইভার কনফিগারেশন স্ট্রাকচার

চ্যালেঞ্জ: এমন একটি কনফিগারেশন স্ট্রাকচার তৈরি করুন যা লারাভেলের ডিফল্ট database.php বা filesystems.php এর মতো কাজ করে। অর্থাৎ, ইউজার একটি default ড্রাইভার সেট করবে এবং drivers অ্যারেতে সেটার ক্রেডেনশিয়াল থাকবে।

সমাধান (Config File):

php
return [
    'default' => env('INVOICE_DRIVER', 'smtp'),

    'drivers' => [
        'smtp' => [
            'host' => 'smtp.mailtrap.io',
            'port' => 2525,
        ],
        'api' => [
            'endpoint' => 'https://api.invoicelite.com',
            'token' => 'xyz',
        ],
    ],
];

ব্যবহার (Service Class):

php
public function getDriverConfig()
{
    $default = config('invoicelite.default'); // e.g., 'smtp'
    return config("invoicelite.drivers.{$default}"); // returns smtp array
}

এটি একটি স্কেলেবল ডিজাইন প্যাটার্ন যা বড় প্যাকেজগুলোতে (যেমন Spatie এর প্যাকেজ) দেখা যায়।

🔨 ৫.৮ Practical Implementation: Enhanced Configuration & Asset Publishing

এখন আমরা Chapter 3 এর service container implementation কে enhance করব advanced configuration ও asset publishing দিয়ে।

৫.৮.১ Enhanced Configuration File

Chapter 2 এর basic config কে আরও sophisticated করি:

php
// packages/DevMaster/InvoiceBuilder/config/invoice-builder.php
<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Default Driver
    |--------------------------------------------------------------------------
    | The default driver for invoice generation and delivery
    */
    'default' => env('INVOICE_BUILDER_DRIVER', 'local'),

    /*
    |--------------------------------------------------------------------------
    | Invoice Drivers
    |--------------------------------------------------------------------------
    | Different drivers for invoice generation and delivery
    */
    'drivers' => [
        'local' => [
            'disk' => env('INVOICE_LOCAL_DISK', 'public'),
            'path' => env('INVOICE_LOCAL_PATH', 'invoices'),
        ],
        'email' => [
            'mailer' => env('INVOICE_EMAIL_MAILER', 'smtp'),
            'from' => [
                'address' => env('INVOICE_FROM_ADDRESS', 'noreply@example.com'),
                'name' => env('INVOICE_FROM_NAME', 'Invoice System'),
            ],
            'attach_pdf' => env('INVOICE_EMAIL_ATTACH_PDF', true),
        ],
        'api' => [
            'endpoint' => env('INVOICE_API_ENDPOINT'),
            'token' => env('INVOICE_API_TOKEN'),
            'timeout' => env('INVOICE_API_TIMEOUT', 30),
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Company Information
    |--------------------------------------------------------------------------
    */
    'company' => [
        'name' => env('INVOICE_COMPANY_NAME', 'Your Company'),
        'address' => env('INVOICE_COMPANY_ADDRESS', ''),
        'email' => env('INVOICE_COMPANY_EMAIL', ''),
        'phone' => env('INVOICE_COMPANY_PHONE', ''),
        'website' => env('INVOICE_COMPANY_WEBSITE', ''),
        'logo' => env('INVOICE_COMPANY_LOGO', null), // Path to logo file
    ],

    /*
    |--------------------------------------------------------------------------
    | Currency Settings
    |--------------------------------------------------------------------------
    */
    'currency' => env('INVOICE_CURRENCY', 'USD'),
    'currency_symbol' => env('INVOICE_CURRENCY_SYMBOL', '$'),
    'currency_position' => env('INVOICE_CURRENCY_POSITION', 'before'), // before|after
    'decimal_places' => env('INVOICE_DECIMAL_PLACES', 2),

    /*
    |--------------------------------------------------------------------------
    | Invoice Number Generation
    |--------------------------------------------------------------------------
    */
    'invoice_number' => [
        'driver' => env('INVOICE_NUMBER_DRIVER', 'sequential'), // sequential|uuid|custom
        'prefix' => env('INVOICE_NUMBER_PREFIX', 'INV-'),
        'suffix' => env('INVOICE_NUMBER_SUFFIX', ''),
        'length' => env('INVOICE_NUMBER_LENGTH', 6),
        'start_from' => env('INVOICE_NUMBER_START', 1),
        'custom_format' => env('INVOICE_NUMBER_FORMAT', '{prefix}{year}{month}{sequential}'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Tax Configuration
    |--------------------------------------------------------------------------
    */
    'tax' => [
        'default_rate' => env('INVOICE_TAX_RATE', 0.0),
        'label' => env('INVOICE_TAX_LABEL', 'Tax'),
        'included' => env('INVOICE_TAX_INCLUDED', false),
        'regions' => [
            'BD' => 0.15, // 15% VAT in Bangladesh
            'US' => 0.08, // 8% sales tax
            'UK' => 0.20, // 20% VAT
            'CA' => 0.13, // 13% HST
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | PDF Generation Settings
    |--------------------------------------------------------------------------
    */
    'pdf' => [
        'driver' => env('INVOICE_PDF_DRIVER', 'mpdf'), // mpdf|dompdf|wkhtmltopdf
        'format' => env('INVOICE_PDF_FORMAT', 'A4'),
        'orientation' => env('INVOICE_PDF_ORIENTATION', 'P'), // P|L
        'margin' => [
            'top' => env('INVOICE_PDF_MARGIN_TOP', 15),
            'bottom' => env('INVOICE_PDF_MARGIN_BOTTOM', 15),
            'left' => env('INVOICE_PDF_MARGIN_LEFT', 15),
            'right' => env('INVOICE_PDF_MARGIN_RIGHT', 15),
        ],
        'font' => [
            'default' => env('INVOICE_PDF_FONT', 'dejavu-sans'),
            'size' => env('INVOICE_PDF_FONT_SIZE', 10),
        ],
        'watermark' => [
            'enabled' => env('INVOICE_PDF_WATERMARK', false),
            'text' => env('INVOICE_PDF_WATERMARK_TEXT', 'DRAFT'),
            'opacity' => env('INVOICE_PDF_WATERMARK_OPACITY', 0.1),
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Template Settings
    |--------------------------------------------------------------------------
    */
    'templates' => [
        'default' => env('INVOICE_TEMPLATE', 'default'),
        'available' => [
            'default' => 'Default Template',
            'modern' => 'Modern Template',
            'classic' => 'Classic Template',
            'minimal' => 'Minimal Template',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Database Settings
    |--------------------------------------------------------------------------
    */
    'database' => [
        'connection' => env('INVOICE_DB_CONNECTION', null),
        'table_prefix' => env('INVOICE_TABLE_PREFIX', 'invoice_'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Notification Settings
    |--------------------------------------------------------------------------
    */
    'notifications' => [
        'enabled' => env('INVOICE_NOTIFICATIONS', true),
        'channels' => ['mail', 'database'], // mail, database, slack, etc.
        'events' => [
            'created' => env('INVOICE_NOTIFY_CREATED', true),
            'sent' => env('INVOICE_NOTIFY_SENT', true),
            'paid' => env('INVOICE_NOTIFY_PAID', true),
            'overdue' => env('INVOICE_NOTIFY_OVERDUE', true),
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Cache Settings
    |--------------------------------------------------------------------------
    */
    'cache' => [
        'enabled' => env('INVOICE_CACHE_ENABLED', true),
        'ttl' => env('INVOICE_CACHE_TTL', 3600), // 1 hour
        'prefix' => env('INVOICE_CACHE_PREFIX', 'invoice_builder'),
    ],
];

৫.৮.২ Configuration Manager Service

Configuration access এর জন্য dedicated service তৈরি করি:

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

namespace DevMaster\InvoiceBuilder\Services;

class ConfigManager
{
    protected array $config;
    protected string $configKey;

    public function __construct(array $config, string $configKey = 'invoice-builder')
    {
        $this->config = $config;
        $this->configKey = $configKey;
    }

    /**
     * Get configuration value with dot notation
     */
    public function get(string $key, $default = null)
    {
        return data_get($this->config, $key, $default);
    }

    /**
     * Get current driver configuration
     */
    public function getDriverConfig(string $driver = null): array
    {
        $driver = $driver ?? $this->getDefaultDriver();
        return $this->get("drivers.{$driver}", []);
    }

    /**
     * Get default driver name
     */
    public function getDefaultDriver(): string
    {
        return $this->get('default', 'local');
    }

    /**
     * Get company information
     */
    public function getCompanyInfo(): array
    {
        return $this->get('company', []);
    }

    /**
     * Get currency settings
     */
    public function getCurrencySettings(): array
    {
        return [
            'code' => $this->get('currency', 'USD'),
            'symbol' => $this->get('currency_symbol', '$'),
            'position' => $this->get('currency_position', 'before'),
            'decimal_places' => $this->get('decimal_places', 2),
        ];
    }

    /**
     * Get tax rate for specific region
     */
    public function getTaxRate(string $region = null): float
    {
        if ($region && $this->get("tax.regions.{$region}") !== null) {
            return $this->get("tax.regions.{$region}");
        }
        
        return $this->get('tax.default_rate', 0.0);
    }

    /**
     * Get PDF settings
     */
    public function getPdfSettings(): array
    {
        return $this->get('pdf', []);
    }

    /**
     * Get template settings
     */
    public function getTemplateSettings(): array
    {
        return $this->get('templates', []);
    }

    /**
     * Check if feature is enabled
     */
    public function isEnabled(string $feature): bool
    {
        $features = [
            'notifications' => 'notifications.enabled',
            'cache' => 'cache.enabled',
            'watermark' => 'pdf.watermark.enabled',
        ];

        if (isset($features[$feature])) {
            return (bool) $this->get($features[$feature], false);
        }

        return false;
    }

    /**
     * Runtime configuration update
     */
    public function set(string $key, $value): void
    {
        config(["{$this->configKey}.{$key}" => $value]);
        
        // Update local config array
        data_set($this->config, $key, $value);
    }
}

৫.৮.৩ Enhanced Service Provider with Publishing

Chapter 3 এর service provider কে configuration ও asset publishing দিয়ে 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;
use DevMaster\InvoiceBuilder\Services\ConfigManager;

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

        // ২. Register configuration manager
        $this->registerConfigManager();

        // ৩. Register dependency services
        $this->registerDependencies();

        // ৪. Register main service
        $this->registerMainService();

        // ৫. Register interface contracts
        $this->registerContracts();
    }

    /**
     * Bootstrap any application services.
     */
    public function boot()
    {
        // Publishing assets
        if ($this->app->runningInConsole()) {
            $this->publishAssets();
        }

        // Load package resources
        $this->loadPackageResources();
    }

    /**
     * Register configuration manager
     */
    protected function registerConfigManager()
    {
        $this->app->singleton(ConfigManager::class, function ($app) {
            return new ConfigManager(
                $app['config']['invoice-builder'],
                'invoice-builder'
            );
        });

        // Alias for easier access
        $this->app->singleton('invoice-builder.config', function ($app) {
            return $app->make(ConfigManager::class);
        });
    }

    /**
     * Register dependency services with configuration
     */
    protected function registerDependencies()
    {
        // Enhanced TaxCalculator with ConfigManager
        $this->app->singleton(TaxCalculator::class, function ($app) {
            $configManager = $app->make(ConfigManager::class);
            return new TaxCalculator($configManager->get('tax', []));
        });

        // Enhanced NumberGenerator with ConfigManager
        $this->app->singleton(NumberGenerator::class, function ($app) {
            $configManager = $app->make(ConfigManager::class);
            return new NumberGenerator($configManager->get('invoice_number', []));
        });
    }

    /**
     * Register main service with enhanced dependencies
     */
    protected function registerMainService()
    {
        $this->app->bind(InvoiceService::class, function ($app) {
            $configManager = $app->make(ConfigManager::class);
            
            return new InvoiceService(
                $configManager->config, // Full config array
                $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()
    {
        $this->app->bind(InvoiceServiceInterface::class, InvoiceService::class);
    }

    /**
     * Publish package assets
     */
    protected function publishAssets()
    {
        // Configuration files
        $this->publishes([
            __DIR__.'/../config/invoice-builder.php' => config_path('invoice-builder.php'),
        ], 'invoice-builder-config');

        // View templates
        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/invoice-builder'),
        ], 'invoice-builder-views');

        // CSS/JS assets
        $this->publishes([
            __DIR__.'/../resources/assets' => public_path('vendor/invoice-builder'),
        ], 'invoice-builder-assets');

        // Migration files
        $this->publishes([
            __DIR__.'/../database/migrations' => database_path('migrations'),
        ], 'invoice-builder-migrations');

        // Language files
        $this->publishes([
            __DIR__.'/../resources/lang' => lang_path('vendor/invoice-builder'),
        ], 'invoice-builder-lang');

        // Publish everything at once
        $this->publishes([
            __DIR__.'/../config/invoice-builder.php' => config_path('invoice-builder.php'),
            __DIR__.'/../resources/views' => resource_path('views/vendor/invoice-builder'),
            __DIR__.'/../resources/assets' => public_path('vendor/invoice-builder'),
        ], 'invoice-builder');
    }

    /**
     * Load package resources
     */
    protected function loadPackageResources()
    {
        // Load views
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'invoice-builder');

        // Load translations
        $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'invoice-builder');

        // Load routes (will be added in Chapter 6)
        // $this->loadRoutesFrom(__DIR__.'/../routes/web.php');

        // Load migrations (will be added in Chapter 7)  
        // $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
    }
}

৫.৮.৪ Asset Files Structure

Package এ asset files তৈরি করি:

CSS Asset:

css
/* packages/DevMaster/InvoiceBuilder/resources/assets/css/invoice-builder.css */
.invoice-builder {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

.invoice-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 30px;
    border-bottom: 2px solid #e2e8f0;
    padding-bottom: 20px;
}

.company-logo {
    max-height: 80px;
    max-width: 200px;
}

.invoice-number {
    font-size: 24px;
    font-weight: bold;
    color: #2d3748;
}

.invoice-table {
    width: 100%;
    border-collapse: collapse;
    margin: 20px 0;
}

.invoice-table th,
.invoice-table td {
    padding: 12px;
    text-align: left;
    border-bottom: 1px solid #e2e8f0;
}

.invoice-table th {
    background-color: #f7fafc;
    font-weight: 600;
}

.invoice-total {
    text-align: right;
    font-size: 18px;
    font-weight: bold;
    margin-top: 20px;
    color: #2d3748;
}

@media print {
    .invoice-builder {
        max-width: none;
        margin: 0;
        padding: 0;
    }
}

JavaScript Asset:

javascript
/* packages/DevMaster/InvoiceBuilder/resources/assets/js/invoice-builder.js */
class InvoiceBuilder {
    constructor(options = {}) {
        this.options = {
            autoCalculate: true,
            currency: 'USD',
            taxRate: 0,
            ...options
        };
        
        this.init();
    }

    init() {
        this.bindEvents();
        if (this.options.autoCalculate) {
            this.calculateTotals();
        }
    }

    bindEvents() {
        // Auto-calculate when quantities or prices change
        document.addEventListener('change', (e) => {
            if (e.target.matches('.item-quantity, .item-price')) {
                this.calculateTotals();
            }
        });
    }

    calculateTotals() {
        let subtotal = 0;
        const items = document.querySelectorAll('.invoice-item');
        
        items.forEach(item => {
            const quantity = parseFloat(item.querySelector('.item-quantity')?.value || 0);
            const price = parseFloat(item.querySelector('.item-price')?.value || 0);
            const total = quantity * price;
            
            const totalElement = item.querySelector('.item-total');
            if (totalElement) {
                totalElement.textContent = this.formatCurrency(total);
            }
            
            subtotal += total;
        });

        this.updateTotals(subtotal);
    }

    updateTotals(subtotal) {
        const taxAmount = subtotal * this.options.taxRate;
        const total = subtotal + taxAmount;

        this.updateElement('.invoice-subtotal', subtotal);
        this.updateElement('.invoice-tax', taxAmount);
        this.updateElement('.invoice-total', total);
    }

    updateElement(selector, amount) {
        const element = document.querySelector(selector);
        if (element) {
            element.textContent = this.formatCurrency(amount);
        }
    }

    formatCurrency(amount) {
        return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: this.options.currency
        }).format(amount);
    }
}

// Auto-initialize if config is available
if (window.InvoiceBuilderConfig) {
    window.invoiceBuilder = new InvoiceBuilder(window.InvoiceBuilderConfig);
}

৫.৮.৫ View Templates

Basic view templates তৈরি করি:

php
{{-- packages/DevMaster/InvoiceBuilder/resources/views/templates/default.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Invoice #{{ $invoice['invoice_number'] }}</title>
    <link rel="stylesheet" href="{{ asset('vendor/invoice-builder/css/invoice-builder.css') }}">
</head>
<body>
    <div class="invoice-builder">
        {{-- Header --}}
        <div class="invoice-header">
            <div class="company-info">
                @if(!empty($invoice['company']['logo']))
                    <img src="{{ $invoice['company']['logo'] }}" alt="Company Logo" class="company-logo">
                @endif
                <h1>{{ $invoice['company']['name'] ?? 'Your Company' }}</h1>
                <p>{{ $invoice['company']['address'] ?? '' }}</p>
                <p>{{ $invoice['company']['email'] ?? '' }}</p>
                <p>{{ $invoice['company']['phone'] ?? '' }}</p>
            </div>
            
            <div class="invoice-info">
                <div class="invoice-number">Invoice #{{ $invoice['invoice_number'] }}</div>
                <p><strong>Date:</strong> {{ $invoice['date'] }}</p>
                <p><strong>Due Date:</strong> {{ $invoice['due_date'] }}</p>
            </div>
        </div>

        {{-- Customer Info --}}
        <div class="customer-info">
            <h3>Bill To:</h3>
            <p><strong>{{ $invoice['customer']['name'] ?? 'N/A' }}</strong></p>
            <p>{{ $invoice['customer']['email'] ?? '' }}</p>
            <p>{{ $invoice['customer']['address'] ?? '' }}</p>
        </div>

        {{-- Items Table --}}
        <table class="invoice-table">
            <thead>
                <tr>
                    <th>Description</th>
                    <th>Quantity</th>
                    <th>Price</th>
                    <th>Total</th>
                </tr>
            </thead>
            <tbody>
                @foreach($invoice['items'] as $item)
                <tr class="invoice-item">
                    <td>{{ $item['description'] }}</td>
                    <td class="item-quantity">{{ $item['quantity'] }}</td>
                    <td class="item-price">{{ number_format($item['price'], 2) }}</td>
                    <td class="item-total">{{ number_format($item['total'], 2) }}</td>
                </tr>
                @endforeach
            </tbody>
        </table>

        {{-- Totals --}}
        <div class="invoice-totals">
            <p><strong>Subtotal:</strong> <span class="invoice-subtotal">{{ number_format($invoice['subtotal'], 2) }}</span></p>
            <p><strong>Tax:</strong> <span class="invoice-tax">{{ number_format($invoice['tax_amount'], 2) }}</span></p>
            <p class="invoice-total"><strong>Total:</strong> {{ number_format($invoice['total'], 2) }}</p>
        </div>

        {{-- Notes --}}
        @if(!empty($invoice['notes']))
        <div class="invoice-notes">
            <h3>Notes:</h3>
            <p>{{ $invoice['notes'] }}</p>
        </div>
        @endif
    </div>

    <script src="{{ asset('vendor/invoice-builder/js/invoice-builder.js') }}"></script>
</body>
</html>

৫.৮.৬ Language Files

php
// packages/DevMaster/InvoiceBuilder/resources/lang/en/messages.php
<?php

return [
    'invoice_number' => 'Invoice Number',
    'date' => 'Date',
    'due_date' => 'Due Date',
    'bill_to' => 'Bill To',
    'description' => 'Description',
    'quantity' => 'Quantity',
    'price' => 'Price',
    'total' => 'Total',
    'subtotal' => 'Subtotal',
    'tax' => 'Tax',
    'notes' => 'Notes',
    'thank_you' => 'Thank you for your business!',
];
php
// packages/DevMaster/InvoiceBuilder/resources/lang/bn/messages.php
<?php

return [
    'invoice_number' => 'ইনভয়েস নম্বর',
    'date' => 'তারিখ',
    'due_date' => 'মেয়াদ',
    'bill_to' => 'বিল প্রাপক',
    'description' => 'বিবরণ',
    'quantity' => 'পরিমাণ',
    'price' => 'মূল্য',
    'total' => 'মোট',
    'subtotal' => 'উপমোট',
    'tax' => 'কর',
    'notes' => 'টিকা',
    'thank_you' => 'আপনার ব্যবসার জন্য ধন্যবাদ!',
];

৫.৮.৭ Testing Configuration & Assets

Installation and Publishing:

bash
# Install/update package
composer require devmaster/invoice-builder:@dev

# Publish all assets
php artisan vendor:publish --provider="DevMaster\InvoiceBuilder\InvoiceBuilderServiceProvider"

# Or publish specific tags
php artisan vendor:publish --tag=invoice-builder-config
php artisan vendor:publish --tag=invoice-builder-views  
php artisan vendor:publish --tag=invoice-builder-assets

Configuration Testing:

php
php artisan tinker

>>> // Test ConfigManager
>>> $config = app(DevMaster\InvoiceBuilder\Services\ConfigManager::class);
>>> $config->get('currency'); // Should return 'USD'
>>> $config->getCurrencySettings(); 
>>> $config->getTaxRate('BD'); // Should return 0.15
>>> $config->getDriverConfig(); // Default driver config

>>> // Test runtime config changes
>>> $config->set('currency', 'BDT');
>>> config('invoice-builder.currency'); // Should return 'BDT'

>>> // Test enhanced services with new config
>>> $taxCalc = app(DevMaster\InvoiceBuilder\Services\TaxCalculator::class);
>>> $taxCalc->getEffectiveRate('BD');

>>> $invoice = app(DevMaster\InvoiceBuilder\Services\InvoiceService::class);
>>> $invoice->create()->addItem('Test', 100)->getData();

Asset Verification:

bash
# Check published assets
ls -la public/vendor/invoice-builder/
ls -la resources/views/vendor/invoice-builder/
ls -la config/invoice-builder.php

# Check view loading
php artisan tinker
>>> view('invoice-builder::templates.default', ['invoice' => ['invoice_number' => 'TEST-001']]);

৫.৯ Chapter 5 Checkpoint

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

  • Advanced Configuration System - Multi-driver, environment-based config
  • Configuration Manager Service - Centralized config access with helpers
  • Asset Publishing - CSS, JS, views, migrations, language files
  • Template System - Customizable invoice templates
  • Multi-language Support - Language files with publishing
  • Runtime Configuration - Dynamic config changes
  • Environment Integration - Full .env variable support

Test Commands Summary:

bash
# Configuration publishing
php artisan vendor:publish --tag=invoice-builder-config

# Asset publishing
php artisan vendor:publish --tag=invoice-builder-assets

# Configuration testing
php artisan tinker
>>> app('invoice-builder.config')->getTaxRate('BD')

# View rendering
>>> view('invoice-builder::templates.default', $data)

Next Chapter: Chapter 6 এ আমরা Routes & Controllers implementation করব যাতে web interface পাই।

Key Concepts Mastered:

  • Advanced configuration architecture
  • Asset publishing strategies
  • Runtime configuration management
  • Template and view systems
  • Multi-language support

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