Skip to content

How-to : Ajouter l'agrégation de rapports admin

Construire des endpoints d'agrégation de type tableau de bord avec des filtres de plage de dates, des regroupements et une limitation du paramètre limit.

Schéma

sql
CREATE TABLE orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    customer_id TEXT NOT NULL, item_name TEXT NOT NULL,
    amount INTEGER NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK(status IN ('pending','completed','refunded','cancelled')),
    created_at TEXT NOT NULL
);

Routes

MéthodeCheminDescription
POST/ordersInsérer une commande
GET/reports/summaryTotal commandes, revenu, moyenne, nombre de complétées
GET/reports/dailyCommandes groupées par date
GET/reports/by-statusCommandes groupées par statut
GET/reports/top-itemsTop N articles par revenu

Paramètres de requête (tous les rapports) : from=YYYY-MM-DD, to=YYYY-MM-DD

Filtre de plage de dates (paramétré sécurisé)

Construire la clause WHERE dynamiquement, passer les valeurs comme paramètres liés — ne jamais interpoler :

php
private function dateFilter(?string $from, ?string $to): array
{
    $conditions = [];
    $params     = [];
    if ($from !== null) { $conditions[] = 'created_at >= ?'; $params[] = $from; }
    if ($to !== null)   { $conditions[] = 'created_at <= ?'; $params[] = $to; }
    $where = $conditions !== [] ? 'WHERE ' . implode(' AND ', $conditions) : '';
    return [$where, $params];
}

Validation des dates (protection contre l'injection)

Rejeter les dates non-ISO-8601 avant qu'elles n'atteignent la requête :

php
private function isValidDate(string $date): bool
{
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
        return false;
    }
    $d = \DateTimeImmutable::createFromFormat('Y-m-d', $date);
    return $d !== false && $d->format('Y-m-d') === $date; // rejeter 2026-13-01
}

Rejeter from > to :

php
if ($from !== null && $to !== null && $from > $to) {
    $errors[] = new ValidationError('from', 'from must be before or equal to to', 'invalid');
}

Limitation du paramètre limit

php
private const int MAX_LIMIT = 100;

$limit = min((int) $q['limit'], self::MAX_LIMIT);
if ($limit <= 0) { /* 422 */ }

Requêtes d'agrégation

Résumé :

sql
SELECT COUNT(*) AS total_orders,
       COALESCE(SUM(amount), 0) AS total_revenue,
       COALESCE(AVG(amount), 0) AS avg_order_value,
       COUNT(CASE WHEN status = 'completed' THEN 1 END) AS completed_orders
FROM orders {where}

Répartition quotidienne (sous-chaîne de date SQLite) :

sql
SELECT substr(created_at, 1, 10) AS date, COUNT(*) AS order_count, SUM(amount) AS revenue
FROM orders {where}
GROUP BY date ORDER BY date ASC

Top articles :

sql
SELECT item_name, COUNT(*) AS order_count, SUM(amount) AS revenue
FROM orders {where}
GROUP BY item_name ORDER BY revenue DESC LIMIT ?

Notes de sécurité

  • Tous les paramètres de requête validés avant utilisation — l'injection SQL via from/to/limit est rejetée avec 422.
  • Les noms d'articles et IDs clients stockés via des requêtes paramétrées — les caractères spéciaux et les tentatives d'injection sont des chaînes littérales.
  • COALESCE(SUM(...), 0) empêche les NULL dans les résumés quand aucune ligne ne correspond.
  • Le limit est limité à MAX_LIMIT — empêche l'épuisement des ressources par des valeurs LIMIT énormes.

Publié sous licence MIT.