L’Architecture Hexagonale est une façon de structurer une application pour qu’elle soit indépendante des détails externes. Elle insiste sur la séparation des préoccupations en découpant l’application en plusieurs couches distinctes.

Le but est de rendre l’application indépendante des technologies spécifiques, en organisant les interactions entre le cœur de l’application (la logique métier) et le monde extérieur (base de données, interfaces utilisateur, API, frameworks, etc.), tout ça via des Interfaces et des Implémentations.

Concept de base

Imagine ton application comme un hexagone avec, au centre, le domain (la logique métier). Ce cœur métier ne doit pas être directement lié aux détails d’implémentation (comme Symfony, Doctrine, une API tierce, etc.). Pour y arriver, il va falloir créer des interfaces appelées Ports, et ces interfaces vont être implémentées par des Adapters, qui s’occupent de la communication avec les services externes.

🛂 Ports : autrement dit, les interfaces par lesquelles le monde extérieur peut interagir avec le noyau métier. Les ports, ce sont les points d’entrée ou de sortie de l’application (API, requêtes HTTP, commandes CLI, etc.).

🎯 Adapters : les adapters sont les implémentations concrètes de ces interfaces. Ils traduisent les demandes ou réponses externes en actions compréhensibles pour la logique métier. Cela inclut les interactions avec les bases de données, les frameworks, les API, etc.

Pourquoi utiliser l’Architecture Hexagonale ?

  1. En séparant ta logique métier des implémentations externes (base de données, HTTP, API externes), tu rends ton code plus flexible. Changer de base de données ou de framework devient plus simple et moins risqué.

  2. Comme la logique métier n’est pas liée à des éléments externes, tu peux facilement mock ou remplacer les adaptateurs dans tes tests.

  3. Avec cette séparation claire, il est plus facile d’ajouter de nouvelles fonctionnalités, de modifier des comportements ou de réutiliser certains composants sans toucher à la logique métier.

  4. Tu peux réutiliser le même cœur métier avec différents types d’adaptateurs. Par exemple, la même logique métier peut être utilisée pour une application web, une API REST ou encore une interface en ligne de commande.

Structure d’une application

1. Domain: le coeur du métier

C’est le noyau de l’application, la partie qui n’a aucune dépendance externe. C’est là que réside la logique métier, comme la création d’un utilisateur, la validation de ses données, etc.

namespace App\Domain\Utilisateur;
 
readonly class Utilisateur
{
	public function __construct(
		private string $id,
		private string $nom,
		private string $prenom,
	) {}
 
	public function validate(): bool
	{
		// Validation de l'objet
	}
 
	// Getter, setter, etc.
}

2. Ports: les interfaces

Les Ports définissent les interfaces que les adaptateurs vont devoir implémenter pour que la logique métier puisse fonctionner. Ils peuvent être des points d’entrée dans le système (comme des requêtes HTTP ou bien des CLI) ou des points de sortie (comme les appels à la base de données, API externes, etc.).

namespace App\Domain\Utilisateur\Repository;
 
use App\Domain\Utilisateur\Utilisateur;
 
interface UtilisateurRepositoryInterface
{
	public function save(Utilisateur $utilisateur): void;
	public function find(string $id): Utilisateur;
}

Ici l’interface sert à définir les méthodes à implémenter dans les adaptateurs. Le domaine ne sais pas comment et où les données vont être sauvegardé ou récupéré.

3. Adapters: les implémentations

L'Adapter, c’est tout simplement l’implémentation concrète d’un Port. L’Adapter va permettre de traduire :

  • toutes les interactions externes vers la logique métier
  • la logique métier vers des actions concrètes (comme sauvegarder dans une base de données).
namespace App\Infrastructure\Repository;
 
use App\Domain\Utilisateur\Utilisateur;
use App\Domain\Utilisateur\Repository\UtilisateurRepositoryInterface;
 
readonly class MongoUtilisateurRepository implements UtilisateurRepositoryInterface
{
	public function __construct(
		private Mongo\Collection $collection
	) {}
 
	public function save(Utilisateur $utilisateur): void
	{
		$this->collection->set($utilisateur->toArray());
	}
 
	public function find(string $id): Utilisateur
	{
		return $this->collection->find(['id' => $id]);
	}
}

Ici l’adaptateur traduit l’interaction entre notre Domainet notre base de donnée Mongo.

4. Controller: exemple de point d’entrée

Le contrôleur va agir comme un point d’entrée pour notre application. C’est lui qui interagit avec l’utilisateur via les requêtes HTTP, puis délègue les tâches au domaine via les Ports et Adapters.

namespace App\Infrastructure\Controller;
 
use App\Domain\Utilisateur\Utilisateur;
use App\Domain\Utilisateur\Repository\UtilisateurRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
 
readonly class UtilisateurController extends AbstractController
{
	public function __construct(
		private UtilisateurRepositoryInterface $utilisateurRepository
	) {}
 
	public function create(Request $request): JsonResponse
	{
		$data = json_decode($request->getContent(), true);
		$user = new Utilisateur(uniqid(), $data['nom'], $data['prenom']);
 
		if ($user->validate() === false) {
			return new JsonResponse(['error' => 'Données invalides'], 400);
		}
 
		$this->utilisateurRepository->save($user);
 
		return new JsonResponse(['message' => 'Utilisateur créé'], 201);
	}
}

Ici, on peut voir que le contrôleur ne connaît que l’interface UtilisateurRepositoryInterface et pas l’implémentation concrète. L’injection de dépendance, présente dans la plupart des frameworks, s’occupera de trouver l’implémentation correspondante. Cela permet de rendre le code testable, extensible, et indépendant des frameworks et technologies externes (Symfony, Doctrine, etc.).

Information

Laravel nécessite de lier les interfaces aux implémentations. Voir la documentation