Giới thiệu
Laravel Facades như Auth
, View
, Mail
, hay các helpers như auth()
, view()
,… có nhiều magic ẩn dưới giúp code chúng ta ngắn gọn hơn và làm được việc nhanh hơn. Nhưng vì quá magic nên khá là khó để hiểu sâu hay khi debug, ta chỉ biết dùng nó mà chẳng biết nó hoạt động như thế nào? Rồi trong một project, chỗ thì dùng Facade, chỗ thì dùng helpers lung tung hết cả lên??
Đã bao giờ bạn xem code của class Auth
, thực ra nó là IlluminateSupportFacadesAuth
, Auth
là alias config ở file config/app.php
, code của nó như thế này:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <span class="token php language-php"><span class="token delimiter important"><?php</span> <span class="token keyword">namespace</span> <span class="token package">IlluminateSupportFacades</span><span class="token punctuation">;</span> <span class="token comment">/** * @method static mixed guard(string|null $name = null) * @method static void shouldUse(string $name); * @method static bool check() * @method static bool guest() * @method static IlluminateContractsAuthAuthenticatable|null user() * @method static int|null id() * @method static bool validate(array $credentials = []) * @method static void setUser(IlluminateContractsAuthAuthenticatable $user) * @method static bool attempt(array $credentials = [], bool $remember = false) * @method static bool once(array $credentials = []) * @method static void login(IlluminateContractsAuthAuthenticatable $user, bool $remember = false) * @method static IlluminateContractsAuthAuthenticatable loginUsingId(mixed $id, bool $remember = false) * @method static bool onceUsingId(mixed $id) * @method static bool viaRemember() * @method static void logout() * @method static SymfonyComponentHttpFoundationResponse|null onceBasic(string $field = 'email',array $extraConditions = []) * @method static null|bool logoutOtherDevices(string $password, string $attribute = 'password') * @method static IlluminateContractsAuthUserProvider|null createUserProvider(string $provider = null) * @method static IlluminateAuthAuthManager extend(string $driver, Closure $callback) * @method static IlluminateAuthAuthManager provider(string $name, Closure $callback) * * @see IlluminateAuthAuthManager * @see IlluminateContractsAuthFactory * @see IlluminateContractsAuthGuard * @see IlluminateContractsAuthStatefulGuard */</span> <span class="token keyword">class</span> <span class="token class-name">Auth</span> <span class="token keyword">extends</span> <span class="token class-name">Facade</span> <span class="token punctuation">{</span> <span class="token comment">/** * Get the registered name of the component. * * @return string */</span> <span class="token keyword">protected</span> <span class="token keyword">static</span> <span class="token keyword">function</span> <span class="token function">getFacadeAccessor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token single-quoted-string string">'auth'</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> </span> |
Facade hoạt động như thế nào
Nếu bạn lần mò vào method Auth::user()
từ IDE hay editor, thì nó sẽ chỉ bạn đến dòng:
1 2 3 4 | <span class="token comment">/** * @method static IlluminateContractsAuthAuthenticatable|null user() */</span> |
Bạn tự nghĩ, “ô, wtf?? code đâu??”. Có thể bạn đã thừa biết, dòng trên chỉ là 1 một comment trong docblock được sử dụng khi generate document với phpdoc hay để gợi ý cho IDE autocomplete các “magic” method. Nó chỉ là 1 dạng tài liệu. Còn code thật tất nhiên là magic rồi. Magic ở đây chính là PHP Magic Method __callStatic()
:
vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token comment">/** * Handle dynamic, static calls to the object. * * @param string $method * @param array $args * @return mixed * * @throws RuntimeException */</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">function</span> <span class="token function">__callStatic</span><span class="token punctuation">(</span><span class="token variable">$method</span><span class="token punctuation">,</span> <span class="token variable">$args</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$instance</span> <span class="token operator">=</span> <span class="token keyword">static</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">getFacadeRoot</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span> <span class="token variable">$instance</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">RuntimeException</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'A facade root has not been set.'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token variable">$instance</span><span class="token operator">-</span><span class="token operator">></span><span class="token variable">$method</span><span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token variable">$args</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Khi bạn gọi Auth::user()
thì nó sẽ chạy như thế này:
- Do class
Auth
không có public static method nào làuser()
nên magic method__callStatic('user')
sẽ được gọi - Bên trong Laravel sẽ lấy ra instance
auth
từ service container - Gọi method
user()
của instanceauth
Để biết instance auth
được bind vào service container ở đâu thì bạn phải xem trong service provider, rất may là nó thường được đặt tên theo convention, ví dụ có Auth Facade thì sẽ có Auth Service Provider => vendor/laravel/framework/src/Illuminate/Auth/AuthServiceProvider.php
:
1 2 3 4 5 6 7 8 9 | <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">app</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">singleton</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'auth'</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token variable">$app</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Once the authentication service has actually been requested by the developer</span> <span class="token comment">// we will set a variable in the application indicating such. This helps us</span> <span class="token comment">// know that we need to set any queued cookies in the after event later.</span> <span class="token variable">$app</span><span class="token punctuation">[</span><span class="token single-quoted-string string">'auth.loaded'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">AuthManager</span><span class="token punctuation">(</span><span class="token variable">$app</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Vậy instance auth
ở đây là instance thuộc class IlluminateAuthAuthManager
. Bên trong class này lại có thêm magic method nữa, các bạn có thể đọc code và tìm hiểu thêm
Contracts (Interface)
auth
này là service thuộc loại core service nên nó cũng có thêm alias được khai báo trong vendor/laravel/framework/src/Illuminate/Foundation/Application.php
:
1 2 3 4 | <span class="token punctuation">[</span> <span class="token single-quoted-string string">'auth'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">[</span><span class="token package">IlluminateAuthAuthManager</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token package">IlluminateContractsAuthFactory</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">]</span> |
Tức là:
1 2 3 4 | <span class="token function">app</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">make</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'auth'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">app</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">make</span><span class="token punctuation">(</span><span class="token package">IlluminateAuthAuthManager</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">app</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">make</span><span class="token punctuation">(</span><span class="token package">IlluminateContractsAuthFactory</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Ba dòng này sẽ cho ra kết quả giống nhau, đều resolved instance thuộc class IlluminateAuthAuthManager
.
Như vậy ta có ý tưởng cho việc sử dụng DI ở đây là inject class IlluminateAuthAuthManager
hoặc theo cách chính xác nhất đó là inject contract (interface) là IlluminateContractsAuthFactory
thay vì concrete (bê tông?) class, giống như code của function auth()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token comment">/** * Get the available auth instance. * * @param string|null $guard * @return IlluminateContractsAuthFactory|IlluminateContractsAuthGuard|IlluminateContractsAuthStatefulGuard */</span> <span class="token keyword">function</span> <span class="token function">auth</span><span class="token punctuation">(</span><span class="token variable">$guard</span> <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">is_null</span><span class="token punctuation">(</span><span class="token variable">$guard</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">app</span><span class="token punctuation">(</span>AuthFactory<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token function">app</span><span class="token punctuation">(</span>AuthFactory<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">guard</span><span class="token punctuation">(</span><span class="token variable">$guard</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Giải thích có phần nguy hiểm vậy thôi, chứ Laravel docs – Facades, Laravel docs – Contracts cũng đã đề cập về vấn đề này Việc sử dụng facades hay DI là tùy vào trải nghiệm của từng cá nhân hay team, chúng ta đã sử dụng nhiều đến facade và các helper functions, thử dùng contract để inject dependency xem nó như thế nào?
Một số contracts và facades thường dùng:
Contract | Facade | Class | Core Service |
---|---|---|---|
IlluminateContractsAuthAccessGate | Gate | IlluminateContractsAuthAccessGate | |
IlluminateContractsAuthFactory | Auth | IlluminateAuthAuthManager | auth |
IlluminateContractsAuthGuard | Auth::guard() | IlluminateContractsAuthGuard | auth.driver |
IlluminateContractsAuthPasswordBroker | Password::broker() | IlluminateAuthPasswordsPasswordBroker | auth.password.broker |
IlluminateContractsAuthPasswordBrokerFactory | Password | IlluminateAuthPasswordsPasswordBrokerManager | auth.password |
IlluminateContractsBroadcastingFactory | Broadcast | IlluminateContractsBroadcastingFactory | |
IlluminateContractsBroadcastingBroadcaster | Broadcast::connection() | IlluminateContractsBroadcastingBroadcaster | |
IlluminateContractsCacheFactory | Cache | IlluminateCacheCacheManager | cache |
IlluminateContractsCacheRepository | Cache::driver() | IlluminateCacheRepository | cache.store |
IlluminateContractsConfigRepository | Config | IlluminateConfigRepository | config |
IlluminateContractsConsoleKernel | Artisan | IlluminateContractsConsoleKernel | artisan |
IlluminateContractsContainerContainer | App | ||
IlluminateContractsCookieFactory | Cookie | IlluminateCookieCookieJar | cookie |
IlluminateContractsEventsDispatcher | Event | IlluminateEventsDispatcher | events |
IlluminateContractsFilesystemCloud | Storage::cloud() | filesystem.cloud | |
IlluminateContractsFilesystemFactory | Storage | IlluminateFilesystemFilesystemManager | filesystem |
IlluminateContractsFilesystemFilesystem | Storage::disk() | IlluminateContractsFilesystemFilesystem | filesystem.disk |
IlluminateContractsFoundationApplication | App | IlluminateFoundationApplication | app |
IlluminateContractsHashingHasher | Hash | IlluminateContractsHashingHasher | hash |
IlluminateContractsMailMailer | Mail | IlluminateMailMailer | mailer |
IlluminateContractsNotificationsFactory | Notification | IlluminateNotificationsChannelManager | |
IlluminateContractsQueueFactory | Queue | IlluminateQueueQueueManager | queue |
IlluminateContractsQueueQueue | Queue::connection() | IlluminateQueueQueue | queue.connection |
IlluminateContractsRedisFactory | Redis | IlluminateRedisRedisManager | redis |
IlluminateContractsRoutingRegistrar | Route | IlluminateRoutingRouter | router |
IlluminateContractsRoutingResponseFactory | Response | IlluminateRoutingResponseFactory | |
IlluminateContractsRoutingUrlGenerator | URL | IlluminateRoutingUrlGenerator | url |
IlluminateContractsSessionSession | Session::driver() | IlluminateSessionStore | session.store |
IlluminateContractsTranslationTranslator | Lang | IlluminateTranslationTranslator | translator |
IlluminateContractsValidationFactory | Validator | IlluminateValidationFactory | validator |
IlluminateContractsValidationValidator | Validator::make() | IlluminateValidationValidator | |
IlluminateContractsViewFactory | View | IlluminateViewFactory | view |
IlluminateContractsViewView | View::make() | IlluminateViewView | |
IlluminateLogLogManager | log | ||
IlluminateHttpRequest | request | ||
IlluminateRoutingRedirector | redirect | ||
IlluminateDatabaseConnection | db.connection | ||
IlluminateDatabaseDatabaseManager | db |
Ví dụ
Chúng ta sẽ lấy code ở repo laravel-test-example để thực hiện refactor, vì nó đã được viết unit test với tỷ lệ coverage là ~90% =)) nên có thể yên tâm refactor mà không làm ảnh hưởng đến behavior hay function của hệ thống.
1 2 3 | git clone https://github.com/tuanpht/laravel-test-example/ code laravel-test-example |
Nếu bạn dùng vscode bạn có thể tìm kiếm theo regex này để tìm những chỗ đang dùng facade: [A-Z][a-z]+::w+(
.
Bắt đầu với class app/Http/Middleware/RedirectIfAuthenticated.php
:
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">namespace</span> <span class="token package">AppHttpMiddleware</span><span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Closure</span><span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">IlluminateContractsAuthFactory</span><span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name">RedirectIfAuthenticated</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">handle</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">,</span> Closure <span class="token variable">$next</span><span class="token punctuation">,</span> <span class="token variable">$guard</span> <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>Auth<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">guard</span><span class="token punctuation">(</span><span class="token variable">$guard</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">check</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">redirect</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'/home'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token variable">$next</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
After:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token keyword">namespace</span> <span class="token package">AppHttpMiddleware</span><span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">Closure</span><span class="token punctuation">;</span> <span class="token keyword">use</span> <span class="token package">IlluminateContractsAuthFactory</span><span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name">RedirectIfAuthenticated</span> <span class="token punctuation">{</span> <span class="token keyword">protected</span> <span class="token variable">$authFactory</span><span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">__construct</span><span class="token punctuation">(</span>Factory <span class="token variable">$authFactory</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">authFactory</span> <span class="token operator">=</span> <span class="token variable">$authFactory</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">handle</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">,</span> Closure <span class="token variable">$next</span><span class="token punctuation">,</span> <span class="token variable">$guard</span> <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">authFactory</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">guard</span><span class="token punctuation">(</span><span class="token variable">$guard</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">check</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">redirect</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'/home'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token variable">$next</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Diff:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | namespace AppHttpMiddleware; use Closure; <span class="token deleted">-use IlluminateSupportFacadesAuth;</span> <span class="token inserted">+use IlluminateContractsAuthFactory;</span> class RedirectIfAuthenticated { <span class="token inserted">+ protected $authFactory;</span> <span class="token inserted">+</span> <span class="token inserted">+ public function __construct(Factory $authFactory)</span> <span class="token inserted">+ {</span> <span class="token inserted">+ $this->authFactory = $authFactory;</span> <span class="token inserted">+ }</span> <span class="token inserted">+</span> public function handle($request, Closure $next, $guard = null) { <span class="token deleted">- if (Auth::guard($guard)->check()) {</span> <span class="token inserted">+ if ($this->authFactory->guard($guard)->check()) {</span> return redirect('/home'); } |
Tests vẫn pass! Vì thực ra chưa có test case cho class này =))
Tiếp tục là đến class app/Http/Controllers/Web/RegisterController.php
có 2 chỗ sử dụng Mail
facade, ta có thể refactor như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | use AppHttpRequestsWebRegisterRequest; use AppServicesWebUserService; <span class="token deleted">-use IlluminateSupportFacadesMail;</span> use AppMailUserRegistered; use IlluminateHttpRequest; <span class="token inserted">+use IlluminateContractsMailMailer;</span> class RegisterController extends Controller { protected $userService; <span class="token deleted">- public function __construct(UserService $userService)</span> <span class="token inserted">+ protected $mailer;</span> <span class="token inserted">+</span> <span class="token inserted">+ public function __construct(UserService $userService, Mailer $mailer)</span> { $this->userService = $userService; <span class="token inserted">+ $this->mailer = $mailer;</span> } /** @@ -39,7 +42,7 @@ class RegisterController extends Controller $user = $this->userService->create($inputs); <span class="token deleted">- Mail::to($user)->send(new UserRegistered($user->getKey(), $user->name));</span> <span class="token inserted">+ $this->mailer->to($user)->send(new UserRegistered($user->getKey(), $user->name));</span> return redirect()->action([static::class, 'showRegisterSuccess']); } @@ -68,7 +71,7 @@ class RegisterController extends Controller $user = $this->userService->findByEmail($request->input('email')); if ($user && !$user->hasVerifiedEmail()) { <span class="token deleted">- Mail::to($user)->send(new UserRegistered($user->getKey(), $user->name));</span> <span class="token inserted">+ $this->mailer->to($user)->send(new UserRegistered($user->getKey(), $user->name));</span> } return redirect()->action([static::class, 'showFormVerification'])->with('resent', true); |
Lần này thì tất nhiên test failed vì ta đã thay đổi constructor của class. Fix the tests!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | use AppModelsUser; <span class="token inserted">+use IlluminateSupportTestingFakesMailFake;</span> use SymfonyComponentHttpKernelExceptionNotFoundHttpException; class RegisterControllerTest extends TestCase @@ -21,13 +22,17 @@ class RegisterControllerTest extends TestCase /** @var UserService|MockeryMockInterface */ private $userService; <span class="token inserted">+ /** @var MailFake */</span> <span class="token inserted">+ private $mailer;</span> <span class="token inserted">+</span> public function setUp(): void { parent::setUp(); $this->userService = Mockery::mock(UserService::class); <span class="token inserted">+ $this->mailer = new MailFake;</span> <span class="token deleted">- $this->registerController = new RegisterController($this->userService);</span> <span class="token inserted">+ $this->registerController = new RegisterController($this->userService, $this->mailer);</span> } public function testShowFormRegister() @@ -60,11 +65,10 @@ class RegisterControllerTest extends TestCase ->shouldReceive('create') ->with($filteredInputs) ->andReturn(new User($filteredInputs)); <span class="token deleted">- Mail::fake();</span> $response = $this->registerController->register($request); <span class="token deleted">- Mail::assertQueued(UserRegistered::class);</span> <span class="token inserted">+ $this->mailer->assertQueued(UserRegistered::class);</span> $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals( action([RegisterController::class, 'showRegisterSuccess']), @@ -121,11 +125,10 @@ class RegisterControllerTest extends TestCase ->shouldReceive('findByEmail') ->with($inputs['email']) ->andReturn($fakeUser); <span class="token deleted">- Mail::fake();</span> $response = $this->registerController->resendVerificationLink($request); <span class="token deleted">- Mail::assertQueued(UserRegistered::class);</span> <span class="token inserted">+ $this->mailer->assertQueued(UserRegistered::class);</span> $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertEquals( action([RegisterController::class, 'showFormVerification']), |
Ở đây có thể dùng Mockery, sau đó setup expectation cho các method to()
, send()
nhưng nó sẽ phức tạp hơn chút nên mình không đề cập ở đây =)) Giải pháp ở bài này là sử dụng class MailFake
do Laravel cung cấp để hỗ trợ việc testing, khá tiện, code cũng không phải thay đổi nhiều.
Refactor with Rector
Tương tự với các class khác. Làm bằng tay thủ công để hiểu thêm chứ thật ra có tool tự động refactor được một số task common này đó là Rector – Upgrade Your Legacy App to a Modern Codebase. Giới thiệu qua thì đây là tool chuyên hỗ trợ việc refactor code tự động bằng việc phân tích source code, tương tự như một số static code analysis tools, một số tính năng:
- Đổi tên classes, methods, properties, namespaces or constants
- Upgrade PHP code từ version lên 7.4
- Migrate từ Nette sang Symfony
- Áp dụng PHP 7.4 typed property
- Refactor Laravel facades to DI
- Trả nợ kỹ thuật =))
- …
Mình thử install bằng composer nhưng bị conflict với Laravel nên thử dùng docker:
1 2 3 4 | docker run -it --rm -v <span class="token variable"><span class="token variable">$(</span><span class="token function">pwd</span><span class="token variable">)</span></span>:/project --entrypoint <span class="token string">"/bin/bash"</span> rector/rector:latest <span class="token function">cd</span> /project /rector/bin/rector process app --set laravel-static-to-injection --dry-run |
=> Kết quả chạy
Tool thì nó cũng là phần mềm, mà đã là phần mềm thì phải có bug. Nên vẫn cần review lại và cải thiện, ví dụ một số refactor mà Rector đang dùng là concrete class => chuyển sang interface tương ứng…
- Không thể áp dụng DI vào constructor của Service Provider vì nó chỉ chấp nhận một tham số là
IlluminateContractsFoundationApplication $app
- Không dùng được DI trong Model class
- Interface
IlluminateContractsRoutingUrlGenerator
không có methodtemporarySignedRoute()
thay vào đó phải dùng concrete classIlluminateRoutingUrlGenerator
- Với class Mailable
UserRegistered
, để thuận tiện khi gọi thì sẽ không dùng constructor DI, thay vào đó sẽ inject vào methodbuild(UrlGenerator $urlGenerator)
vì nó được gọi bởi service container (tương tự như methodhandle()
của Queue Job)12<span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">mailer</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">to</span><span class="token punctuation">(</span><span class="token variable">$user</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">send</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">UserRegistered</span><span class="token punctuation">(</span><span class="token variable">$user</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">getKey</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token variable">$user</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">name</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Seevendor/laravel/framework/src/Illuminate/Mail/Mailable.php
:123456789101112131415161718192021<span class="token comment">/*** Send the message using the given mailer.** @param IlluminateContractsMailMailer $mailer* @return void*/</span><span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">send</span><span class="token punctuation">(</span>MailerContract <span class="token variable">$mailer</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">return</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">withLocale</span><span class="token punctuation">(</span><span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">locale</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">use</span> <span class="token punctuation">(</span><span class="token variable">$mailer</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>Container<span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">call</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token variable">$this</span><span class="token punctuation">,</span> <span class="token single-quoted-string string">'build'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">return</span> <span class="token variable">$mailer</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">send</span><span class="token punctuation">(</span><span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">buildView</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">buildViewData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token variable">$message</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token variable">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">buildFrom</span><span class="token punctuation">(</span><span class="token variable">$message</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">buildRecipients</span><span class="token punctuation">(</span><span class="token variable">$message</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">buildSubject</span><span class="token punctuation">(</span><span class="token variable">$message</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">runCallbacks</span><span class="token punctuation">(</span><span class="token variable">$message</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">buildAttachments</span><span class="token punctuation">(</span><span class="token variable">$message</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span>
Using PHPCS?
Trong project cũng có thể thiết lập thêm convention là không sử dụng facade hay helpers, sử dụng custom snifffs được chia sẻ ở repo: https://github.com/vladyslavstartsev/laravel-strict-coding-standard
Cách sử dụng:
1 2 | composer require --dev vladyslavstartsev/laravel-strict-coding-standard |
Sau đó thêm rule vào file phpcs.xml
của dự án:
1 2 3 4 5 6 7 8 9 10 | <span class="token prolog"><?xml version="1.0"?></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ruleset</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>Project convention<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>rule</span> <span class="token attr-name">ref</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>LaravelStrictCodingStandard.Laravel.DisallowUseOfGlobalFunctions<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>rule</span> <span class="token attr-name">ref</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>LaravelStrictCodingStandard.Laravel.DisallowUseOfFacades<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>properties</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>property</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>laravelApplicationInstancePath<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>string<span class="token punctuation">"</span></span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>../bootstrap/app.php<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>properties</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>rule</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ruleset</span><span class="token punctuation">></span></span> |
VD:
1 2 3 4 5 6 7 8 9 10 11 12 | $ ./vendor/bin/phpcs app/Mail/UserRegistered.php -s FILE: /home/ubuntu/Projects/laravel-test-example/app/Mail/UserRegistered.php ------------------------------------------------------------------------------------------------------------- FOUND 2 ERRORS AFFECTING 2 LINES ------------------------------------------------------------------------------------------------------------- 43 <span class="token operator">|</span> ERROR <span class="token operator">|</span> It is strongly discouraged not to use URL Laravel Facade, switch to constructor injection <span class="token operator">|</span> <span class="token operator">|</span> <span class="token punctuation">(</span>LaravelStrictCodingStandard.Laravel.DisallowUseOfFacades.LaravelFacadeInstanceUsage<span class="token punctuation">)</span> 45 <span class="token operator">|</span> ERROR <span class="token operator">|</span> Laravel <span class="token keyword">function</span> now<span class="token punctuation">(</span><span class="token punctuation">)</span> has been deprecated, it is highly recommended not to use it <span class="token operator">|</span> <span class="token operator">|</span> <span class="token punctuation">(</span>LaravelStrictCodingStandard.Laravel.DisallowUseOfGlobalFunctions.LaravelGlobalFunctionUsage<span class="token punctuation">)</span> ------------------------------------------------------------------------------------------------------------- |
DI trong Blade view?
Vậy còn blade view, làm sao để tránh không sử dụng facade hay global helper?
Laravel cũng support sử dụng DI trong blade, sử dụng directive @inject
https://laravel.com/docs/7.x/blade#service-injection
1 2 3 4 5 6 | @<span class="token function">inject</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'authFactory'</span><span class="token punctuation">,</span> <span class="token single-quoted-string string">'IlluminateContractsAuthFactory'</span><span class="token punctuation">)</span> <span class="token operator"><</span>div<span class="token operator">></span> Welcome<span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">$authFactory</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">guard</span><span class="token punctuation">(</span><span class="token single-quoted-string string">'web'</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">user</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token operator">></span><span class="token property">name</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">.</span> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> |
Kết luận
Rốt cuộc là làm thế này để làm gì??
Vấn đề với facade là nó làm cho logic code gắn chặt vào framework, khó để extends hay customize. Thực ra rất ít khi “người ta” thay đổi framework hay customize lại core service, nhưng cứ liệt kê ra đây cho bạn nào thích mày mò sâu hơn =))
Vì sử dụng facade liên quan nhiều đến magic methods nên rất khó để IDE có thể support autocomplete hiệu quả, phải cần đến package bổ sung như IDE helper.
Lợi ích rõ nhất của việc dùng DI đó là chúng ta biết được class phụ thuộc vào những class hay interface nào khác, dễ dàng track xem các method được gọi, IDE và code editor support tốt hơn vì variable đã được type-hinting (cũng là 1 bước để tiến tới more strongly type ) trong contructor.
Các framework lớn khác thường áp dụng DI đó là Symfony và Magento 2, ở đó sẽ có file config dạng yaml hay xml để bind interface và class (mình sẽ nói trong series về Magento), rất dễ để biết được ứng với interface được inject là concrete class nào và cũng dễ dàng để thay thế hay extend core service. Còn việc track down facade, service provider, alias của Laravel để tìm ra class implement function tương ứng thì hơi khó so với người mới bắt đầu.
Dùng DI thì code nó có chút dài dòng hơn vì phải khởi tạo trong constructor, nhưng nếu bạn cảm thấy constructor có quá nhiều dependencies thì đó là do class của bạn đang làm quá nhiều việc, đến lúc refactor. Laravel docs cũng có note:
However, some care must be taken when using facades. The primary danger of facades is class scope creep. Since facades are so easy to use and do not require injection, it can be easy to let your classes continue to grow and use many facades in a single class. Using dependency injection, this potential is mitigated by the visual feedback a large constructor gives you that your class is growing too large. So, when using facades, pay special attention to the size of your class so that its scope of responsibility stays narrow.
Tạm dịch: Tuy nhiên bạn phải chú ý khi dùng facade. Vì rất là dễ dàng để sử dụng facade mà không cần thông qua injection, nó có thể khiến phạm vi class càng phình to, class làm quá nhiều việc. Nếu sử dụng DI, nguy cơ này có thể dễ dàng được feedback bằng việc constructor càng trở nên dài dòng. Vì vậy, khi dùng facade hãy chú ý đến phạm vi và chức năng của class để đảm bảo nó không làm quá nhiều việc.
Thói quen dùng facade của Laravel có lẽ khó mà thay thế được bởi, nhưng hy vọng bài viết giúp bạn có thêm chút kiến thức và tiến tới mục đích làm được những project tầm cỡ, đa dạng hơn để còn có điều kiện nâng cao trình độ, kinh nghiệm
References
- https://laravel.com/docs/5.8/facades
- https://laravel.com/docs/5.8/contracts
- https://github.com/rectorphp/rector/
- https://www.tomasvotruba.com/blog/2019/03/04/how-to-turn-laravel-from-static-to-dependency-injection-in-one-day/
- https://www.freecodecamp.org/news/moving-away-from-magic-or-why-i-dont-want-to-use-laravel-anymore-2ce098c979bd/
- https://github.com/vladyslavstartsev/laravel-strict-coding-standard
- https://github.com/Dealerdirect/phpcodesniffer-composer-installer