Architecture en Couches¶
⬇️ Télécharger cette page en Markdown
Vue d'Ensemble¶
Urbafix utilise une architecture en 4 couches distinctes :
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¶
-
Validation inputs :
-
Authentification :
-
Appel Services Layer :
-
Formatage réponse :
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¶
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