Polar Code 🎭

Command Palette

Search for a command to run...

11
Pièce N°11

Module 11 : Architecture - Le Plan de la Cité

Le plan était étalé sur la table. Un quadrillage précis. Des zones, des liaisons, des flux. L'architecture, c'était ça : organiser le chaos. Donner à chaque élément sa place. Sa fonction. En PHP, un projet mal organisé, c'était comme une ville sans rues. On se perdait. On s'étouffait. Il fallait une structure. MVC. Des dossiers logiques. Composer pour gérer les dépendances. Des règles pour que le code reste lisible, même à 4 heures du matin, sous la pluie, avec une migraine.

Organisation d'un Projet - La Structure

<?php
// Structure de projet typique
/*
projet_noir/
├── public/                    # Point d'entrée unique
│   ├── index.php             # Front controller
│   ├── css/
│   ├── js/
│   └── uploads/              # Accès public contrôlé
├── src/                      # Code source
│   ├── Controllers/          # Contrôleurs
│   ├── Models/               # Modèles
│   ├── Views/                # Vues
│   ├── Services/             // Services métier
│   ├── Entities/             // Entités métier
│   ├── Repositories/         // Accès aux données
│   └── Utils/                // Utilitaires
├── config/                   // Configuration
│   ├── database.php
│   ├── routes.php
│   └── services.php
├── vendor/                   // Dépendances Composer
├── tests/                    // Tests
├── logs/                     // Journaux
├── temp/                     // Fichiers temporaires
└── .env                      // Variables d'environnement
*/
?>

Front Controller - Le Guichet Unique

<?php
// public/index.php
declare(strict_types=1);

// Chargement de l'autoloader Composer
require __DIR__ . '/../vendor/autoload.php';

// Configuration
$config = require __DIR__ . '/../config/app.php';

// Initialisation de l'application
$app = new \ProjetNoir\Core\Application($config);

// Traitement de la requête
$response = $app->handle($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);

// Envoi de la réponse
$response->send();
?>

MVC - La Séparation des Pouvoirs

<?php
// src/Controllers/AgentController.php
namespace ProjetNoir\Controllers;

use ProjetNoir\Models\Agent;
use ProjetNoir\Services\SecurityService;
use ProjetNoir\Views\AgentView;

class AgentController {
    private $agentModel;
    private $securityService;
    private $view;
    
    public function __construct() {
        $this->agentModel = new Agent();
        $this->securityService = new SecurityService();
        $this->view = new AgentView();
    }
    
    public function show(int $id): void {
        // Récupération depuis le modèle
        $agent = $this->agentModel->find($id);
        
        if (!$agent) {
            $this->view->renderNotFound();
            return;
        }
        
        // Vérification des permissions
        if (!$this->securityService->canView($agent)) {
            $this->view->renderForbidden();
            return;
        }
        
        // Affichage via la vue
        $this->view->renderAgent($agent);
    }
    
    public function create(array $data): void {
        // Validation des données
        if (!$this->validateAgentData($data)) {
            $this->view->renderError('Données invalides');
            return;
        }
        
        // Création via le modèle
        $agentId = $this->agentModel->create($data);
        
        // Redirection ou affichage
        $this->view->renderCreated($agentId);
    }
}
?>

<?php
// src/Models/Agent.php
namespace ProjetNoir\Models;

use ProjetNoir\Core\Database;

class Agent {
    private $db;
    
    public function __construct() {
        $this->db = Database::getInstance();
    }
    
    public function find(int $id): ?array {
        $stmt = $this->db->prepare("SELECT * FROM agents WHERE id = ? AND deleted = 0");
        $stmt->execute([$id]);
        return $stmt->fetch() ?: null;
    }
    
    public function create(array $data): int {
        $sql = "INSERT INTO agents (code_name, speciality, status, created_at) 
                VALUES (?, ?, ?, NOW())";
        
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $data['code_name'],
            $data['speciality'],
            'active'
        ]);
        
        return $this->db->lastInsertId();
    }
    
    public function update(int $id, array $data): bool {
        $allowedFields = ['code_name', 'speciality', 'status'];
        $updates = [];
        $params = [];
        
        foreach ($data as $field => $value) {
            if (in_array($field, $allowedFields)) {
                $updates[] = "$field = ?";
                $params[] = $value;
            }
        }
        
        if (empty($updates)) {
            return false;
        }
        
        $params[] = $id;
        $sql = "UPDATE agents SET " . implode(', ', $updates) . " WHERE id = ?";
        
        $stmt = $this->db->prepare($sql);
        return $stmt->execute($params);
    }
}
?>

<?php
// src/Views/AgentView.php
namespace ProjetNoir\Views;

class AgentView {
    public function renderAgent(array $agent): void {
        // Template séparé
        extract($agent);
        require __DIR__ . '/../Templates/agent/show.php';
    }
    
    public function renderNotFound(): void {
        http_response_code(404);
        require __DIR__ . '/../Templates/errors/404.php';
    }
    
    public function renderForbidden(): void {
        http_response_code(403);
        require __DIR__ . '/../Templates/errors/403.php';
    }
}
?>

Templates - La Séparation Logique/Vue

<?php
// src/Templates/agent/show.php
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Agent <?= htmlspecialchars($code_name) ?></title>
    <style>
        body { font-family: 'Courier New', monospace; background: #0a0a0a; color: #ccc; }
        .agent-card { border: 1px solid #333; padding: 20px; margin: 20px; background: #111; }
        .code-name { color: #8b0000; font-size: 1.5em; }
    </style>
</head>
<body>
    <div class="agent-card">
        <h1 class="code-name"><?= htmlspecialchars($code_name) ?></h1>
        <p><strong>Spécialité:</strong> <?= htmlspecialchars($speciality) ?></p>
        <p><strong>Statut:</strong> <?= htmlspecialchars($status) ?></p>
        <p><strong>Dernière mission:</strong> <?= htmlspecialchars($last_mission_date) ?></p>
    </div>
</body>
</html>
?>

Composer - Le Gestionnaire de Dépendances

<?php
// composer.json
{
    "name": "projet-noir/archives",
    "description": "Système de gestion des archives confidentielles",
    "type": "project",
    "license": "proprietary",
    "require": {
        "php": "^8.0",
        "ext-pdo": "*",
        "vlucas/phpdotenv": "^5.4",           // Gestion des variables d'environnement
        "monolog/monolog": "^2.8",           // Logging
        "symfony/http-foundation": "^6.0",   // Requêtes/Réponses HTTP
        "symfony/routing": "^6.0",           // Routage
        "doctrine/dbal": "^3.5"              // Abstraction base de données
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5",
        "squizlabs/php_codesniffer": "^3.7",
        "phpstan/phpstan": "^1.8"
    },
    "autoload": {
        "psr-4": {
            "ProjetNoir\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "ProjetNoir\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "check-style": "phpcs --standard=PSR12 src/",
        "fix-style": "phpcbf --standard=PSR12 src/",
        "analyse": "phpstan analyse src/ --level=8"
    }
}
?>

Configuration - Les Paramètres Extériorisés

<?php
// config/database.php
return [
    'driver' => 'mysql',
    'host' => $_ENV['DB_HOST'] ?? 'localhost',
    'database' => $_ENV['DB_NAME'] ?? 'archives',
    'username' => $_ENV['DB_USER'] ?? 'root',
    'password' => $_ENV['DB_PASS'] ?? '',
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix' => '',
    'options' => [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false
    ]
];
?>

Services - La Logique Métier

<?php
// src/Services/MissionService.php
namespace ProjetNoir\Services;

use ProjetNoir\Repositories\MissionRepository;
use ProjetNoir\Entities\Mission;
use ProjetNoir\Utils\Logger;

class MissionService {
    private $missionRepository;
    private $logger;
    
    public function __construct(MissionRepository $missionRepository, Logger $logger) {
        $this->missionRepository = $missionRepository;
        $this->logger = $logger;
    }
    
    public function assignMission(int $agentId, array $missionData): Mission {
        // Validation métier
        if (!$this->canAgentAcceptMission($agentId)) {
            throw new \Exception("Agent non disponible");
        }
        
        // Création de l'entité
        $mission = new Mission();
        $mission->setAgentId($agentId);
        $mission->setTitle($missionData['title']);
        $mission->setDescription($missionData['description']);
        $mission->setPriority($missionData['priority']);
        $mission->setStatus('assigned');
        $mission->setAssignedAt(new \DateTime());
        
        // Persistance
        $savedMission = $this->missionRepository->save($mission);
        
        // Logging
        $this->logger->info("Mission assignée", [
            'agent_id' => $agentId,
            'mission_id' => $savedMission->getId()
        ]);
        
        return $savedMission;
    }
    
    private function canAgentAcceptMission(int $agentId): bool {
        $activeMissions = $this->missionRepository->countActiveMissions($agentId);
        return $activeMissions < 3; // Maximum 3 missions simultanées
    }
}
?>

Repositories - L'Accès aux Données

<?php
// src/Repositories/MissionRepository.php
namespace ProjetNoir\Repositories;

use ProjetNoir\Entities\Mission;
use ProjetNoir\Core\Database;

class MissionRepository {
    private $db;
    
    public function __construct(Database $db) {
        $this->db = $db;
    }
    
    public function save(Mission $mission): Mission {
        if ($mission->getId()) {
            return $this->update($mission);
        }
        
        return $this->insert($mission);
    }
    
    private function insert(Mission $mission): Mission {
        $sql = "INSERT INTO missions (agent_id, title, description, priority, status, assigned_at) 
                VALUES (?, ?, ?, ?, ?, ?)";
        
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $mission->getAgentId(),
            $mission->getTitle(),
            $mission->getDescription(),
            $mission->getPriority(),
            $mission->getStatus(),
            $mission->getAssignedAt()->format('Y-m-d H:i:s')
        ]);
        
        $mission->setId($this->db->lastInsertId());
        return $mission;
    }
    
    public function countActiveMissions(int $agentId): int {
        $sql = "SELECT COUNT(*) FROM missions 
                WHERE agent_id = ? AND status IN ('assigned', 'in_progress')";
        
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$agentId]);
        
        return (int) $stmt->fetchColumn();
    }
}
?>

Logger - La Traçabilité

<?php
// src/Utils/Logger.php
namespace ProjetNoir\Utils;

use Monolog\Logger as MonologLogger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;

class Logger {
    private $logger;
    
    public function __construct(string $name = 'app') {
        $this->logger = new MonologLogger($name);
        
        // Format personnalisé
        $dateFormat = "Y-m-d H:i:s";
        $output = "[%datetime%] %channel%.%level_name%: %message% %context%\n";
        $formatter = new LineFormatter($output, $dateFormat);
        
        // Handler pour les logs quotidiens
        $stream = new StreamHandler(
            __DIR__ . '/../../logs/app_' . date('Y-m-d') . '.log',
            MonologLogger::DEBUG
        );
        $stream->setFormatter($formatter);
        
        $this->logger->pushHandler($stream);
    }
    
    public function emergency(string $message, array $context = []): void {
        $this->logger->emergency($message, $context);
    }
    
    public function alert(string $message, array $context = []): void {
        $this->logger->alert($message, $context);
    }
    
    public function critical(string $message, array $context = []): void {
        $this->logger->critical($message, $context);
    }
    
    public function error(string $message, array $context = []): void {
        $this->logger->error($message, $context);
    }
    
    public function warning(string $message, array $context = []): void {
        $this->logger->warning($message, $context);
    }
    
    public function notice(string $message, array $context = []): void {
        $this->logger->notice($message, $context);
    }
    
    public function info(string $message, array $context = []): void {
        $this->logger->info($message, $context);
    }
    
    public function debug(string $message, array $context = []): void {
        $this->logger->debug($message, $context);
    }
}
?>

Bonnes Pratiques - Le Code Propre

<?php
// Exemples de code propre

// 1. Noms explicites
// Mauvais
function proc($d) { return $d * 2; }

// Bon
function calculateMissionDuration(int $days): int {
    return $days * 2;
}

// 2. Fonctions courtes et simples
class MissionCalculator {
    // Mauvais : trop long, fait plusieurs choses
    public function processEverything(array $data): array {
        // 50 lignes de code...
    }
    
    // Bon : fonctions atomiques
    public function calculateDuration(Mission $mission): int {
        // 10 lignes max
    }
    
    public function validateResources(array $resources): bool {
        // 10 lignes max
    }
}

// 3. DRY - Don't Repeat Yourself
// Mauvais : duplication
class AgentService {
    public function createAgent(array $data) {
        // Validation...
        if (empty($data['name'])) {
            throw new Exception("Name required");
        }
        // ...
    }
    
    public function updateAgent(array $data) {
        // Même validation...
        if (empty($data['name'])) {
            throw new Exception("Name required");
        }
        // ...
    }
}

// Bon : factorisation
class AgentService {
    public function createAgent(array $data) {
        $this->validateAgentData($data);
        // ...
    }
    
    public function updateAgent(array $data) {
        $this->validateAgentData($data);
        // ...
    }
    
    private function validateAgentData(array $data): void {
        if (empty($data['name'])) {
            throw new Exception("Name required");
        }
        // Validation centralisée
    }
}

// 4. Typage strict
declare(strict_types=1);

class SecureAgent {
    private int $id;
    private string $codeName;
    private DateTimeImmutable $createdAt;
    
    public function __construct(int $id, string $codeName, DateTimeImmutable $createdAt) {
        $this->id = $id;
        $this->codeName = $codeName;
        $this->createdAt = $createdAt;
    }
    
    public function getId(): int {
        return $this->id;
    }
    
    public function getCodeName(): string {
        return $this->codeName;
    }
    
    public function getAgeInDays(): int {
        $now = new DateTimeImmutable();
        $interval = $this->createdAt->diff($now);
        return (int) $interval->format('%a');
    }
}

// 5. Gestion d'erreurs
class MissionHandler {
    private Logger $logger;
    
    public function assign(Mission $mission, Agent $agent): void {
        try {
            // Logique métier...
            
        } catch (DatabaseException $e) {
            $this->logger->error('Database error during mission assignment', [
                'mission' => $mission->getId(),
                'agent' => $agent->getId(),
                'error' => $e->getMessage()
            ]);
            throw new AssignmentException('Could not assign mission');
            
        } catch (ValidationException $e) {
            // Ne pas logger les erreurs de validation utilisateur
            throw $e;
            
        } catch (\Throwable $e) {
            $this->logger->critical('Unexpected error', [
                'exception' => get_class($e),
                'message' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
            throw new SystemException('Internal error');
        }
    }
}

// 6. Configuration extériorisée
class Config {
    public static function get(string $key, $default = null) {
        static $config = null;
        
        if ($config === null) {
            $config = require __DIR__ . '/../config/app.php';
        }
        
        return $config[$key] ?? $default;
    }
}

// Utilisation
$dbHost = Config::get('database.host', 'localhost');
?>

Je roulai le plan. L'architecture. Un projet bien organisé, c'était comme une ville sous la pluie. Chaque rue avait son nom. Chaque bâtiment sa fonction. MVC séparait la logique de la présentation. Composer gérait les dépendances comme un chef d'orchestre. Le code était lisible, même à 4 heures du matin. Même avec la migraine. Parce que dans l'ombre, un code mal organisé, c'était une faille. Une faille où se glissait l'erreur. L'erreur qui faisait tout tomber. L'architecture, c'était pas du luxe. C'était la fondation. La première pierre. La dernière chance.