Tái cấu trúc mặt tiền của Laravel để tiêm phụ thuộc

Tram Ho

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:

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:

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

Khi bạn gọi Auth::user() thì nó sẽ chạy như thế này:

  1. Do class Auth không có public static method nào là user() nên magic method __callStatic('user') sẽ được gọi
  2. Bên trong Laravel sẽ lấy ra instance auth từ service container
  3. Gọi method user() của instance auth

Để 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:

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:

Tức là:

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():

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:

ContractFacadeClassCore Service
IlluminateContractsAuthAccessGateGateIlluminateContractsAuthAccessGate
IlluminateContractsAuthFactoryAuthIlluminateAuthAuthManagerauth
IlluminateContractsAuthGuardAuth::guard()IlluminateContractsAuthGuardauth.driver
IlluminateContractsAuthPasswordBrokerPassword::broker()IlluminateAuthPasswordsPasswordBrokerauth.password.broker
IlluminateContractsAuthPasswordBrokerFactoryPasswordIlluminateAuthPasswordsPasswordBrokerManagerauth.password
IlluminateContractsBroadcastingFactoryBroadcastIlluminateContractsBroadcastingFactory
IlluminateContractsBroadcastingBroadcasterBroadcast::connection()IlluminateContractsBroadcastingBroadcaster
IlluminateContractsCacheFactoryCacheIlluminateCacheCacheManagercache
IlluminateContractsCacheRepositoryCache::driver()IlluminateCacheRepositorycache.store
IlluminateContractsConfigRepositoryConfigIlluminateConfigRepositoryconfig
IlluminateContractsConsoleKernelArtisanIlluminateContractsConsoleKernelartisan
IlluminateContractsContainerContainerApp
IlluminateContractsCookieFactoryCookieIlluminateCookieCookieJarcookie
IlluminateContractsEventsDispatcherEventIlluminateEventsDispatcherevents
IlluminateContractsFilesystemCloudStorage::cloud()filesystem.cloud
IlluminateContractsFilesystemFactoryStorageIlluminateFilesystemFilesystemManagerfilesystem
IlluminateContractsFilesystemFilesystemStorage::disk()IlluminateContractsFilesystemFilesystemfilesystem.disk
IlluminateContractsFoundationApplicationAppIlluminateFoundationApplicationapp
IlluminateContractsHashingHasherHashIlluminateContractsHashingHasherhash
IlluminateContractsMailMailerMailIlluminateMailMailermailer
IlluminateContractsNotificationsFactoryNotificationIlluminateNotificationsChannelManager
IlluminateContractsQueueFactoryQueueIlluminateQueueQueueManagerqueue
IlluminateContractsQueueQueueQueue::connection()IlluminateQueueQueuequeue.connection
IlluminateContractsRedisFactoryRedisIlluminateRedisRedisManagerredis
IlluminateContractsRoutingRegistrarRouteIlluminateRoutingRouterrouter
IlluminateContractsRoutingResponseFactoryResponseIlluminateRoutingResponseFactory
IlluminateContractsRoutingUrlGeneratorURLIlluminateRoutingUrlGeneratorurl
IlluminateContractsSessionSessionSession::driver()IlluminateSessionStoresession.store
IlluminateContractsTranslationTranslatorLangIlluminateTranslationTranslatortranslator
IlluminateContractsValidationFactoryValidatorIlluminateValidationFactoryvalidator
IlluminateContractsValidationValidatorValidator::make()IlluminateValidationValidator
IlluminateContractsViewFactoryViewIlluminateViewFactoryview
IlluminateContractsViewViewView::make()IlluminateViewView
IlluminateLogLogManagerlog
IlluminateHttpRequestrequest
IlluminateRoutingRedirectorredirect
IlluminateDatabaseConnectiondb.connection
IlluminateDatabaseDatabaseManagerdb

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.

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:

After:

Diff:

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:

Lần này thì tất nhiên test failed vì ta đã thay đổi constructor của class. Fix the tests!

Ở đâ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:

=> 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…

=> Update kết quả

  • 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ó method temporarySignedRoute() thay vào đó phải dùng concrete class IlluminateRoutingUrlGenerator
  • 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 method build(UrlGenerator $urlGenerator) vì nó được gọi bởi service container (tương tự như method handle() của Queue Job)

    See vendor/laravel/framework/src/Illuminate/Mail/Mailable.php:

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:

Sau đó thêm rule vào file phpcs.xml của dự án:

VD:

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

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

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo