+10

Laravel MacroableTrait

Giới thiệu

Ý tưởng về macro thực ra đã có từ rất lâu và trong mỗi ngôn ngữ, mỗi công cụ nó lại mang những ý nghĩa khác nhau. Ta có thể điểm qua một vài khái niệm về macro như sau:

  • Trong ngôn ngữ visual basic (vb-script) cũng có khái niệm macro, trong đó macro là những hàm (hay chương trình con) không có tham số truyền vào.
  • Trong ngôn ngữ C, macro lại là một dạng ngôn ngữ lập trình nhỏ nằm trong ngôn ngũ lập trình C, giúp chúng ta có thể thay đổi nội dung của chương trình trước khi gửi đến cho bộ biên dịch (complier).
  • Trong Microsoft Office như Excel, ta cũng thấy có khái niệm macro để viết ra những đoạn chương trình nhỏ phục vụ cho việc tự động hóa những nhiệm vụ cụ thể (VD: sau khi tôi ghi đầy đủ các dữ liệu, tôi có thể gán macro cho việc xử lý thống kê).

Bạn có thể thấy dù nhiệm vụ của macro khác nhau ở các ngôn ngữ tuy nhiên nó cũng mang một sứ mệnh chung đó là thực hiện một nhiệm vụ mà chúng ta định sẵn.

Trong bài viết ngày hôm nay chúng ta sẽ cùng nhau tìm hiểu về việc Laravel đã triển khai ý tưởng của macro như thế nào. Tôi sẽ sử dụng Laravel 5.2 để phục vụ cho việc demo code, bản thân Laravel đã tự xây dựng cho mình một Trait có tên là Macroable.

Việc tại sao dùng Trait chúng ta sẽ biết ở phần sau của bài viết khi đi sâu vào code. Còn nếu bạn chưa biết Trait là gì? Bạn có thể tham khảo qua bài viết Traits in PHP and Laravel của tôi (bow)

Macroable có ý nghĩa gì trong Laravel?

Để bắt đầu chúng ta sẽ xem qua ví dụ sau. Tôi khởi tạo mới một project Laravel 5.2 và thêm một vài dòng code vào routes.php:

// app/Http/routes.php
class TestMacro
{

}

Route::get('/', function () {
    $test = new TestMacro();
    $test->hello();
});

Những việc tôi đã làm:

  • Khai báo một class TestMacro rỗng.
  • Trong route mặc định, tôi khởi tạo một instance $test từ class TestMacro và gọi phương thức hello().

Đoạn chương trình này khi chạy chắn chắn sẽ gặp lỗi vì phương thức hello chưa được khai báo trong class TestMacro. Chúng ta nhận được thông báo Call to undefined method TestMacro::hello().

Ta sẽ sửa lại đoạn chương trình trên bằng việc thêm Macroable Trait vào class TestMacro:

class TestMacro
{
	use Illuminate\Support\Traits\Macroable;
}

Và giờ chạy lại chương trình thì ta vẫn nhận được lỗi tương tự như trên, lý do rất đơn giản là dù bạn có đọc code của Macroable đi nữa bản thân nó vẫn không hề có phương thức hello để có thể sử dụng. Vậy thì hãy cùng sửa thêm code một chút nữa như sau:

class TestMacro
{
	use Illuminate\Support\Traits\Macroable;
}

Route::get('/', function () {
    TestMacro::macro('hello', function() {
    	echo "Tungshooter<br/>";
    });

    $test = new TestMacro();

    $test->hello();
    TestMacro::hello();
});
  • Ta gọi phương thức tĩnh macro của class TestMacro, tuy nhiên bản thân nó không có phương thức này mà được kế thừa từ Macroable Trait.
  • Sau đó khởi tạo instance từ class TestMacro, thực hiện gọi phương thức hello thông qua instance $test vừa được tạo ra. Ngay sau đó ta gọi phương thức hello từ chính class TestMacro như một static function.

Điều gì sẽ xảy ra ở đây? Phương thức hello vừa được gọi như non-static function vừa được gọi như static function liệu có gây lỗi?

Kết quả nhận được không có lỗi gì xảy ra hết và chúng ta có dữ liệu trên màn hình sau khi chạy:

Tungshooter
Tungshooter

Đây chính là những điều kỳ diệu mà Macroable đã làm cho chúng ta, nó cho phép chúng ta thêm một phương thức (hay có thể gọi là một macro) vào một class có sẵn. Tưởng tượng khi ta gọi static function macro(), phương thức mà chúng ta thêm mới vào (trong ví dụ của tôi là phương thức hello()) sẽ coi như được thêm vào chính class TestMacro và thêm vào các instance của class TestMacro nữa (VD: $test).

(Bạn có thể thử nghiệm bằng cách khởi tạo instance $test rồi sau đó mới thực hiện gọi hàm macro cho class TestMacro, bạn sẽ thấy dù instance được khởi tạo rồi nhưng nó vẫn được thêm phương thức hello).

Chúng ta hãy cùng tìm hiểu xem Laravel đã xử lý như thế nào và thực tế thì case study cụ thể ra sao.

Macroable Trait hoạt động như thế nào

MacroableTrait thực sự đơn giản hơn nhiều so với những gì chúng ta tưởng tượng, hãy cùng tìm đến đường dẫn vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php hoặc xem online tại đường dẫn https://github.com/laravel/framework/blob/5.2/src/Illuminate/Support/Traits/Macroable.php

Đầu tiên hãy xem phương thức macro() mà chúng ta gọi bên trên được định nghĩa:

// vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php
public static function macro($name, callable $macro)
{
    static::$macros[$name] = $macro;
}

Phương thức này làm nhiệm vụ lưu trữ các hàm PHP không định danh (anonymous function) trong một mảng tĩnh, được đánh chỉ mục bằng tham số $name. Mảng tĩnh này được khai báo ngay dưới khai báo class:

protected static $macros = [];

Ngay phía dưới phương thức này là hai magic method rất thân thuộc với chúng ta. Bạn có thể tìm hiểu thêm về PHP Magic Method thông qua bài viết Laravel and PHP Magic Methods của tác giả @dinhhoanglong91.

Hai Magic Method chúng ta có là __call__callStatic được định nghĩa như sau:

// vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php
public static function __callStatic($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException("Method {$method} does not exist.");
    }

    if (static::$macros[$method] instanceof Closure) {
        return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters);
    }

    return call_user_func_array(static::$macros[$method], $parameters);
}
// vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php
public function __call($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException("Method {$method} does not exist.");
    }

    if (static::$macros[$method] instanceof Closure) {
        return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters);
    }

    return call_user_func_array(static::$macros[$method], $parameters);
}

Xem qua code ta có thể thấy các Magic Method làm nhiệm vụ nhận anonymous function khi ta gọi hàm macro() và thực hiện gọi function đó với hàm call_user_func_array mà PHP cung cấp.

Có vẻ hơi khó hiểu một chút nên tôi sẽ phân tích lại luồng hoạt động. Quay lại ví dụ của tôi bên trên, ta đã làm các bước như sau:

  1. Gọi khai báo macro để thêm phương thức có tên hello vào trong class TestMacro cùng function thực thi phương thức đó.
  2. Khởi tạo đối tượng $test từ class TestMacro.
  3. Gọi phương thức hello từ đối tượng $test.
  4. Gọi phương thức hello tĩnh thông qua class TestMacro.

Ta sẽ đi lần lượt từng bước và phân tích. Khi tôi gọi khai báo macro:

TestMacro::macro('hello', function() {
    echo "Tungshooter<br/>";
});

Ta thông báo cho class TestMacro lưu lại anonymous function ở trong static::$macro['hello']. Anonymous function này sẽ được phép sử dụng cho cả những instance được tạo ra dù trước hay sau khi khai báo macro.

Sau đó khi ta gọi:

$test = new TestMacro();

$test->hello();
TestMacro::hello();

Như ta đã nói ở trên, phương thức hello() không hề tồn tại trong class TestMacro, lúc này nhờ có Magic Method __call chúng ta sẽ vào đây và xử lý tiếp. Ta sẽ viết lại phương thức này dựa trên những tham số truyền vào:

// vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php
public function __call('hello', $parameters)
{
    if (! static::hasMacro('hello')) {
        throw new BadMethodCallException("Method {'hello'} does not exist.");
    }

    if (static::$macros['hello'] instanceof Closure) {
        return call_user_func_array(static::$macros['hello']->bindTo($this, static::class), $parameters);
    }

    return call_user_func_array(static::$macros['hello'], $parameters);
}

Thực ra thì lúc này static::$macro['hello'] chính là:

function() {
    echo "Tungshooter<br/>";
}

Và như vậy anonymous function này sẽ được gọi khi ta gọi $test->hello(). Kịch bản tương tự cũng diễn ra với việc gọi TestMacro::hello() các bạn có thể dễ dàng nhận ra.

Tại sao nên sử dụng Macroable Trait

Trước khi tìm hiểu lý do Laravel đưa vào Trait Macroable chúng ta sẽ cùng điểm qua những class sử dụng Macroable. Nếu bạn đã từng làm việc với Laravel Collective và đọc document của package này, chúng ta có thể sử dụng macro để tùy biến Form bằng cách khai báo:

Form::macro('myField', function()
{
    return '<input type="awesome">';
});

Và sử dụng trong blade view:

{{ Form::myField(); }}

Ta thấy rằng trong class FormBuilder cũng có khai báo sử dụng Trait Macroable (https://github.com/LaravelCollective/html/blob/5.2/src/FormBuilder.php#L16)

Ngoài ra còn một class khác cũng sử dụng Macroable Trait đó là Collection (https://github.com/LaravelCollective/html/blob/5.2/src/FormBuilder.php#L16). Giả sử chúng ta đặt ra một bài toán đơn giản là muốn viết hoa ký tự đầu tiên của từng từ trong một mảng các chuỗi ký tự. Ta sẽ sử dụng hàm ucwords trong PHP để thực hiện như sau:

$ucwords = collect(['hello world', 'good morning'])->map(function($text) {
    return ucwords($text);
});
dd($ucwords);

Kết quả nhận được:

Collection {#143 ▼
  #items: array:2 [▼
    0 => "Hello World"
    1 => "Good Morning"
  ]
}

Nhưng hãy thử tưởng tượng chúng ta không chỉ làm việc với một Collection như trên mà còn làm việc với nhiều Collection khác nữa. Việc viết đi viết loại đoạn Closure kia quả thật là mệt và code của chúng ta sẽ bị lặp phải không? Chúng ta sẽ khắc phục bằng cách khai báo một macro mới cho Collection:

Collection::macro('ucwords', function() {
    return collect($this->items)->map(function($text) {
        return ucwords($text);
    });
});

Bạn có thể đăng ký đoạn code này với Service Provider khi ứng dụng của bạn boot thành công để có thể tái sử dụng. Và để sử dụng hết sức đơn giản, ta có thể viết:

$ucwords = collect(['hello world', 'good morning', 'nice day'])->ucwords();
dd($ucwords);

Vậy ý kiến đặt ra là tại sao ta lại phải viết macro trong khi có một solution khác hoàn toàn có thể giải quyết vấn đề này. Tôi có thể định nghĩa trước một hàm (kiểu dạng như Helper) với đối số truyền vào là Collection:

function collectionUcwords($collection)
{
    // code
}

$ucwords = collectionUcwords(collect(['a', 'b']));

Việc làm này cũng hoàn toàn hợp lý, tuy nhiên có những nhược điểm nhất định. Đầu tiên, Collection của chúng ta bị đóng gói vào hàm collectionUcwords(). Sau đó những gì được thực hiện cuối cùng (việc ucwords các chuỗi trong mảng) lại được đưa lên đầu lời gọi (mặc dù nó vẫn trả ra kết quả đúng) dẫn tới việc đọc code sẽ khó khăn trong việc đoán ra logic xử lý. Thêm nữa nếu chúng ta có nhiều function custom hơn (ngoài những function mặc định của collection) các bạn có thể tự trải nghiệm xem cách viết nào sẽ dễ đọc và hợp lý hơn qua ví dụ sau:

// Tổ chức nhiều functions
function4(function3(function2(function1(collect(['a','b'])))));

// Tổ chức nhiều macros
collect(['a', 'b'])
  ->function1()
  ->function2()
  ->function3()
  ->function4();

Kết luận

Thông qua bài viết tôi muốn giới thiệu cho các bạn về Macroable Trait trong Laravel, nó hoạt động ra sao, đồng thời cũng đưa ra những trường hợp cụ thể mà Laravel đã sử dụng. Hy vọng bài viết sẽ giúp ích cho các bạn khi làm việc với Laravel và cụ thể là một số class như Form hay Collection. Ngoài ra các bạn có thể tìm hiểu một số class khác cũng có sử dụng Macroable Trait như Str, Router, ResponseFactory, ...

  1. http://alanstorm.com/laravels_macroabletrait
  2. https://murze.be/2015/12/using-collection-macros-in-laravel/
  3. https://laravel.com/docs/5.2/responses#response-macros
  4. https://github.com/laravel/framework/blob/5.2/src/Illuminate/Support/Traits/Macroable.php
  5. https://github.com/mhmoudsami/Example-Use-of-laravel-macro-trait

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í