Đào sâu Facade của laravel.

Vâng, cách mạng mãi vẫn chưa thành thì người nông dân vẫn lại phải quay về với cái máng cũ thôi vậy. Another month, another gruding report, another 2 days salary saved (orz)

Let's looking into some simple code.

Đầu tiên, chúng ta cùng đi từ một đoạn code max đơn giản.

DB::select('SELECT * FROM users');

cái này cho ra kết quả thế nào thì đơn giản quá rồi, không có gì phải ngợi nữa. Nhưng chính vì nó đơn giản quá, trước giờ chúng ta vẫn dùng những đoạn code như thế này rất nhiều, nên đôi khi lại làm ta không hiểu lắm về chúng. Có bao giờ bạn tự hỏi, rốt cuộc thì khi ta viết như thế, framework nó xử lí thế nào chưa?

Nghi vấn đầu tiên của ta là, có thể có một class tên là DB trong global namespace, và chúng ta đang gọi đến class này. Nhưng thử search trong toàn bộ dự án của mình mà xem, đảm bảo không có một class nào như thế. Có thể bạn sẽ tìm thấy một class có vẻ giống giống, đó là class DB extends Facade nằm trong namespace Illuminate\Support\Facades. Thế nhưng bạn đâu có làm động tác gì để bao gồm class này vào trong code của mình đâu, tại sao ta vẫn viết được như trên.

DB::select('SELECT * FROM users');

Nếu bạn đã biết, hoặc chưa biết thì có thể xem qua bài viết lần trước của mình, có thể ta sẽ đi đến khả năng thứ 2: DB ở đây là một class alias. Trong laravel thì ta đã biết, class aliases được khai báo trong file config/app.php. Ta mò vào đó xem thử. Quả nhiên là có một đoạn

    'aliases' => [
    
        'DB' => Illuminate\Support\Facades\DB::class,
        
    ],

Vâng, vậy là ta đã biết được, quả nhiên là ở đây, ta đang gọi đến class Illuminate\Support\Facades\DB. Thử hí hửng mò vào soi thử xem sao

<?php

namespace Illuminate\Support\Facades;

/**
 * @see \Illuminate\Database\DatabaseManager
 * @see \Illuminate\Database\Connection
 */
class DB extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'db';
    }
}

Đây là nội dung toàn bộ class đó, nội dung này có thể nói là gần như chả có gì. Thế thì tại sao code của ta vẫn chạy? Trong tình huống này thì suy luận đầu tiên của ta là, có thể nó kế thừa method select từ class cha chăng. Lại vào soi thử tiếp bên trong class Illuminate\Support\Facades\Facade xem sao. Đáng tiếc là, vẫn không thấy có method nào như thế, và class này chẳng kế thừa từ đâu cả. Hướng đi này đến đây coi như tịt. Tuy nhiên, ta thấy bên trong Illuminate\Support\Facades\Facade có hàm callStatic(). Lại tiếp tục quảng cáo, nếu bạn chưa biết cái hàm này có gì đặc biệt, mời xem kì trước. Vậy là ở đây, ta đã dùng class alias để trỏ đến class Illuminate\Support\Facades\DB, sau đó gọi static đến hàm select(). Vì hàm này chưa được định nghĩa, nên code của ta nhảy đến magic method __callStatic(). Tổng kết lại quá trình soi mói của chúng ta cho đến lúc này thì là như thế. OK, cùng soi nội dung hàm __callStatic().

    public static function __callStatic($method, $args)
    {
        $instance = static::getFacadeRoot();

        if (! $instance) {
            throw new RuntimeException('A facade root has not been set.');
        }

        switch (count($args)) {
            case 0:
                return $instance->$method();
            case 1:
                return $instance->$method($args[0]);
            case 2:
                return $instance->$method($args[0], $args[1]);
            case 3:
                return $instance->$method($args[0], $args[1], $args[2]);
            case 4:
                return $instance->$method($args[0], $args[1], $args[2], $args[3]);
            default:
                return call_user_func_array([$instance, $method], $args);
        }
    }

Ở đây, hàm này đang nhận 2 giá trị đầu vào là

    $method = 'select';
    $args = ['SELECT * FROM users'];

Đoạn tra ngược tiếp theo thật ra hoàn toàn chả có cái vẹo gì cả, các bạn có thể skip qua luôn cho đỡ nhàm, nhảy thẳng xuống phần kết luận. Tuy nhiên, để bài viết cho nó được đầy đủ, mình cứ xin viết tất tần tật ra đây vậy. Đến đây, code của ta đang thành

$instance = static::getFacadeRoot();
$instance->select('SELECT * FROM users');

Xem tiếp hàm getFacadeRoot()

    public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }

Ta có giá trị tra về của static::getFacadeAccessor()'db' ( viết trong class Facades/DB) và resolveFacadeInstance()

return static::$resolvedInstance[$name] = static::$app[$name];

Tóm lại là, khi ta viết

DB::select('SELECT * FROM users');

thực ra ta đã gọi

    app()->make('db')->select('SELECT * FROM users');

Ra là thế, giờ ta đã hiểu cơ chế hoạt động của đoạn code tưởng chừng hết sức đơn giản này rồi nhỉ ...

Hell never end

Hay là chưa =)).

Theo lí thông thường thì, đọc đến đây, ta sẽ nghĩ : app()->make('db') sẽ trả về cho ta một object của class DB, giờ vào đó tìm method select() thôi là xong đúng ko? Hình như có gì đó sai sai ở đây ??? Vâng, ngay từ đầu, chuyện đầu tiên ta đi tìm là cái class DB huyền thoại đó rồi, và kết luận của ta đưa ra là, không có class nào như thế ? Oát dờ hợi! Chịu khó để ý kĩ hơn tí thì, ngay trong class Illuminate\Support\Facades\DB, nó đã tử tế để lại cho ta một ít gợi ý dưới dạng comment rồi.

/**
 * @see \Illuminate\Database\DatabaseManager
 * @see \Illuminate\Database\Connection
 */

Still doesn't make sense. Thôi xắn tay vào lội code tiếp vậy. Trong laravel, ta đã biết khi gọi app(), sẽ trả về cho ta một đối tượng của class Illuminate\Foundation\Application. Mò vào trong đó xem nào, may quá, nó có hàm make().

public function make($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($abstract);

        if (isset($this->deferredServices[$abstract])) {
            $this->loadDeferredProvider($abstract);
        }

        return parent::make($abstract, $parameters);
    }

Gần đến rồi, giờ xem nó alias của db là gì thôi. Phần này core của laravel, chắc nó lại lấy trong config ra? Hơi chột dạ khi nhớ ra, trong config/app.php, nó chỉ có

'DB' => Illuminate\Support\Facades\DB::class,

Thế là lại quay lại ban đầu à? Thôi không đoán nữa, xem cụ thể cái hàm getAlias() nào. Vào Illuminate\Foundation\Application, BUT IT DONT HAVE THAT DAMN METHOD !!!!!! Bình tĩnh lại mà nhìn, thì Illuminate\Foundation\Application extend từ Illuminate\Foundation\Container mà ra, và trong đó thì có hàm getAlias().

protected function getAlias($abstract)
    {
        if (! isset($this->aliases[$abstract])) {
            return $abstract;
        }

        return $this->getAlias($this->aliases[$abstract]);
    }

Vẫn chưa kết thúc đâu các ông ạ, giờ phải xem cái thằng $this->aliases nó là cái vẹo gì nữa. Nhảy ngược về class Application đi xem nào, ngay trong hàm __construct của nó, nó gọi một cái như thế này $this->registerCoreContainerAliases(); Nghe tên có vẻ tiềm năng đó nhỉ, nhảy vào hàm đó xem nào, trong đó nó có khai báo một biến

$aliases = [
    //Vân vân và mây mây
    'db'                   => ['Illuminate\Database\DatabaseManager'],
   //Mây mây và vân vân
],

Vâng, ơn chúa, ra rồi =)) ( Thật ra còn một tẹo nữa, đoạn nó gọi sang hàm alias() viết bên Container cơ, nhưng thôi thế tạm đủ rồi).

Thế nghĩa là, app()->make('db) của ta, đang trả về cho ta một object của class 'Illuminate\Database\DatabaseManager'. Cố lên sắp xong rồi.

Vâng như mà đời nó đéo như là mơ, vào trong đó soi, thì nó không có hàm select(). Nhưng soi kĩ hơn thì nó có magic method __call(). Soi tiếp thằng đó, thì tóm lại là, nó dựng lên một cái object \Illuminate\Database\Connection. Vào trong đso soi thì thấy, ơn chúa , nó có hàm select() rồi.

Tl;dr : Khi ta viết

DB::select('SELECT * FROM users');

thực ra ta đã gọi

    app()->make('db')->select('SELECT * FROM users');

và cái này tương đương với chuyện, ta tạo ra một object $connection thuộc class Illuminate\Database\Connection và gọi

    $connection->select('SELECT * FROM users');

Ích lợi gì

Vâng, ích lợi đầu tiên thu ra được là

  • Giờ thì bạn đã hiểu tại sao rất nhiều bài viết giới thiệu về Laravel, cả những bài viết về phần base nữa, người ta không đi vào chi tiết Facade hoạt động thế nào. Mệt vãi cả ra.