+3

Series Thiết kế Kiến trúc Modules Thủ công | Bài 2: Quy hoạch "Vùng đất" Module và Chế tạo lệnh Sinh code tự động

Chào anh em! Ở Bài 1, chúng ta đã hiểu được "ma thuật" đằng sau kiến trúc module thủ công: chỉ cần cấu hình psr-4 trong composer.json, viết một file ServiceProvider và đăng ký nó vào config/app.php. Rất gọn gàng và không phụ thuộc bất kỳ package nào.

Nhưng vấn đề nảy sinh ở đây là: Mỗi lần có một module mới (ví dụ: Inventory, Customer, Report...), anh em lại phải hì hục click chuột phải "New Folder", "New File" cả chục lần để tạo ra cấu trúc chuẩn. Điều này rất nhàm chán và dễ dẫn đến sai sót (gõ sai tên thư mục, sai namespace).

Trong bài viết này, chúng ta sẽ thống nhất một cấu trúc chuẩn cho Module, sau đó tự tay viết một lệnh Artisan (Custom Command) giống hệt module:make để tự động hóa toàn bộ quá trình này!

1. Quy hoạch cấu trúc chuẩn của một Module

Dù là tự build, chúng ta vẫn nên bám sát cấu trúc nguyên thủy của Laravel để bất kỳ developer nào nhìn vào cũng hiểu ngay. Một Module tiêu chuẩn (ví dụ Module Order) đặt trong thư mục gốc modules/ sẽ có hình hài như sau:

modules/Order/
├── Http/
│   ├── Controllers/
│   ├── Middleware/
│   └── Requests/
├── Providers/
│   └── OrderServiceProvider.php  <-- (Trái tim của Module)
├── Routes/
│   ├── api.php
│   └── web.php
├── Database/
│   ├── Migrations/
│   └── Seeders/
└── Resources/
    ├── views/
    └── lang/

Cấu trúc này mô phỏng lại đúng thư mục app/ và database/ của Laravel, giúp phân tách rành mạch tầng Giao tiếp (Http), tầng Cấu hình (Providers) và tầng Dữ liệu (Database).

2. Chế tạo "Vũ khí" tự động hóa: Lệnh make:module

Thay vì tạo tay đống thư mục trên, chúng ta sẽ dạy cho Laravel cách tự làm việc đó. Bắt đầu bằng việc tạo một Artisan Command mới:

php artisan make:command MakeModuleCommand

Mở file app/Console/Commands/MakeModuleCommand.php lên và chúng ta bắt đầu "bơm" logic cho nó:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class MakeModuleCommand extends Command
{
    // Tên lệnh bạn sẽ gõ trong terminal
    protected $signature = 'make:module {name}';

    protected $description = 'Tạo mới một kiến trúc Module thủ công chuẩn';

    public function handle()
    {
        // Lấy tên module từ tham số truyền vào (vd: Order)
        $moduleName = ucfirst($this->argument('name'));
        $modulePath = base_path('modules/' . $moduleName);

        // Kiểm tra nếu Module đã tồn tại thì báo lỗi
        if (File::exists($modulePath)) {
            $this->error("Module {$moduleName} đã tồn tại!");
            return Command::FAILURE;
        }

        // 1. Tạo các thư mục cốt lõi
        $directories = [
            'Http/Controllers',
            'Providers',
            'Routes',
            'Database/Migrations',
            'Resources/views'
        ];

        foreach ($directories as $dir) {
            File::makeDirectory("{$modulePath}/{$dir}", 0755, true);
        }

        // 2. Sinh code cho file ServiceProvider
        $this->generateServiceProvider($moduleName, $modulePath);

        // 3. Sinh code cho file Routes
        $this->generateRoutes($modulePath);

        $this->info("Tuyệt vời! Module {$moduleName} đã được khởi tạo thành công tại: {$modulePath}");
        $this->line("Đừng quên thêm \Modules\\{$moduleName}\Providers\\{$moduleName}ServiceProvider::class vào config/app.php nhé!");
        
        return Command::SUCCESS;
    }

    protected function generateServiceProvider($moduleName, $modulePath)
    {
        $stub = <<<PHP
<?php

namespace Modules\\{$moduleName}\\Providers;

use Illuminate\Support\ServiceProvider;

class {$moduleName}ServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Nạp Routes
        \$this->loadRoutesFrom(__DIR__ . '/../Routes/api.php');
        
        // Nạp Views
        \$this->loadViewsFrom(__DIR__ . '/../Resources/views', strtolower('{$moduleName}'));
        
        // Nạp Migrations
        \$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
    }

    public function register()
    {
        // Đăng ký bindings tại đây
    }
}
PHP;
        File::put("{$modulePath}/Providers/{$moduleName}ServiceProvider.php", $stub);
    }

    protected function generateRoutes($modulePath)
    {
        $stub = <<<PHP
<?php

use Illuminate\Support\Facades\Route;

Route::prefix(strtolower(basename(dirname(__DIR__))))->group(function () {
    Route::get('/ping', function () {
        return response()->json(['message' => 'Module is working!']);
    });
});
PHP;
        File::put("{$modulePath}/Routes/api.php", $stub);
    }
}

3. Chạy thử và Tận hưởng

Bây giờ, hãy mở terminal lên và gõ:

php artisan make:module Payment

Chỉ mất chưa tới 1 giây, Laravel đã đẻ ra cho bạn một thư mục modules/Payment với cấu trúc hoàn chỉnh. File PaymentServiceProvider.php cũng đã được sinh ra với sẵn các đoạn code loadRoutesFrom, loadViewsFrom chuẩn xác. File Routes/api.php cũng đã có sẵn một route test.

Bước cuối cùng: Anh em chỉ cần mở config/app.php lên và chèn 1 dòng duy nhất vào mảng providers để kích hoạt:

Modules\Payment\Providers\PaymentServiceProvider::class,

Mở Postman lên và test GET /api/payment/ping, nếu nhận được chữ "Module is working!" thì xin chúc mừng, anh em đã thiết lập thành công!

Tổng kết Bài 2

Với việc tự viết một Artisan Command, anh em đã chứng minh được rằng: Không có package nào là không thể thay thế. Kiến trúc module thủ công kết hợp với các công cụ tự động hóa do chính tay mình viết không chỉ giúp project nhẹ nhàng, mà còn mang lại cảm giác làm chủ hoàn toàn hệ thống.

Ở Bài 3, chúng ta sẽ đi vào phần xương xẩu nhất trong các hệ thống Multi-Modules: Xử lý Dependency giữa các Module. Nếu Module Order cần lấy data từ Module Customer thì gọi như thế nào để không bị "dính chùm" code vào nhau? Hẹn anh em ở bài sau!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí