Laravel MacroableTrait
Bài đăng này đã không được cập nhật trong 8 năm
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ừ classTestMacro
và gọi phương thứchello()
.
Đ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 classTestMacro
, 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ứchello
thông qua instance$test
vừa được tạo ra. Ngay sau đó ta gọi phương thứchello
từ chính classTestMacro
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
và __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:
- Gọi khai báo
macro
để thêm phương thức có tênhello
vào trong classTestMacro
cùng function thực thi phương thức đó. - Khởi tạo đối tượng
$test
từ classTestMacro
. - Gọi phương thức
hello
từ đối tượng$test
. - Gọi phương thức
hello
tĩnh thông qua classTestMacro
.
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
, ...
- http://alanstorm.com/laravels_macroabletrait
- https://murze.be/2015/12/using-collection-macros-in-laravel/
- https://laravel.com/docs/5.2/responses#response-macros
- https://github.com/laravel/framework/blob/5.2/src/Illuminate/Support/Traits/Macroable.php
- https://github.com/mhmoudsami/Example-Use-of-laravel-macro-trait
All rights reserved