Top Code 2024, les challenges sont de nouveau disponibles dans les boards pour les participant(e)s => Boards
T: / Corrigés des challenges / PHP
Les Collections permettent de manipuler efficacement les tableaux en PHP. Cela fonctionne sur les objets aussi !
On avait déjà fait une brève présentation des Collections en PHP dans un précédent corrigé. Pour rappel, les Collections sont des outils permettant d’homogénéiser la manipulation des tableaux en PHP, là où les fonctions disponibles nativement sont un peu « disparates » en terme d’organisation et de fonctionnement (ordre des paramètres, par référence ou pas, etc.). Les Collections viennent aussi avec des fonctionnalités supplémentaires et une syntaxe objet qui permet de « chainer » les méthodes, un peu comme on le ferait en javascript.
Les Collections permettent de manipuler des tableaux. Mais on va voir dans ce corrigé, que l’on peut surtout s’en servir pour manipuler des tableaux d’objets. On va s’appuyer pour cela sur le challenge « Petits Monstres #3« .
Au programme, en avançant pas à pas :
Dans le challenge « Petits Monstres #3 », il s’agit de regrouper les monstres en fonction d’une de leurs caractéristiques, à savoir leur type de nourriture (4 types différents).
Une fois qu’on a constitué les 4 groupes, on va ordonner les monstres selon une autre caractéristique, à savoir leur poids. Ils seront rangés soit dans l’ordre croissant, soit dans l’ordre décroissant (encore une fois selon le type de nourriture).
Voyons voir comment on peut coder tout ça !
Pour commencer, on a un enum pour stocker le type de nourriture, et une classe pour gérer les monstres et le parsing de leurs informations :
enum FoodType: string
{
case FRUITS = 'F';
case GRASS = 'G';
case ROCK = 'R';
case WOOD = 'W';
}
class Monster
{
public function __construct(
public readonly string $name,
public readonly FoodType $foodType,
public readonly int $weight,
) {}
public static function createFromText(string $informations): Monster
{
$data = explode(':', $informations);
return new self(
name: $data[0],
foodType: FoodType::from($data[1]),
weight: (int) $data[2]
);
}
}
Un peu d’explications :
Maintenant créons une classe MonstersLines qui représente ces lignes de petits monstres (trop) mignons :
class MonstersLines
{
/**
* @var Monster[]
*/
private array $monsters = [];
public function addMonsterFromText(string $informations): void
{
$this->monsters[] = Monster::createFromText($informations);
}
public function getMonstersFromFoodType(FoodType $foodType, int $nbMonsters = 3): string
{
// 1. Initialisation d'un tableau vide
$monsters = [];
// 2. Je filtre selon le type de nourriture
foreach ($this->monsters as $monster) {
if ($monster->foodType === $foodType) {
$monsters[] = $monster;
}
}
// 3. Je trie dans un sens ou dans l'autre, selon le type de nourriture
// grâce à la fonction usort, en comparant les poids
if ($foodType === FoodType::FRUITS || $foodType === FoodType::GRASS) {
usort($monsters, function (Monster $m1, Monster $m2) {
return $m1->weight <=> $m2->weight;
});
} else {
// Sens inverse
usort($monsters, function (Monster $m1, Monster $m2) {
return $m2->weight <=> $m1->weight;
});
}
// 4. Je garde les N premiers monstres
$monsters = array_slice($monsters, 0, $nbMonsters);
// 5. J'extraie les noms des monstres
$names = array_column($monsters, 'name');
// 6. Je lie les noms avec un "-"
return implode('-', $names);
}
public function getAllFirstThree(): string
{
// 0. Je précise l'ordre attendu
$foodTypes = [
FoodType::FRUITS,
FoodType::GRASS,
FoodType::ROCK,
FoodType::WOOD
];
// 0. J'initialise un tableau vide en vue de mon implode final
$result = [];
// 1. Je parcours chaque type pour récupérer les 3 premiers monstres
foreach ($foodTypes as $foodType) {
$result[] = $this->getMonstersFromFoodType($foodType, 3);
}
// 2. Je lie les éléments avec un "-"
return implode('-', $result);
}
}
Un peu d’explications :
On va voir comment améliorer tout ça avec les Collections…
Les collections proposent des fonctions « sortBy » et « sortByDesc » qui permettent de trier dans un ordre croissant ou décroissant selon une certaine propriété.
On va en profiter pour stocker dans une variable la fonction à utiliser. Et l’appeler dynamiquement lors du « chainage » des Collections.
Sans les Collections (même code que + haut)
public function getMonstersFromFoodType(FoodType $foodType, int $nbMonsters = 3): string
{
$monsters = [];
foreach ($this->monsters as $monster) {
if ($monster->foodType === $foodType) {
$monsters[] = $monster;
}
}
if ($foodType === FoodType::FRUITS || $foodType === FoodType::GRASS) {
usort($monsters, function (Monster $m1, Monster $m2) {
return $m1->weight <=> $m2->weight;
});
} else {
usort($monsters, function (Monster $m1, Monster $m2) {
return $m2->weight <=> $m1->weight;
});
}
$monsters = array_slice($monsters, 0, $nbMonsters);
$names = array_column($monsters, 'name');
return implode('-', $names);
}
Avec les Collections
public function getMonstersFromFoodType(FoodType $foodType, int $number = 3): string
{
// Gestion du tri, par défaut, dans l'ordre croissant, décroissant pour ROCK et WOOD
$sort = 'sortBy';
if ($foodType === FoodType::ROCK || $foodType === FoodType::WOOD) {
$sort = 'sortByDesc';
}
return collect($this->monsters)
->filter(function(Monster $item) use ($foodType) {
return $item->foodType === $foodType;
})
->$sort('weight')
->slice(0, $number)
->implode('name', '-');
}
Plus concis et mieux organisé, non ?!
Un peu d’explications pour la partie Collections :
Pour la refonte de la méthode getAllFirstThree, on va utiliser la méthode natives « cases » des enums. C’est pour ça qu’on a fait attention à l’ordre défini dans l’enum.
Sans les Collections (même code que + haut)
public function getAllFirstThree(): string
{
$foodTypes = [
FoodType::FRUITS,
FoodType::GRASS,
FoodType::ROCK,
FoodType::WOOD
];
$result = [];
foreach ($foodTypes as $foodType) {
$result[] = $this->getMonstersFromFoodType($foodType, 3);
}
return implode('-', $result);
}
Avec les Collections
public function getAllFirstThree(): string
{
return collect(FoodType::cases())
->map( fn(FoodType $foodType) => $this->getMonstersFromFoodType($foodType, 3) )
->implode('-');
}
Convaincu ?!
Un peu d’explications :
Pour réaliser le challenge, je lance ce code :
$monsterLine = new MonstersLines;
foreach ($monsters as $monsterInformations) {
$monsterLine->addMonsterFromText($monsterInformations);
}
$solution = $monsterLine->getAllFirstThree();
On a donc vu dans ce corrigé que l’utilisation des Collections sur les tableaux d’objets est tout à fait fonctionnelle et pratique, notamment pour trier selon une propriété, récupérer les valeurs d’une propriété, etc.
Les Collections proposent encore de nombreuses fonctionnalités, que vous pouvez découvrir dans la documentation officielle des Collections en PHP (dans le doc de Laravel).
L’ensemble du code peut être retrouvé sur Github, ainsi que les tests unitaires réalisés avec Pest PHP. Ils ont d’ailleurs été très pratiques pour refactoriser le code avec les Collections. J’ai suivi ce process :
Pour tester les 2 classes, j’ai créé des tests avec des dataset. Chaque dataset contient les noms des classes pour pouvoir les instancier dynamiquement. Voici ce que ça donne :
test('Tri des monstres ROCK', function(string $className) {
$monsters = ['Drex29:R:16', 'Traz65:W:74', 'Blit86:R:27', 'Brux18:F:83', 'Cobi82:R:72', 'Brux61:F:56', 'Draz68:G:97'];
$monsterLine = new $className;
foreach ($monsters as $monsterInformations) {
$monsterLine->addMonsterFromText($monsterInformations);
}
expect($monsterLine->getMonstersFromFoodType(FoodType::ROCK, 3))->toBe('Cobi82-Blit86-Drex29');
})->with([
[MonstersLinesWC::class],
[MonstersLines::class]
]);
Ce n’est pas forcément l’approche « parfaite ». On pourrait à minima créer une interface pour s’assurer que les 2 classes ont bien les mêmes méthodes avant de faire ça. Ou sinon se diriger vers un pattern… comme le design pattern Strategy 😉
Autres contenus à découvrir
Corrigés, challenges, actualités, veille technique... aucun spam.