L’intégration de services externes, publics ou privés, via des API est une tâche courante dans le développement Laravel. Si elle paraît simple au départ, cette intégration peut rapidement devenir difficile à maintenir, à tester, et à faire évoluer proprement sans une architecture claire.
Pour remédier à cela, l’approche des modules de service propose une structure modulaire, réutilisable et testable, s’appuyant sur des patterns éprouvés (Repository, Factory, DTO) et des bonnes pratiques Laravel.
Problème courant : une intégration directe dans les contrôleurs
Prenons l’exemple d’un service météo interrogé via une API. Il est fréquent de voir l’appel direct à l’API réalisé dans le contrôleur :
namespace App\\Http\\Controllers;
use Illuminate\\Support\\Facades\\Http;
class WeatherController extends Controller
{
    public function getWeather($city)
    {
        $response = Http::get('<https://api.weatherapi.com/v1/current.json>', [
            'key' => config('services.weatherapi.key'),
            'q' => $city,
        ]);
        if ($response->failed()) {
            return response()->json(['error' => 'Impossible de récupérer les données météo'], 500);
        }
        $weather = $response->json();
        return response()->json([
            'city' => $weather['location']['name'],
            'temperature' => $weather['current']['temp_c'],
            'condition' => $weather['current']['condition']['text']
        ]);
    }
}
Bien que fonctionnelle, cette approche présente plusieurs limites :
- Duplication de la logique : en cas de réutilisation ailleurs, le code est copié, multipliant les points de rupture possibles.
 - Couplage fort : les contrôleurs dépendent directement de l’API.
 - Tests difficiles : le test unitaire nécessite de simuler l’appel HTTP à chaque fois.
 - Absence d’abstraction : aucune structuration claire des données retournées.
 - Gestion des erreurs dispersée : chaque point d’appel nécessite une logique d’erreur propre.
 
Étape 1 : structuration du service
La création d’un dossier Services dans app/ permet de regrouper la logique métier liée à un service externe. Chaque service (ex. Weather) peut contenir :
Repositories: logique métier et appels externesDTOs: structure des donnéesFacades: points d’entrée simplifiésProviders: injection de dépendancesExceptions: gestion d’erreurs dédiée
Étape 2 : le Repository
Ce composant centralise la logique d’appel à l’API.
namespace App\\Services\\Weather\\Repositories;
use Illuminate\\Support\\Facades\\Http;
class WeatherRepository
{
    public function getCurrentWeather($city)
    {
        $response = Http::get('<https://api.weatherapi.com/v1/current.json>', [
            'key' => config('services.weatherapi.key'),
            'q' => $city,
        ]);
        if ($response->failed()) {
            throw new \\Exception('Impossible de récupérer les données météo');
        }
        return $response->json();
    }
}
Étape 3 : le DTO (Data Transfer Object)
Le DTO permet de manipuler les données de manière structurée.
namespace App\\Services\\Weather\\DTOs;
class WeatherData
{
    public $city;
    public $temperature;
    public $condition;
    public function __construct(array $data)
    {
        $this->city = $data['location']['name'];
        $this->temperature = $data['current']['temp_c'];
        $this->condition = $data['current']['condition']['text'];
    }
}
Étape 4 : l’Exception personnalisé
Permet de centraliser la gestion des erreurs spécifiques à un service.
namespace App\\Services\\Weather\\Exceptions;
use Exception;
class WeatherServiceException extends Exception
{
    //
}
Étape 5 : le Service Provide
Configure l’injection du repository dans le conteneur de services Laravel.
namespace App\\Services\\Weather\\Providers;
use Illuminate\\Support\\ServiceProvider;
use App\\Services\\Weather\\Repositories\\WeatherRepository;
class WeatherServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(WeatherRepository::class, function ($app) {
            return new WeatherRepository();
        });
    }
}
Étape 6 : la Facade
Fournit une interface simple pour accéder au service.
namespace App\\Services\\Weather\\Facades;
use Illuminate\\Support\\Facades\\Facade;
class Weather extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'App\\Services\\Weather\\Repositories\\WeatherRepository';
    }
}
Étape 7 : utilisation dans un contrôleur
Avec tous les composants en place, l’appel dans le contrôleur est propre, simple et testable :
namespace App\\Http\\Controllers;
use App\\Services\\Weather\\Facades\\Weather;
use App\\Services\\Weather\\DTOs\\WeatherData;
class WeatherController extends Controller
{
    public function getWeather($city)
    {
        try {
            $data = Weather::getCurrentWeather($city);
            $weather = new WeatherData($data);
            return response()->json([
                'city' => $weather->city,
                'temperature' => $weather->temperature,
                'condition' => $weather->condition
            ]);
        } catch (\\Exception $e) {
            return response()->json(['error' => 'Impossible de récupérer les données météo'], 500);
        }
    }
}
Pour aller plus loin
Afin d’accélérer la mise en place de cette architecture, un package Laravel est disponible :
bash
CopierModifier
composer require shreifelagamy/laravel-service-modules
Ce package permet de générer automatiquement la structure complète d’un module de service via des commandes Artisan.
Ecrit par 
 Alyson Paya
Partager l'article :
Site, application ou automatisation de process : nos équipes conçoivent et développent des solutions sur-mesure qui répondent à vos enjeux métier.