Stop Using Facades
Apparently when experienced developers are introduced to Laravel they're immediately disgusted by all of the static method calls they see. As soon as someone vocalizes this "atrocity" there are multiple prominent Laravel ambassadors to quickly defend the framework by explaining the facade pattern, "Nope, you're wrong! It's actually using OOP behind the scenes!"
Both sides have valid points. Here's my case against using them...
It seems like an anti-pattern
Many people view injecting your IoC container into your classes as an anti-pattern. I can understand this. If I were looking at someone else's class and trying to figure out what its dependencies were, I would absolutely prefer to be able to look at the constructor rather than track down everywhere the injected container is used.
Injecting the IoC container seems like an easy way to accidentally turn your unit tests into integration tests. Imagine this, you come back to work on a class which you've injected the container. In this class, you add code to use the container to retrieve and use a new service object. It's much easier to forget to go back to your unit test and mock that new service object. Your test might even still pass if you forget to come back. You wouldn't have that problem if you injected the service object rather than resolved it out of the IoC. Your unit tests would fail immediately if you used constructor injection(which would be a good thing).
In my opinion, using facades in Laravel introduces the same baggage as injecting the IoC container. It's practically the same thing, if not worse. The application/IoC container is statically available on all facade classes. This service location is available everywhere. With Laravel, you don't have a choice. However, Laravel facades are great for new devs getting their feet wet with OOP in a solid framework. I can completely understand why Taylor included them. It's sexy for some people.
They tie you to Laravel
Recently there's been a lot of twitter/blog/reddit bantering about being more mindful of creating framework agnostic packages. Phil Sturgeon recently posted an article on his blog about ditching framework fanboy-ism and focusing more on interoperability among code bases. He ruffled some feathers but I agree with his main point. If you develop framework agnostic code, you and others are going to be able to use it when you work in another project. I don't think anyone will argue that that's not a good thing.
You probably don't have framework interoperability code as your primary focus when developing, but by using Laravel's facades you're immediately tying that code to Laravel.
There are some places, like controllers, that this argument is moot. You're most likely not going to share controllers. I'd still advocate ditching facades in your controllers just for the sake of establishing the habit. Establish this habit for your service objects! There's a far better chance you're going to like a service class you wrote and want to use that code again. It'll be much easier if you typehinted an interface into the constructor of that service object's class rather than have to refactor the facades out to be able to use it in framework X.
Okay, I'm sold. How do I do it?
Let's start with the default HomeController
shipped with every Laravel install.
class HomeController extends BaseController
{
public function showWelcome()
{
return View::make('hello');
}
}
Let's first inject the renderer. If we take a peek under the hood at the facade located at, project/vendor/laravel/framework/src/Illuminate/Support/Facades/View.php
we can see the string view
being returned from the getFacadeAccessor()
method. This facade is pulling out the object from the IoC container with the key of view
. With this information we can register a binding.
// project/bootstrap/start.php
/* --other code-- */
$app->bind('HomeController', function($app) {
return new HomeController($app->make('view'));
});
return $app;
Now we have to update update our controller...but we don't actually know what in the world $app->make('view')
returns. After a bit of searching you can see in the Illuminate\View\ViewServiceProvider::registerEnvironment()
method the actual object is an instance of Illuminate\View\Environment
. Let's modify the controller.
use Illuminate\View\Environment as View;
class HomeController extends BaseController
{
protected $view;
public function __construct(View $view)
{
$this->view = $view;
}
public function showWelcome()
{
return $this->view->make('hello');
}
}
View
to be consistent with the facade. This will probably make people using facades more comfortable with your code.
This is already a heck of lot clearer to see what classes are being used. It will be much easier to tell if we're adhering to the Single Responsibility Principle.
Let's accommodate the Input
facade for funzies.
This one is a little harder to track down. If you open up the Input.php
facade file, you'll see request
being returned. I found this binding in the project/vendor/laravel/framework/src/Illuminate/Foundation/Application.php
file. You can see the request
binding in the registerBaseBindings()
method. We can add this to the controller like so:
use Illuminate\Http\Request as Input;
use Illuminate\View\Environment as View;
class HomeController extends BaseController
{
protected $input;
protected $view;
public function __construct(Input $input, View $view)
{
$this->input = $input;
$this->view = $view;
}
public function showWelcome()
{
$test = $this->input->get('test');
return $this->view->make('hello');
}
}
Then update our binding.
// project/bootstrap/start.php
/* --other code-- */
$app->bind('HomeController', function($app) {
return new HomeController($app->make('request'), $app->make('view'));
});
return $app;
Now if we go to /?test=123
, $test
will equal "123". Perfect.
Stripping facades from service objects
You'll notice we didn't create any interfaces for the controller. Again, this is because we're not going to be sharing controllers across frameworks.
I would create my own interface if I were writing code that I, or others, could possibly be used in the future outside of Laravel. Take the following example.
use MyUserInterface as UserInterface;
class PasswordResetService
{
protected $user;
public function setUser(UserInterface $user)
{
$this->user = $user;
}
public function fire()
{
$email = $this->user->getEmail();
$message = 'Follow the link: www.project.dev/reset/' . $this->user->getUniqueString();
Queue::push('SendEmail', array('email' => $email, 'message' => $message));
}
}
Because we're using the Queue
facade, we've unnecessarily tied this service to Laravel. Let's fix that.
use MyUserInterface as UserInterface;
use MyQueueInterface as QueueInterface;
class PasswordResetService
{
protected $queue;
protected $user;
public function __construct(QueueInterface $queue)
{
$this->queue = $queue;
}
public function setUser(UserInterface $user)
{
$this->user = $user;
}
public function fire()
{
$email = $this->user->getEmail();
$message = 'Follow the link: www.project.dev/reset/' . $this->user->getUniqueString();
$this->queue->push('SendEmail', array('email' => $email, 'message', $message));
}
}
Here's what those new interfaces and class could look like.
interface MyUserInterface {
public function getEmail();
public function getUniqueString();
}
interface MyQueueInterface {
public function push($service, array $data);
}
class MyQueueService implements MyQueueInterface
{
public function push($service, array $data)
{
Queue::push($service, $data);
}
}
We're basically just wrapping the facade. We won't use MyQueueService
outside of Laravel. It's adapter code. Now we just need to bind it in the IoC container.
// project/bootstrap/start.php
/* --other code-- */
$app->bind('PasswordResetService', function($app) {
return new PasswordResetService($app->make('MyQueueService'));
});
return $app;
This is a very simple example but we've completely removed Laravel from this service class. We could package our interfaces and service class up and use it in any other framework now. Awesome.
Get in the habit
Hopefully I've adequately illustrated the benefits of not using facades. Although Laravel's facades are super easy to use, injecting the actual dependencies could really pay off down the line. I think everyone, even Laravel users, will prosper if the use of facades gets phased out. Go forth and develop framework agnostic code by ditching facades!
Categories: PHP