T: / Corrigés des challenges / PHP
En s’appuyant sur la programmation orienté objet, on met en pratique des concepts avancés sur un challenge débutant.
Encore une fois, on va partir d’un challenge de code débutant pour mettre en pratique des concepts plus avancés. On va travailler en programmation orientée objet et passer en revue les concepts suivants :
Le tout en conservant un niveau de contrôle du code avec PHPStan au niveau max.
Dans ce challenge, il est question de constituer la meilleure équipe de pokemons et de retourner la puissance totale de cette meilleure équipe.
En lisant la phrase ci-dessus, on voit que 2 termes se dégagent « Pokemon » et « Equipe ».
Ensuite les Pokemons sont définis par leur type (Herbe, Feu, Eau, etc.). Ces types sont connus et bien définis. L’utilisation d’un enum est appropriée ici.
On va donc avoir ces 3 fichiers :
Les enums permettent de définir un type avec un ensemble de valeurs possibles. Cette structure permet vraiment d’améliorer la lisibilité du code et d’éviter les erreurs car tous les choix possibles sont prédéfinis.
Voici le code de notre enum :
// Notre enum est ici typé. Tous les cas possibles seront des chaines de caractères
enum PokemonType: string
{
// Types classiques
case HERBE = 'Herbe';
case FEU = 'Feu';
case EAU = 'Eau';
// Types rares
case GLACE = 'Glace';
case AIR = 'Air';
case POISON = 'Poison';
case PSYCHIQUE = 'Psychique';
case INSECTE = 'Insecte';
}
Mais ce n’est pas tout ! A la manière d’une classe, un enum peut contenir des méthodes. On va lui rajouter 2 méthodes qui vont permettre de retourner les catégories de types :
/**
* @return PokemonType[]
*/
public static function rares(): array
{
return [self::GLACE, self::AIR, self::POISON, self::PSYCHIQUE, self::INSECTE];
}
/**
* @return PokemonType[]
*/
public static function classiques(): array
{
return [self::HERBE, self::FEU, self::EAU];
}
Ces 2 méthodes retournent chacune un tableau contenant des cas de notre enum.
Un Value Object est une classe qui permet d’encapsuler des propriétés spécifiques, sans « identité propre ». Ici un Pokemon est défini par ses propriétés, dont son type. De plus dans un Value Object, les données seront immuables, c’est à dire qu’elles n’évolueront pas dans le temps. Une très bonne occasion d’utiliser la propriété readonly ! Voici la classe :
class Pokemon
{
public function __construct(
public readonly PokemonType $type,
public readonly int $power
) {}
public static function createFromText(string $informations): Pokemon
{
$data = explode(':', $informations);
return new self(
PokemonType::from($data[0]),
(int) $data[1]
);
}
}
Un peu d’explications :
La classe PokemonTeam va avoir beaucoup de boulot ! Elle doit tout d’abord stocker les pokemons puis disposer d’une méthode pour les ajouter. Voici le début de cette classe :
class PokemonTeam
{
/**
* @var Pokemon[] $pokemons
*/
private array $pokemons = [];
public function addPokemon(string $informations): void
{
$this->pokemons[] = Pokemon::createFromText($informations);
}
}
Un peu d’explications :
Maintenant, j’ai envie d’ajouter tous mes pokemons d’un coup, c’est à dire de passer en paramètre tout un tableau d’informations à parser (ce qui est aussi en passant le jeu de données du challenge 😉 ).
Je vais pouvoir le faire de 2 façons :
Avec un foreach :
/**
* @param string[] $informations
*/
public function addPokemons(array $informations): void
{
foreach ($informations as $informationsPokemons) {
$this->addPokemon($informationsPokemons);
}
}
Avec array_map :
/**
* @param string[] $informations
*/
public function addPokemons(array $informations): void
{
array_map([$this, 'addPokemon'], $informations);
}
Dans cette utilisation de array_map, on n’utilise pas le retour de array_map, on n’en a pas besoin. Comme les objets fonctionnent par référence, on appelle dynamiquement la méthode de la classe en passant un tableau comme paramètre de callback à array_map avec [$this, ‘addPokemon’].
Maintenant on va chercher le meilleur pokémon selon un ou plusieurs types. Soit je veux par exemple le meilleur pokémon de type « Herbe », soit je veux le meilleur pokémon rare, avec donc un ensemble de types.
On crée une méthode qui pourra prendre en paramètre les 2 cas de figure : un seul type ou un tableau de types (de pokemon) :
Voici la déclaration de cette méthode :
/**
* @param PokemonType|PokemonType[] $pokemonTypes
*/
public function getBestPokemonFromTypes(PokemonType|array $pokemonTypes): int
On utilise le caractère | pour définir une union de type et donc la possibilité d’avoir 2 types de paramètres différents en entrée.
Pour contrôler la correspondance du ou des types, on va travailler avec la fonction in_array. Celle-ci prend en paramètre un tableau. Même si j’ai un seul type, il me faut un tableau. On va pouvoir faire comme ça, grâce à la fonction PHP is_array :
if (! is_array($pokemonTypes)) {
$pokemonTypes = [$pokemonTypes]; // On met l'élément seul dans un tableau
}
Et voici la méthode avec un foreach et une recherche de maximum classique :
/**
* @param PokemonType|PokemonType[] $pokemonTypes
*/
public function getBestPokemonFromTypes(PokemonType|array $pokemonTypes): int
{
if (! is_array($pokemonTypes)) {
$pokemonTypes = [$pokemonTypes];
}
$max = 0;
foreach ($this->pokemons as $pokemon) {
if (! in_array($pokemon->type, $pokemonTypes)) {
continue;
}
if ($pokemon->power > $max) {
$max = $pokemon->power;
}
}
return $max;
}
On retrouve le in_array utilisé sur $pokemonTypes qui sera bien toujours un tableau.
Et maintenant avec le array_map, avec au passage l’utilisation d’un ternaire à la place du if :
/**
* @param PokemonType|PokemonType[] $pokemonTypes
*/
public function getBestPokemonFromTypes(PokemonType|array $pokemonTypes): int
{
if (! is_array($pokemonTypes)) {
$pokemonTypes = [$pokemonTypes];
}
return max(
array_map(function (Pokemon $pokemon) use ($pokemonTypes) {
return in_array($pokemon->type, $pokemonTypes) ? $pokemon->power : 0;
}, $this->pokemons)
);
}
Un peu d’explications :
Enfin, il reste à faire la somme des puissances. Cette dernière étape est l’occasion d’utiliser les méthodes créées dans l’enum.
Voici le code :
Avec un foreach :
public function getPowerOfBestTeam(): int
{
$total = 0;
foreach (PokemonType::classiques() as $pokemonTypeClassique) {
$total += $this->getBestPokemonFromTypes($pokemonTypeClassique);
}
$total += $this->getBestPokemonFromTypes(PokemonType::rares());
return $total;
}
Avec array_map :
public function getPowerOfBestTeam(): int
{
return array_sum(
array_map([$this, 'getBestPokemonFromTypes'], PokemonType::classiques())
)
+ $this->getBestPokemonFromTypes(PokemonType::rares());
}
On a ici un mélange des 2 premiers cas, on utilise à nouveau une méthode interne comme callback et on applique array_sum sur le retour de array_map (comme on utilisait max juste avant).
On voit là tout l’intérêt des méthodes de l’enum qui permettent un code propre et très explicite.
Cette dernière méthode cloture le challenge, voici comment le challenge peut être résolu :
$pokemonTeam = new PokemonTeam;
$pokemonTeam->addPokemons($pokemons);
echo $pokemonTeam->getPowerOfBestTeam();
On commence par tester le parsing lors de la création d’un Pokemon :
test('Création d\'un pokemon', function() {
$informations = 'Glace:34';
$pokemon = Pokemon::createFromText($informations);
expect($pokemon->type)->toBe(PokemonType::GLACE);
expect($pokemon->power)->toBe(34);
});
Ensuite on peut tester les méthodes de PokemonTeam :
Voici la totalité de ces tests :
test('Ajout d\'un pokemon', function() {
$informations = 'Feu:16';
$pokemonTeam = new PokemonTeam;
$pokemonTeam->addPokemon($informations);
$pokemon = $pokemonTeam->getPokemons()[0]; // Un seul Pokemon
expect($pokemon->type)->toBe(PokemonType::FEU);
expect($pokemon->power)->toBe(16);
});
test('Ajout de plusieurs pokemons', function() {
$informations = ['Feu:16', 'Air:20', 'Herbe:31'];
$pokemonTeam = new PokemonTeam;
$pokemonTeam->addPokemons($informations);
$pokemonFeu = $pokemonTeam->getPokemons()[0];
$pokemonAir = $pokemonTeam->getPokemons()[1];
$pokemonHerbe = $pokemonTeam->getPokemons()[2];
expect($pokemonFeu->type)->toBe(PokemonType::FEU);
expect($pokemonFeu->power)->toBe(16);
expect($pokemonAir->type)->toBe(PokemonType::AIR);
expect($pokemonAir->power)->toBe(20);
expect($pokemonHerbe->type)->toBe(PokemonType::HERBE);
expect($pokemonHerbe->power)->toBe(31);
});
test('Recherche du meilleur Pokemon selon 1 type', function() {
$informations = ['Feu:16', 'Air:20', 'Feu:35', 'Herbe:31', 'Feu:52', 'Insecte:17'];
$pokemonTeam = new PokemonTeam;
$pokemonTeam->addPokemons($informations);
expect($pokemonTeam->getBestPokemonFromTypes(PokemonType::FEU))->toBe(52);
});
test('Recherche du meilleur Pokemon selon plusieurs types', function() {
$informations = ['Feu:16', 'Air:20', 'Feu:35', 'Herbe:31', 'Feu:52', 'Insecte:17'];
$pokemonTeam = new PokemonTeam;
$pokemonTeam->addPokemons($informations);
expect($pokemonTeam->getBestPokemonFromTypes([PokemonType::HERBE, PokemonType::AIR]))->toBe(31);
});
test('Puissance meilleure équipe', function() {
// Jeu de données issu de Tainix directement
$informations = ['Psychique:52', 'Eau:48', 'Feu:41', 'Eau:20', 'Herbe:34', 'Herbe:14', 'Feu:48', 'Insecte:98', 'Psychique:76', 'Air:40', 'Herbe:31'];
$pokemonTeam = new PokemonTeam;
$pokemonTeam->addPokemons($informations);
expect($pokemonTeam->getPowerOfBestTeam())->toBe(228);
});
Tu peux aussi chercher un challenge de code pour mettre en application tous les concepts vus ici.
Autres contenus à découvrir
Corrigés, challenges, actualités, veille technique... aucun spam.