Skip to content

অধ্যায় ৬: রাউটিং সিস্টেম এবং কন্ট্রোলার আর্কিটেকচার

৬.১ প্যাকেজ রাউটিং

লারাভেল অ্যাপ্লিকেশনে আমরা সাধারণত routes/web.php ফাইলে রাউট লিখি এবং RouteServiceProvider সেটি লোড করে। কিন্তু প্যাকেজের ক্ষেত্রে আমাদের নিজস্ব কোনো RouteServiceProvider ডিফল্টভাবে থাকে না (যদি না আমরা তৈরি করি)। আমাদের মূল ServiceProvider থেকেই রাউট লোড করতে হয়।

৬.১.১ রাউট লোডিং মেকানিজম

প্যাকেজের রাউট লোড করার জন্য আমরা loadRoutesFrom মেথড ব্যবহার করব, যা লারাভেলের Illuminate\Support\ServiceProvider ক্লাসে ডিফাইন করা আছে।

php
// InvoiceLiteServiceProvider.php

public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}

ইন্টারনাল মেকানিজম (কিভাবে কাজ করে?):

যখন loadRoutesFrom কল করা হয়, লারাভেল মূলত নিচের কাজগুলো করে:

  1. চেক করে অ্যাপ্লিকেশন ক্যাশড রাউট ব্যবহার করছে কি না
  2. যদি রাউট ক্যাশ করা থাকে, তবে এই মেথডটি Skip করা হয় (কারণ ক্যাশ ফাইলে আপনার রাউট আগেই কম্পাইল হয়ে গেছে)
  3. যদি ক্যাশ না থাকে, তবে ফাইলটি require করা হয় এবং লারাভেলের গ্লোবাল RouteCollection-এ আপনার রাউটগুলো যুক্ত (Append) হয়ে যায়

আর্কিটেক্ট নোট: রাউট ক্যাশিং এর কারণে, প্রোডাকশনে ডিপ্লয় করার পর অবশ্যই php artisan route:clear বা route:cache চালাতে হবে, নইলে আপনার প্যাকেজের নতুন রাউটগুলো লোড না ও হতে পারে।

৬.২ রাউট ফাইল ডিজাইন এবং বেস্ট প্র্যাকটিস

প্যাকেজের রাউট ফাইলে সরাসরি Route::get লেখা উচিত নয়। কারণ এতে মিডলওয়্যার বা প্রিফিক্স কন্ট্রোল করা কঠিন হয়ে পড়ে। আমরা রাউট গ্রুপ ব্যবহার করব।

ফাইল: InvoiceLite/routes/web.php

php
use Illuminate\Support\Facades\Route;
use DevMaster\InvoiceLite\Http\Controllers\InvoiceController;

Route::group([
    'prefix' => config('invoicelite.route_prefix', 'invoices'),
    'middleware' => config('invoicelite.middleware', ['web', 'auth']),
    'as' => 'invoicelite.', // Route Name Prefix
], function () {
    Route::get('/', [InvoiceController::class, 'index'])->name('index');
    Route::get('/create', [InvoiceController::class, 'create'])->name('create');
    Route::post('/', [InvoiceController::class, 'store'])->name('store');
    Route::get('/{id}', [InvoiceController::class, 'show'])->name('show');
    Route::get('/{id}/download', [InvoiceController::class, 'download'])->name('download');

});

৬.২.১ কনফিগারেশন-ড্রিভেন রাউটিং (Configuration-Driven Routing)

ওপরের কোডে লক্ষ্য করুন, আমরা হার্ডকোডেড প্রিফিক্স ('invoices') বা মিডলওয়্যার (['web']) ব্যবহার করিনি। আমরা config() হেল্পার ব্যবহার করেছি।

কেন? ধরুন, ইউজার চায় আপনার প্যাকেজের ড্যাশবোর্ডটি /admin/invoices ইউআরএলে চলুক এবং শুধুমাত্র admin গার্ড দিয়ে প্রোটেক্টেড থাকুক। কনফিগারেশন ফাইলে সে পরিবর্তন করবে:

php
'route_prefix' => 'admin/invoices',
'middleware' => ['web', 'auth', 'admin'],

এতে প্যাকেজের সোর্স কোডে হাত না দিয়েই ইউজার সম্পূর্ণ রাউটিং বিহেভিয়ার পরিবর্তন করতে পারবে।

৬.৩ কন্ট্রোলার আর্কিটেকচার

প্যাকেজ কন্ট্রোলারগুলো সাধারণ লারাভেল কন্ট্রোলারের মতোই, তবে নেমস্পেস ম্যানেজমেন্টে সতর্ক থাকতে হয়।

আমরা কন্ট্রোলারগুলোকে src/Http/Controllers ফোল্ডারে রাখব।

৬.৩.১ বেস কন্ট্রোলার স্ট্র্যাটেজি

সরাসরি লারাভেলের App\Http\Controllers\Controller কে এক্সটেন্ড করা যাবে না, কারণ ইউজারের অ্যাপে সেই ক্লাসটি নাও থাকতে পারে বা ভিন্ন লোকেশনে থাকতে পারে।

আমরা লারাভেলের কোর কন্ট্রোলার Illuminate\Routing\Controller কে এক্সটেন্ড করব।

ফাইল: src/Http/Controllers/BaseController.php

php
namespace DevMaster\InvoiceLite\Http\Controllers;

use Illuminate\Routing\Controller;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

class BaseController extends Controller
{
    // লারাভেলের কমন ট্রেইটগুলো ব্যবহার করা হলো
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

৬.৩.২ মেইন কন্ট্রোলার ইমপ্লিমেন্টেশন

ফাইল: src/Http/Controllers/InvoiceController.php

php
namespace DevMaster\InvoiceLite\Http\Controllers;

use Illuminate\Http\Request;
use DevMaster\InvoiceLite\Facades\InvoiceLite;

class InvoiceController extends BaseController
{
    public function index()
    {
        // প্যাকেজের ভিউ রিটার্ন করা হচ্ছে
        return view('invoicelite::index', [
            'invoices' => InvoiceLite::all()
        ]);
    }

    public function store(Request $request)
    {
        // ভ্যালিডেশন লজিক
        $validated = $request->validate([
            'client_name' => 'required|string|max:255',
            'amount'      => 'required|numeric|min:0',
        ]);

        $invoice = InvoiceLite::create($validated);

        // রাউট নেম প্রিফিক্স ব্যবহার করে রিডাইরেক্ট
        return redirect()->route('invoicelite.show', $invoice->id)
                         ->with('success', 'Invoice generated successfully!');
    }
}

৬.৪ ভিউ নেমস্পেস রেজল্যুশন (View Namespace Resolution)

কন্ট্রোলারে আমরা view('invoicelite::index') ব্যবহার করেছি। এই :: সিনট্যাক্সটি লারাভেলকে বলে দেয় যে এটি একটি (Named View Space)

কিভাবে কাজ করে?

সার্ভিস প্রভাইডারে আমরা লিখেছিলাম:

php
$this->loadViewsFrom(__DIR__.'/../resources/views', 'invoicelite');

যখন View::make('invoicelite::index') কল হয়:

  1. লারাভেল ViewFinder প্রথমে চেক করে: resources/views/vendor/invoicelite/index.blade.php (ইউজারের ওভাররাইড করা ফাইল)
  2. যদি না পায়, তবে সে প্যাকেজের ডিরেক্টরিতে যায়: packages/DevMaster/InvoiceLite/resources/views/index.blade.php

এই "User First, Package Second" পলিসিটি লারাভেল ফ্রেমওয়ার্কের মূল দর্শন, যা ইউজারকে সর্বোচ্চ কাস্টমাইজেশনের সুযোগ দেয়।

🔨 ৬.৫ Practical Implementation: Web Interface for InvoiceBuilder

এখন আমরা Chapter 3 ও 5 এর service container এবং configuration system এর উপর ভিত্তি করে complete web interface তৈরি করব।

৬.৫.১ Enhanced Configuration for Routes

প্রথমে Chapter 5 এর configuration এ routing options যোগ করি:

php
// packages/DevMaster/InvoiceBuilder/config/invoice-builder.php এ যোগ করুন

/*
|--------------------------------------------------------------------------
| Route Configuration
|--------------------------------------------------------------------------
*/
'routes' => [
    'enabled' => env('INVOICE_ROUTES_ENABLED', true),
    'prefix' => env('INVOICE_ROUTES_PREFIX', 'invoices'),
    'name_prefix' => env('INVOICE_ROUTES_NAME_PREFIX', 'invoice-builder'),
    'middleware' => [
        'web' => env('INVOICE_MIDDLEWARE_WEB', 'web'),
        'auth' => env('INVOICE_MIDDLEWARE_AUTH', 'auth'),
        'api' => env('INVOICE_MIDDLEWARE_API', 'api'),
    ],
    'domain' => env('INVOICE_ROUTES_DOMAIN', null),
],

/*
|--------------------------------------------------------------------------
| View Configuration
|--------------------------------------------------------------------------
*/
'views' => [
    'theme' => env('INVOICE_VIEW_THEME', 'default'),
    'layout' => env('INVOICE_VIEW_LAYOUT', 'invoice-builder::layouts.app'),
    'pagination' => env('INVOICE_VIEW_PAGINATION', 15),
],

৬.৫.২ Base Controller with Dependency Injection

php
// packages/DevMaster/InvoiceBuilder/src/Http/Controllers/BaseController.php
<?php

namespace DevMaster\InvoiceBuilder\Http\Controllers;

use Illuminate\Routing\Controller;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use DevMaster\InvoiceBuilder\Services\InvoiceService;
use DevMaster\InvoiceBuilder\Services\ConfigManager;

class BaseController extends Controller
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

    protected InvoiceService $invoiceService;
    protected ConfigManager $config;

    public function __construct(InvoiceService $invoiceService, ConfigManager $config)
    {
        $this->invoiceService = $invoiceService;
        $this->config = $config;
        
        // Apply middleware from configuration
        $this->middleware($this->config->get('routes.middleware.web', 'web'));
    }

    /**
     * Get view with package theme
     */
    protected function view(string $view, array $data = [])
    {
        $theme = $this->config->get('views.theme', 'default');
        $layoutData = [
            'config' => $this->config,
            'company' => $this->config->getCompanyInfo(),
            'currency' => $this->config->getCurrencySettings(),
        ];

        return view("invoice-builder::themes.{$theme}.{$view}", array_merge($layoutData, $data));
    }

    /**
     * JSON response helper
     */
    protected function jsonResponse($data, $message = null, $status = 200)
    {
        return response()->json([
            'success' => $status >= 200 && $status < 300,
            'message' => $message,
            'data' => $data,
        ], $status);
    }

    /**
     * Error response helper
     */
    protected function errorResponse($message, $status = 400, $errors = null)
    {
        return response()->json([
            'success' => false,
            'message' => $message,
            'errors' => $errors,
        ], $status);
    }
}

৬.৫.৩ Invoice Controller with Full CRUD

php
// packages/DevMaster/InvoiceBuilder/src/Http/Controllers/InvoiceController.php
<?php

namespace DevMaster\InvoiceBuilder\Http\Controllers;

use Illuminate\Http\Request;
use DevMaster\InvoiceBuilder\Http\Requests\InvoiceStoreRequest;
use DevMaster\InvoiceBuilder\Http\Requests\InvoiceUpdateRequest;
use DevMaster\InvoiceBuilder\Services\PdfGenerator;

class InvoiceController extends BaseController
{
    protected PdfGenerator $pdfGenerator;

    public function __construct(
        \DevMaster\InvoiceBuilder\Services\InvoiceService $invoiceService,
        \DevMaster\InvoiceBuilder\Services\ConfigManager $config,
        PdfGenerator $pdfGenerator
    ) {
        parent::__construct($invoiceService, $config);
        $this->pdfGenerator = $pdfGenerator;
    }

    /**
     * Display a listing of invoices
     */
    public function index(Request $request)
    {
        $perPage = $this->config->get('views.pagination', 15);
        $search = $request->get('search');
        $status = $request->get('status');

        // In real app, this would query database
        $invoices = collect([
            [
                'id' => 1,
                'invoice_number' => 'INV-000001',
                'customer_name' => 'John Doe',
                'total' => 150.00,
                'status' => 'paid',
                'created_at' => now()->subDays(5),
            ],
            [
                'id' => 2,
                'invoice_number' => 'INV-000002', 
                'customer_name' => 'Jane Smith',
                'total' => 250.00,
                'status' => 'pending',
                'created_at' => now()->subDays(2),
            ]
        ]);

        // Apply filters
        if ($search) {
            $invoices = $invoices->filter(function ($invoice) use ($search) {
                return str_contains($invoice['customer_name'], $search) || 
                       str_contains($invoice['invoice_number'], $search);
            });
        }

        if ($status) {
            $invoices = $invoices->where('status', $status);
        }

        return $this->view('invoices.index', [
            'invoices' => $invoices,
            'search' => $search,
            'status' => $status,
            'statuses' => ['pending', 'paid', 'overdue', 'cancelled'],
        ]);
    }

    /**
     * Show the form for creating a new invoice
     */
    public function create()
    {
        $invoice = $this->invoiceService->create();
        
        return $this->view('invoices.create', [
            'invoice' => $invoice->getData(),
            'templates' => $this->config->get('templates.available', []),
        ]);
    }

    /**
     * Store a newly created invoice
     */
    public function store(InvoiceStoreRequest $request)
    {
        try {
            $invoice = $this->invoiceService->create()
                ->customer($request->get('customer'))
                ->notes($request->get('notes', ''));

            // Add items
            foreach ($request->get('items', []) as $item) {
                $invoice->addItem(
                    $item['description'],
                    (float) $item['price'],
                    (int) $item['quantity']
                );
            }

            $invoiceData = $invoice->getData();

            // In real app, save to database here
            // $savedInvoice = Invoice::create($invoiceData);

            if ($request->expectsJson()) {
                return $this->jsonResponse($invoiceData, 'Invoice created successfully!');
            }

            return redirect()
                ->route($this->config->get('routes.name_prefix') . '.show', ['id' => 1])
                ->with('success', 'Invoice created successfully!');

        } catch (\Exception $e) {
            if ($request->expectsJson()) {
                return $this->errorResponse('Failed to create invoice: ' . $e->getMessage());
            }

            return back()
                ->withInput()
                ->withErrors(['error' => 'Failed to create invoice: ' . $e->getMessage()]);
        }
    }

    /**
     * Display the specified invoice
     */
    public function show(Request $request, $id)
    {
        // In real app, fetch from database
        $invoice = [
            'id' => $id,
            'invoice_number' => 'INV-' . str_pad($id, 6, '0', STR_PAD_LEFT),
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
            'company' => $this->config->getCompanyInfo(),
            'customer' => [
                'name' => 'John Doe',
                'email' => 'john@example.com',
                'address' => '123 Main St, City, Country',
            ],
            'items' => [
                [
                    'description' => 'Web Development Service',
                    'quantity' => 1,
                    'price' => 1000.00,
                    'total' => 1000.00,
                ],
                [
                    'description' => 'Domain Registration',
                    'quantity' => 1,
                    'price' => 15.00,
                    'total' => 15.00,
                ],
            ],
            'subtotal' => 1015.00,
            'tax_amount' => 152.25,
            'total' => 1167.25,
            'notes' => 'Thank you for your business!',
            'status' => 'pending',
        ];

        if ($request->expectsJson()) {
            return $this->jsonResponse($invoice);
        }

        return $this->view('invoices.show', [
            'invoice' => $invoice,
        ]);
    }

    /**
     * Show the form for editing the specified invoice
     */
    public function edit($id)
    {
        // In real app, fetch from database
        $invoice = [
            'id' => $id,
            'invoice_number' => 'INV-' . str_pad($id, 6, '0', STR_PAD_LEFT),
            'customer' => [
                'name' => 'John Doe',
                'email' => 'john@example.com',
            ],
            'items' => [
                [
                    'description' => 'Web Development Service',
                    'quantity' => 1,
                    'price' => 1000.00,
                ],
            ],
            'notes' => 'Thank you for your business!',
        ];

        return $this->view('invoices.edit', [
            'invoice' => $invoice,
        ]);
    }

    /**
     * Update the specified invoice
     */
    public function update(InvoiceUpdateRequest $request, $id)
    {
        try {
            // In real app, update database record
            $invoice = $request->validated();
            $invoice['id'] = $id;

            if ($request->expectsJson()) {
                return $this->jsonResponse($invoice, 'Invoice updated successfully!');
            }

            return redirect()
                ->route($this->config->get('routes.name_prefix') . '.show', ['id' => $id])
                ->with('success', 'Invoice updated successfully!');

        } catch (\Exception $e) {
            if ($request->expectsJson()) {
                return $this->errorResponse('Failed to update invoice: ' . $e->getMessage());
            }

            return back()
                ->withInput()
                ->withErrors(['error' => 'Failed to update invoice: ' . $e->getMessage()]);
        }
    }

    /**
     * Remove the specified invoice
     */
    public function destroy(Request $request, $id)
    {
        try {
            // In real app, delete from database
            // Invoice::findOrFail($id)->delete();

            if ($request->expectsJson()) {
                return $this->jsonResponse(null, 'Invoice deleted successfully!');
            }

            return redirect()
                ->route($this->config->get('routes.name_prefix') . '.index')
                ->with('success', 'Invoice deleted successfully!');

        } catch (\Exception $e) {
            if ($request->expectsJson()) {
                return $this->errorResponse('Failed to delete invoice: ' . $e->getMessage());
            }

            return back()->withErrors(['error' => 'Failed to delete invoice: ' . $e->getMessage()]);
        }
    }

    /**
     * Download invoice as PDF
     */
    public function download($id)
    {
        try {
            // Get invoice data (same as show method)
            $invoice = [
                'id' => $id,
                'invoice_number' => 'INV-' . str_pad($id, 6, '0', STR_PAD_LEFT),
                'date' => now()->format('Y-m-d'),
                'company' => $this->config->getCompanyInfo(),
                // ... other data
            ];

            $pdf = $this->pdfGenerator->generate($invoice);

            return response($pdf)
                ->header('Content-Type', 'application/pdf')
                ->header('Content-Disposition', 'attachment; filename="invoice-' . $invoice['invoice_number'] . '.pdf"');

        } catch (\Exception $e) {
            return back()->withErrors(['error' => 'Failed to generate PDF: ' . $e->getMessage()]);
        }
    }
}

৬.৫.৪ Form Request Classes

php
// packages/DevMaster/InvoiceBuilder/src/Http/Requests/InvoiceStoreRequest.php
<?php

namespace DevMaster\InvoiceBuilder\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class InvoiceStoreRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true; // In real app, check permissions
    }

    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'customer.name' => 'required|string|max:255',
            'customer.email' => 'required|email|max:255',
            'customer.address' => 'nullable|string|max:500',
            'items' => 'required|array|min:1',
            'items.*.description' => 'required|string|max:255',
            'items.*.quantity' => 'required|integer|min:1',
            'items.*.price' => 'required|numeric|min:0',
            'notes' => 'nullable|string|max:1000',
            'template' => 'nullable|string|in:default,modern,classic,minimal',
        ];
    }

    /**
     * Get custom messages for validator errors.
     */
    public function messages(): array
    {
        return [
            'customer.name.required' => 'Customer name is required',
            'customer.email.required' => 'Customer email is required',
            'customer.email.email' => 'Please provide a valid email address',
            'items.required' => 'At least one item is required',
            'items.*.description.required' => 'Item description is required',
            'items.*.quantity.required' => 'Item quantity is required',
            'items.*.quantity.min' => 'Quantity must be at least 1',
            'items.*.price.required' => 'Item price is required',
            'items.*.price.min' => 'Price cannot be negative',
        ];
    }
}
php
// packages/DevMaster/InvoiceBuilder/src/Http/Requests/InvoiceUpdateRequest.php
<?php

namespace DevMaster\InvoiceBuilder\Http\Requests;

class InvoiceUpdateRequest extends InvoiceStoreRequest
{
    // Inherits all rules from InvoiceStoreRequest
    // Can override specific rules if needed

    public function rules(): array
    {
        $rules = parent::rules();
        
        // For updates, we might want to make some fields optional
        $rules['customer.name'] = 'sometimes|required|string|max:255';
        $rules['customer.email'] = 'sometimes|required|email|max:255';
        
        return $rules;
    }
}

৬.৫.৫ PDF Generator Service

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

namespace DevMaster\InvoiceBuilder\Services;

use Mpdf\Mpdf;

class PdfGenerator
{
    protected ConfigManager $config;

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

    /**
     * Generate PDF from invoice data
     */
    public function generate(array $invoice): string
    {
        $pdfSettings = $this->config->getPdfSettings();
        
        // Initialize mPDF with configuration
        $mpdf = new Mpdf([
            'format' => $pdfSettings['format'] ?? 'A4',
            'orientation' => $pdfSettings['orientation'] ?? 'P',
            'margin_top' => $pdfSettings['margin']['top'] ?? 15,
            'margin_bottom' => $pdfSettings['margin']['bottom'] ?? 15,
            'margin_left' => $pdfSettings['margin']['left'] ?? 15,
            'margin_right' => $pdfSettings['margin']['right'] ?? 15,
            'default_font' => $pdfSettings['font']['default'] ?? 'dejavu-sans',
            'default_font_size' => $pdfSettings['font']['size'] ?? 10,
        ]);

        // Add watermark if enabled
        if ($this->config->isEnabled('watermark')) {
            $mpdf->SetWatermarkText(
                $pdfSettings['watermark']['text'] ?? 'DRAFT',
                $pdfSettings['watermark']['opacity'] ?? 0.1
            );
            $mpdf->showWatermarkText = true;
        }

        // Generate HTML content
        $html = $this->generateHtml($invoice);
        
        // Write HTML to PDF
        $mpdf->WriteHTML($html);

        // Return PDF content as string
        return $mpdf->Output('', 'S');
    }

    /**
     * Generate HTML content for PDF
     */
    protected function generateHtml(array $invoice): string
    {
        $template = $this->config->get('templates.default', 'default');
        
        return view("invoice-builder::pdf.{$template}", [
            'invoice' => $invoice,
            'config' => $this->config,
        ])->render();
    }
}

৬.৫.৬ Routes Definition

php
// packages/DevMaster/InvoiceBuilder/routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use DevMaster\InvoiceBuilder\Http\Controllers\InvoiceController;

// Only register routes if enabled
if (config('invoice-builder.routes.enabled', true)) {
    $prefix = config('invoice-builder.routes.prefix', 'invoices');
    $namePrefix = config('invoice-builder.routes.name_prefix', 'invoice-builder');
    $middleware = config('invoice-builder.routes.middleware.web', 'web');
    $domain = config('invoice-builder.routes.domain');

    $routeGroup = [
        'prefix' => $prefix,
        'as' => $namePrefix . '.',
        'middleware' => $middleware,
    ];

    if ($domain) {
        $routeGroup['domain'] = $domain;
    }

    Route::group($routeGroup, function () {
        // Invoice CRUD routes
        Route::get('/', [InvoiceController::class, 'index'])->name('index');
        Route::get('/create', [InvoiceController::class, 'create'])->name('create');
        Route::post('/', [InvoiceController::class, 'store'])->name('store');
        Route::get('/{id}', [InvoiceController::class, 'show'])->name('show');
        Route::get('/{id}/edit', [InvoiceController::class, 'edit'])->name('edit');
        Route::put('/{id}', [InvoiceController::class, 'update'])->name('update');
        Route::delete('/{id}', [InvoiceController::class, 'destroy'])->name('destroy');
        
        // Additional routes
        Route::get('/{id}/download', [InvoiceController::class, 'download'])->name('download');
        Route::post('/{id}/send-email', [InvoiceController::class, 'sendEmail'])->name('send-email');
    });
}

// API routes (separate file could be created)
if (config('invoice-builder.api.enabled', false)) {
    Route::group([
        'prefix' => 'api/' . config('invoice-builder.routes.prefix', 'invoices'),
        'as' => config('invoice-builder.routes.name_prefix', 'invoice-builder') . '.api.',
        'middleware' => config('invoice-builder.routes.middleware.api', 'api'),
    ], function () {
        Route::apiResource('invoices', InvoiceController::class);
    });
}

৬.৫.৭ Enhanced Service Provider with Route Loading

php
// packages/DevMaster/InvoiceBuilder/src/InvoiceBuilderServiceProvider.php এ boot() method update করুন

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

    // Load package resources
    $this->loadPackageResources();
    
    // Register additional services
    $this->registerAdditionalServices();
}

/**
 * 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
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');

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

/**
 * Register additional services for web functionality
 */
protected function registerAdditionalServices()
{
    // Register PDF Generator
    $this->app->singleton(
        \DevMaster\InvoiceBuilder\Services\PdfGenerator::class,
        function ($app) {
            return new \DevMaster\InvoiceBuilder\Services\PdfGenerator(
                $app->make(\DevMaster\InvoiceBuilder\Services\ConfigManager::class)
            );
        }
    );
}

৬.৫.৮ Basic View Templates

php
{{-- packages/DevMaster/InvoiceBuilder/resources/views/themes/default/invoices/index.blade.php --}}
@extends(config('invoice-builder.views.layout'))

@section('title', 'Invoices')

@section('content')
<div class="container">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1>Invoices</h1>
        <a href="{{ route(config('invoice-builder.routes.name_prefix') . '.create') }}" class="btn btn-primary">
            Create New Invoice
        </a>
    </div>

    {{-- Search and Filter --}}
    <div class="row mb-3">
        <div class="col-md-6">
            <form method="GET">
                <div class="input-group">
                    <input type="text" name="search" class="form-control" placeholder="Search invoices..." value="{{ $search }}">
                    <button class="btn btn-outline-secondary" type="submit">Search</button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form method="GET">
                <select name="status" class="form-select" onchange="this.form.submit()">
                    <option value="">All Statuses</option>
                    @foreach($statuses as $statusOption)
                        <option value="{{ $statusOption }}" {{ $status === $statusOption ? 'selected' : '' }}>
                            {{ ucfirst($statusOption) }}
                        </option>
                    @endforeach
                </select>
                <input type="hidden" name="search" value="{{ $search }}">
            </form>
        </div>
    </div>

    {{-- Invoices Table --}}
    <div class="table-responsive">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Invoice #</th>
                    <th>Customer</th>
                    <th>Amount</th>
                    <th>Status</th>
                    <th>Created</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                @forelse($invoices as $invoice)
                <tr>
                    <td>{{ $invoice['invoice_number'] }}</td>
                    <td>{{ $invoice['customer_name'] }}</td>
                    <td>{{ $currency['symbol'] ?? '$' }}{{ number_format($invoice['total'], 2) }}</td>
                    <td>
                        <span class="badge bg-{{ $invoice['status'] === 'paid' ? 'success' : ($invoice['status'] === 'pending' ? 'warning' : 'danger') }}">
                            {{ ucfirst($invoice['status']) }}
                        </span>
                    </td>
                    <td>{{ $invoice['created_at']->format('M j, Y') }}</td>
                    <td>
                        <div class="btn-group" role="group">
                            <a href="{{ route(config('invoice-builder.routes.name_prefix') . '.show', $invoice['id']) }}" class="btn btn-sm btn-outline-primary">View</a>
                            <a href="{{ route(config('invoice-builder.routes.name_prefix') . '.edit', $invoice['id']) }}" class="btn btn-sm btn-outline-secondary">Edit</a>
                            <a href="{{ route(config('invoice-builder.routes.name_prefix') . '.download', $invoice['id']) }}" class="btn btn-sm btn-outline-success">PDF</a>
                        </div>
                    </td>
                </tr>
                @empty
                <tr>
                    <td colspan="6" class="text-center">No invoices found</td>
                </tr>
                @endforelse
            </tbody>
        </table>
    </div>
</div>
@endsection

৬.৫.৯ Testing Routes and Controllers

Route Testing:

bash
# Check if routes are registered
php artisan route:list | grep invoice

# Test route configuration
php artisan tinker
>>> config('invoice-builder.routes')
>>> route('invoice-builder.index')
>>> route('invoice-builder.create')

Controller Testing:

bash
# Start development server
php artisan serve

# Visit routes
# GET /invoices - Invoice index
# GET /invoices/create - Create form
# GET /invoices/1 - Show specific invoice
# GET /invoices/1/download - Download PDF

Tinker Testing:

php
>>> // Test controller dependencies
>>> $controller = app(DevMaster\InvoiceBuilder\Http\Controllers\InvoiceController::class);
>>> 
>>> // Test service injection
>>> $request = new Illuminate\Http\Request();
>>> $response = $controller->index($request);
>>> $response->getStatusCode(); // Should be 200
>>>
>>> // Test PDF generation
>>> $pdfGen = app(DevMaster\InvoiceBuilder\Services\PdfGenerator::class);
>>> $pdf = $pdfGen->generate(['invoice_number' => 'TEST-001']);
>>> strlen($pdf) > 0; // Should be true

৬.১০ Chapter 6 Checkpoint

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

  • Complete Web Interface - Full CRUD operations for invoices
  • Route Configuration - Environment-driven route setup
  • Controller Architecture - Proper dependency injection with base controller
  • Form Validation - Dedicated request classes with validation rules
  • PDF Generation - Integrated mPDF with configuration
  • View Templates - Responsive Bootstrap templates
  • API Support - JSON responses for API endpoints
  • Configuration-Driven - Middleware, prefixes, themes all configurable

Test Commands Summary:

bash
# Route verification
php artisan route:list | grep invoice

# Web interface testing
php artisan serve
# Visit: /invoices

# Controller dependency testing
php artisan tinker
>>> app(DevMaster\InvoiceBuilder\Http\Controllers\InvoiceController::class)

# PDF generation testing
>>> app(DevMaster\InvoiceBuilder\Services\PdfGenerator::class)

Next Chapter: Chapter 7 এ আমরা Database Models, Migrations এবং persistent storage যোগ করব।

Key Concepts Mastered:

  • Configuration-driven routing
  • Dependency injection in controllers
  • Form validation with Request classes
  • PDF generation with mPDF
  • View namespacing and theme system
  • JSON API responses

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