- 20/06/2023
[Laravel Concept] Giải mã bí thuật Facade
Trong series Laravel concept, chúng ta sẽ cùng nhau đi tìm hiểu về các kĩ thuật được coi là core của framework Laravel: Service Container, Service Provider, Facades…
Trong bài đầu tiên chúng ta sẽ giải mã một kĩ thuật luôn mang lại sự đau đầu không hề nhẹ cho các Laravel devs, mà mỗi khi nhắc tới nó, từ luôn hiện lên trong đầu chúng ta có lẽ là “ma thuật”
Khái niệm, lợi ích
Tổng quan
Chắc hẳn nếu đã làm việc với Laravel thì anh chị em đều đã từng nhìn qua những đoạn code như sau:
Cache::set('access_token', \Illuminate\Support\Facades\Request::get('token'));
DB::table('users')->whereDate('created_at', '2022-02-31');
Http::get('/products')->json();
Hãy chú ý tới Cache::set(), Request::get(), DB::table(), Http::get()
: đây chính là Facade
.
Vậy Facade
là gì?
Facade cung cấp 1 lớp static-proxy cho các lớp trong container, giúp cho việc call các non-static functions theo cách static.
Nghe khá là loằng ngoằng đúng không? Vậy hãy nhìn qua 1 ví dụ dưới đây:
Đây là cách gọi non-static function theo cách thông thường:
$cache = new CacheManager(app());
$cache->set('access_token', $token);
Còn đây là cache gọi static:
Cache::set('access_token', $token);
Với kiến thức PHP cơ bản, chúng ta đều đã biết từ khóa :: giúp việc call 1 static function mà không cần khởi tạo đối tượng class.
Lợi ích, các trường hợp sử dụng Facades
- Cú pháp ngắn gọn, dễ nhớ: nhìn lại ví dụ trên về Cache Facade, nếu sử dụng theo cách non-facade thì ta cần nhớ tên của class CacheManager, trong các tình huống khác nhau nó hoàn toàn có thể có tên khác như CacheService, CacheDriver bla bla… Tuy nhiên với việc sử dụng cái tên Cache thì nó đã rất sát với khái niệm gốc (là cache), nên việc ghi nhớ là vô cùng dễ dàng. Ngoài ra việc call static cũng rất ngắn gọn (1 dòng thì phải ngắn hơn 2 dòng rồi 🤣)
- Không cần quan tâm đến dependencies hay configs: hãy nhìn vào đoạn new CacheManager(app()), rõ ràng khi khởi tạo class thì ta cần phải cung cấp đủ các dependencies hay những giá trị config cho nó, và biết đâu đó ở trong denpendency lại có dependency khác, sẽ là một cơn ác mộng khi phải khai báo đủ. Tuy nhiên với Facade ta hoàn toàn có thể bỏ qua vấn đề này, mọi thứ sẽ được framework thực hiện.
- Che dấu đi implement thực sự phía dưới: khi gọi Http::get(), ta đâu cần biết trong đó thực hiện những gì, đơn giản là call và hưởng thụ kết quả 😊. Hay như một ví dụ InvoiceFacade::send(‘target@gmail.com’, ‘2022-02’, ‘month’), đọc qua có thể dễ hiểu logic là gửi invoice tới địa chỉ target@gmail.com, dữ liệu tổng hợp theo tháng, trong thời gian là tháng 02/2022. Trong hàm send có thể là một loạt các logic như get dữ liệu, lọc dữ liệu, kiểm tra email, tạo file, upload s3, bla bla… Nhưng khi tạo facade thì ta chỉ cần gọi hàm send là đủ. Đây chỉ là ví dụ sơ bộ, trong thực tế tùy bài toán thì chúng ta cần linh hoạt vận dụng.
- Dễ dàng thực hiện testing
Cách hoạt động của Facades
Ok, bây h là lúc chúng ta thực sự đi tìm hiểu xem Facade hoạt động ra sao.
Sử dụng ví dụ sau:
use Illuminate\Support\Facades\URL;
public function Test()
{
return URL::route('new');
}
Trên đây là ví dụ sử dụng URL Facade. Có thể thấy ta đã use class Illuminate\Support\Facades\URL
và sử dụng như một cách thông thường. Tuy nhiên khi mở class đó lên thì, bùm, tất cả những gì chúng ta nhận được chỉ là một class với một function duy nhất:
/**
* Get the registered name of the component.
*
* </em><strong><em>@return </em></strong><em>string
*/
protected static function getFacadeAccessor()
{
return 'url';
}
à khoan, class Cache
đang extends class Facade
, để mở class đó lên biết đâu function set() được định nghĩa trong đó thì sao? open class Ồ, cũng chẳng có cái gì hết 😒.
Đây chính là lúc Facade
thể hiện ma thuật của mình.
Quay trở lại PHP, khi một function static được call không tồn tại trong class, thì một magic-method tên là __callStatic
sẽ được gọi thay thế, cùng nhìn qua đoạn code dưới đây:
<?php
class Test {
public static function get() {
echo "Get function \n";
}
public static function __callStatic($method, $args) {
echo "__callStatic: $method function " . json_encode($args) . "\n";
}
}
Test::get();
Test::set();
Copy đoạn code trên và chạy test, có thể dễ dàng thấy khi ta call function get()
thì sẽ nhận được kết quả “Get function”, điều này đơn giản dễ hiểu.
Tuy nhiên khi call function set() vấn đề mới bắt đầu xuất hiện. Trong class Test không hề tồn tại function set()
. Lúc này function __callStatic()
sẽ được call với đầy đủ tên hàm và argruments. Tham chiếu kết quả ta có:
__callStatic: set function [].
Mở lại class Facade, và tìm đến function __callStatic
, ta có thể hiểu rằng khi ta call function URL::route()
thì __callStatic
sẽ được thực thi mà không bị throw Exception.
/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
Nhưng thực sự hàm route()
của URL::route()
nó ở đâu và được viết như thế nào?
Đây là các bước Facade thực hiện khi call __callStatic:
- Lấy ra giá trị binding của instance thông qua hàm getFacadeAccessor(), ở class Illuminate\Support\Facades\URL.php ta có thể thấy hàm này đang return về giá trị ‘url’
- Tìm trong service container giá trị đang được binding vào facade accessor: ở đây đang là ‘url’: tham khảo class Illuminate\Routing\RoutingServiceProvider.php line 64.
- Resolved giá trị binding từ service container, ta có instance của class Illuminate\Routing\UrlGenerator.php, đây chính là nơi viết code của function route(): sau khi resolved ta tạm gọi kết quả là $instanceResolved
- Gọi function từ $instanceResolved một cách non-static.
Thực hành tạo 1 Facade
Tạo class Invoice.php
<?php
namespace App;
class Invoice
{
public function __construct(public string|null $emailFrom)
{
if (! $emailFrom) {
$this->emailFrom = 'default@gmail.com';
}
}
public function send(string $to)
{
echo "Get data from DB \n";
echo "Filter data \n";
echo "Make PDF file \n";
echo "Upload to S3 \n";
echo "Send email with attached S3 URL to: $to";
}
}
Binding class Invoice vào Container
Trong class AppServiceProvider, thêm đoạn code sau:
public function register(): void
{
$this->app->singleton('invoice', function () {
return new Invoice(config('app.email_from'));
});
}
Tạo class InvoiceFacade
<?php
namespace App;
use Illuminate\Support\Facades\Facade;
class InvoiceFacade extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'invoice';
}
}
Chú ý rằng giá trị trả về của hàm getFacadeAccessor() phải trùng khớp với giá trị được binding vào container, ở đây là ‘invoice’.
Sử dụng Invoice Facade
public function Test()
{
return InvoiceFacade::send('customer@gmail.com');
}
Như vậy có thể thấy ta đã gọi hàm send()
của Invoice Facade mà không cần biết phải config khởi tạo hay implement thực sự của nó.
Facade Aliases
Quay trở lại ví dụ về Route::URL(), sẽ ra sao nếu ra xóa bỏ dòng code sau:
use Illuminate\Support\Facades\URL;
Câu trả lời là code vẫn hoạt động bình thường. Đó là do 1 cơ chế là facade aliases.
Mở lại class Illuminate\Support\Facades\Facade.php
tìm tới hàm defaultAliases()
, đây là nơi define những aliases cho những facade mặc định, chính vì vậy ta không cần thiết phải use class.
Ở trong ví dụ thực hành, nếu ta cũng muốn sử dụng alias thì sao? Hãy làm theo các bước sau nhé:
Tạo alias
Thêm đoạn code sau vào configs/app.php
:
'aliases' => Facade::defaultAliases()->merge([
'Invoice' => \App\InvoiceFacade::class,
])->toArray(),
Xóa bỏ dòng sau ở nơi đang sử dụng facade:
use App\InvoiceFacade;
Hãy thử lại, kết quả vẫn sẽ như vậy
Tổng Kết
Như vậy chúng ta đã tìm hiểu cách hoạt động khá là magic của Laravel Facade, trong bài tiếp theo kĩ thuật được giải mã sẽ là Service Container, cảm ơn tất cả mọi người đã dõi theo bài viết.
Và đừng quên là thực hành để có thể hiểu rõ hơn, mọi góp ý vui lòng để lại comment phía dưới 😍