Aller au contenu

Architecture en Couches

⬇️ Télécharger cette page en Markdown


Vue d'Ensemble

Urbafix utilise une architecture en 4 couches distinctes :

Empilement des 4 couches applicatives : HTTP Layer, Services Layer, Infrastructure Layer, Data Layer


Couche 1 : HTTP Layer

Rôle

Gestion des requêtes HTTP, validation inputs, formatage réponses.

Composants

API Publique (/public/backend/api/) : - get_types.php - get_incidents.php - submit_incident.php - get_user_incidents.php

Admin Controllers (/public/backend/) : - index.php (dashboard) - incidents.php - types.php - services.php - users.php

Responsabilités

  1. Validation inputs :

    $codePostal = $_GET['code_postal'] ?? null;
    if (!$codePostal || !preg_match('/^\d{5}$/', $codePostal)) {
        http_response_code(400);
        echo json_encode(['success' => false, 'error' => 'Code postal invalide']);
        exit;
    }
    

  2. Authentification :

    Auth::requireAuth();
    $user = Auth::getUser();
    

  3. Appel Services Layer :

    $incidentService = new IncidentService($db, $cache);
    $incidents = $incidentService->listIncidents($filters, $typeIds);
    

  4. Formatage réponse :

    header('Content-Type: application/json');
    echo json_encode(['success' => true, 'data' => $incidents]);
    


Couche 2 : Services Layer

Rôle

Encapsulation de la logique métier, réutilisabilité, testabilité.

Services Principaux

IncidentService : - createIncident() - listIncidents() - updateStatus() - getById()

TypeService : - getByMairieId() - getContacts() - isActive()

UploadService : - handlePhotos() - handleVideos() - validateGPS()

MairieService : - getByCodePostal() - getById()

FaceBlurringService : - detectFaces() - applyBlur()

Exemple : IncidentService

class IncidentService {
    private $db;
    private $cache;

    public function listIncidents($filters, $typeIds = null) {
        $sql = "SELECT * FROM incidents WHERE mairie_id = ?";
        $params = [$filters['mairie_id']];

        if ($typeIds !== null) {
            $placeholders = implode(',', array_fill(0, count($typeIds), '?'));
            $sql .= " AND type_id IN ($placeholders)";
            $params = array_merge($params, $typeIds);
        }

        if (!empty($filters['statut'])) {
            $sql .= " AND statut = ?";
            $params[] = $filters['statut'];
        }

        return $this->db->fetchAll($sql, $params);
    }
}

Couche 3 : Infrastructure Layer

Rôle

Abstractions techniques pour découpler l'infrastructure de la logique métier.

Interfaces

SessionInterface :

interface SessionInterface {
    public function start(): bool;
    public function get(string $key): mixed;
    public function set(string $key, $value): void;
    public function destroy(): bool;
}

StorageInterface :

interface StorageInterface {
    public function put(string $path, string $data): bool;
    public function get(string $path): string;
    public function exists(string $path): bool;
    public function delete(string $path): bool;
    public function url(string $path): string;
}

CacheInterface :

interface CacheInterface {
    public function get(string $key): mixed;
    public function set(string $key, $value, int $ttl): bool;
    public function remember(string $key, int $ttl, callable $callback): mixed;
    public function forget(string $key): bool;
}

QueueInterface :

interface QueueInterface {
    public function push(string $jobClass, array $data): void;
    public function pop(): ?array;
}

Factories

SessionFactory :

class SessionFactory {
    public static function create(): SessionInterface {
        $driver = Config::getInstance()->get('session.driver');
        return match($driver) {
            'redis' => new RedisSessionHandler(),
            'file' => new FileSessionHandler(),
            default => new FileSessionHandler()
        };
    }
}


Couche 4 : Data Layer

Rôle

Persistance des données, transactions, intégrité.

Composants

Database Singleton :

class Database {
    private static $instance = null;
    private $pdo;

    public static function getInstance(): Database {
        if (self::$instance === null) {
            self::$instance = new Database();
        }
        return self::$instance;
    }

    public function fetchAll(string $sql, array $params = []): array {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

Redis Client :

class RedisClient {
    private $redis;

    public function __construct() {
        $this->redis = new Redis();
        $this->redis->connect(
            Config::getInstance()->get('redis.host'),
            Config::getInstance()->get('redis.port')
        );
        $this->redis->auth(Config::getInstance()->get('redis.password'));
    }

    public function get($key) {
        return $this->redis->get($key);
    }
}


Flux de Données Entre Couches

Création Incident

Séquence de création d'incident à travers les 4 couches applicatives, de HTTP Layer à Data Layer


Principes d'Architecture

Separation of Concerns

Chaque couche a une responsabilité unique : - HTTP : Communication - Services : Logique métier - Infrastructure : Abstractions techniques - Data : Persistance

Dependency Inversion

Les couches hautes ne dépendent pas des couches basses :

// ✅ Service dépend d'interface
class IncidentService {
    public function __construct(
        Database $db,
        CacheInterface $cache,  // Interface, pas implémentation
        StorageInterface $storage
    ) {}
}

// ❌ Service dépend d'implémentation concrète
class IncidentService {
    public function __construct(
        Database $db,
        RedisCache $cache,  // Couplage fort
        LocalStorage $storage
    ) {}
}

Testabilité

Les interfaces permettent le mock :

// Test unitaire
$mockCache = $this->createMock(CacheInterface::class);
$mockCache->method('remember')->willReturn($fakeData);

$service = new IncidentService($db, $mockCache, $storage);
$result = $service->listIncidents($filters);

$this->assertEquals($fakeData, $result);


Avantages de l'Architecture

Scalabilité

Changement d'infrastructure sans modification métier :

# Dev : File sessions, no cache
SESSION_DRIVER=file
CACHE_DRIVER=null

# Prod : Redis sessions + cache
SESSION_DRIVER=redis
CACHE_DRIVER=redis

Maintenabilité

Code organisé, responsabilités claires, réutilisable :

// Réutilisation IncidentService dans plusieurs endpoints
// /api/get_incidents.php
$service->listIncidents($filters);

// /api/get_user_incidents.php
$service->listIncidents(['citoyen_id' => $citoyenId]);

// /backend/incidents.php
$service->listIncidents($filters, $accessibleTypeIds);

Testabilité

Chaque couche testable isolément : - HTTP : Tests d'intégration (curl) - Services : Tests unitaires (PHPUnit) - Infrastructure : Tests d'interface - Data : Tests base de données


Anti-Patterns Évités

Dieu Objet

❌ Avant : submit_incident.php (500+ lignes, tout dans un fichier)

✅ Après : - HTTP layer : 50 lignes (validation, routing) - Services : 200 lignes (métier réutilisable) - Infrastructure : 300 lignes (abstractions)

Couplage Fort

❌ Avant : $_SESSION hardcodé partout

✅ Après : SessionInterface injectable

Logique Dupliquée

❌ Avant : Validation GPS répétée dans 3 fichiers

✅ Après : UploadService::validateGPS() unique


Références