Pourquoi j'ai besoin des "Facades" ?
Imaginez que vous souhaitiez réécrire un système de persistance de données dans votre projet Laravel. Dans la "vraie" vie nous ne le ferions certainement pas, mais imaginons pour l'exemple que nous en avons vraiment besoin pour un projet.
Dans un premier temps, votre projet va être hébergé sur un petit serveur, vous pourrez vous servir du disque dur du serveur pour stocker vos fichiers, mais avec le temps vous savez que vous allez devoir modifier votre code pour héberger vos données sur d'autres systèmes de fichiers, comme Amazon S3 ou Google Drive par exemple.
La pratique SOLID serait de créer une interface qui va spécifier les fonctions à implémenter par vos classes. Une fois les implémentations réalisées, il vous faudra injecter la bonne implémentation dans vos classes métier.
On ne va pas se mentir, si c'est tenable sur un petit projet, sur de gros projet cela peut vite tourner au cauchemar avec des dizaines de dépendances à injecter et parfois un code un peu lourd pour instancier ces dépendances. Tout cela risque de nuire à la bonne lisibilité du code.
Ce qui serait ultime, c'est de pouvoir utiliser un objet, capable de modifier son comportement interne en fonction d'un fichier de configuration. Une sorte de proxy
permettant de pouvoir, sans toucher à notre code métier, modifier où et comment vont s'enregistrer nos fichiers.
C'est justement là qu'intervient le pattern de façade :
Les façades sont des proxy élégants entre votre code métier et vos implémentation.
Le pattern de façade va vous permettre de gagner à la fois en flexibilité mais aussi en lisibilité, en masquant l'instanciation et en rendant inutile l'injection de la dépendance.
Dit comme ça, ça ressemble presque à de la magie... Démystifions donc ce pattern dans Laravel.
Comment ça marche avec Laravel
Pour réaliser le pattern de facade, Laravel utiliser deux mécanismes :
- Le premier mécanisme qui va lier un
identifiant
et unefonction
qui sert souvent renvoyer une instance d'objet. (le binding) - Le second mécanisme qui va utiliser le mécanisme précédent pour simplifier l'accès à cette
instance
. (la facade)
Réalisation d'une facade "custom"
Pour réaliser cela je procéderais en 4 étapes :
- Préparer les fichiers pour la démonstration (le plus long).
- Réaliser le
binding
. - Créer une
facade
pour utiliser cebinding
. - Ajouter un alias à la façade pour l'utiliser de manière élégante dans le code.
1) Préparation des fichiers
Ajoutez ces 3 fichiers dans votre projet Laravel :
app\FileSystems\FileSystemInterface.php
: Ce fichier va définir le comportement commun que doivent implémenter de nos classes.
<?php
namespace App\FileSystems;
interface FileSystemInterface
{
public static function getContent(string $file): string;
}
app\FileSystems\LocalFileSystem.php
: Cette classe est chargée de lire un fichier sur le disque local et de retourner son contenu. On peut imaginer cela comme notre première implémentation.
<?php
namespace App\FileSystems;
class LocalFileSystem extends FileSystemInterface
{
public static function getContent(string $file): string
{
return file_get_contents($file);
}
}
app\FileSystems\MockFileSystem.php
: Cette classe va prendre n'importe quel nom de fichier et simuler une lecture en retournant toujours la chaîne foobar
. On considérera cette classe comme notre seconde implémentation.
<?php
namespace App\FileSystems;
class MockFileSystem extends FileSystemInterface
{
public static function getContent(string $file): string
{
return "foobar";
}
}
Tant que nous y sommes, nous allons aussi ajouter un fichier de configuration afin de définir le comportement de notre façade :
config\custom.php
<?php
return [
'filesystem' => 'local'
];
2) Binding des classes
Pour réaliser le binding, nous allons associer un identifiant
et une méthode
d'instanciation dans votre application Laravel. Le binding est géré par le ServiceContainer
. Plus précisément, c'est réalisé dans l'un des ServiceProvider
que votre application aura référencée dans le fichier config/app.php
.
Promis, les ServiceProviders
feront l'objet d'un prochain article, mais pour que cet article reste court, nous placerons le code dans la méthode register
du fichier app/Providers/AppServiceProvider
/**
* Register any application services.
*/
public function register(): void
{
// Ajouter ce code pour réaliser le binding
$this->app->bind('customFileSystem', function() {
$fileSystem = config('custom.filesystem');
return ($fileSystem == 'local')
? new \App\FileSystems\LocalFileSystem()
: new \App\FileSystems\MockFileSystem();
});
}
Ce binding
va associer l'identifiant customFileSystem
à l'appel de fonction passé en paramètre juste après.
A chaque fois que le Laravel tentera de résoudre (resolve
) à quoi correspond l'identifiant customFileSystem
, la fonction associée sera appelée, et en fonction du paramètre de configuration, la fonction retournera soit l'instance LocalFileSystem
soit l'instance MockFileSystem
.
On peut tester ce comportement via tinker :
> resolve('customFileSystem')
Ceci devrait vous donner une instance d'objet différente en fonction de votre fichier de configuration.
Pensez à RELANCER
tinker
pour que les modifications de configuration soient prises en compte ;)
3) La facade
Il ne nous reste plus qu'à intégrer ceci dans une façade pour utiliser ce binding comme un objet dans notre code. Pour cela nous allons ajouter le fichier suivant :
app\FileSystems\FileSystemFacade.php
<?php
namespace App\FileSystems;
use Illuminate\Support\Facades\Facade;
class FileSystemFacade extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'customFileSystem'; // C'est le nom du binding !
}
}
Dans Tinker
nous pouvons maintenant utiliser :
> App\FileSystems\FileSystemFacade::getContent('./config/custom.php');
Le résultat sera différents selon la configuration de votre fichier config/custom.php
4) Ajouter un alias sur la facade
Même si c'est déjà utilisable, il est possible de rendre les choses encore plus magiques en utilisant un alias !
Pour cela, ouvrez le fichier config/app.php
. Tout en bas, vous trouverez un tableau clé/valeur à remplir.
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => Facade::defaultAliases()->merge([
'CustomFileSystem' => App\FileSystems\FileSystemFacade::class,
])->toArray(),
Dans Tinker
comme partout dans notre code nous pouvons maintenant utiliser :
> CustomFileSystem::getContent('./config/custom.php')
Conclusion
J'espère que ce billet vous a aidé à mieux comprendre le concept de facades en Laravel. Bien que ce pattern puisse paraître un peu magique, il est extrêmement utile pour rendre une application modulaire.
Utiliser les façades en Laravel c'est comme prendre une assurance sur l'avenir. Si vous implémentez aujourd'hui une API via ce pattern, nous n'aurez aucun mal à ajouter un autre fournisseur d'API sans toucher à votre code métier à l'avenir. C'est très pratique quand vos applications doivent durer dans le temps, comme celles de NCI.
Merci pour votre lecture !