01
Flexible mais ambigu

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 :

Paramètre de tableau ambiguPHP8.1+
<?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.

02
Évitez les abstractions inutiles

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.

01 · PHPDOC

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.

02 · ANALYSE STATIQUE

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.

03 · COLLECTE

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.

03
Un design minimaliste et moderne

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.

IteratorAggregate

Renvoie une vue parcourue de la liste interne. Il n’y a pas de curseur itérateur mutable à maintenir.

ArrayAccess

Prise en charge $users[0], remplacement, ajouter avec $users[], et la suppression.

Countable

Permet count($users) renvoie le nombre d'objets stockés.

Un interne 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.

04
Syntaxe familière

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.

Utilisation après avoir chargé les classes complètes ci-dessousPHP8.1+
<?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.

05
Génériques facultatifs au niveau des outils

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.

06
Utilisez des allégations précises

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.

Il documente l'intention

Une méthode acceptant UserCollection communique qu'il attend un groupe ordonné d'utilisateurs.

Il rejette les mauvais types d'objets

Une tentative d'insertion d'un autre objet échoue immédiatement avec InvalidArgumentException.

Il améliore le retour d'information sur les outils

Les annotations de retour et d'itérateur donnent aux IDE et aux analyseurs statiques plus d'informations sur chaque élément.

Il ne nettoie pas les entrées

Un valide User L'objet peut toujours contenir un nom, une adresse e-mail ou une autre valeur non sécurisé.

Il n'autorise pas une demande

Sachant qu'un objet est un User ne prouve pas que le demandeur actuel puisse le consulter ou le modifier.

Ce n'est pas un raccourci de performances

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.

07
Une règle pratique

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.

Choisissez une collection

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é.

Choisissez un tableau documenté

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.

08
Copiez, exécutez et adaptez

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.

Exemple complet de PHP 8.1 UserCollectionScript d'auto-test
<?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

À propos de l'auteur

Cory Marais

Cory a plus de 20 ans d'expérience en sécurité Internet et est l'un des principaux développeurs du projet BitFire.

Lire la suite de la recherche BitFire →
Gardez le contrat visible

Utilisez une collection lorsque la liste a un travail.

Commencez avec un tableau documenté. Ajoutez une application d’exécution uniquement lorsque les valeurs traversent les frontières ou que la collection possède un comportement de domaine utile.

Protéger mon site gratuitement â†'