01
Flexibel, aber mehrdeutig

Ein Array erklärt seinen Inhalt nicht

Ein natives Array kann eine Liste, eine Karte oder eine Mischung aus Schlüsseln und Werten darstellen. Der Variablenname allein ist kein Vertrag.

A Liste verwendet aufeinanderfolgende Ganzzahlschlüssel, beginnend bei Null. A Karte Ordnet Schlüssel Werten zu. PHP verwendet dasselbe array Typ für beide, daher sagt diese Parameterdeklaration sehr wenig aus:

Mehrdeutiger Array-ParameterPHP 8.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')]);

Daraus muss der Leser schließen $users sollte eine Liste von sein User Objekte. Eine IDE oder ein statischer Analysator kann einige Werte aus benachbartem Code ableiten, diese Informationen werden jedoch weniger zuverlässig, wenn das Array Methoden, Dienste oder externe Grenzen überschreitet.

02
Vermeiden Sie unnötige Abstraktionen

Wählen Sie zuerst das einfachste Werkzeug

Eine Klasse ist nur dann nützlich, wenn sie einen Vertrag oder ein Verhalten hinzufügt, das der Code tatsächlich benötigt.

01 · PHPDOC

Kommentieren Sie eine lokale Liste

Benutzen list<User> oder array<int, User> für ein kurzlebiges Array. IDEs und Analysatoren können die erwarteten Werte ohne ein neues Laufzeitobjekt verstehen.

02 · STATISCHE ANALYSE

Beschreiben Sie wiederverwendbare Generika

PHPStan und Psalm verstehen Vorlagenanmerkungen wie @template T of object. Dadurch wird eine Sammlungsimplementierung während der Analyse auf mehrere Objekttypen skaliert.

03 · KOLLEKTION

Erzwingen Sie eine Domänengrenze

Erstellen UserCollection Wenn Werte eine Grenze überschreiten, müssen falsche Werte zur Laufzeit fehlschlagen, oder die Liste weist ihr eigenes nützliches Verhalten auf.

PHP 8.1 bietet keine nativen Userland-Generika. Vorlagenanmerkungen sind Verträge für Entwicklungstools; PHP selbst erzwingt sie nicht. Die folgende Sammlung fügt die Laufzeitprüfung hinzu.

03
Ein minimalistisches modernes Design

Wickeln Sie das Array ein und halten Sie den Vertrag klein

Verwenden Sie Standardschnittstellen, damit das Objekt unterstützt wird foreach, indizierter Zugriff und count() ohne die Iteratorposition manuell zu verwalten.

IteratorAggregate

Gibt eine durchlaufbare Ansicht der internen Liste zurück. Es muss kein veränderlicher Iterator-Cursor verwaltet werden.

ArrayAccess

Unterstützt $users[0], Ersatz, anhängen mit $users[], und Entfernung.

Countable

Lasst uns count($users) Gibt die Anzahl der gespeicherten Objekte zurück.

Eine interne list<User>

Die Klasse validiert jede Einfügung und behält nach dem Entfernen fortlaufende Ganzzahlschlüssel bei.

Die Methodensignaturen müssen mit den PHP-Schnittstellen übereinstimmen: Offsets und eingefügte Werte kommen als an mixed. Die Implementierung prüft sie vor der Verwendung. Anhängen geschieht nur, wenn $offset === null, also Index 0 wird niemals mit einem leeren Wert verwechselt.

04
Vertraute Syntax

Nutzen Sie die Sammlung wie eine fokussierte Liste

Konstruktion, Anhängen, indizierter Zugriff, Iteration und Zählung bleiben vertraut.

Verwendung nach dem Laden der vollständigen Klassen untenPHP 8.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;
}

Weil offsetGet() kehrt zurück User und getIterator() ist dokumentiert als Traversable<int, User>, viele IDEs können die Vervollständigung von Mitgliedern anbieten $users[0]->name und $user->email. Das genaue Verhalten hängt von der IDE und ihren Analyseeinstellungen ab.

05
Optionale Generika auf Tool-Ebene

Fügen Sie statische Analysevorlagen hinzu, wenn eine Wiederverwendung dies rechtfertigt

Ein Beton UserCollection ist am einfachsten zu lesen. Eine generische Basis wird nur dann nützlich, wenn mehrere Sammlungen die gleiche Mechanik verwenden.

PHPStan und Psalm können eine Basissammlung mit Anmerkungen wie modellieren @template T of object, @implements IteratorAggregate<int, T>, und @implements ArrayAccess<int, T>. Eine Unterklasse bindet dann T zu User.

Diese Anmerkungen erstellen keine Laufzeitgenerika. Eine wiederverwendbare Basis benötigt weiterhin einen zuverlässigen Laufzeitvalidator – etwa einen von der Unterklasse übergebenen Klassenstring –, wenn Werte während der PHP-Ausführung erzwungen werden müssen. Für Teams, die keinen statischen Analysator ausführen, sollte die konkrete Implementierung an erster Stelle stehen.

06
Verwenden Sie präzise Aussagen

Welche Art von Sicherheit gilt – und was nicht?

Ein Elementvertrag verhindert eine Fehlerkategorie. Es handelt sich nicht um eine allgemeine Sicherheitsgrenze.

Es dokumentiert die Absicht

Eine Methode, die akzeptiert UserCollection teilt mit, dass es eine geordnete Gruppe von Benutzern erwartet.

Es lehnt falsche Objekttypen ab

Der Versuch, ein anderes Objekt einzufügen, schlägt sofort fehl InvalidArgumentException.

Es verbessert das Werkzeug-Feedback

Rückgabe- und Iteratoranmerkungen geben IDEs und statischen Analysatoren mehr Informationen zu jedem Element.

Eingaben werden dadurch nicht bereinigt

Eine gültige User Das Objekt kann weiterhin einen unsicheren Namen, eine unsichere E-Mail-Adresse oder einen anderen Wert enthalten.

Eine Anfrage wird dadurch nicht autorisiert

Zu wissen, dass ein Objekt ein ist User beweist nicht, dass der aktuelle Antragsteller es einsehen oder ändern darf.

Es handelt sich nicht um eine Leistungsabkürzung

Durch das Umschließen eines Arrays werden Methodenaufrufe und Validierungen hinzugefügt. Vergleichen Sie Ihre tatsächliche Arbeitsbelastung, bevor Sie einen Leistungsanspruch geltend machen.

07
Eine praktische Regel

Verwenden Sie eine Sammlung, wenn die Liste einen Auftrag hat

Bevorzugen Sie einen normal typisierten Parameter plus PHPDoc, wenn das Array lokal und temporär ist.

Wählen Sie eine Sammlung

Die Werte überschreiten Controller-, Dienste-, Repository- oder API-Grenzen. Laufzeitablehnung ist wichtig; oder die Liste besitzt Verhalten wie das Finden aktiver Benutzer oder das Erzwingen der Einzigartigkeit.

Wählen Sie ein dokumentiertes Array

Die Liste existiert innerhalb einer kleinen Funktion, benötigt kein Verhalten und wird bereits von einem konfigurierten statischen Analysator überprüft.

Vermeiden Sie es, eine Sammlung jede Array-Funktion imitieren zu lassen. Fügen Sie Domänenmethoden hinzu, die den Aufrufcode klarer machen. Wenn das Objekt zu einer Wundertüte für Sortierung, Filterung, Persistenz und Präsentationsverhalten wird, teilen Sie diese Verantwortlichkeiten auf.

08
Kopieren, ausführen und anpassen

8.5 Fallbeispiel:

Dieses eigenständige Skript verwendet eine wiederverwendbare Basisklasse für typisierte Listen und schränkt dann die öffentliche Sammlungs-API für Benutzerobjekte ein.

Beispiel für PHP 8.5 UserCollectionAusführbares Skript
<?php

declare(strict_types=1);

final class User
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
    ) {}
}

/**
 * @template T of object
 * @implements IteratorAggregate<int, T>
 * @implements ArrayAccess<int, T>
 */
abstract class TypedList implements IteratorAggregate, ArrayAccess, Countable
{
    /** @var list<T> */
    private array $items = [];

    /** @return class-string<T> */
    abstract protected function itemClass(): string;

    /** @param iterable<T> $items */
    protected function __construct(iterable $items = [])
    {
        foreach ($items as $item) {
            $this->append($item);
        }
    }

    /** @param T $item */
    protected function append(object $item): void
    {
        $this->items[] = $this->typed($item);
    }

    /** @return T */
    protected function at(int $index): object
    {
        return $this->items[$index]
            ?? throw new OutOfBoundsException('Unknown list index.');
    }

    /** @return Traversable<int, T> */
    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);
    }

    /** @return T */
    public function offsetGet(mixed $offset): object
    {
        return $this->at($this->index($offset));
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $value = is_object($value)
            ? $this->typed($value)
            : throw new InvalidArgumentException('List values must be objects.');

        if ($offset === null) {
            $this->items[] = $value;
            return;
        }

        $this->items[$this->existingIndex($offset)] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        array_splice($this->items, $this->existingIndex($offset), 1);
    }

    protected function index(mixed $offset): int
    {
        return is_int($offset)
            ? $offset
            : throw new InvalidArgumentException('List index must be an integer.');
    }

    private function existingIndex(mixed $offset): int
    {
        $index = $this->index($offset);

        if (!array_key_exists($index, $this->items)) {
            throw new OutOfBoundsException('Unknown list index.');
        }

        return $index;
    }

    /**
     * @param object $value
     * @return T
     */
    private function typed(object $value): object
    {
        $class = $this->itemClass();

        return $value instanceof $class
            ? $value
            : throw new InvalidArgumentException("Expected {$class}.");
    }
}

/** @extends TypedList<User> */
final class UserCollection extends TypedList
{
    /** @param iterable<User> $users */
    public function __construct(iterable $users = [])
    {
        parent::__construct($users);
    }

    #[Override]
    protected function itemClass(): string
    {
        return User::class;
    }

    public function add(User $user): void
    {
        $this->append($user);
    }

    public function get(int $index): User
    {
        return $this->at($index);
    }

    #[Override]
    public function offsetGet(mixed $offset): User
    {
        return $this->get($this->index($offset));
    }
}

$users = new UserCollection([
    new User('Ada', 'ada@example.com'),
    new User('Linus', 'linus@example.com'),
]);

$users->add(new User('Grace', 'grace@example.com'));
$users[0] = new User('Ada Lovelace', 'ada@example.com');

foreach ($users as $user) {
    echo $user->name, ' <', $user->email, '>', PHP_EOL;
}

try {
    $users[] = new stdClass();
} catch (InvalidArgumentException $error) {
    echo $error->getMessage(), PHP_EOL;
}

Speichern Sie den dekodierten Block unter user-collection.php, dann rennen php user-collection.php. Es sollte die drei Benutzer drucken und dann den falschen Objekttyp ablehnen.

Referenzen

Über den Autor

Cory Marsh

Cory verfügt über mehr als 20 Jahre Erfahrung im Bereich Internetsicherheit und ist einer der Hauptentwickler des BitFire-Projekts.

Lesen Sie mehr über die BitFire-Forschung →
Halten Sie den Vertrag sichtbar

Verwenden Sie eine Sammlung, wenn die Liste einen Auftrag hat.

Beginnen Sie mit einem dokumentierten Array. Fügen Sie die Laufzeiterzwingung nur hinzu, wenn Werte Grenzen überschreiten oder die Sammlung über nützliches Domänenverhalten verfügt.

Schützen Sie meine Website kostenlos –