Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 25: Service Container và Facade trong Laravel 13

Từ Bài 5 chúng ta đã inject object vào controller và dùng rất nhiều facade như DB, Log, Storage mà chưa biết bên trong chúng chạy thế nào. Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về service container và facade trong Laravel 13.

1. Service container là gì?

Service container là nơi Laravel tạo và quản lý object của ứng dụng, đồng thời tự truyền các object phụ thuộc (dependency) vào cho chúng.

Khi tạo controller, container đọc kiểu của từng tham số constructor (bằng reflection), tự tạo object tương ứng rồi truyền vào. Class cụ thể (không phải interface) thì không cần khai báo gì, đó là lý do ProjectRepository ở Bài 5 inject được ngay. Lấy object thủ công thì dùng helper app(), facade App::make() hoặc $this->app->make() trong service provider:

$service = app(TaskStatsService::class);
$service = App::make(TaskStatsService::class);

2. Tách TaskStatsService.

Query thống kê của Bài 14 đang nằm trong TaskStatsController. Mình chuyển nó ra một class service để chỗ khác (API, command...) dùng lại được:

php artisan make:class Services/TaskStatsService
#    INFO  Class [app/Services/TaskStatsService.php] created successfully.

- app/Services/TaskStatsService.php

<?php

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class TaskStatsService
{
    // số task theo trạng thái của từng project thuộc $user (query của Bài 14)
    public function forUser(User $user): Collection
    {
        return DB::table('projects')
            ->leftJoin('tasks', fn ($join) => $join->on('tasks.project_id', '=', 'projects.id')->whereNull('tasks.deleted_at'))
            ->where('projects.user_id', $user->id)
            ->select('projects.id', 'projects.name')
            ->selectRaw('count(case when tasks.status = ? then 1 end) as todo', ['todo'])
            ->selectRaw('count(case when tasks.status = ? then 1 end) as doing', ['doing'])
            ->selectRaw('count(case when tasks.status = ? then 1 end) as done', ['done'])
            ->selectRaw('count(tasks.id) as total')
            ->groupBy('projects.id', 'projects.name')
            ->orderBy('projects.id')
            ->get();
    }
}

- app/Http/Controllers/TaskStatsController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use App\Services\TaskStatsService;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\View\View;

class TaskStatsController extends Controller
{
    public function __construct(
        protected TaskStatsService $stats,
    ) {}

    public function __invoke(#[CurrentUser] User $user): View
    {
        return view('stats', ['stats' => $this->stats->forUser($user)]);
    }
}

Trong đó:

  • protected TaskStatsService $stats: container tự tạo service khi tạo controller, không cần new.
  • #[CurrentUser]: một contextual attribute, container truyền vào user đang đăng nhập, thay cho $request->user().

Trang /stats vẫn hiển thị như cũ.

3. Binding trong service container.

Binding là khai báo cho container biết cách tạo một class hay interface. Có 4 cách hay dùng, thường viết trong method register() của service provider:

// mỗi lần resolve tạo một object mới
$this->app->bind(TaskStatsService::class, fn (Application $app) => new TaskStatsService);

// chỉ tạo một lần, các lần sau trả lại đúng object đó
$this->app->singleton(TaskStatsService::class);

// đưa vào một object có sẵn
$this->app->instance(TaskStatsService::class, $service);

// như singleton nhưng tạo lại cho mỗi request/job mới (Octane, queue worker)
$this->app->scoped(TaskStatsService::class);

Trong đó:

  • Tham số đầu là key, thường là tên class/interface. Tham số thứ hai là closure tạo object (nhận chính container $app, kiểu Illuminate\Contracts\Foundation\Application) hoặc tên class. Bỏ trống thì container tự tạo class ở key.
  • Với PHP-FPM mỗi request chạy lại từ đầu nên singleton và scoped như nhau; chúng chỉ khác nhau ở tiến trình sống lâu như queue worker (Bài 28) hay Octane.

Ngoài ra, gắn attribute #[Singleton] (hoặc #[Scoped]) trong Illuminate\Container\Attributes lên class cũng có tác dụng như singleton() mà không cần đăng ký.

4. Service provider.

Service provider là nơi khởi động ứng dụng: đăng ký binding, rate limiter, gate... Như Bài 2 đã nói, Laravel gọi register() của mọi provider trước rồi mới gọi boot():

  • register(): chỉ binding vào container, vì lúc này provider khác có thể chưa chạy.
  • boot(): mọi service đã sẵn sàng, dùng cho phần còn lại (RateLimiter::for(), Model::preventLazyLoading() trong AppServiceProvider).

Mình tạo provider riêng cho các service của task, lệnh này tự thêm class vào mảng trong bootstrap/providers.php:

php artisan make:provider TaskServiceProvider
#    INFO  Provider [app/Providers/TaskServiceProvider.php] created successfully.

- app/Providers/TaskServiceProvider.php (bỏ method boot() trống)

<?php

namespace App\Providers;

use App\Services\TaskStatsService;
use Illuminate\Support\ServiceProvider;

class TaskServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // cả ứng dụng dùng chung một object TaskStatsService
        $this->app->singleton(TaskStatsService::class);
    }
}

Chạy php artisan migrate:fresh --seed nếu ở bài trước các bạn đã tạo thêm task, rồi thử trong php artisan tinker:

$a = app(App\Services\TaskStatsService::class);
$a === app(App\Services\TaskStatsService::class); // true
$a->forUser(User::find(1))->pluck('total', 'name')->all();
// ["Website toidicode.com" => 4, "TaskFlow API" => 3]

app()->bind(App\Services\TaskStatsService::class); // đổi sang bind trong phiên tinker này
app(App\Services\TaskStatsService::class) === app(App\Services\TaskStatsService::class); // false

Lưu ý: trong tinker nên gõ tên đầy đủ App\Services\TaskStatsService. Tên ngắn là alias do tinker tạo (cần composer dump-autoload, Bài 15), khi đó TaskStatsService::class chỉ là chuỗi "TaskStatsService", không khớp key đã bind nên phép so sánh trên ra false.

5. Interface và contextual binding.

Container không tự tạo được interface: inject một interface chưa bind sẽ lỗi Target [...] is not instantiable. VD: giả sử có interface App\Contracts\TaskExporter với hai class CsvTaskExporter, JsonTaskExporter (chỉ minh họa, TaskFlow không giữ):

// trong register(): ai cần TaskExporter thì nhận CsvTaskExporter
$this->app->bind(TaskExporter::class, CsvTaskExporter::class);

// contextual binding: riêng ApiExportController nhận JsonTaskExporter
$this->app->when(ApiExportController::class)
    ->needs(TaskExporter::class)
    ->give(JsonTaskExporter::class);

Dòng bind() có thể thay bằng attribute #[Bind(CsvTaskExporter::class)] đặt ngay trên interface TaskExporter. Code chỉ phụ thuộc interface, muốn đổi cách xuất file thì sửa một dòng binding.

Với giá trị cấu hình, disk, guard... Laravel có sẵn các contextual attribute như #[CurrentUser] ở trên: Config, Storage, Auth, DB, Log, Cache... (namespace Illuminate\Container\Attributes):

public function __construct(
    #[Config('app.timezone')] protected string $timezone, // "UTC"
) {}

6. Facade hoạt động thế nào?

Facade là class cho gọi method kiểu static tới một object trong container. Class Illuminate\Support\Facades\DB không hề có method table(), nó chỉ khai báo key của binding:

class DB extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'db';
    }
}

Khi gọi DB::table('projects'), method magic __callStatic() của class Facade lấy app('db') rồi gọi table('projects') trên object đó. Kiểm tra trong tinker:

get_class(DB::getFacadeRoot()); // "Illuminate\Database\DatabaseManager"
DB::getFacadeRoot() === app('db'); // true
get_class(Cache::getFacadeRoot()); // "Illuminate\Cache\CacheManager"

Facade, helper và injection cùng gọi tới một object: Cache::get('key') giống cache('key') hay inject Illuminate\Contracts\Cache\Repository. Facade và helper gọn; injection thấy ngay class phụ thuộc vào gì ở constructor. Facade vẫn thay được bằng object giả khi test (Cache::shouldReceive(), Bài 30).

Real-time facade: thêm tiền tố Facades\ vào namespace khi use, class bất kỳ thành facade, object vẫn lấy từ container (singleton vẫn giữ):

// trong một class bất kỳ
use Facades\App\Services\TaskStatsService;

$stats = TaskStatsService::forUser($user);

Thử trong tinker (gõ tên đầy đủ): Facades\App\Services\TaskStatsService::forUser(User::find(2))->pluck('total', 'name')->all(); ra ["Du lịch Đà Lạt" => 3].

TaskFlow vẫn inject service vào controller như mục 2 để thấy rõ phụ thuộc.

7. Lời kết.

Như vậy chúng ta đã tìm hiểu xong service container và facade trong Laravel 13: auto-resolution, các kiểu binding, service provider, interface, contextual binding và cách facade lấy object từ container. Các bạn xem thêm tại tài liệu chính thức. Bài sau mình sẽ giới thiệu về event và listener trong Laravel 13, để xử lý việc khi một task được hoàn thành.

Đăng ký nhận tin.

Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha