paint-brush
Laravel Under The Hood - Mặt tiền là gì?từ tác giả@oussamamater
260 lượt đọc

Laravel Under The Hood - Mặt tiền là gì?

từ tác giả Oussama Mater14m2024/06/29
Read on Terminal Reader

dài quá đọc không nổi

Laravel có nhiều Facade mà chúng ta thường sử dụng. Chúng ta sẽ thảo luận về chúng là gì, cách chúng ta có thể tạo Mặt tiền của riêng mình và cũng tìm hiểu về Mặt tiền thời gian thực.
featured image - Laravel Under The Hood - Mặt tiền là gì?
Oussama Mater HackerNoon profile picture

Bạn vừa cài đặt một ứng dụng Laravel mới, khởi động nó và có trang chào mừng. Giống như những người khác, bạn thử xem nó được hiển thị như thế nào, vì vậy bạn truy cập vào tệp web.php và gặp mã này:

 <?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); });

Rõ ràng là chúng ta đã có được chế độ xem chào mừng như thế nào, nhưng bạn tò mò về cách hoạt động của bộ định tuyến của Laravel nên bạn quyết định đi sâu vào mã. Giả định ban đầu là: Có một lớp Route mà trên đó chúng ta đang gọi một phương thức tĩnh get() . Tuy nhiên, khi nhấp vào nó, không có phương thức get() nào ở đó. Vậy loại ma thuật đen tối nào đang xảy ra? Hãy làm sáng tỏ điều này!

Mặt tiền thông thường

Xin lưu ý rằng tôi đã loại bỏ hầu hết PHPDocs và nội tuyến các loại chỉ để đơn giản, "..." đề cập đến nhiều mã hơn.

Tôi thực sự khuyên bạn nên mở IDE của mình và làm theo mã để tránh mọi nhầm lẫn.


Theo ví dụ của chúng tôi, hãy cùng khám phá lớp Route .

 <?php namespace Illuminate\Support\Facades; class Route extends Facade { // ... protected static function getFacadeAccessor(): string { return 'router'; } }


Ở đây không có gì nhiều, chỉ có phương thức getFacadeAccessor() trả về chuỗi router . Hãy ghi nhớ điều này, hãy chuyển sang lớp cha.

 <?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }

Trong lớp cha, có rất nhiều phương thức, tuy nhiên không có phương thức get() . Nhưng có một điều thú vị, đó là phương thức __callStatic() . Đó là một phương thức kỳ diệu , được gọi bất cứ khi nào một phương thức tĩnh không xác định, như get() trong trường hợp của chúng ta, được gọi. Do đó, lệnh gọi của chúng tôi __callStatic('get', ['/', Closure()]) thể hiện những gì chúng tôi đã chuyển khi gọi Route::get() , tuyến đường /Closure() trả về chế độ xem chào mừng.


Khi __callStatic() được kích hoạt, trước tiên, nó sẽ cố gắng đặt một biến $instance bằng cách gọi getFacadeRoot() , $instance giữ lớp thực tế mà cuộc gọi sẽ được chuyển tiếp, chúng ta hãy xem xét kỹ hơn, nó sẽ có ý nghĩa một chút

 // Facade.php public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); }


Này, nhìn này, đây là getFacadeAccessor() từ lớp con Route mà chúng ta biết đã trả về chuỗi router . Sau đó, chuỗi router này được chuyển đến resolveFacadeInstance() , cố gắng phân giải nó thành một lớp, một loại ánh xạ có nội dung "Chuỗi này đại diện cho lớp nào?" Hãy xem nào.

 // Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }

Đầu tiên, nó kiểm tra xem một mảng tĩnh, $resolvedInstance , có giá trị được đặt với $name đã cho hay không (một lần nữa, là router ). Nếu tìm thấy kết quả khớp, nó sẽ trả về giá trị đó. Đây là bộ nhớ đệm của Laravel để tối ưu hóa hiệu suất một chút. Bộ nhớ đệm này xảy ra trong một yêu cầu duy nhất. Nếu phương thức này được gọi nhiều lần với cùng một đối số trong cùng một yêu cầu thì phương thức này sẽ sử dụng giá trị được lưu trong bộ nhớ đệm. Giả sử đó là cuộc gọi đầu tiên và tiếp tục.


Sau đó, nó sẽ kiểm tra xem $app có được đặt hay không và $app có phải là một phiên bản của vùng chứa ứng dụng hay không

 // Facade.php protected static \Illuminate\Contracts\Foundation\Application $app;

Nếu bạn tò mò về vùng chứa ứng dụng là gì, hãy coi nó như một hộp nơi các lớp của bạn được lưu trữ. Khi bạn cần những lớp đó, bạn chỉ cần chạm vào hộp đó. Đôi khi, thùng chứa này thực hiện một chút phép thuật. Ngay cả khi hộp trống và bạn với tay lấy một lớp, nó sẽ lấy lớp đó cho bạn. Đó là một chủ đề cho một bài viết khác.


Bây giờ, bạn có thể thắc mắc, "Khi nào $app được đặt?", bởi vì nó cần phải như vậy, nếu không, chúng ta sẽ không có $instance . Vùng chứa ứng dụng này được thiết lập trong quá trình khởi động ứng dụng của chúng tôi. Chúng ta hãy xem nhanh lớp \Illuminate\Foundation\Http\Kernel :

 <?php namespace Illuminate\Foundation\Http; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Facade; use Illuminate\Contracts\Http\Kernel as KernelContract; // ... class Kernel implements KernelContract { // ... protected $app; protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, // <- this guy \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; public function bootstrap(): void { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } }

Khi có yêu cầu được gửi đến, nó sẽ được gửi đến bộ định tuyến. Ngay trước đó, phương thức bootstrap() được gọi, phương thức này sử dụng mảng bootstrappers để chuẩn bị ứng dụng. Nếu bạn khám phá phương thức bootstrapWith() trong lớp \Illuminate\Foundation\Application , nó sẽ lặp qua các trình khởi động này, gọi phương thức bootstrap() của chúng.


Để đơn giản, chúng ta hãy tập trung vào \Illuminate\Foundation\Bootstrap\RegisterFacades mà chúng ta biết có chứa phương thức bootstrap() sẽ được gọi trong bootstrapWith()

 <?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\PackageManifest; use Illuminate\Support\Facades\Facade; class RegisterFacades { // ... public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); // Interested here AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); } }


Và thế là chúng ta đang thiết lập vùng chứa ứng dụng trên lớp Facade bằng phương thức tĩnh setFacadeApplication().

 // RegisterFacades.php public static function setFacadeApplication($app) { static::$app = $app; }


Hãy xem, chúng tôi chỉ định thuộc tính $app mà chúng tôi đang thử nghiệm trong resolveFacadeInstance() . Điều này trả lời câu hỏi; tiếp tục đi.

 // Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }

Chúng tôi xác nhận rằng $app được đặt trong quá trình khởi động ứng dụng. Bước tiếp theo là kiểm tra xem phiên bản đã giải quyết có nên được lưu vào bộ đệm hay không bằng cách xác minh $cached , giá trị mặc định là true. Cuối cùng, chúng tôi truy xuất phiên bản từ vùng chứa ứng dụng, trong trường hợp của chúng tôi, nó giống như yêu cầu static::$app['router'] cung cấp bất kỳ lớp nào được liên kết với chuỗi router .


Bây giờ, bạn có thể thắc mắc tại sao chúng ta truy cập $app giống như một mảng mặc dù nó là một phiên bản của vùng chứa ứng dụng, do đó, một object . Vâng, bạn nói đúng! Tuy nhiên, vùng chứa ứng dụng triển khai giao diện PHP có tên ArrayAccess , cho phép truy cập giống như mảng. Chúng ta có thể xem xét nó để xác nhận sự thật này:

 <?php namespace Illuminate\Container; use ArrayAccess; // <- this guy use Illuminate\Contracts\Container\Container as ContainerContract; class Container implements ArrayAccess, ContainerContract { // ... }


Vì vậy, resolveFacadeInstance() thực sự trả về một phiên bản được liên kết với chuỗi router , cụ thể là \Illuminate\Routing\Router . Làm sao tôi biết được? Hãy nhìn vào mặt tiền Route ; thông thường, bạn sẽ tìm thấy PHPDoc @see gợi ý về những gì mặt tiền này che giấu hoặc chính xác hơn là lớp mà các lệnh gọi phương thức của chúng ta sẽ được ủy quyền.


Bây giờ, hãy quay lại phương thức __callStatic của chúng tôi.

 <?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }


Chúng ta có $instance , một đối tượng của lớp \Illuminate\Routing\Router . Chúng tôi kiểm tra xem nó đã được đặt chưa (trong trường hợp của chúng tôi là được xác nhận) và chúng tôi trực tiếp gọi phương thức trên đó. Vì vậy, chúng tôi kết thúc với.

 // Facade.php return $instance->get('/', Closure());


Và bây giờ, bạn có thể xác nhận get() tồn tại trong lớp \Illuminate\Routing\Router .

 <?php namespace Illuminate\Routing; use Illuminate\Routing\Route; use Illuminate\Contracts\Routing\BindingRegistrar; use Illuminate\Contracts\Routing\Registrar as RegistrarContract; // ... class Router implements BindingRegistrar, RegistrarContract { // ... public function get(string $uri, array|string|callable|null $action = null): Route { return $this->addRoute(['GET', 'HEAD'], $uri, $action); } }

Điều đó kết thúc nó! Cuối cùng điều đó không khó khăn sao? Tóm lại, một mặt tiền trả về một chuỗi được liên kết với vùng chứa. Ví dụ: hello-world có thể bị ràng buộc với lớp HelloWorld . Khi chúng ta gọi tĩnh một phương thức không xác định trên một mặt tiền, chẳng hạn như HelloWorldFacade , __callStatic() sẽ bước vào.


Nó phân giải chuỗi đã đăng ký trong phương thức getFacadeAccessor() của nó thành bất kỳ chuỗi nào bị ràng buộc trong vùng chứa và ủy quyền lệnh gọi của chúng ta tới phiên bản được truy xuất đó. Vì vậy, chúng tôi kết thúc với (new HelloWorld())->method() . Đó là bản chất của nó! Vẫn chưa nhấp chuột cho bạn? Hãy tạo mặt tiền của chúng ta sau đó!

Hãy làm mặt tiền của chúng tôi

Giả sử chúng ta có lớp này:

 <?php namespace App\Http\Controllers; class HelloWorld { public function greet(): string { return "Hello, World!"; } }


Mục tiêu là gọi HelloWorld::greet() . Để làm điều này, chúng ta sẽ liên kết lớp của mình với vùng chứa ứng dụng. Đầu tiên, điều hướng đến AppServiceProvider .

 <?php namespace App\Providers; use App\Http\Controllers; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind('hello-world', function ($app) { return new HelloWorld; }); } // ... }


Bây giờ, bất cứ khi nào chúng ta yêu cầu hello-world từ vùng chứa ứng dụng của mình (hoặc hộp, như tôi đã đề cập trước đó), nó sẽ trả về một phiên bản của HelloWorld . Những gì còn lại? Chỉ cần tạo một mặt tiền trả về chuỗi hello-world .

 <?php namespace App\Http\Facades; use Illuminate\Support\Facades\Facade; class HelloWorldFacade extends Facade { protected static function getFacadeAccessor() { return 'hello-world'; } }


Với điều này tại chỗ, chúng tôi đã sẵn sàng để sử dụng nó. Hãy gọi nó trong web.php.

 <?php use App\Http\Facades; use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorldFacade::greet(); // Hello, World! });


Chúng tôi biết rằng greet() không tồn tại trên mặt tiền HelloWorldFacade , __callStatic() được kích hoạt. Nó kéo một lớp được biểu thị bằng một chuỗi ( hello-world trong trường hợp của chúng tôi) từ vùng chứa ứng dụng. Và chúng tôi đã thực hiện ràng buộc này trong AppServiceProvider ; chúng tôi đã hướng dẫn nó cung cấp một phiên bản HelloWorld bất cứ khi nào ai đó yêu cầu hello-world . Do đó, bất kỳ lệnh gọi nào, chẳng hạn như greet() , sẽ hoạt động trên phiên bản được truy xuất đó của HelloWorld . Và thế là xong.


Chúc mừng! Bạn đã tạo ra mặt tiền của riêng mình!

Mặt tiền thời gian thực của Laravel

Bây giờ bạn đã hiểu rõ về mặt tiền, sẽ có thêm một trò ảo thuật nữa sắp được tiết lộ. Hãy tưởng tượng bạn có thể gọi HelloWorld::greet() mà không cần tạo mặt tiền, sử dụng mặt tiền thời gian thực .


Chúng ta hãy có một cái nhìn:

 <?php use Facades\App\Http\Controllers; // Notice the prefix use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorld::greet(); // Hello, World! });


Bằng cách thêm tiền tố vào không gian tên của bộ điều khiển với Facades , chúng ta đạt được kết quả tương tự như trước đó. Tuy nhiên, chắc chắn rằng bộ điều khiển HelloWorld không có bất kỳ phương thức tĩnh nào có tên là greet() ! Và Facades\App\Http\Controllers\HelloWorld thậm chí đến từ đâu? Tôi hiểu điều này có vẻ giống như một phép thuật nào đó, nhưng một khi bạn nắm bắt được nó thì nó khá đơn giản.


Chúng ta hãy xem xét kỹ hơn về \Illuminate\Foundation\Bootstrap\RegisterFacades mà chúng ta đã kiểm tra trước đó, lớp chịu trách nhiệm thiết lập $app:

 <?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\PackageManifest; use Illuminate\Support\Facades\Facade; class RegisterFacades { public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); // Interested here } }


Bạn có thể thấy ở phần cuối phương thức register() được gọi. Chúng ta hãy nhìn vào bên trong:

 <?php namespace Illuminate\Foundation; class AliasLoader { // ... protected $registered = false; public function register(): void { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } }


Biến $registered ban đầu được đặt thành false . Do đó, chúng ta nhập câu lệnh if và gọi phương thức prependToLoaderStack() . Bây giờ, hãy khám phá việc thực hiện nó.

 // AliasLoader.php protected function prependToLoaderStack(): void { spl_autoload_register([$this, 'load'], true, true); }


Đây là nơi phép thuật xảy ra! Laravel đang gọi hàm spl_autoload_register() , một hàm PHP tích hợp kích hoạt khi cố gắng truy cập một lớp không xác định. Nó xác định logic để thực thi trong những tình huống như vậy. Trong trường hợp này, Laravel chọn gọi phương thức load() khi gặp một cuộc gọi không xác định.


Ngoài ra, spl_autoload_register() tự động chuyển tên của lớp không xác định cho bất kỳ phương thức hoặc hàm nào mà nó gọi.


Hãy cùng khám phá phương thức load() ; nó phải là chìa khóa.

 // AliasLoader.php public function load($alias) { if (static::$facadeNamespace && str_starts_with($alias, static::$facadeNamespace)) { $this->loadFacade($alias); return true; } if (isset($this->aliases[$alias])) { return class_alias($this->aliases[$alias], $alias); } }

Chúng tôi kiểm tra xem $facadeNamespace có được đặt hay không và nếu bất kỳ lớp nào được thông qua, trong trường hợp của chúng tôi, Facades\App\Http\Controllers\HelloWorld bắt đầu với bất kỳ lớp nào được đặt trong $facadeNamespace


Logic kiểm tra xem $facadeNamespace có được đặt hay không và liệu lớp đã truyền, trong trường hợp của chúng ta là Facades\App\Http\Controllers\HelloWorld (không xác định), có bắt đầu bằng giá trị được chỉ định trong $facadeNamespace.

 // AliasLoader.php protected static $facadeNamespace = 'Facades\\';


Vì chúng tôi đã thêm tiền tố vào không gian tên của bộ điều khiển bằng Facades , thỏa mãn điều kiện, nên chúng tôi tiến hành loadFacade()

 // AliasLoader.php protected function loadFacade($alias) { require $this->ensureFacadeExists($alias); }


Ở đây, phương thức này yêu cầu bất kỳ đường dẫn nào được trả về từ ensureFacadeExists() . Vì vậy, bước tiếp theo là đi sâu vào việc thực hiện nó.

 // AliasLoader.php protected function ensureFacadeExists($alias) { if (is_file($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) { return $path; } file_put_contents($path, $this->formatFacadeStub( $alias, file_get_contents(__DIR__.'/stubs/facade.stub') )); return $path; }


Đầu tiên, chúng tôi thực hiện kiểm tra để xác định xem tệp có tên framework/cache/facade-'.sha1($alias).'.php' có tồn tại hay không. Trong trường hợp của chúng tôi, tệp này không xuất hiện, điều này sẽ kích hoạt bước tiếp theo: file_put_contents() . Hàm này tạo một tệp và lưu nó vào $path được chỉ định. Nội dung của tệp được tạo bởi formatFacadeStub() , dựa trên tên của nó, sẽ tạo ra một mặt tiền từ một sơ khai. Nếu bạn kiểm tra facade.stub , bạn sẽ thấy như sau:

 <?php namespace DummyNamespace; use Illuminate\Support\Facades\Facade; /** * @see \DummyTarget */ class DummyClass extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'DummyTarget'; } }


Trông quen quen nhỉ? Về cơ bản đó là những gì chúng tôi đã làm bằng tay. Bây giờ, formatFacadeStub() thay thế nội dung giả bằng lớp không xác định của chúng ta sau khi xóa tiền tố Facades\\ . Mặt tiền cập nhật này sau đó được lưu trữ. Do đó, khi loadFacade() yêu cầu tệp, nó thực hiện chính xác và cuối cùng nó yêu cầu tệp sau:

 <?php namespace Facades\App\Http\Controllers; use Illuminate\Support\Facades\Facade; /** * @see \App\Http\Controllers\HelloWorld */ class HelloWorld extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'App\Http\Controllers\HelloWorld'; } }

Và bây giờ, theo quy trình thông thường, chúng tôi yêu cầu vùng chứa ứng dụng trả về bất kỳ phiên bản nào được liên kết với chuỗi App\Http\Controllers\HelloWorld . Bạn có thể thắc mắc, chúng tôi không liên kết chuỗi này với bất kỳ thứ gì, chúng tôi thậm chí còn không chạm vào AppServiceProvider của mình. Nhưng hãy nhớ những gì tôi đã đề cập về vùng chứa ứng dụng ngay từ đầu?


Ngay cả khi hộp trống, nó sẽ trả về thể hiện , nhưng với một điều kiện, lớp không được có hàm tạo. Nếu không, nó sẽ không biết cách xây dựng nó cho bạn. Trong trường hợp của chúng tôi, lớp HelloWorld không cần xây dựng bất kỳ đối số nào. Vì vậy, vùng chứa sẽ giải quyết nó, trả về nó và tất cả các cuộc gọi sẽ được ủy quyền cho nó.


Tóm tắt lại các mặt tiền thời gian thực: Chúng tôi đã thêm tiền tố cho lớp của mình bằng Facades . Trong quá trình khởi động ứng dụng, Laravel đăng ký spl_autoload_register() , điều này sẽ kích hoạt khi chúng ta gọi các lớp không xác định. Cuối cùng nó dẫn tới phương thức load() . Bên trong load() , chúng tôi kiểm tra xem lớp không xác định hiện tại có tiền tố là Facades hay không. Nó khớp nên Laravel cố tải nó.


Vì mặt tiền không tồn tại nên nó tạo nó từ một sơ khai và sau đó yêu cầu tệp. Và Voila! Bạn có một mặt tiền thông thường, nhưng mặt tiền này được tạo ra một cách nhanh chóng. Khá tuyệt phải không?

Phần kết luận

Chúc mừng bạn đã tiến xa đến mức này! Tôi hiểu nó có thể hơi quá sức. Vui lòng quay lại và đọc lại bất kỳ phần nào chưa phù hợp với bạn. Theo dõi IDE của bạn cũng có thể hữu ích. Nhưng này, không còn ma thuật đen nữa, hẳn là cảm thấy dễ chịu, ít nhất đó là cảm giác của tôi lần đầu tiên!


Và hãy nhớ rằng, lần tới khi bạn gọi một phương thức tĩnh, điều đó có thể không đúng như vậy 🪄