Refactoring Laravel facades to Dependency Injection

Tram Ho

Introduce

Laravel Facades like Auth , View , Mail , or helpers like auth() , view() , … have many hidden magic to make our code shorter and faster. But because it is so magic, it is quite difficult to understand deeply or when debugging, we only know how to use it but not know how it works? Then in a project, some use Facade, some use helpers all over the place ??

Have you ever seen the code of the Auth class, actually it is IlluminateSupportFacadesAuth , Auth is alias config in config/app.php file, its code is like this:

How Facade works

If you look into the Auth::user() method from the IDE or the editor, it will point you to the line:

You think to yourself, “oh, where’s the ?? code ??”. As you probably already know, the above line is just a comment in the docblock used when generating documents with phpdoc or to suggest IDE autocomplete the “magic” method. It is just a document type. And the code is of course magic. The magic here is the PHP Magic Method __callStatic() :

vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php

When you call Auth::user() , it will run like this:

  1. Since the Auth class has no public static method, user() , the __callStatic('user') magic method will be called.
  2. Inside Laravel will retrieve the auth instance from the service container
  3. Call the user() method of the auth instance

To know where the auth instance is bound to the service container, you have to look in the service provider, fortunately it is often named by convention, for example, with Auth Facade, there will be Auth Service Provider => vendor/laravel/framework/src/Illuminate/Auth/AuthServiceProvider.php :

So the auth instance here is an instance of class IlluminateAuthAuthManager . Inside this class there is an additional magic method, you can read the code and learn more

Contracts (Interface)

This auth is a service of core service type, so it also has additional alias declared in vendor/laravel/framework/src/Illuminate/Foundation/Application.php :

It mean:

These three lines will produce the same result, both resolved instances of class IlluminateAuthAuthManager .

So we have the idea for using DI here is to inject IlluminateAuthAuthManager or the most accurate way is inject contract (interface) is IlluminateContractsAuthFactory instead of concrete (concrete? ) class, just like the code of the auth() function:

The explanation is somewhat dangerous, but Laravel docs – Facades , Laravel docs – Contracts also mentioned about this issue.  The use of facades or DI depends on the experience of each individual or team, we have used a lot of facade and helper functions, try using contract to inject dependency to see how it is.

Some commonly used contracts and facades:

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

For example

We will take the code in repo laravel-test-example to perform refactor, because it was written unit test with coverage ratio of ~ 90% =)), so you can rest assured refactor without affecting behavior or function of the system.

If you use vscode you can search this regex to find places using the facade: [AZ][az]+::w+( .

Start with the app/Http/Middleware/RedirectIfAuthenticated.php :

Before:

After:

Diff:

Tests still pass! Because there is actually no test case for this class =))

Next is to go to the app/Http/Controllers/Web/RegisterController.php has 2 places using the Mail facade, we can refactor as follows:

This time, of course, the test failed because we changed the constructor of the class. Fix the tests!

Here you can use Mockery, then setup expectation for to() , send() methods but it will be a bit more complicated, so I will not mention here =)) The solution in this article is to use the MailFake class by Laravel Provided to support testing, quite convenient, the code does not have to change much.

Refactor with Rector

Similar to other classes. Manually manual to understand more, but actually a tool that automatically refactor some of these common tasks which is Rector – Upgrade Your Legacy App to a Modern Codebase . Referring to this, this tool is specialized in supporting automatic refactor code by analyzing source code, similar to some static code analysis tools , some features:

  • Rename classes, methods, properties, namespaces or constants
  • Upgrade PHP code from version to 7.4
  • Migrate from Nette to Symfony
  • Apply PHP 7.4 typed property
  • Refactor Laravel facades to DI
  • Technical debt repayment =))

I tried to install with composer but got conflicted with Laravel, so I tried using docker:

=> Running results

The tool is also a software, but a software must have a bug. Should still need to review and improve, for example some refactor that Rector is using is concrete class => switch to the corresponding interface …

=> Update results

  • Cannot apply DI to the Service Provider constructor because it only accepts a parameter of IlluminateContractsFoundationApplication $app
  • Cannot use DI in Model class
  • The IlluminateContractsRoutingUrlGenerator does not have a temporarySignedRoute() method, instead the concrete class IlluminateRoutingUrlGenerator must be used.
  • With the Mailable UserRegistered class, for convenience, it will not use the constructor DI, instead inject inject into the build(UrlGenerator $urlGenerator) method build(UrlGenerator $urlGenerator) because it is called by the service container (similar to Queue Job ‘s method handle() )

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

Using PHPCS?

In the project you can also set up a convention that doesn’t use the facade or helpers, use the custom snifffs shared at the repo: https://github.com/vladyslavstartsev/laravel-strict-coding-standard

Using:

Then add the rule to the project’s phpcs.xml file:

For example:

DI in Blade view?

What about blade view, how to avoid using the facade or global helper?

Laravel also supports using DI in blades, using the @inject directive https://laravel.com/docs/7.x/blade#service-injection

Conclude

What is this for?

The problem with the facade is that it makes the logic code tightly tied to the framework, making it difficult to extend or customize. In fact, very rarely “people” change the framework or customize the core service, but just list out here for anyone who wants to dig deeper =))

Because the facade is related to magic methods, it is difficult for the IDE to support autocomplete effectively, requiring an additional package such as the IDE helper .

The most obvious benefit of using DI is that we know the class depends on which other classes or interfaces, it is easier to track which methods are called, the IDE and the code editor support better because the variable has been type-hinting (also is a step towards more strongly type ? ) in the contructor.

Other major frameworks that often apply DI are Symfony and Magento 2, there will be yaml or xml configuration files to bind interfaces and classes (I will talk in the series about Magento), it is easy to know which concrete class is injected with the interface and it is also easy to replace or extend core service. As for the track down facade, service provider, and alias of Laravel to find the corresponding implement function class, it is a bit difficult for beginners.

Using DI, the code is a bit longer because it requires initialization in the constructor, but if you feel that the constructor has too many dependencies, it is because your class is doing too much work, until refactor. Laravel docs also have notes:

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.

Temporarily translated: However, you must take care when using the facade. Because it is easy to use the facade without injection, it can make the class scope bigger, the class does too much work. If using DI, this risk can easily be answered by constructors becoming more verbose. So when using the facade, pay attention to the scope and functionality of the class to make sure it doesn’t do too much work.

The habit of using the Laravel facade may be difficult to replace, but hopefully the article will help you gain a little more knowledge and progress toward the purpose of making larger, more diverse projects so that you can have more advanced skills. degrees, experience

References

Share the news now

Source : Viblo