diff --git a/source/_posts/2025-07-18-why-i-disallow-facade-usage.md b/source/_posts/2025-07-18-why-i-disallow-facade-usage.md new file mode 100644 index 00000000..ab58bbcf --- /dev/null +++ b/source/_posts/2025-07-18-why-i-disallow-facade-usage.md @@ -0,0 +1,338 @@ +--- +layout: post +title: Why I disallow Laravel Facade usages +category: Security +tags: [ "PHP", "Laravel", "Facade", "Anti-Patterns" ] +year: 2025 +month: 07 +day: 18 +published: true +summary: "Laravel Facades: why I disallow their usage in my software project" +description: A technical overview of why Facades are problematic from a software engineering point of view, and why I disallow them in my software projects +--- + +
+ For non-Laravel developers: beware that this is not about the facade pattern. +
+ ++ Recently, I had a heated discussion with a software developer: they approached me, asking + me why I had flagged all usages of Laravel facades in a code review. +
+ ++ Laravel Facades are a pattern that has been deeply ingrained in the development practices of teams that + solely design Laravel code, and they are mostly promoted in terms of DX, + rather than technical merits. +
+ ++ Following is a piece of code that uses a Laravel Facade to store some data in cache: +
+ +@TODO change this example to use a different facade: something that sends an email notification would be nice + +~~~php +heavyLifting(); + + Cache::put('a key', $result); + + return $result; + } + + private function heavyLifting(): Result + { + // ... irrelevant ... + } +} +~~~ + ++ From an inexperienced observer's point of view, this code is relatively straightforward: it + does some heavy work, it stores the information in a cache, then returns. +
+ ++ @TODO we need a stronger example here: what's a good "whoops" example, like a checkout on the wrong banking coordinates? +
+ ++ @TODO expose how this problem occurs especially when: + * an application grows in size, accommodating for multiple service instances + * tests: multiple services needed, one per test! +
+ ++ A second problem occurs when running this code in isolation: assuming you have **only** + autoloading configured, what will it do? +
+ +~~~php +doSomeHeavyWork(); // this will crash: the cache was never configured! +~~~ + ++ This kind of crash is very similar to what you'd experience with the service location pattern: since + you are deferring all instantiation to the point at which a dependency is effectively invoked, you + may run in crashes due to a missed dependency. +
+ +
+ In addition to the disadvantages of service-location, you also have the hidden dependency of the facade's
+ internal
+
+ static::$app
+ .
+
+ Your code cannot work until you've bootstrapped a Laravel application, which is a complex and heavy + (performance-wise) operation. +
+ + +
+ Another issue with our MyService is that we have widely expanded the contact surface with the
+ framework: our code now depends on illuminate/support, and also on illuminate/contracts
+ in order for it to function at basic level.
+
+ This is both an issue of Laravel, which exposes a very bloated illuminate/contracts package, and
+ of our code, since framework upgrades can now affect our ability to perform upgrades.
+
+ Coming myself from the + DDD + community, isolating software dependencies is critical for the long-term maintenance of a system, and reducing + the amount of dependencies is always a good idea, as it is entropy that will easily spin out of control. +
+ ++ In this case, we wanted a cache, not the entire framework. +
+ ++ @TODO expose here how adding a facade introduces magic method calls, stack frames, static analysis complexity. +
+ ++ @TODO expose concept of "simple" != "easy". Systems can be complex and easy, or harder to use, but simple. +
+ ++ @TODO expose how the facade is "hidden API" - not all interactions with the object are exposed by the public + class signature anymore, while constructor shows clear inputs/outputs. +
+ ++ Here's our service re-implemented to make things simple and explicit: +
+ +~~~php +heavyLifting(); + + $this->cache->put('a key', $result); + + return $result; + } + + private function heavyLifting(): Result + { + // ... irrelevant ... + } +} +~~~ + ++ The above is a little more verbose, but follows very old and well-functioning + dependency injection rules. +
+ + ++ By adding 2 lines of code, we: +
+ ++ Does the above solution have worse DX? + Given all the listed advantages, I think it provides much better experience. +
+ ++ You have to also remember that facades were designed and built in an age when auto-wiring dependency injection + containers weren't common in PHP: the simpler approach may actually even be easier, when using the full framework. +
+ ++ @TODO Laravel has MyFacade::spy() and MyFacade::mock() helpers - let's document those +
+ +~~~php +with('a key', 'a value'); + + $service = new MyService(); + + $result = $service->doSomeHeavyWork(); + + // more assertions on $result + } +} +~~~ + ++ The above example swaps the facade underlying service location mechanism with a mock, and relies on an + + extremely complex base test class + to do framework startup/cleanup operations. +
+ ++ Here's the "simple" version instead: +
+ + +~~~php +createMock(Cache::class); + + $cache->expects(self::once()) + ->method('put') + ->with('a key', 'a value'); + + $service = new MyService($cache); + + $result = $service->doSomeHeavyWork(); + + // more assertions on $result + } +} +~~~ + ++ Notice how we got rid of: +
+ ++ Don't like using automatic mocks by PHPUnit? Bring your own! +
+ +~~~php + */ + private array $recorded = []; + function put(string $key, mixed $value) { + $this->recorded[$key] = $value; + } +} + +// look! No testing framework either! +function my_test() { + $cache = new CacheSpy() + + $service = new MyService($cache); + + $result = $service->doSomeHeavyWork(); + + assert('a value' === $cache->recorded['a key']); + + // more assertions +} +~~~ + ++ The above is obviously brought to extremes, but it highlights the added degrees of fredom that are introduced. +
+ +
+ Now imagine having to upgrade a test suite with 5000 tests, all depending on Facade spies
+ and Illuminate\Foundation\Testing\TestCase: sounds fun? No? I had to do it, a few times,
+ and I can also assure you it's not fun.
+
+ From my point of view, Facades are technical debt, and of a particularly bad and sneaky kind. +
+ ++ There is no reason to skip the extra legwork to keep a system simple: systems increase complexity + over time by nature, and it is our job as software designers to keep it at bay. +
+ ++ Introducing complexity for the sole purpose of some very questionable DX + claims is therefore not acceptable, and is not something I accept in software systems that I manage. +
\ No newline at end of file