Un tableau n'explique pas son contenu
Un tableau natif peut représenter une liste, une carte ou un mélange de clés et de valeurs. Le nom de la variable à lui seul ne constitue pas un contrat.
Un liste utilise des clés entières consécutives commençant à zéro. Un carte associe des clés à des valeurs. PHP utilise la même chose array tapez pour les deux, donc cette déclaration de paramètre dit très peu :
<?php
declare(strict_types=1);
final class User
{
public function __construct(
public readonly string $email,
) {}
}
function sendWelcomeEmails(array $users): void
{
foreach ($users as $user) {
echo $user->email, PHP_EOL;
}
}
sendWelcomeEmails([new User('owner@example.com')]);Un lecteur doit en déduire que $users devrait être une liste de User objets. Un IDE ou un analyseur statique peut déduire certaines valeurs du code voisin, mais ces informations deviennent moins fiables à mesure que le tableau traverse des méthodes, des services ou des frontières externes.
Choisissez d'abord l'outil le plus simple
Une classe n'est utile que lorsqu'elle ajoute un contrat ou un comportement dont le code a réellement besoin.
Annoter une liste locale
Utiliser list<User> ou array<int, User> pour un tableau de courte durée. Les IDE et les analyseurs peuvent comprendre les valeurs attendues sans nouvel objet d'exécution.
Décrire les génériques réutilisables
PHPStan et Psalm comprennent les annotations de modèles telles que @template T of object. Cela met à l’échelle une implémentation de collection sur plusieurs types d’objets lors de l’analyse.
Appliquer une limite de domaine
Créer UserCollection lorsque des valeurs franchissent une limite, des valeurs incorrectes doivent échouer au moment de l'exécution, ou la liste a son propre comportement utile.
PHP 8.1 ne fournit pas de génériques natifs pour l'espace utilisateur. Les annotations de modèles sont des contrats pour les outils de développement ; PHP lui-même ne les applique pas. La collection ci-dessous ajoute la vérification d'exécution.
Enveloppez le tableau et gardez le contrat petit
Utilisez des interfaces standard pour que l'objet prenne en charge foreach, accès indexé, et count() sans gérer manuellement la position de l’itérateur.
IteratorAggregateRenvoie une vue parcourue de la liste interne. Il n’y a pas de curseur itérateur mutable à maintenir.
ArrayAccessPrise en charge $users[0], remplacement, ajouter avec $users[], et la suppression.
CountablePermet count($users) renvoie le nombre d'objets stockés.
list<User>La classe valide chaque insertion et conserve les clés entières consécutives après la suppression.
Les signatures de méthodes doivent correspondre aux interfaces PHP : les décalages et les valeurs insérées arrivent comme mixed. L'implémentation les vérifie avant utilisation. L'ajout se produit uniquement lorsque $offset === null, donc index 0 n'est jamais confondu avec une valeur vide.
Utilisez la collection comme une liste ciblée
La construction, l'ajout, l'accès indexé, l'itération et le décompte restent familiers.
<?php
$users = new UserCollection([
new User('Ada', 'ada@example.com'),
new User('Linus', 'linus@example.com'),
]);
$users[] = new User('Grace', 'grace@example.com');
$users[0] = new User('Ada Lovelace', 'ada@example.com');
echo $users[0]->name, PHP_EOL;
echo count($users), PHP_EOL;
foreach ($users as $user) {
echo $user->email, PHP_EOL;
}Parce que offsetGet() retours User et getIterator() est documenté comme Traversable<int, User>, de nombreux IDE peuvent proposer la complétion des membres pour $users[0]->name et $user->email. Le comportement exact dépend de l'EDI et de ses paramètres d'analyse.
Ajoutez des modèles d'analyse statique lorsque la réutilisation le justifie
Un béton UserCollection est le plus facile à lire. Une base générique ne devient utile que lorsque plusieurs collections partagent les mêmes mécaniques.
PHPStan et Psalm peuvent modéliser une collection de base avec des annotations telles que @template T of object, @implements IteratorAggregate<int, T>, et @implements ArrayAccess<int, T>. Une sous-classe se lie alors T à User.
Ces annotations ne créent pas de génériques d'exécution. Une base réutilisable a toujours besoin d'un validateur d'exécution fiable, tel qu'une chaîne de classe transmise par la sous-classe, si les valeurs doivent être appliquées pendant l'exécution de PHP. Conservez d'abord la mise en œuvre concrète pour les équipes qui n'exécutent pas d'analyseur statique.
Ce que fait la sécurité de type – et ne fait pas –
Un contrat de type élément évite une catégorie d’erreurs. Il ne s'agit pas d'une frontière de sécurité générale.
Une méthode acceptant UserCollection communique qu'il attend un groupe ordonné d'utilisateurs.
Une tentative d'insertion d'un autre objet échoue immédiatement avec InvalidArgumentException.
Les annotations de retour et d'itérateur donnent aux IDE et aux analyseurs statiques plus d'informations sur chaque élément.
Un valide User L'objet peut toujours contenir un nom, une adresse e-mail ou une autre valeur non sécurisé.
Sachant qu'un objet est un User ne prouve pas que le demandeur actuel puisse le consulter ou le modifier.
L'emballage d'un tableau ajoute des appels de méthode et une validation. Comparez votre charge de travail réelle avant de faire une réclamation de performance.
Utiliser une collection lorsque la liste a un travail
Préférez un paramètre typé normal plus PHPDoc lorsque le tableau est local et temporaire.
Les valeurs traversent les contrôleurs, les services, les référentiels ou les limites des API ; le rejet à l'exécution est important ; ou la liste possède un comportement tel que la recherche d'utilisateurs actifs ou l'application de l'unicité.
La liste existe dans une petite fonction, n'a pas besoin de comportement et est déjà vérifiée par un analyseur statique configuré.
Évitez de faire en sorte qu'une collection imite chaque fonction de tableau. Ajoutez des méthodes de domaine qui rendent le code d'appel plus clair. Si l'objet devient un sac de comportement de tri, de filtrage, de persistance et de présentation, partagez ces responsabilités.
Exemple complet de PHP 8.1
Ce script autonome teste la construction, l'ajout, le remplacement à l'index zéro, l'itération, le nombre, le rejet de type non valide et l'accès hors plage.
<?php
declare(strict_types=1);
final class User
{
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
}
/**
* @implements IteratorAggregate<int, User>
* @implements ArrayAccess<int, User>
*/
final class UserCollection implements IteratorAggregate, ArrayAccess, Countable
{
/** @var list<User> */
private array $items = [];
/** @param iterable<User> $users */
public function __construct(iterable $users = [])
{
foreach ($users as $user) {
$this[] = $user;
}
}
/** @return Traversable<int, User> */
public function getIterator(): Traversable
{
yield from $this->items;
}
public function count(): int
{
return count($this->items);
}
public function offsetExists(mixed $offset): bool
{
return is_int($offset)
&& array_key_exists($offset, $this->items);
}
public function offsetGet(mixed $offset): User
{
if (!$this->offsetExists($offset)) {
throw new OutOfBoundsException('Unknown user index.');
}
return $this->items[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
if (!$value instanceof User) {
throw new InvalidArgumentException(
'UserCollection accepts only User objects.'
);
}
if ($offset === null) {
$this->items[] = $value;
return;
}
if (!is_int($offset) || $offset < 0 || $offset > count($this->items)) {
throw new OutOfBoundsException('Invalid user index.');
}
$this->items[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
if (!$this->offsetExists($offset)) {
throw new OutOfBoundsException('Unknown user index.');
}
array_splice($this->items, $offset, 1);
}
}
$check = static function (bool $condition, string $message): void {
if (!$condition) {
throw new RuntimeException($message);
}
};
$users = new UserCollection([
new User('Ada', 'ada@example.com'),
new User('Linus', 'linus@example.com'),
]);
$users[] = new User('Grace', 'grace@example.com');
$check(count($users) === 3, 'Append or count failed.');
$users[0] = new User('Ada Lovelace', 'ada@example.com');
$check($users[0]->name === 'Ada Lovelace', 'Index zero failed.');
$names = [];
foreach ($users as $user) {
$names[] = $user->name;
}
$check($names === ['Ada Lovelace', 'Linus', 'Grace'], 'Iteration failed.');
$wrongTypeRejected = false;
try {
$users[] = new stdClass();
} catch (InvalidArgumentException) {
$wrongTypeRejected = true;
}
$check($wrongTypeRejected, 'Invalid object type was accepted.');
$outOfRangeRejected = false;
try {
$users[99];
} catch (OutOfBoundsException) {
$outOfRangeRejected = true;
}
$check($outOfRangeRejected, 'Out-of-range access was accepted.');
echo "All UserCollection checks passed.", PHP_EOL;Enregistrez le bloc décodé sous user-collection.php, puis exécutez php user-collection.php. Il doit afficher « Tous les contrôles UserCollection réussis ».
Références
- Manuel PHP : ArrayAccess
Signatures requises et accès aux objets de style tableau.
- Manuel PHP : IteratorAggregate
Itération externe via
getIterator(). - Types PHPStan PHPDoc et Types de tableaux de psaumes
Liste au niveau des outils, forme de tableau et syntaxe de type générique.