Agence web » Actualités du digital » Approches de création de tableaux typés en PHP –

Approches de création de tableaux typés en PHP –

Logo PHP

PHP ne vous permet pas de définir des tableaux typés. Tout tableau peut contenir n’importe quelle valeur, ce qui rend difficile l’application de la cohérence dans votre base de code. Voici quelques solutions de contournement pour vous aider à créer des collections d’objets typés à l’aide des fonctionnalités PHP existantes.

Identifier le problème

Les tableaux PHP sont une structure de données très flexible. Vous pouvez ajouter ce que vous voulez à un tableau, allant des valeurs scalaires aux objets complexes:

$arr = [
    "foobar",
    123,
    new DateTimeImmutable()
];

En pratique, il est rare que vous souhaitiez un tableau avec une plage de valeurs aussi variée. Il est plus probable que vos tableaux contiendront plusieurs instances du même type de valeur.

$times = [
    new DateTimeImmutable(),
    new DateTimeImmutable(),
    new DateTimeImmutable()
];

Vous pouvez alors créer une méthode qui agit sur toutes les valeurs de votre tableau:

final class Stopwatch {
 
    protected array $laps = [];
 
    public function recordLaps(array $times) : void {
        foreach ($times as $time) {
            $this -> laps[] = $time -> getTimestamp();
        }
    }
 
}

Ce code itère sur le DateTimeInterface instances dans $times. La représentation d’horodatage Unix du temps (secondes mesurées comme un entier) est ensuite stockée dans $laps.

Le problème avec ce code est qu’il fait un supposition cette $times est composé entièrement de DateTimeInterface les instances. Rien ne garantit que ce soit le cas, donc un appelant peut toujours transmettre un tableau de valeurs mixtes. Si l’une des valeurs n’a pas été implémentée DateTimeInterface, l’appel à getTimestamp() serait illégal et une erreur d’exécution se produirait.

$stopwatch = new Stopwatch();
 
// OK
$stopwatch -> recordLaps([
    new DateTimeImmutable(),
    new DateTimeImmutable()
]);
 
// Crash!
$stopwatch -> recordLaps([
    new DateTimeImmutable(),
    123     // can't call `getTimestamp()` on an integer!
]);

Ajout de la cohérence de type avec des arguments variadiques

Idéalement, le problème serait résolu en spécifiant que le $times le tableau ne peut contenir que DateTimeInterface les instances. Comme PHP ne prend pas en charge les tableaux typés, nous devons plutôt nous tourner vers des fonctionnalités de langage alternatives.

La première option consiste à utiliser des arguments variadiques et à décompresser le $times tableau avant qu’il ne soit passé à recordLaps(). Les arguments variadiques permettent à une fonction d’accepter un nombre inconnu d’arguments qui sont ensuite rendus disponibles sous forme de tableau unique. Surtout pour notre cas d’utilisation, vous pouvez taper des arguments variadiques comme d’habitude. Chaque argument passé doit alors être du type donné.

Les arguments variadiques sont couramment utilisés pour les fonctions mathématiques. Voici un exemple simple qui résume chaque argument donné:

function sumAll(int ...$numbers) {
    return array_sum($numbers);
}
 
echo sumAll(1, 2, 3, 4, 5);     // emits 15

sumAll() n’est pas passé un tableau. Au lieu de cela, il reçoit plusieurs arguments que PHP combine dans le $numbers déployer. Le int typehint signifie que chaque valeur doit être un entier. Cela agit comme une garantie que $numbers ne sera composé que d’entiers. Nous pouvons maintenant appliquer ceci à l’exemple du chronomètre:

final class Stopwatch {
 
    protected array $laps = [];
 
    public function recordLaps(DateTimeInterface ...$times) : void {
        foreach ($times as $time) {
            $this -> laps[] = $time -> getTimestamp();
        }
    }
 
}
 
$stopwatch = new Stopwatch();
 
$stopwatch -> recordLaps(
    new DateTimeImmutable(),
    new DateTimeImmutable()
);

Il n’est plus possible de transmettre des types non pris en charge dans recordLaps(). Les tentatives en ce sens seront faites beaucoup plus tôt, avant la getTimestamp() l’appel est tenté.

Si vous avez déjà un éventail d’heures à passer recordLaps(), vous devrez le décompresser avec l’opérateur splat (...) lorsque vous appelez la méthode. Essayer de le passer directement échouera – il serait traité comme l’un des temps variables, qui doivent être un int et pas un array.

$times = [
    new DateTimeImmutable(),
    new DateTimeImmutable()
];
 
$stopwatch -> recordLaps(...$times);

Limitations des arguments variadiques

Les arguments variadiques peuvent être d’une grande aide lorsque vous devez passer un tableau d’éléments à une fonction. Cependant, il existe certaines restrictions sur la façon dont ils peuvent être utilisés.

La limitation la plus importante est que vous ne pouvez utiliser qu’un seul ensemble d’arguments variadiques par fonction. Cela signifie que chaque fonction ne peut accepter qu’un seul tableau «typé». De plus, l’argument variadique doit être défini en dernier, après tout argument régulier.

function variadic(string $something, DateTimeInterface ...$times);

Par nature, les arguments variadiques ne peuvent être utilisés qu’avec des fonctions. Cela signifie qu’ils ne peuvent pas vous aider lorsque vous en avez besoin boutique un tableau en tant que propriété, ou le renvoyer à partir d’une fonction. Nous pouvons le voir dans le code du chronomètre – le Stopwatch la classe a un laps tableau qui est destiné à stocker uniquement des horodatages entiers. Il n’y a actuellement aucun moyen que nous puissions appliquer c’est le cas.

Classes de collection

Dans ces circonstances, une approche différente doit être choisie. Une façon de créer quelque chose de proche d’un «tableau typé» en PHP userland est d’écrire une classe de collection dédiée:

final class User {
 
    protected string $Email;
 
    public function getEmail() : string {
        return $this -> Email;
    }
 
}
 
final class UserCollection implements IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}

Le UserCollection La classe peut maintenant être utilisée partout où vous attendez normalement un tableau de User les instances. UserCollection utilise des arguments variadiques pour accepter une série de User instances dans son constructeur. Bien que le $Users la propriété doit être typée comme le générique array, il est garanti qu’il se compose entièrement d’instances utilisateur car il n’est écrit que dans le constructeur.

Il peut sembler tentant de fournir un get() : array méthode qui expose tous les éléments de la collection. Cela doit être évité car cela nous ramène au vague array problème de typehint. Au lieu de cela, la collection est rendue itérable afin que les consommateurs puissent l’utiliser dans un foreach boucle. De cette façon, nous avons réussi à créer un «tableau» de type hintable dont notre code peut supposer en toute sécurité qu’il contient uniquement des utilisateurs.

function sendMailToUsers(UserCollection $Users) : void {
    foreach ($Users as $User) {
        mail($user -> getEmail(), "Test Email", "Hello World!");
    }
}
 
$users = new UserCollection(new User(), new User());
sendMailToUsers($users);

Rendre les collections plus semblables à des tableaux

Les classes de collection résolvent le problème de typehinting mais signifient que vous perdez une partie des utile fonctionnalité des tableaux. Fonctions PHP intégrées telles que count() et isset() ne fonctionnera pas avec votre classe de collection personnalisée.

La prise en charge de ces fonctions peut être ajoutée en implémentant des interfaces intégrées supplémentaires. Si vous implémentez Countable, votre classe sera utilisable avec count():

final class UserCollection implements Countable, IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function count() : int {
        return count($this -> Users);
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}
 
$users = new UserCollection(new User(), new User());
echo count($users);     // 2

Exécution ArrayAccess vous permet d’accéder aux éléments de votre collection à l’aide de la syntaxe de tableau. Il permet également le isset() et unset() les fonctions. Vous devez implémenter quatre méthodes pour que PHP puisse interagir avec vos éléments.

final class UserCollection implements ArrayAccess, IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function offsetExists(mixed $offset) : bool {
        return isset($this -> Users[$offset]);
    }
 
    public function offsetGet(mixed $offset) : User {
        return $this -> Users[$offset];
    }
 
    public function offsetSet(mixed $offset, mixed $value) : void {
        if ($value instanceof User) {
            $this -> Users[$offset] = $value;
        }
        else throw new TypeError("Not a user!");
    }
 
    public function offsetUnset(mixed $offset) : void {
        unset($this -> Users[$offset]);
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}
 
$users = new UserCollection(
    new User("example@example.com"),
    new User("hello@world.com")
);
 
echo $users[1] -> getEmail();   // hello@world.com
var_dump(isset($users[2]));     // false

Vous avez maintenant une classe qui ne peut contenir que User instances et qui ressemble et se sent également comme un tableau. Un point à noter sur ArrayAccess est le offsetSet mise en œuvre – comme $value doit être mixed, cela pourrait permettre l’ajout de valeurs incompatibles à votre collection. Nous vérifions explicitement le type du passé $value pour éviter cela.

Conclusion

Les récentes versions de PHP ont fait évoluer le langage vers un typage plus fort et une plus grande cohérence. Cependant, cela ne s’étend pas encore aux éléments de tableau. Taper une indication contre array est souvent trop détendu, mais vous pouvez contourner les limitations en créant vos propres classes de collection.

Lorsqu’il est combiné avec des arguments variadiques, le modèle de collection est un moyen viable d’appliquer les types de valeurs agrégées dans votre code. Vous pouvez taperhint vos collections et les parcourir en sachant qu’un seul type de valeur sera présent.

★★★★★