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:
<?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.
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.
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.
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.
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.
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.
IteratorAggregateGibt eine durchlaufbare Ansicht der internen Liste zurück. Es muss kein veränderlicher Iterator-Cursor verwaltet werden.
ArrayAccessUnterstützt $users[0], Ersatz, anhängen mit $users[], und Entfernung.
CountableLasst uns count($users) Gibt die Anzahl der gespeicherten Objekte zurück.
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.
Nutzen Sie die Sammlung wie eine fokussierte Liste
Konstruktion, Anhängen, indizierter Zugriff, Iteration und Zählung bleiben vertraut.
<?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.
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.
Welche Art von Sicherheit gilt – und was nicht?
Ein Elementvertrag verhindert eine Fehlerkategorie. Es handelt sich nicht um eine allgemeine Sicherheitsgrenze.
Eine Methode, die akzeptiert UserCollection teilt mit, dass es eine geordnete Gruppe von Benutzern erwartet.
Der Versuch, ein anderes Objekt einzufügen, schlägt sofort fehl InvalidArgumentException.
Rückgabe- und Iteratoranmerkungen geben IDEs und statischen Analysatoren mehr Informationen zu jedem Element.
Eine gültige User Das Objekt kann weiterhin einen unsicheren Namen, eine unsichere E-Mail-Adresse oder einen anderen Wert enthalten.
Zu wissen, dass ein Objekt ein ist User beweist nicht, dass der aktuelle Antragsteller es einsehen oder ändern darf.
Durch das Umschließen eines Arrays werden Methodenaufrufe und Validierungen hinzugefügt. Vergleichen Sie Ihre tatsächliche Arbeitsbelastung, bevor Sie einen Leistungsanspruch geltend machen.
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.
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.
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.
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.
<?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
- PHP-Handbuch: ArrayAccess
Erforderliche Signaturen und Objektzugriff im Array-Stil.
- PHP-Handbuch: IteratorAggregate
Externe Iteration durch
getIterator(). - PHPStan PHPDoc-Typen und Psalm-Array-Typen
Liste, Array-Form und generische Typsyntax auf Tool-Ebene.


