https://d226lax1qjow5r.cloudfront.net/blog/blogposts/type-safety-done-right-php-array-hacking/type-safety-done-right.png

Typsicherheit richtig gemacht - PHP Array Hacking

Zuletzt aktualisiert am October 31, 2023

Lesedauer: 6 Minuten

In den ersten fünf oder sechs Jahren meiner Erfahrung in der Entwicklung größerer PHP-Applikationen habe ich viele der Probleme erlebt und geschaffen, die Menschen, die in die Backend-Entwicklung einsteigen, oft gesehen haben. Erst jetzt, insbesondere nach der Übernahme des Vonage PHP SDKhabe ich erkannt, dass ich in einer Position bin, in der ich anderen beibringen kann, nicht dasselbe zu tun. Diese Probleme haben ihren Ursprung in der Art und Weise, wie ich PHP gesehen habe im Speziellen über die Jahre hinweg geschrieben wurde, unabhängig davon, ob ein Framework verwendet wird oder nicht. Lassen Sie uns die erste Granate werfen:

PHP-Arrays sind irgendwie schrecklich

OK, sie sind nicht furchtbar, das wäre ja auch seltsam zu sagen. Was ich meine, ist, die Art und Weise, wie sie oft verwendet werden ist furchtbar. Dafür gibt es einen sehr guten Grund; als ich mit PHP als erste Sprache anfing (nach VBA und SQL) war mir nicht bewusst, wie andere Sprachen mit Datensammlungen umgehen. Ich habe einfach angenommen, dass sie dasselbe wie PHP sind:

  • Sie haben ein Index-Array, das einen numerischen Schlüssel hat oder

  • Sie haben ein zugehöriges Array, das alphanumerische Schlüssel enthält.

Beide werden als derselbe Variablentyp betrachtet, d.h. arrayweil die Engine geklärt hat, welche Art von Array Sie erstellen wollen.

$myAssociatedArray = ['foo' => 'bar']; // array
$myHashedArray = ['foo', 'bar']; // also array

So funktioniert wahrscheinlich alles andere auch, oder?

Nein, ist es nicht. Meine Naivität bei der Verwendung von PHP hat dazu geführt, dass ich mir der folgenden Aussage nicht bewusst war:

In jeder anderen wichtigen Backend-Sprache sind indizierte und assoziative Arrays in zwei oder mehr verschiedene Basisklassen aufgeteilt.

Außerdem sind assoziative Arrays ein Begriff, der nur in PHP verwendet wird. Von nun an nennen wir assoziative Arrays Hash-Arrays bezeichnen, da dieser Begriff anderswo am häufigsten verwendet wird.

Hier ist das Verhalten für andere Sprachen:

NodeJS

  • Array (indiziertes Array)

  • Object (hashed array)

Python

  • List (indiziertes Array)

  • Dictionary (hashed array), unter Verwendung von List Comprehension

Ruby

  • Array (indiziertes Array)

  • Hash (hashed array)

Es ist erwähnenswert, dass Ruby tatsächlich ein assoziatives Array auf die gleiche Weise wie PHP haben kann, aber aufgrund der Existenz des Hash-Objekts ist es sehr selten, dass ein Array als Hash verwendet wird.

Go(lang)

  • Slice (indiziertes Array)

  • Map (hashed array)

Java

  • Array (indiziertes Array, primitiv)

  • List (Schnittstelle, implementiert von ArrayList)

  • ArrayList (indiziertes Array, erzeugt ein neues Objekt zur Größenbestimmung)

  • Hashmap (hashed array, Implementierung von Map)

  • LinkedHashMap (hashed array, trainiert die Einfügereihenfolge neu)

Es ist erwähnenswert, dass Java, wie Sie sehen können, sehr strikt mit Datenstrukturmengen ist. Sie sind alle veränderbar, d. h. man kann ihre Werte ändern, aber man kann nicht die Größe des Objekts in Bezug auf seine Knoten ändern. Aus diesem Grund können Sie in ArrayListein neues Objekt erzeugen, wenn es verändert wird.

C#

  • Array (indiziertes Array)

  • List (indiziertes Array mit Generika, veränderbar)

  • Dictionary (hashed array)

In Anbetracht der Tatsache, dass alle diese verschiedenen Klassen in der Basis-API dieser Sprachen enthalten sind, bedeutet dies, dass sie alle die Möglichkeit haben, ein strengeres Typverhalten einzuführen.

Und was nun?

Das haben wir in PHP nicht. Es bedeutet PHP-Arrays können sozusagen alles sein. Diese Tatsache hat zu der Art von Code geführt, den ich seit Jahren lese und der wie folgt aussieht:

public function updateRecord(int $id, array $options) {
	// do some stuff here
}

Ich möchte die ID von etwas aktualisieren. Und dann ... habe ich $options. Weil es Optionen sind. Aber es ist ein einfaches PHP-Array. Was ist da drin? Die Funktion hat keine Ahnung.

Was ist an diesem Code falsch? Wenn man von persönlichen Erfahrungen ausgehen kann, vier Stunden von xdebug um herauszufinden, woher ein Array-Schlüssel stammt. Wir haben keine Ahnung, wie das aussehen soll. Diese besondere Bugbear von mir ist etwas, das ich jahrelang selbst programmiert habe, insbesondere vor allem im Agenturumfeld, wo Zeit Geld ist. Wenn die einzige Sorge ist: "Funktioniert es gemäß den Anforderungen des Kunden?", dann wird an allen Ecken und Enden gespart, um rechtzeitig fertig zu werden. Die Person nach Ihnen, die einen Fehler beheben muss, der durch einen Nebeneffekt entstanden ist? Das ist technische Verschuldung in Aktion für Sie.

OK, was nun?

Der Weg, den ich für wartbaren Code eingeschlagen habe, ist die Verwendung von Typ-Hinweisen überall und immer zu verwenden. Dies ist die Art der Softwareentwicklung, die, wenn sie durchgängig und gründlich durchgeführt wird, sollte zu dem mythischen selbstdokumentierenden Code führen sollte. Aber wir können nur hint an arrayeingeben, also hilft uns das hier nicht weiter. Wir haben also drei Möglichkeiten:

  • Generika

  • Array-Form (via. hacked Generics)

  • Wertvolle Objekte

Generika

Tut mir leid, ich habe Sie ausgetrickst: Wir haben keine Generics in PHP, weil die Engine Einschränkungen hat. Da das Wort jedoch ein wenig umhergeschleudert wird, sollten Sie den folgenden Code aus TypeScript:

interface UserProfile {
    id: string;
    name: string;
    age: integer;
}

class Collection<Type> {
    private items: Type[] = [];

    add(item: Type) {
        this.items.push(item);
    }

    get(index: number): Type | undefined {
        return this.items[index];
    }
}

const userProfiles = new Collection<UserProfile>();

Als jemand, der sehr auf der Seite der "Mach dieses PHP so robust wie möglich, mach es härter als Valyrischen Stahl" steht, sind Generika wie diese eine wahre Freude bei der Arbeit. Die <> Klammern besagen, dass beim Erstellen eines neuen Collection Objekt, es muss nur den Typ enthalten darf, der bei seiner Instanziierung angegeben wurde. Wenn wir also ein neues Collection<UserProfile>erstellen, sagen wir damit, dass dieses Objekt nur UserProfile Objekte enthalten darf. Das, Freunde und Feinde, ist selbstdokumentierender Code.

Kann PHP dies auch auf andere Weise tun?

Gut, dass du fragst: Ja, das geht! Dies führt uns zum zweiten Punkt, der Definition eines Arrays. Das klingt so dumm aber bis ich bei Vonage angefangen habe, war mir nie in den Sinn gekommen, dass ich eine Erweiterung von SPL-Kern Objekte innerhalb der PHP-API erweitern kann. Also, Sie können Arrays erweitern!

class Collection extends \ArrayObject {

	protected function __construct(...$args) {
		parent::__construct(...args);
	}
}

Gegenwärtig tut dies nichts, abgesehen davon, dass die Argumente an die übergeordnete Klasse mit dem Splat-Operator übergeben werden, der ein ArrayObject. Aber wir haben es erweitert, was bedeutet, dass wir das Verhalten überladen können!

Wir müssen hier zwei Dinge tun:

  • Setzen eines Typs, genau wie in TypeScript

  • Überladen Sie die offsetSet() Methode, die die SPL-Methode für das Hinzufügen zu einem Array ist (entweder beim Hinzufügen eines Wertes nach der Instanziierung oder während der Erstellung des Konstruktors).

Also, lassen Sie uns das angehen:

class Collection extends \ArrayObject {  

	protected $typeError = 'Only UserProfile objects can be added!'
	
    public function __construct(...$args) {  
    
        foreach ($args as $arg) {  
            if (!$arg instanceof UserProfile) {  
                throw new \TypeError($this->typeError);  
            }  
        }  
        
        parent::__construct(...$args);  
    }  
  
    public function offsetSet($key, $value): void  
    {  
  
        if (!$value instanceof UserProfile) {  
            throw new \TypeError($this->typeError);  
        }  
  
        parent::offsetSet($key, $value);  
    }  
}

Und da haben wir es. offsetSet ist überladen, so dass nur UserProfile Objekte hinzugefügt werden können, und bei der Erstellung des Collection können Sie eine beliebige Anzahl von Argumenten übergeben, solange es sich um UserProfile Objekte sind.

Wert-Objekte

Es gibt eine viel einfachere Lösung für das Problem des Herumwerfens mit Arrays: es nicht zu tun. Ich habe die Arbeit meiner Vorgänger am Vonage PHP Core SDK fortgesetzt, um sicherzustellen, dass alles, was an Client-Methoden übergeben wird, ein Wertobjekt ist.

Ja, das hat auch seine Schattenseiten. Es bedeutet, dass Klasseneigenschaften definiert werden müssen, es bedeutet, dass Getter- und Setter-Methoden hinzugefügt werden müssen. Das bedeutet mehr Code. Allerdings gibt es auch Werkzeuge, die den Code in modernem PHP weniger aufblähen, entweder durch die Verwendung von nativen enum Klassen oder der Förderung von Konstruktoreigenschaften mit Access Modifiers, um die Zeilenraten niedrig zu halten.

Ein gutes Beispiel dafür war die PHP SDK-Implementierung von Verify v2. Das Senden einer Verifizierungsanfrage würde ohne Wertobjekte wie folgt aussehen:

$payload = [
	'locale' => 'en_us',
	'channel_timeout' => 300,
	'client_ref' => 'a-reference',
	'code_length' => 4,
	'workflow' => [
		'channel' => 'sms',
		'to' => '123456789'
	]
];

$myVonageClient = new Client('apiKey', 'apiSecret');
$myVonageClient->verify2()->startVerification($payload);

Das Client-Objekt weiß nichts von dem Array, das ihm übergeben wird. Was ist, wenn ein Schlüssel falsch ist? Was ist, wenn ein Schlüssel nicht vorhanden ist? Es gibt nichts im Code, was Sie über das Verhalten informiert: und wenn ich habe Validierung in die startVerification() Methode eingebaut hätte, müssten Sie meinen Code durchforsten, um herauszufinden, was ich mir dabei gedacht habe.

Stattdessen verlagern wir die Logik mit Hilfe von Wertobjekten in gut getipptes PHP. Es wird selbstdokumentierend und gehärtet durch Design.

class SMSRequest extends BaseVerifyRequest  
{
	public function __construct(  
	    protected string $to,  
	    protected string $brand,  
	    protected ?VerificationLocale $locale = null,  
	) {  
	    if (!$this->locale) {  
	        $this->locale = new VerificationLocale();  
	    }  
	  
	    $workflow = new VerificationWorkflow(VerificationWorkflow::WORKFLOW_SMS, $to);  
	  
	    $this->addWorkflow($workflow);  
	}
}

Dieses Objekt wird nun an die startVerification() Methode übergeben. Was Sie in den übergebenen Objekten tun können und was nicht, kann nun auf der untersten Ebene des Codes definiert werden. Hier ist zum Beispiel das VerificationLocale Objekt.

class VerificationLocale  
{  
    private array $allowedCodes = [  
        'en-us',  
        'en-gb',  
        'es-es',  
        'es-mx',  
        'es-us',  
        'it-it',  
        'fr-fr',  
        'de-de',  
        'ru-ru',  
        'hi-in',  
        'pt-br',  
        'pt-pt',  
        'id-id',  
    ];  
  
    public function __construct(protected string $code = 'en-us')  
    {  
        if (! in_array($code, $this->allowedCodes, true)) {  
            throw new \InvalidArgumentException('Invalid Locale Code Provided');  
        }  
    }  
  
    public function getCode(): string  
    {  
        return $this->code;  
    }  
  
    public function setCode(string $code): static  
    {  
        $this->code = $code;  
  
        return $this;  
    }  
}

Und da haben wir es. Selbst-dokumentierender Code. Ja, Sie müssen alle Objekte erstellen, um sie alle zusammenzufügen, aber auch wie sich unser SDK verhält, ist sowohl streng als auch explizit.

Teilen Sie:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeSenior PHP Entwickler Advocate

Als ausgebildeter Schauspieler mit einer Dissertation in Standup-Comedy bin ich über die Meetup-Szene zur PHP-Entwicklung gekommen. Man findet mich, wenn ich über Technik spreche oder schreibe, oder wenn ich seltsame Platten aus meiner Vinylsammlung spiele oder kaufe.