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