+57

Các Request được xử lý như thế nào trong Laravel Framework?

Laravel Framework

Introduction

Chắc hẳn nhiều người trong chúng ta đã từng làm quen cũng như làm việc với Laravel framework, bản thân mình cũng mới chỉ biết đến Laravel trong khoảng thời gian hơn 1 năm. Ấn tượng đầu tiên của mình đối với framework này có lẽ là do nó khá dễ dàng cho người mới bắt đầu (kể cả những người không có nhiều kiến thức sâu về ngôn ngữ PHP - như mình), từ Framework Documentation, Laracasts, Laravel Ecosystem, Conferences cũng như Framework Community; tất cả những điều đó làm cho việc tiếp cận với framework trở nên dễ dàng hơn bao giờ hết.

Tuy nhiên trong thời gian hơn 1 năm đó, mình cũng không dành quá nhiều thời gian để tìm hiểu về cách framework này hoạt động, tại sao nó có làm được những thứ "vi diệu" như vậy. Những khi không hiểu một vấn đề gì đó, chắc điều đầu tiên mình tìm đến sẽ là Laravel Documentation; trên thực tế Documentation của framework thay đổi khá thường xuyên vì vậy sau khoảng hai tháng mình thường đọc lại một lượt và lần nào cũng tìm thấy một điều gì đó mới do không đọc kỹ những lần trước đó. Trong thời gian gần đây, sự thay đổi giữa các phiên bản không còn quá khác biệt như trong thời gian đầu của framework, Taylor Otwell cũng đã từng nói Laravel hiện đang là một "old web technology". Do vậy mình có dành thời gian để tìm hiểu qua các component cấu thành nên framework, nhưng vấn đề là mình không biết bắt đầu từ đâu cho hợp lý. Sau một vài giờ đọc các bài viết cũng như xem qua một số video khá cũ về framework, mình quyết định sẽ tìm hiểu cách mà request được xử lý bởi framework cũng như cách thức hoạt động của framework ở mức cơ bản nhất.

Nội dung của bài viết này sẽ đề cập đến cách thức hoạt động của framework ở mức cơ bản và sơ lược nhất, chúng ta sẽ không đi qua tất cả các component vì như vậy bài viết này sẽ kéo dài mãi mất (và một phần mình cũng chưa hiểu được tất cả các component của framework). Hy vọng bài viết sẽ mang lại cho các bạn một cái nhìn tổng quan về framework cũng như một số lưu ý khi sử dụng nó trong công việc sau này.

HTTP Kernel

The Entry Point - index.php

Điểm khởi đầu của Laravel framework chính là file public/index.php, nơi mà các request được tiếp nhận và xử lý để trả về các response cần thiết. Trước khi đi sâu vào nội dung của file này, chúng ta cần biết rằng framework có hai loại kernel: HTTP KernelConsole Kernel. Laravel framework cung cấp sẵn cho chúng ta hai kernel class (hai class này đều kế thừa từ hai kernel class trong core framework) tương ứng: app/Console/Kernel.phpapp/Http/Kernel.php giúp cho việc mở rộng dễ dàng hơn. Nếu làm việc với Laravel trong một khoảng thời gian ngắn, chúng ta có thể thấy HTTP Kernel thường là nơi chúng ta định nghĩa các middlewares (global middlewares, middleware groups, và route middlewares?), còn Console Kernel sẽ là nơi chúng ta định nghĩa và lập lich cho các Artisan commands. Như vậy, các request có thể tiếp cận với framework qua hai cách: HTTP requestsConsole requests. Trong bài viết này, mình sẽ đề cập chủ yếu đến HTTP requests dó đó là loại request chiếm đa số trong thực tế.

Hai kernel nói trên có nhiều điểm khá giống nhau trong cách hoạt động (chính xác hơn là cách tổ chức) vì vậy chúng ta nói Console requests như trên nhắm đảm bảo tính thống nhất.

Quay trở lại với file index.php của chúng ta, nếu lược bỏ toàn bộ phần comment thì cấu trúc của file còn khá đơn giản:

require __DIR__.'/../bootstrap/autoload.php';

Dòng đầu tiên sẽ là nới chúng ta đăng ký Composer Autoloader (Composer là công cụ mà mọi lập trình viên PHP đều đã rất quen thuộc), ở đây chúng ta đơn giản require file bootstrap/autoload.php - nơi chúng ta thực sự có reference đến file autoload.php mà Composer đã tạo ra. Nếu bạn để ý thì trong những phiên bản trước 5.4, trong file bootstrap/autoload.php chúng ta còn require thêm một file nữa đó là bootstrap/cache/compiled.php (nơi mà các class liên quan đến request được gộp lại trong một file duy nhất nhằm tăng hiệu năng của framewok - reduce disk I/O latency), file này sẽ được tạo ra khi bạn chạy lệnh php artisan optimize. Tuy nhiên trong phiên bản 5.4 mới ra mắt phần logic này đã được loại bỏ. Nguyên nhân ở đây là với phiên bản PHP7 cùng với việc sử dụng OPCache extension thì việc tạo pre-compiled classes là không cần thiết nữa.

Thông thường PHP scripts sẽ được thông dịch tại thời điểm runtime và việc chuyển đổi scripts mà chúng ta viết chắc chắn sẽ mất một khoảng thời gian nhất định. OPCache (opcode caching) cho phép việc chuyển đổi các scripts được thực hiện một lần và kết quả sẽ được lưu lại trong bộ nhớ cache. Trong những lần tiếp theo, scripts sẽ được lấy ra từ trong cache thay vì phải trải qua một loạt các bước trước khi được chuyển đổi thành ngôn ngữ mà máy tính có thể hiểu được. Do vậy việc sử dụng OPCache cùng với PHP7 sẽ cải thiện hiệu năng khá nhiều trong hầu hết các trường hợp.

$app = require_once __DIR__.'/../bootstrap/app.php';

Dòng tiếp theo trong file index.php sẽ là nơi mà chúng ta khởi tạo một instance mới của Laravel Application và đăng ký một số shared binding quan trọng với Laravel Service Container, ở đây chúng ta cũng require một file khác bên trong thư mục bootstrap - bootstrap/app.php. Hãy cùng tìm hiểu nội dung của file này: Chúng ta sẽ bắt đầu bằng việc tạo một instance của Laravel Application - Illuminate\Foundation\Application với đối số trong constructor là đường dẫn đến thư mục gốc của application. Tuy nhiên nếu chỉ dừng lại ở đây thì bạn có thể đặt ra câu hỏi là: làm cách nào mà Laravel biết đến sự tồn tại của các requests?. Đó là lý do tại sao, trong các dòng tiếp theo framework cần đăng ký một số binding với application instance mà chúng ta vừa tạo. Hai contract quan trọng nhất liên quan đến việc xử lý các HTTP requests và Console requests sẽ được đăng ký binding đầu tiên, ngoài ra còn một contract nữa liên quan đến việc xử lý exceptions. Cuối cùng chúng ta sẽ trả về application instance nói trên.

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

Trên thực tế, chúng ta có thể sử dụng file bootstrap/app.php khi chúng ta cần một fresh instance của Laravel Application. Một ví dụ thực thế đó là TestCase class mà Laravel cung cấp sẵn cho chúng ta, class đó sử dụng một trait có tên là CreatesApplication (khi testing Laravel controller hoặc sử dụng mock chúng ta sẽ cần một fresh instance của application) chứa một phương thức khá đơn giản. Phương thức này sẽ require file bootstrap/app.php để lấy ra một instance mới của application, sau đó sẽ bootstrap Console Kernel (chúng ta sẽ nói về vấn đề này sau) và trả về instance đó.

public function createApplication()
{
    $app = require __DIR__.'/../bootstrap/app.php';
    $app->make(Kernel::class)->bootstrap();
    return $app;
}

Laravel Service Container (IoC Container) là điều làm cho Laravel đặc biệt hơn so với các framework khác. Việc hiểu được bản chất và cách hoạt động của Service Container là khá quan trọng trong quá trình làm việc với framework. More about Service Container

Các tên gọi khác của Laravel Service Container: The Application (Laravel Application là một lớp kế thừa từ Container), IoC Container (Inversion of Control Container), DI Container (Dependency Injection Container).

Binding trong Laravel có thể chia thành hai nhóm chính:

  • Autowiring (sử dụng Reflection để tự động xác định và khời tạo các dependencies.
  • Manual Binding: định nghĩa việc khởi tạo một object với các dependencies cần thiết và sử dụng định nghĩa đó để tự động inject dependencies cho class. Loại binding này có bốn cách để thực liện: bind, singleton, instancealias.

Việc tiếp theo trong file index.phpresolve một instance của HTTP Kernel (chúng ta đã có một binding của HTTP Kernel bên trong bootstrap/app.php nên việc resolve instance ở đây là hoàn toàn có thể). Bên trong HTTP kernel (tương tự Console kernel) chúng ta sẽ gọi đến phương thức handle với đối số là request instance và giá trị trả về là một response instance.

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

Phương thức capture thực hiện hai việc chính đó là cho phép chúng ta fake một số loại request ngoài GETPOST bằng cách sử dụng _method request parameter (hay chúng ta sẽ quen thuộc hơn khi nhắc đến method_field() global function trong Laravel - tạo hidden input với name attribute là _method) và tạo một instance của request từ các global variables của PHP như: SERVER,_SERVER**, **_POST, FILE,_FILE**, **_COOKIE,...

Hai dòng cuối cùng trong file index.php có nhiệm vụ trả về response cho người dùng, và terminate các middleware, chính xác hơn là các terminable middlewares, ví dụ như session middleware.

The Kernel

Khi nhắc đến HTTP Kernel, chúng ta thường nghĩ đến nó như một nơi để đăng ký các middlewares. Laravel cung cấp sẵn cho chúng ta một HTTP kernel class app/Http/Kernel.php với ba mảng: $middleware, $middlewareGroups, $routeMiddleware. Chúng ta sẽ không đi chi tiết về middleware của Laravel trong bài viết này. Tuy nhiên hiểu một cách khái quát nhất thì middleware là một cơ chế dùng để filter các HTTP request đến application, chúng ta có thể reject các request hoặc thực hiện một số việc trước và sau khi request được chuyển đến middleware tiếp theo hoặc được tiếp nhận bởi application.

Chúng ta sẽ có global middlewares - các middlewares mà mọi request đến đều phải đi qua. Trong phiên bản Laravel 5.4, hai global middleware mới được thêm vào là TrimStrings (loại bỏ các khoảng trắng bên trong các request input) và ConvertEmptyStringsToNull (có tác dụng duyệt qua parameter bag và chuyển đổi các request input có giá trị là empty string thành null, giúp cho việc validate request trở nên dễ dàng hơn). Tiếp theo đó, chúng ta sẽ có các middleware groups, ở đây chúng ta có thể gộp nhiều middleware thành một nhóm và alias nó cho một key (như webapi mà Laravel cung cấp sẵn) và sử dụng key đó khi chúng ta muốn apply nhiều middleware cùng một lúc. Và cuối cùng chúng ta có route middlewares, nơi mà chúng ta sẽ định nghĩa các middleware cho từng route hoặc route groups.

Tuy nhiên, chúng ta thấy app/Http/Kernel.php class kế thừa từ Illuminate\Foundation\Http\Kernel class, hãy dành một chút thời gian để tìm hiểu về class này. Kernel API

Bên trong constructor của Kernel class, các middleware groupsroute middlewares sẽ được đăng ký với router. Phương thức quan trọng nhất trong class này là handle nơi mà các HTTP request sẽ được tiếp nhận và xử lý, chúng ta cần chú ý đến dòng lệnh sau:

$response = $this->sendRequestThroughRouter($request);

Bạn có thể thắc mắc rằng tại sao parameter $request của phương thức handle nói trên cũng như phương thức handle khi chúng ta tạo middleware mới không sử dụng type hinting với kiểu là Illuminate\Http\Request. Vấn đề ở đây có liên quan một chút đến standard cụ thể là PSR-7 standard (standard này định nghĩa các interfaces cho HTTP messages); Laravel cho phép chúng ta sử dụng PRS-7 - more here thay vì kiểu request truyền thống. Việc chuyển đổi sang hẳn PSR-7 vẫn chưa chính thức được xác nhận thực hiện, do vậy việc không type hinting ở đây có một mục đích duy nhất là keeping things flexible và không tạo ra các breaking changes trong quá trình phát triển sau này.

Phương thức sendRequestThroughRouter sẽ thực hiện một số việc sau:

  • Đăng ký request hiện tại với Laravel Service Container (sử dụng phương thức instance())
  • Loại bỏ các request facade Facade::clearResolvedInstance('request')
  • Bootstrap application
  • Gửi request qua các middleware layers và dispatch request đó đến router.

Ở bước đầu tiên, chúng ta đơn giản thay thế request hiện tại bằng request mới nhất mà application nhận được. Trong bước thứ hai, chúng ta sẽ loại bỏ các request instance đã được tạo ra (resolve) từ Service Container (chúng ta cần một instance mới nhất của request thay vì instance đã outdated).

Sau bước thứ hai, ta đã có một instance của Laravel application, tuy nhiên chúng ta chưa thể thực hiện một số công việc như: caching, encrypting, processing queue jobs... đến thời điểm này chúng ta mới chỉ có HTTP Kernel, Console Kernel, Exception Handler được bind vào Service Container và đó là tất cả những gì chúng ta có. Việc đó cũng là nguyên nhân tại sao trong bước tiếp theo chúng ta cần bootstrapping Laravel application với các bootstrappers. Nếu để ý đến bootstrappers property bên trong Kernel.php class chúng ta có thể thấy việc bootstrap application trải qua khá nhiều công đoạn, từ việc load biến môi trường, cấu hình, cho đến việc đăng ký các facades và register các service providers (chúng ta sẽ bàn về service providers trong phần sau của bài viết).

Việc bootstrapping sẽ được thực hiện một lần duy nhất.

Chúng ta có thể override $bootstrappers property bên trong HTTP Kernel class nếu chúng ta cần thực hiện một công việc nào đó sớm trong framework life cycle; tuy nhiên trong hầu hết các trường hợp việc này là không cần thiết.

Note About Request: do ở bước đầu tiên, chúng ta đã bind instance của request hiện tại trong Server Container nên trong các methods của controller chúng ta có thể truy cập đến dữ liệu bên trong request bằng việc định nghĩa một parameter với kiểu là Illuminate\Http\Request. Tuy nhiên việc inject một instace của Request thông qua constructor là không được khuyến khích do việc đó có thể gây ra một số nhầm lẫn khi phát triển. Giả sử chúng ta có một class của một component bất kỳ nào đó và class nó cần truy cập đến một instace của request; cách đơn giản nhất để thực hiện việc này là định nghĩa và khởi tạo một property với kiểu là Illuminate\Http\Request thông qua constructor. Vấn đề ở đây là nếu class đó được đăng ký một shared binding (singleton) với Service Container, trong một vài trường hợp request instance bên trong class đó sẽ bị outdated. Do đó chúng ta nên sử dụng request instace từ controllers (hoặc trong Route closure) và pass xuống các component nhỏ hơn để đảm bảo request đó luôn là request mới nhất.

Tới thời điểm hiện tại tất cả các công việc liên quan đến việc cài đặt biến môi trường và cấu hình cho framework đã hoàn tất, các core service của framework đã được bootstrapping, chúng ta đã sẵn sàng để xử lý request nhận được.

return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());

Đoạn logic trên sử dụng một component ít được biết đến của Laravel là Pipeline để gửi request cần được xử lý qua các lớp middleware và sau đó là router. Ở đây request sẽ đi qua các global middewares trước tiên (ví dụ như kiểm tra maintenance mode, trim strings,...) sau đó là các route middlewarescontroller middlewares. Cuối cùng request sẽ được dispatch đến router để nhận về một \Illuminate\Http\Response

Như vậy chúng ta đã đi sơ lược qua life cycle của một HTTP request trong framework, không quá phức tạp tuy nhiên cũng có khá nhiều điểm cần chú ý nếu không muốn mắc sai lầm trong quá trình phát triển ứng dụng.

Middleware - Gotchas

Trên Viblo hiện tại đã có khá nhiều bài viết về middleware trong Laravel. Tuy nhiên trong phần này của bài viết mình chỉ đề cập đến một số vấn hay gây nhầm lẫn khi sử dụng middleware.

Nếu đã từng làm qua một vài dự án sử dụng Laravel, chắc hẳn chúng ta đôi lúc cần truy cập đến thông tin người dùng hiện tại (currently authenticated user). Vấn đề này khá đơn giản và chúng ta cũng có khá nhiều cách để thực hiện, dưới đây là một số cách thông dụng:

  • Auth Facade: Auth::user() hoặc Auth::id() để lấy ra id của người dùng hiện tại
  • Global function: auth()->user()
  • Request object: $request->user()
  • Guard Implementation - Illuminate\Contracts\Auth\Guard contract: $guard->user()

Trong nhiều trường hợp chúng ta muốn logic thực hiện việc lấy ra authenticated user được viết một lần duy nhất (như trong trường hợp các controller), đặc biệt khi chúng ta có nhiều loại user (guard) trong hệ thống. Cách giải quyết cho vấn đề này là tạo một property bên trong base controller - ví dụ như $authUser và khởi tạo giá trị cho nó bên trong contructor của base controller. Cách làm này có những nhược điểm sau:

  • Các controller kế thừa từ base controller (Laravel không yêu cầu các controller đều phải kế thừa từ base controller) đều phải dùng parent::_construct() trong constructor của mình nếu muốn truy cập $authUser property.
  • Từ phiên bản 5.3, việc truy cập session data trong constructor là không khả thi và bạn sẽ nhận được giá trị là null cho $authUser ngay cả khi đã có user log in vào hệ thống (khá khó debug khi mới gặp, tuy nhiên trong Upgrade Guide của framework cũng đã đề cập đến vẫn đề này).
  • Làm cho logic trở lên phức tạp và khó maintain hơn.

Lời giải thích cho nhược điểm thứ hai đó chính là cách mà request đi qua các middleware pipeline:

  • Trong phiên bản 5.2, thứ tự sẽ là: [global middleware pipeline]->[route middleware pipeline]->[controller middleware pipeline]
  • Trong phiên bản 5.3, thứ tự sẽ là: [global middleware pipeline]->[route & controller middleware pipeline]

Rõ ràng trong phiên bản 5.2, global middlewaresroute middlewares (chứa một middleware khá quan trọng là StartSession) sẽ là nơi mà request được gửi qua đầu tiên. Tiếp sau đó framework sẽ khởi tạo controller và kiểm tra các controller middleware cần thiết ở đây; tại thời điểm này request và session data đã sẵn sàng do đó việc lấy ra người dùng hiện tại như trong ví dụ trên ở đây sẽ không gây ra vấn đề gì.

Trong phiên bản 5.3, việc thu thập các middleware có đôi chút khác biệt. Request sẽ vẫn đi qua các global middlewares như trên. Tuy nhiên sau bước này framework sẽ thu thập các route middlewarescontroller middlewares liên quan đến request cùng lúc, tất nhiên một instace của controller sẽ được tạo ra trong quá trình đó. Không may cho chúng ta là trong quá trình khởi tạo controller, session chưa được khởi tạo do vậy truy cập đến người dùng hiện tại sẽ trả về null.

Việc truy cập session data trực tiếp bên trong contructor của controller không được khuyến khích!

It’s very bad to use session or auth in your constructor as no request has happened yet and session and auth are INHERENTLY tied to an HTTP request. You should receive this request in an actual controller method which you can call multiple times with multiple different requests. By forcing your controller to resolve session or auth information in the constructor you are now forcing your entire controller to ignore the actual incoming request which can cause significant problems when testing, etc. (Taylor Otwell)

Nếu chúng ta vẫn muốn sử dụng phương pháp trên để truy cập đến người dùng hiện tại, có một giải pháp là sử dụng Closure based middleware như sau:

public function __construct()
{
    $this->middleware(function ($request, $next) {
        $this->projects = Auth::user()->projects;

        return $next($request);
    });
}

Pipeline Component

Không giống như một số component Authentication, Queue hay Notifications - Pipeline là một component ít được biết đến trong framework, và chúng ta cũng rất ít khi sử dụng nó trong quá trình phát triển. HTTP Kernel sử dụng component này để gửi request qua các middleware pipeline.

HTTP Kernel class sử dụng Illuminate\Routing\Pipeline - một class kế thừa từ Illuminate\Pipeline\Pipeline và bổ sung thêm một số logic liên quan đến việc xử lý exceptions trong quá trình request đi qua các middleware pipeline.

Pipeline component có chức năng chính là gửi một object (ở đây là request instance) qua một danh sách các object khác (ở đây là các middlewares). Ba phương thức cần chú ý khi sử dụng component này là:

  • public function send($passable): là một setter có nhiệm vụ khởi tạo object sẽ được truyền qua các pipeline.
  • public function through($pipes): cũng là một setter có nhiệm vụ khởi tạo các pipes (hay danh sách các object cần truyền qua).
  • public function then(Closure $destination): phương thức này mặc định truyền các object cần xử lý qua các pipelines (ở đây là request instance sẽ được truyền qua các middleware - khá quen thuộc đúng không). Mặc định phương thức handle bên trong mỗi pipeline object sẽ được gọi đến (đó là lý do tại sao các middleware đều phải chưa phương thức handle với parameter đầu tiên là request). Trong trường hợp chúng ta muốn sử dụng một phương thức khác ngoài handle, sử dụng setter public function via($method).

Pipeline: send($pasable)->through($pipelines)->via($method)->then(...)

Service Providers - Gotchas

Ngoài những tính năng được cung cấp bởi Laravel chắc hẳn bạn đã từng sử dụng một package bên ngoài (hoặc do chính bạn phát triển) để mở rộng tính năng cho framework. Nếu bạn quan sát mã nguồn của những Laravel package tất cả những package đó đều có một class kết thúc với ServiceProvider (theo convention) với hai phương thức chính là registerboot. Thực tế khi phát triển phần mềm với Laravel bạn có thể tạo thêm những service provider ngoài những service provider có sẵn của framework giúp cho việc tổ chức logic gọn gàng hơn.

Trong bài viết này mình cũng sẽ không đi sâu vào cách sử dụng Laravel Service Provider vì trên Viblo cũng đã có khá nhiều bài viết bàn về vấn đề này. Một cách tóm gọn nhất, Service Provider là nơi các services cung cấp bởi framework (authentication, cache, queue, encryption, mail,...) cũng như các service bên thứ ba được bootstrapping trong quá trình khởi tạo của framework. Việc bootstrap ở đây thực chất là việc đăng ký container bindings, event listeners, middlewares, routing đối với framework.

Danh sách các Service Provider có thể được tìm thấy ở file config/app.php trong providers array. Đây cũng là nơi các custom service provider được đăng ký với framework.

Đối với những package chỉ dùng cho môi trường development, việc đăng ký Service Provider của package đó trong file configuration trên đôi khi có thể không cần thiết. Chúng ta có hai cách chính để giải quyết vấn đề này:

  • Bên trong AppServiceProvider tại phương thức boot chúng ta có thể check môi trường hiện tại là local $this->app->environment('local') và đăng ký các package cần thiết $this->app->register(FooServiceProvider::class)
  • Nếu bạn còn nhớ trong phần trước của bài viết, mình có đề cập đến việc thêm bootstrapper cho framework và đây cũng là một trường hợp mà bạn có thể ứng dụng kĩ thuật trên. Mọi bootstrappers trong Laravel đều chứa một phương thức với tên bootstrap với tham số đầu vào là một instance của Illuminate\Foundation\Application, phương thức này sẽ được gọi đến khi framework loop qua một mảng các bootstrappers để thực hiện công việc khởi tạo. Công việc của bạn ở đây là tạo một custom bootstrapper và override phương thức bootstrappers của Illuminate\Foundation\Http\Kernel class bên trong App\Http\Kernel class cung cấp sẵn bởi framework. Done!
/**
 * Get the bootstrap classes for the application.
 *
 * @return array
 */
protected function bootstrappers()
{
    return array_merge(parent::bootstrappers(), [
        RegisterDevelopmentPackages::class,
    ]);
}

Quay trở lại với vấn đề của chúng ta, để ý trong danh sách các bootstrappers của framework, bạn sẽ thấy hai bootstrapper liên quan đến Service Providers:

/**
 * The bootstrap classes for the application.
 *
 * @var array
 */
protected $bootstrappers = [
    //...
    'Illuminate\Foundation\Bootstrap\RegisterProviders',
    'Illuminate\Foundation\Bootstrap\BootProviders',
];

Bạn có thể đặt ra câu hỏi là tại sao chúng ta cần tới hai bootstrapper ở đây và tại sao mỗi Service Provider lại cần có hai phương thức là bootregister?

Để trả lời cho câu hỏi trên, chúng ta để ý rằng RegisterProviders bootstrapper được gọi trước đồng nghĩa với việc phương thức register trong danh sách các Service Provider sẽ được gọi trước, phương thức boot sẽ được gọi sau đó. Công việc chính của phương thức register là đăng ký các bindings với Service Container, phương thức boot sẽ được dùng khi ta muốn khởi tạo một feature nào đó sử dụng các service đã được đăng ký.

Note About Service Providers: bạn không nên gọi đến các service khác bên trong phương thức register của một Service Provider vì không có gì đảm bảo rằng service đó đã được đăng ký với Service Container và sẵn sàng được sử dụng. Thứ tự của các Service Provider trong file configuration cho chúng ta biết thứ tự đăng ký của các service, việc gọi đến một service chưa được đăng ký sẽ gây ra lỗi. Nếu bạn muốn gọi đến các service khác trong một Service Provider, hãy sử dụng phương thức boot!

Console Kernel

Trong phần này, chúng ta sẽ đề cập đến Console Kernel. Tuy nhiên chúng ta sẽ đi khá nhanh vì cách tổ chức và hoạt động của Console Kernel có khá nhiều điểm tương đồng với HTTP Kernel mà chúng ta vừa tìm hiểu trong những phần trước.

The Entry Point - artisan

Chắc hẳn bạn đã dùng php artisan <command> rất nhiều khi làm việc với framework để thực một số công việc thường gặp như: database migration, generator (controllers, models,...), queue management. Artisancommand-line interface được cung cấp bởi framework được xây dựng dựa trên nền của Symfony Console component. Việc khởi chạy một console command sẽ bắt đầu từ file artisan trong thư mục gốc của project. Chúng ta cũng sẽ bắt đầu bằng việc require Composer autoloader, tạo một instace của framework và resolve một instace của Console Kernel.

require __DIR__.'/bootstrap/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

Điểm khác biệt ở đây là cách mà Console Kernel xử lý các request. Phương thức handle của kernel sẽ nhận vào hai đối số:

  • Symfony\Component\Console\Input\ArgvInput: đọc và xử lý input từ CLI arguments, có thể là các required arguments hoặc các options cho command.
  • Symfony\Component\Console\Output\ConsoleOutput: ghi kết quả từ command ra CLI, có thể là các logs trong quá trình thực hiện command hoặc các error messages (using STDOUT và STDERR).

Thay vì trả về Illuminate\Http\Response như HTTP Kernel, chúng ta sẽ nhận được một status code. Cuối cùng kernel sẽ thực hiện quá trình terminate sử dụng input phía trên và status code được trả về.

The Kernel

Chúng ta đều biết, console kernel mà framework cung cấp sẵn - App\Console\Kernel là nơi mà ta đăng ký và lập lịch cho các artisan commands. Class này kế thừa từ Illuminate\Foundation\Console\Kernel, class mà chúng ta sẽ cùng bàn luận sau đây. Tương tự như HTTP Kernel quá trình xử lý console requests sẽ bắt đầu bằng việc bootstrapping framework (khởi tạo biến môi trường và các cấu hình liên quan, đăng ký facade và service providers,...). Nếu để ý trong danh sách các bootstrappers, chúng ta sẽ thấy một bootstrapper khá đặc biệt Illuminate\Foundation\Bootstrap\SetRequestForConsole. Chúng ta đang thực hiện các công việc liên quan đến console, tại sao chúng ta phải quan tấm đến HTTP layer và xử lý các HTTP requests?

Để trả lời cho câu hỏi trên, chúng ta thấy rằng không có gì ngăn chúng ta truy cập đến HTTP layer từ các artisan commands. Bootstrapper trên có tác dụng tạo ra một dummy GET request để tránh các lỗi có thể xảy ra khi chúng ta cố làm một việc gì đó liên quan đến HTTP layer (như generating URL chẳng hạn).

class SetRequestForConsole
{
    public function bootstrap(Application $app)
    {
        $app->instance('request', Request::create(
            $app->make('config')->get('app.url', 'http://localhost'), 'GET', [], [], [], $_SERVER
        ));
    }
}

Bước tiếp theo là load các closure-based artisan commands. Từ phiên bản 5.3 framework cho phép chúng ta định nghĩa các command sử dụng closure, để dễ hiểu chúng ta hay tưởng tượng chúng giống như các closure-based routes (Closure-based Artisan command). Cuối cùng chúng ta sẽ tạo một instace của Illuminate\Console\Application và chạy các command đã được đăng ký.

A Recap

Nếu bạn đã dành thời gian để đọc đến phần này, chắc hẳn bạn đã rất kiên nhẫn. Bài viết này không mang tính ứng dụng cao và khá khô khan, tuy nhiên mình nghĩ khi làm việc với Laravel nói riêng và bất kỳ công nghệ nào khác nói chung, việc tìm hiểu một chút về cách thức hoạt động của công nghệ đó sẽ mang lại lợi ích về sau. Khi nắm rõ một thứ gì đó ở mức nền tảng việc mở rộng cũng như phát triển trên nền tảng đó sẽ trở nên dễ dàng hơn rất nhiều.

Đối với Laravel Framework, bạn nên nhớ và nắm rõ ba thứ nền tảng sau: [Service Container - IoC Container] + [Service Provider] + [Kernels], trên nền đó bạn có thể xây dựng bất kỳ thứ gì bạn muốn !?

References


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.