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

Vollständiges PHP 8.1-Beispiel

Dieses eigenständige Skript testet die Konstruktion, das Anhängen, das Ersetzen am Index Null, die Iteration, die Anzahl, die Ablehnung ungültiger Typen und den Zugriff außerhalb des zulässigen Bereichs.

Vollständiges PHP 8.1 UserCollection-BeispielSelbsttest-Skript
<?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;

Speichern Sie den dekodierten Block unter user-collection.php, dann rennen php user-collection.php. Es sollte „Alle UserCollection-Prüfungen bestanden“ gedruckt werden.

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 –