Jeu : Enquête PHP
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.
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");
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 X | parcourez $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émoin | parcourez $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 ABC | str_starts_with($v["plaque"], "ABC") / str_ends_with(…) / str_contains(…), en reliant $v["habitant_id"] à $h["id"] |
| passé 3 fois à « X » en mai 2024 | un 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 |
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'idde 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(20240116pour le 16 janvier 2024), les heuresHHMM(1830pour 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
$recordmise à jour dans la boucle quand on trouve mieux. - Compter :
$compteur[$cle] = ($compteur[$cle] ?? 0) + 1;(le?? 0donne 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
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
$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 :
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
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
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 petitnumerode 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 parhabitant_id; un fragment de plaque se teste avecstr_starts_with,str_ends_withoustr_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 lenumero) sont directement dans$habitants. - revenu : le
revenuest 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
// 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 parhabitant_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 parhabitant_id; un fragment de plaque se teste avecstr_starts_with,str_ends_withoustr_contains. - revenu : le
revenuest 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
// 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 unnumeroentre 10 et 30. - le numéro le plus grand de « Place Molière » : parcourez
$habitants, gardez le plus grandnumerode 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 parhabitant_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 parhabitant_id; un fragment de plaque se teste avecstr_starts_with,str_ends_withoustr_contains. - adresse : la
rue(et lenumero) 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
// 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).
| Classe | Propriétés | Méthodes |
|---|---|---|
Habitant | id, nom, genre, age, taille, cheveux, rue, numero, revenu (lecture seule) | prenom(): string, habite(string $rue): bool |
Vehicule | habitantId, marque, modele, plaque | appartientA(Habitant $h): bool |
Passage | habitantId, lieu, date, heure | estA(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 :
interface Critere
{
public function accepte(Habitant $h): bool;
}
/** Renvoie les habitants qui passent TOUS les critères. */
function filtrer(Ville $ville, Critere ...$criteres): arrayCô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
$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
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 grandnumerode 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 renvoyernull) puis testez la plaque avecstr_starts_with,str_ends_withoustr_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
rueetnumerodeHabitant(ou la méthodehabite()). - revenu : la propriété
revenudeHabitant.
Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.
Voir l'une des solutions possibles
// 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 :
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 renvoyernull) puis testez la plaque avecstr_starts_with,str_ends_withoustr_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
// 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 :
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 :
$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
rueetnumerodeHabitant(ou la méthodehabite()).
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 renvoyernull) puis testez la plaque avecstr_starts_with,str_ends_withoustr_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 renvoyernull) puis testez la plaque avecstr_starts_with,str_ends_withoustr_contains. - revenu : la propriété
revenudeHabitant.
Cumulez toutes les conditions dans le même if : à chaque indice ajouté, la liste affichée doit raccourcir.
Voir l'une des solutions possibles
// 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): ?arrayqui 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èreOu(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 à unWHERE … 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 !