Skip to content

Jeu : Enquête PHP

Enquête PHP : 500 habitants, un seul coupable

Sommaire

Vous savez écrire des conditions, des boucles et manipuler des tableaux en PHP ? Alors vous savez mener une enquête. Un crime a été commis à Algoville, la police a des fichiers… et rien d'autre. C'est à vous de jouer !

Sept histoires : les quatre premières se jouent avec ce que vous connaissez déjà (foreach, if, quelques fonctions sur les chaînes), les trois dernières reprennent le même principe en orienté objet, à jouer quand vous aurez vu les classes (la suite en orienté objet).

Pour les étudiants en avance

Ce jeu est un bonus : il n'est pas noté et ne demande aucun rendu. Vous pouvez y jouer en autonomie, seul ou à deux, dès que vous avez terminé le TP en cours. Comptez environ une heure par histoire.

À vous de jouer : choisissez votre enquête

Sélectionnez une histoire, lisez le brief, puis écrivez votre code dans l'éditeur. Tout s'exécute dans votre navigateur (le premier « Exécuter » télécharge PHP, environ 13 Mo, une seule fois). Les données et la fonction verifier() sont dépliables juste sous l'éditeur.

Télécharger les données (.php)

Le vol de la médiathèque

Le manuscrit ancien exposé à la médiathèque Jean-Moulin a disparu le 16 janvier 2024. Une enquête idéale pour prendre l'outil en main : nous la résolvons ensemble sur la page du jeu.

Point de départ : le rapport de police, dans $rapport. Quand vous tenez un nom : verifier("Prénom Nom");

Journal de bordÉtape 1 : ?
Les données (4 tableaux)
  • $habitants : id, nom, genre, age, taille, cheveux, rue, numero, revenu
  • $vehicules : habitant_id, marque, modele, plaque
  • $temoignages : habitant_id, texte
  • $passages : habitant_id, lieu, date (AAAAMMJJ), heure (HHMM)
  • Et la variable $rapport (le rapport de police).
Aide-mémoire : traduire un indice en PHP
L'indice dit…Le code
la dernière maison / le plus petit numéro de la rue Xparcourez $habitants, gardez l'habitant au numero le plus grand (ou le plus petit) de la rue
prénommé Lucas, rue X$h["rue"] === "X" && str_starts_with($h["nom"], "Lucas ")
ce que dit un témoinparcourez $temoignages : $t["habitant_id"] === $temoin["id"]
cheveux roux, entre 40 et 45 ans$h["cheveux"] === "roux" && $h["age"] >= 40 && $h["age"] <= 45
plaque qui commence par / finit par / contient ABCstr_starts_with($v["plaque"], "ABC") / str_ends_with(…) / str_contains(…), en reliant $v["habitant_id"] à $h["id"]
passé 3 fois à « X » en mai 2024un tableau compteur : $compteur[$p["habitant_id"]] = ($compteur[$p["habitant_id"]] ?? 0) + 1; puis ($compteur[$h["id"]] ?? 0) === 3
entre 19h et 20h$p["heure"] >= 1900 && $p["heure"] <= 2000
gagne plus de 200 000 € par an$h["revenu"] > 200000
Données chargées, à vous de jouer.

Les règles du jeu

  • Toutes les histoires se passent dans la même ville : mêmes tableaux ($habitants, $vehicules, $temoignages, $passages), reliés par l'id de l'habitant. Seules les données changent.
  • Chaque enquête commence par echo $rapport; : le rapport vous dit comment trouver les témoins, les témoins décrivent le coupable, et parfois le coupable vous mène plus loin encore.
  • Les dates sont des entiers AAAAMMJJ (20240116 pour le 16 janvier 2024), les heures HHMM (1830 pour 18h30).
  • Quand vous pensez avoir trouvé, vous accusez avec verifier("Prénom Nom");. La réponse vous dit si c'est la bonne personne et si l'enquête continue. Le journal de bord au-dessus de l'éditeur coche les étapes réussies.

Interdit

Pas de print_r($habitants); en espérant repérer le coupable à l'œil : il y a 500 habitants. L'objectif est justement d'écrire des boucles et des conditions qui réduisent la liste jusqu'à n'avoir qu'un seul nom. Et évitez les while sans condition de sortie : une boucle infinie fige l'onglet.

Rattrapage express : les outils dont vous aurez besoin
  • foreach ($habitants as $h) { … } : parcourir un tableau ; $h["cle"] : lire une valeur.
  • if ($a && $b) { … } : cumuler plusieurs conditions.
  • str_starts_with($texte, "abc"), str_ends_with(…), str_contains(…) : commence par, se termine par, contient.
  • Garder un maximum : une variable $record mise à jour dans la boucle quand on trouve mieux.
  • Compter : $compteur[$cle] = ($compteur[$cle] ?? 0) + 1; (le ?? 0 donne une valeur par défaut la première fois).

Si vous préférez votre éditeur habituel : le lien « Télécharger les données » donne un fichier .php autonome, à charger avec require "nom_du_fichier.php";.

Enquête n° 1 : on la fait ensemble

Pour prendre en main l'outil, nous allons résoudre Le vol de la médiathèque ensemble. Sélectionnez cette histoire dans l'éditeur, copiez chaque bloc de code et comparez votre résultat au mien. Les autres histoires seront à faire seul, avec la même méthode.

1. Le rapport de police

Le lien « Insérer le code de départ » écrit ce code pour vous :

php
<?php

echo $rapport;

Le rapport nous apprend qu'un témoin a tout vu, et qu'il « vit au dernier numéro de « Rue des Tilleuls » ». Pas de nom : le rapport ne donne jamais un nom, seulement une manière de le retrouver.

2. Trouver le témoin

« Le dernier numéro de la rue », c'est le plus grand numero parmi les habitants de cette rue. On parcourt $habitants, on ignore tout le monde sauf les habitants de la rue, et on garde en mémoire le plus grand numéro rencontré :

php
<?php

$temoin = null;
$record = 0;
foreach ($habitants as $h) {
    if ($h["rue"] === "Rue des Tilleuls" && $h["numero"] > $record) {
        $record = $h["numero"];
        $temoin = $h;
    }
}
echo $temoin["nom"];
Pourquoi $record = 0; avant la boucle ?

On part du plus petit numéro possible. À chaque habitant de la rue dont le numéro dépasse le record, on met à jour le record et on mémorise l'habitant complet (c'est son id qui servira ensuite). Pour le plus petit numéro, inversez la comparaison et partez d'un record très grand (PHP_INT_MAX).

Les autres formulations que vous rencontrerez
Le rapport dit…Vous cherchez…
« le plus petit numéro de la rue X »le même code, comparaison inversée
« prénommé Lucas, rue X »str_starts_with($h["nom"], "Lucas ") en plus du test sur la rue
« la personne au revenu le plus élevé de la rue X »le plus grand revenu de la rue, avec $record

3. Lire son témoignage

Notre témoin s'appelle Gaston Lefebvre. Sa déclaration est dans $temoignages, relié par habitant_id. Ajoutez à la suite de votre code :

php
foreach ($temoignages as $t) {
    if ($t["habitant_id"] === $temoin["id"]) {
        echo $t["texte"];
    }
}

Le témoignage décrit la voleuse : une femme, entre 44 et 48 ans, entre 158 et 161 cm, cheveux noirs.

4. Croiser les indices

Quatre conditions, une boucle, un seul if :

php
<?php

foreach ($habitants as $h) {
    if ($h["genre"] === "femme"
        && $h["cheveux"] === "noir"
        && $h["taille"] >= 158 && $h["taille"] <= 161
        && $h["age"] >= 44 && $h["age"] <= 48) {
        echo $h["nom"] . "\n";
    }
}

Un seul nom s'affiche. Retirez une condition et réexécutez : plusieurs suspects apparaissent. C'est voulu, la ville contient des habitants qui correspondent à presque toute la description. Chaque indice compte.

La couleur est « noir » ou « noirs » ?

La clé cheveux contient la couleur au singulier ("noir", "brun", "roux"…), alors que le témoignage parle français. Quand un test ne renvoie rien, vérifiez toujours la valeur exacte stockée dans les données.

Que se passe-t-il quand un indice dit « trois passages à un endroit » ?

Un simple if ne suffit plus : il faut d'abord compter les passages de chaque habitant dans un tableau compteur ($compteur[$h["id"]]), puis filtrer sur ce compteur. Vous en aurez besoin dès l'histoire 2.

5. Accuser

php
verifier("le nom que vous avez trouvé");

Point de contrôle

Le journal de bord affiche « ✔ Étape 1 » et le message vous félicite. Si le message dit « Ce n'est pas la bonne personne », vérifiez l'orthographe exacte du nom (copiez-collez-le depuis votre sortie). Quand une histoire continue, lisez le témoignage de la personne démasquée : elle a peut-être été payée par quelqu'un.

Enquêtes 2 à 4 : indices et solutions

Les histoires suivantes se corsent : plusieurs témoins, des voitures à croiser avec les habitants, des passages à compter, et même une fausse piste. Essayez d'abord sans indice ; si vous bloquez, dépliez-les dans l'ordre, la solution complète vient en dernier.

Sabotage au gymnase

Structure : 2 témoins, puis un personnage à identifier (réponse 1), puis un personnage à identifier (réponse 2).

Indice 1 : les témoins
  • un prénom sur « Avenue Pasteur » : str_starts_with(...nom..., "Ines ") en plus du test sur la rue.
  • le numéro le plus petit de « Rue du Stade » : parcourez $habitants, gardez le plus petit numero de la rue.

Une fois trouvés, lisez leur témoignage : parcourez $temoignages en comparant habitant_id avec l'id du témoin.

Indice 2 : le coupable

Les indices viennent de : Ines Roussel (physique), Marcel Guerin (vehicule).

  • physique : la description (genre, cheveux, âge, taille) est directement dans $habitants ; les intervalles se testent avec >= et <=.
  • vehicule : la voiture est dans $vehicules, reliée par habitant_id ; un fragment de plaque se teste avec str_starts_with, str_ends_with ou str_contains.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir. Une fois validé, lisez son témoignage : l'enquête continue.

Indice 3 : la personne derrière tout ça

Les indices viennent de : Bruno Marchal (adresse, revenu).

  • adresse : la rue (et le numero) sont directement dans $habitants.
  • revenu : le revenu est directement dans $habitants.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.

Voir l'une des solutions possibles
php
// Témoin : Ines Roussel
$temoin = null;
foreach ($habitants as $h) {
    if ($h["rue"] === "Avenue Pasteur" && str_starts_with($h["nom"], "Ines ")) {
        $temoin = $h;
    }
}
echo $temoin["nom"] . "\n";
foreach ($temoignages as $t) {
    if ($t["habitant_id"] === $temoin["id"]) {
        echo $t["texte"] . "\n";
    }
}

// Témoin : Marcel Guerin
$temoin = null;
$record = PHP_INT_MAX;
foreach ($habitants as $h) {
    if ($h["rue"] === "Rue du Stade" && $h["numero"] < $record) {
        $record = $h["numero"];
        $temoin = $h;
    }
}
echo $temoin["nom"] . "\n";
foreach ($temoignages as $t) {
    if ($t["habitant_id"] === $temoin["id"]) {
        echo $t["texte"] . "\n";
    }
}

// Réponse 1 : Bruno Marchal
$vehiculeDe = [];
foreach ($vehicules as $v) {
    $vehiculeDe[$v["habitant_id"]] = $v;
}
foreach ($habitants as $h) {
    if ($h["genre"] === "homme"
    && $h["cheveux"] === "brun"
    && isset($vehiculeDe[$h["id"]])
    && $vehiculeDe[$h["id"]]["marque"] === "Ford"
    && str_starts_with($vehiculeDe[$h["id"]]["plaque"], "1M6S")) {
        echo $h["nom"] . "\n";
    }
}

verifier("Bruno Marchal");

// Réponse 2 : Josephine Baron
foreach ($habitants as $h) {
    if ($h["rue"] === "Avenue du Parc"
    && $h["numero"] >= 10 && $h["numero"] <= 30
    && $h["revenu"] > 100000) {
        echo $h["nom"] . "\n";
    }
}

verifier("Josephine Baron");

Le disparu du marché couvert

Structure : 1 témoin, puis un personnage à identifier (réponse 1), puis un personnage à identifier (réponse 2).

Indice 1 : les témoins
  • un prénom sur « Rue du Moulin » : str_starts_with(...nom..., "Colette ") en plus du test sur la rue.

Une fois trouvés, lisez leur témoignage : parcourez $temoignages en comparant habitant_id avec l'id du témoin.

Indice 2 : le coupable

Les indices viennent de : Colette Hamon (passage, physique).

  • physique : la description (genre, cheveux, âge, taille) est directement dans $habitants ; les intervalles se testent avec >= et <=.
  • passage : les passages sont dans $passages, reliés par habitant_id ; pour « n fois », comptez avec un tableau compteur : $compteur[$p["habitant_id"]] = ($compteur[$p["habitant_id"]] ?? 0) + 1;.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir. Une fois validé, lisez son témoignage : l'enquête continue.

Indice 3 : la personne derrière tout ça

Les indices viennent de : Thierry Vasseur (revenu, vehicule).

  • vehicule : la voiture est dans $vehicules, reliée par habitant_id ; un fragment de plaque se teste avec str_starts_with, str_ends_with ou str_contains.
  • revenu : le revenu est directement dans $habitants.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.

Voir l'une des solutions possibles
php
// Témoin : Colette Hamon
$temoin = null;
$record = PHP_INT_MAX;
foreach ($habitants as $h) {
    if ($h["rue"] === "Rue du Moulin" && $h["revenu"] < $record) {
        $record = $h["revenu"];
        $temoin = $h;
    }
}
echo $temoin["nom"] . "\n";
foreach ($temoignages as $t) {
    if ($t["habitant_id"] === $temoin["id"]) {
        echo $t["texte"] . "\n";
    }
}

// Réponse 1 : Thierry Vasseur
$compteur = [];
foreach ($passages as $p) {
    if ($p["lieu"] === "Marché couvert" && intdiv($p["date"], 100) === 202405) {
        $compteur[$p["habitant_id"]] = ($compteur[$p["habitant_id"]] ?? 0) + 1;
    }
}
foreach ($habitants as $h) {
    if ($h["genre"] === "homme"
    && $h["cheveux"] === "brun"
    && ($compteur[$h["id"]] ?? 0) === 3) {
        echo $h["nom"] . "\n";
    }
}

verifier("Thierry Vasseur");

// Réponse 2 : Edith Fleury
$vehiculeDe = [];
foreach ($vehicules as $v) {
    $vehiculeDe[$v["habitant_id"]] = $v;
}
foreach ($habitants as $h) {
    if ($h["revenu"] > 150000
    && isset($vehiculeDe[$h["id"]])
    && str_contains($vehiculeDe[$h["id"]]["plaque"], "1F6D")) {
        echo $h["nom"] . "\n";
    }
}

verifier("Edith Fleury");

Fausse piste au carnaval

Structure : 2 témoins, puis une fausse piste, puis un personnage à identifier (réponse 1).

Indice 1 : les témoins
  • un prénom sur « Boulevard Voltaire » : str_starts_with(...nom..., "Louis ") en plus du test sur la rue, et un numero entre 10 et 30.
  • le numéro le plus grand de « Place Molière » : parcourez $habitants, gardez le plus grand numero de la rue.

Une fois trouvés, lisez leur témoignage : parcourez $temoignages en comparant habitant_id avec l'id du témoin.

Indice 2 : la fausse piste

Les indices viennent de : Louis Pasquier (physique), Rose Bertin (passage).

  • physique : la description (genre, cheveux, âge, taille) est directement dans $habitants ; les intervalles se testent avec >= et <=.
  • passage : les passages sont dans $passages, reliés par habitant_id ; pour « n fois », comptez avec un tableau compteur : $compteur[$p["habitant_id"]] = ($compteur[$p["habitant_id"]] ?? 0) + 1;.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir. Une fois validé, lisez son témoignage : l'enquête continue.

Indice 3 : le coupable

Les indices viennent de : Axel Bazin (adresse, vehicule).

  • vehicule : la voiture est dans $vehicules, reliée par habitant_id ; un fragment de plaque se teste avec str_starts_with, str_ends_with ou str_contains.
  • adresse : la rue (et le numero) sont directement dans $habitants.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.

Voir l'une des solutions possibles
php
// Témoin : Louis Pasquier
$temoin = null;
foreach ($habitants as $h) {
    if ($h["rue"] === "Boulevard Voltaire" && str_starts_with($h["nom"], "Louis ") && $h["numero"] >= 10 && $h["numero"] <= 30) {
        $temoin = $h;
    }
}
echo $temoin["nom"] . "\n";
foreach ($temoignages as $t) {
    if ($t["habitant_id"] === $temoin["id"]) {
        echo $t["texte"] . "\n";
    }
}

// Témoin : Rose Bertin
$temoin = null;
$record = 0;
foreach ($habitants as $h) {
    if ($h["rue"] === "Place Molière" && $h["numero"] > $record) {
        $record = $h["numero"];
        $temoin = $h;
    }
}
echo $temoin["nom"] . "\n";
foreach ($temoignages as $t) {
    if ($t["habitant_id"] === $temoin["id"]) {
        echo $t["texte"] . "\n";
    }
}

// Fausse piste : Axel Bazin
$compteur = [];
foreach ($passages as $p) {
    if ($p["lieu"] === "Salle des fêtes" && $p["date"] === 20240212 && $p["heure"] >= 1900 && $p["heure"] <= 2000) {
        $compteur[$p["habitant_id"]] = ($compteur[$p["habitant_id"]] ?? 0) + 1;
    }
}
foreach ($habitants as $h) {
    if ($h["genre"] === "homme"
    && $h["cheveux"] === "noir"
    && ($compteur[$h["id"]] ?? 0) >= 1) {
        echo $h["nom"] . "\n";
    }
}

verifier("Axel Bazin");

// Réponse 1 : Sylvie Delaunay
$vehiculeDe = [];
foreach ($vehicules as $v) {
    $vehiculeDe[$v["habitant_id"]] = $v;
}
foreach ($habitants as $h) {
    if ($h["rue"] === "Allée des Acacias"
    && $h["numero"] >= 26 && $h["numero"] <= 46
    && isset($vehiculeDe[$h["id"]])
    && $vehiculeDe[$h["id"]]["modele"] === "Berlingo"
    && str_ends_with($vehiculeDe[$h["id"]]["plaque"], "4P4Y")) {
        echo $h["nom"] . "\n";
    }
}

verifier("Sylvie Delaunay");

La suite en orienté objet

Les histoires 5 à 7 reprennent exactement le même jeu, mais les habitants ne sont plus des tableaux : ce sont des objets. Même éditeur, mêmes règles, même fonction verifier() ; seule la manière d'écrire le code change, et chaque histoire monte d'un cran.

Pas encore vu les classes ?

Cette partie demande les bases de la POO, les interfaces et idéalement le polymorphisme (l'aide-mémoire POO est un bon compagnon). Si ce n'est pas encore fait, arrêtez-vous ici et revenez après le cours. Le jeu vous attendra.

Les classes fournies et la syntaxe dont vous aurez besoin

Toutes les données sont encapsulées dans un objet $ville, déjà créé pour vous (les classes sont aussi dépliables dans l'éditeur).

ClassePropriétésMéthodes
Habitantid, nom, genre, age, taille, cheveux, rue, numero, revenu (lecture seule)prenom(): string, habite(string $rue): bool
VehiculehabitantId, marque, modele, plaqueappartientA(Habitant $h): bool
PassagehabitantId, lieu, date, heureestA(string $lieu): bool
Ville(privées : c'est l'encapsulation)rapport(): string, habitants(): array, vehiculeDe(Habitant $h): ?Vehicule, temoignageDe(Habitant $h): ?string, passagesDe(Habitant $h): array

Et pour la dernière histoire :

php
interface Critere
{
    public function accepte(Habitant $h): bool;
}

/** Renvoie les habitants qui passent TOUS les critères. */
function filtrer(Ville $ville, Critere ...$criteres): array

Côté syntaxe : $h->age lit une propriété, $ville->habitants() appelle une méthode, class MonCritere implements Critere { … } implémente l'interface, new MonCritere("roux") instancie avec un argument. Une méthode comme vehiculeDe() peut renvoyer null : testez-le.

Pourquoi ne peut-on plus faire $habitants[3]["cheveux"] ? Parce que Ville garde ses tableaux privés et n'expose que des méthodes : vous dépendez du contrat de la classe, pas de la façon dont elle stocke ses données. Si demain Ville lisait une base de données, votre code d'enquête ne changerait pas d'une ligne.

Histoire 5 : l'affaire de l'observatoire, semi-guidée

Remontez à l'éditeur et sélectionnez L'affaire de l'observatoire. Je vous montre le début, vous finissez. Le code de départ devient echo $ville->rapport();.

Le gardien « habite la dernière maison de « Chemin du Parc » ». Même technique du record qu'avant, mais $h est un objet :

php
<?php

$temoin = null;
$record = 0;
foreach ($ville->habitants() as $h) {
    if ($h->habite("Chemin du Parc") && $h->numero > $record) {
        $record = $h->numero;
        $temoin = $h;
    }
}
echo $temoin->nom . "\n";
echo $ville->temoignageDe($temoin);

Remarquez $h->numero au lieu de $h["numero"], et surtout $ville->temoignageDe($temoin) : plus besoin de boucler sur un tableau de témoignages, la ville s'en charge.

Le témoin décrit une femme, son âge, ses cheveux… et sa voiture. La description se teste sur les propriétés de Habitant ; pour la voiture, demandez $ville->vehiculeDe($h), qui peut être null (tout le monde n'a pas de voiture) : testez $v !== null avant de lire $v->marque.

Coup de pouce : la structure de la boucle
php
foreach ($ville->habitants() as $h) {
    $v = $ville->vehiculeDe($h);
    if ($v !== null
        && $h->genre === "..."
        && ...) {
        echo $h->nom . "\n";
    }
}

Un seul nom doit s'afficher. Accusez, lisez le témoignage de la voleuse : l'enquête continue vers un mystérieux collectionneur, que vous trouverez seul (une adresse et un revenu : tout est dans Habitant).

Point de contrôle

Les deux étapes du journal de bord sont cochées. Les indices dégressifs sont ci-dessous si vous bloquez.

Structure : 1 témoin, puis un personnage à identifier (réponse 1), puis un personnage à identifier (réponse 2).

Indice 1 : les témoins
  • le numéro le plus grand de « Chemin du Parc » : parcourez $ville->habitants(), gardez le plus grand numero de la rue.

Une fois trouvés, lisez leur témoignage avec $ville->temoignageDe($temoin).

Indice 2 : le coupable

Les indices viennent de : Hugo Chapuis (physique, vehicule).

  • physique : la description (genre, cheveux, âge, taille) est portée par les propriétés de Habitant.
  • vehicule : demandez $ville->vehiculeDe($h) (qui peut renvoyer null) puis testez la plaque avec str_starts_with, str_ends_with ou str_contains.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir. Une fois validé, lisez son témoignage : l'enquête continue.

Indice 3 : la personne derrière tout ça

Les indices viennent de : Laure Guichard (adresse, revenu).

  • adresse : les propriétés rue et numero de Habitant (ou la méthode habite()).
  • revenu : la propriété revenu de Habitant.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.

Voir l'une des solutions possibles
php
// Témoin : Hugo Chapuis
$temoin = null;
$record = 0;
foreach ($ville->habitants() as $h) {
    if ($h->rue === "Chemin du Parc" && $h->numero > $record) {
        $record = $h->numero;
        $temoin = $h;
    }
}
echo $temoin->nom . "\n";
echo $ville->temoignageDe($temoin) . "\n";

// Réponse 1 : Laure Guichard
foreach ($ville->habitants() as $h) {
    $v = $ville->vehiculeDe($h);
    if ($v !== null
    && $h->genre === "femme"
    && $h->cheveux === "roux"
    && $h->age >= 64 && $h->age <= 69
    && $v->marque === "Toyota"
    && $v->modele === "Yaris") {
        echo $h->nom . "\n";
    }
}

verifier("Laure Guichard");

// Réponse 2 : Albert Delmas
foreach ($ville->habitants() as $h) {
    if ($h->rue === "Impasse de la Paix"
    && $h->numero >= 37 && $h->numero <= 47
    && $h->revenu > 150000) {
        echo $h->nom . "\n";
    }
}

verifier("Albert Delmas");

Histoire 6 : le fantôme du théâtre, dans une classe

Sélectionnez Le fantôme du théâtre. Nouveauté : des indices en « n passages à tel endroit », à compter avec $ville->passagesDe($h). Et une exigence de forme : votre code d'enquête vit dans une classe à vous :

php
final class EnqueteFantome
{
    public function __construct(private Ville $ville)
    {
    }

    /** @return Habitant[] */
    public function suspects(): array
    {
        // votre filtre ici : parcourez les habitants, comptez les passages,
        // et retournez un tableau d'objets Habitant (pas des echo !)
    }
}

$enquete = new EnqueteFantome($ville);
foreach ($enquete->suspects() as $suspect) {
    echo $suspect->nom . "\n";
}

C'est le même filtre qu'avant, mais encapsulé : suspects() retourne des objets, l'affichage reste à l'extérieur. Une méthode privée nombreDePassages(Habitant $h): int gardera suspects() lisible.

Structure : 1 témoin, puis un personnage à identifier (réponse 1), puis un personnage à identifier (réponse 2).

Indice 1 : les témoins
  • un prénom sur « Rue Émile-Zola » : str_starts_with(...nom..., "Margaux ") en plus du test sur la rue.

Une fois trouvés, lisez leur témoignage avec $ville->temoignageDe($temoin).

Indice 2 : le coupable

Les indices viennent de : Margaux Prieur (passage, physique).

  • physique : la description (genre, cheveux, âge, taille) est portée par les propriétés de Habitant.
  • passage : comptez les objets de $ville->passagesDe($h) qui correspondent (lieu, date, heure).

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir. Une fois validé, lisez son témoignage : l'enquête continue.

Indice 3 : la personne derrière tout ça

Les indices viennent de : Serge Humbert (physique, vehicule).

  • physique : la description (genre, cheveux, âge, taille) est portée par les propriétés de Habitant.
  • vehicule : demandez $ville->vehiculeDe($h) (qui peut renvoyer null) puis testez la plaque avec str_starts_with, str_ends_with ou str_contains.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.

Voir l'une des solutions possibles
php
// Témoin : Margaux Prieur
$temoin = null;
$record = 0;
foreach ($ville->habitants() as $h) {
    if ($h->rue === "Rue Émile-Zola" && $h->revenu > $record) {
        $record = $h->revenu;
        $temoin = $h;
    }
}
echo $temoin->nom . "\n";
echo $ville->temoignageDe($temoin) . "\n";

// Réponse 1 : Serge Humbert
final class EnqueteFantome
{
    public function __construct(private Ville $ville)
    {
    }

    /** @return Habitant[] */
    public function suspects(): array
    {
        $ville = $this->ville;
        $resultat = [];
        foreach ($ville->habitants() as $h) {
            $nb = 0;
            foreach ($ville->passagesDe($h) as $p) {
                if ($p->lieu === "Théâtre de la Passerelle" && intdiv($p->date, 100) === 202501) {
                    $nb++;
                }
            }
            if ($h->genre === "homme" && $h->taille >= 163 && $h->taille <= 167 && $nb === 4) {
                $resultat[] = $h;
            }
        }
        return $resultat;
    }
}

$enquete = new EnqueteFantome($ville);
foreach ($enquete->suspects() as $suspect) {
    echo $suspect->nom . "\n";
}

verifier("Serge Humbert");

// Réponse 2 : Monique Vallet
final class EnqueteActrice
{
    public function __construct(private Ville $ville)
    {
    }

    /** @return Habitant[] */
    public function suspects(): array
    {
        $ville = $this->ville;
        $resultat = [];
        foreach ($ville->habitants() as $h) {
            $v = $ville->vehiculeDe($h);
            if ($v !== null && $h->genre === "femme" && $h->cheveux === "blond" && $v->marque === "Citroën" && str_starts_with($v->plaque, "1R9N")) {
                $resultat[] = $h;
            }
        }
        return $resultat;
    }
}

$enquete = new EnqueteActrice($ville);
foreach ($enquete->suspects() as $suspect) {
    echo $suspect->nom . "\n";
}

verifier("Monique Vallet");

Histoire 7 : le naufrage de la régate, avec des critères

Sélectionnez Le naufrage de la régate. Dernier niveau : chaque indice devient une classe critère qui implémente Critere, et filtrer() les combine. Méfiez-vous aussi des fausses pistes…

Un critère, c'est une toute petite classe :

php
final class CheveuxDeCouleur implements Critere
{
    public function __construct(private string $couleur)
    {
    }

    public function accepte(Habitant $h): bool
    {
        return $h->cheveux === $this->couleur;
    }
}

Et l'enquête devient une composition, lisible comme une phrase :

php
$suspects = filtrer(
    $ville,
    new CheveuxDeCouleur("roux"),
    new AgeEntre(40, 45),
);
foreach ($suspects as $suspect) {
    echo $suspect->nom . "\n";
}

À vous d'écrire les critères dont l'histoire a besoin : couleur de cheveux, âge, plaque qui contient un fragment (ce critère aura besoin de $ville dans son constructeur pour appeler vehiculeDe()), nombre de passages à un lieu… Chaque classe fait UNE chose, filtrer() les assemble.

Question : pourquoi filtrer() fonctionne avec n'importe quel critère ?

Parce qu'elle ne connaît que l'interface Critere : elle appelle accepte($h) sans savoir quelle classe concrète est derrière. Dix critères différents, un seul code de filtrage : c'est le polymorphisme du cours, appliqué à quelque chose d'utile.

Structure : 1 témoin, puis une fausse piste, puis un personnage à identifier (réponse 1), puis un personnage à identifier (réponse 2).

Indice 1 : les témoins
  • un prénom sur « Rue du Port » : str_starts_with(...nom..., "Paul ") en plus du test sur la rue.

Une fois trouvés, lisez leur témoignage avec $ville->temoignageDe($temoin).

Indice 2 : la fausse piste

Les indices viennent de : Paul Cordier (adresse, physique).

  • physique : la description (genre, cheveux, âge, taille) est portée par les propriétés de Habitant.
  • adresse : les propriétés rue et numero de Habitant (ou la méthode habite()).

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir. Une fois validé, lisez son témoignage : l'enquête continue.

Indice 3 : le coupable

Les indices viennent de : Denis Leveque (passage, physique, vehicule).

  • physique : la description (genre, cheveux, âge, taille) est portée par les propriétés de Habitant.
  • vehicule : demandez $ville->vehiculeDe($h) (qui peut renvoyer null) puis testez la plaque avec str_starts_with, str_ends_with ou str_contains.
  • passage : comptez les objets de $ville->passagesDe($h) qui correspondent (lieu, date, heure).

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir. Une fois validé, lisez son témoignage : l'enquête continue.

Indice 4 : la personne derrière tout ça

Les indices viennent de : Fanny Monnier (physique, revenu, vehicule).

  • physique : la description (genre, cheveux, âge, taille) est portée par les propriétés de Habitant.
  • vehicule : demandez $ville->vehiculeDe($h) (qui peut renvoyer null) puis testez la plaque avec str_starts_with, str_ends_with ou str_contains.
  • revenu : la propriété revenu de Habitant.

Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.

Voir l'une des solutions possibles
php
// Témoin : Paul Cordier
$temoin = null;
foreach ($ville->habitants() as $h) {
    if ($h->rue === "Rue du Port" && str_starts_with($h->nom, "Paul ")) {
        $temoin = $h;
    }
}
echo $temoin->nom . "\n";
echo $ville->temoignageDe($temoin) . "\n";

// Fausse piste : Denis Leveque
final class GenreEst implements Critere
{
    public function __construct(private string $genre)
    {
    }

    public function accepte(Habitant $h): bool
    {
        return $h->genre === $this->genre;
    }
}
final class CheveuxDeCouleur implements Critere
{
    public function __construct(private string $couleur)
    {
    }

    public function accepte(Habitant $h): bool
    {
        return $h->cheveux === $this->couleur;
    }
}
final class HabiteRue implements Critere
{
    public function __construct(private string $rue, private int $min = 0, private int $max = PHP_INT_MAX)
    {
    }

    public function accepte(Habitant $h): bool
    {
        return $h->rue === $this->rue && $h->numero >= $this->min && $h->numero <= $this->max;
    }
}

$suspects = filtrer(
    $ville,
    new GenreEst("homme"),
    new CheveuxDeCouleur("noir"),
    new HabiteRue("Rue Voltaire", 14, 24),
);
foreach ($suspects as $suspect) {
    echo $suspect->nom . "\n";
}

verifier("Denis Leveque");

// Réponse 1 : Fanny Monnier
final class AgeEntre implements Critere
{
    public function __construct(private int $min, private int $max)
    {
    }

    public function accepte(Habitant $h): bool
    {
        return $h->age >= $this->min && $h->age <= $this->max;
    }
}
final class PlaqueContient implements Critere
{
    public function __construct(private Ville $ville, private string $fragment)
    {
    }

    public function accepte(Habitant $h): bool
    {
        $v = $this->ville->vehiculeDe($h);
        return $v !== null && str_contains($v->plaque, $this->fragment);
    }
}
final class PassagesSaboteur implements Critere
{
    public function __construct(private Ville $ville)
    {
    }

    public function accepte(Habitant $h): bool
    {
        $nb = 0;
        foreach ($this->ville->passagesDe($h) as $p) {
            if ($p->lieu === "Piscine des Lilas" && intdiv($p->date, 100) === 202506) {
                $nb++;
            }
        }
        return $nb === 3;
    }
}

$suspects = filtrer(
    $ville,
    new GenreEst("femme"),
    new AgeEntre(55, 59),
    new PlaqueContient($ville, "1Y6V"),
    new PassagesSaboteur($ville),
);
foreach ($suspects as $suspect) {
    echo $suspect->nom . "\n";
}

verifier("Fanny Monnier");

// Réponse 2 : Charlotte Bailly
final class TailleEntre implements Critere
{
    public function __construct(private int $min, private int $max)
    {
    }

    public function accepte(Habitant $h): bool
    {
        return $h->taille >= $this->min && $h->taille <= $this->max;
    }
}
final class MarqueDeVoiture implements Critere
{
    public function __construct(private Ville $ville, private string $marque)
    {
    }

    public function accepte(Habitant $h): bool
    {
        $v = $this->ville->vehiculeDe($h);
        return $v !== null && $v->marque === $this->marque;
    }
}
final class ModeleDeVoiture implements Critere
{
    public function __construct(private Ville $ville, private string $modele)
    {
    }

    public function accepte(Habitant $h): bool
    {
        $v = $this->ville->vehiculeDe($h);
        return $v !== null && $v->modele === $this->modele;
    }
}
final class RevenuSuperieurA implements Critere
{
    public function __construct(private int $seuil)
    {
    }

    public function accepte(Habitant $h): bool
    {
        return $h->revenu > $this->seuil;
    }
}

$suspects = filtrer(
    $ville,
    new GenreEst("femme"),
    new CheveuxDeCouleur("roux"),
    new TailleEntre(193, 197),
    new MarqueDeVoiture($ville, "Tesla"),
    new ModeleDeVoiture($ville, "Model Y"),
    new RevenuSuperieurA(250000),
);
foreach ($suspects as $suspect) {
    echo $suspect->nom . "\n";
}

verifier("Charlotte Bailly");

Pour aller plus loin

  • Écrivez une fonction trouverParNom(array $habitants, string $nom): ?array qui renvoie un habitant complet à partir de son nom.
  • Refaites l'enquête n° 1 avec array_filter() et une fonction anonyme.
  • Côté POO : ajoutez un critère Non(Critere $c) qui inverse n'importe quel critère, et un critère Ou(Critere ...$criteres). Vous venez d'écrire un moteur de requêtes.
  • Comparez avec l'Enquête SQL : ce sont les mêmes enquêtes, en une ligne de requête. filtrer($ville, …) ressemble beaucoup à un WHERE … AND …, et ce n'est pas un hasard.

Conclusion

En jouant, vous avez :

  • filtré un tableau avec foreach + if, en cumulant les conditions ;
  • cherché un maximum avec une variable record et compté des occurrences avec un tableau compteur ;
  • croisé plusieurs tableaux grâce à un identifiant commun ;
  • côté objet : respecté le contrat d'une classe encapsulée, structuré votre code dans vos propres classes et composé des critères grâce à une interface.

👋 Si vous avez des questions, n'hésitez pas !

×

Reformulation

La reformulation (IA) peut faire des erreurs. Envisagez de vérifier les informations.