In einer Konfigurationsdatei steht der Name einer Klasse, die eine Zahlungsart umsetzen soll. Bevor daraus ein Objekt entsteht, muss feststehen, ob die Klasse überhaupt von der erwarteten Basis abstammt. Ein Objekt gibt es zu diesem Zeitpunkt aber noch nicht.
Das Bild zeigt eine Klassenhierarchie und daneben einen bloßen Namen in Anführungszeichen. Genau mit diesem Namen arbeitet die Funktion.
Was PHP is_subclass_of() macht
Die Signatur von PHP is_subclass_of lautet is_subclass_of(mixed $object_or_class, string $class, bool $allow_string = true): bool. Der erste Parameter nimmt ein Objekt oder einen Klassennamen entgegen, der zweite die erwartete Basis.
<?php
abstract class Zahlungsart {}
class Lastschrift extends Zahlungsart {}
/* Mit Objekt */
var_dump(is_subclass_of(new Lastschrift(), Zahlungsart::class)); /* true */
/* Mit blossem Namen, ohne dass ein Objekt existiert */
var_dump(is_subclass_of('Lastschrift', Zahlungsart::class)); /* true */
Der zweite Aufruf ist der interessante. Er beantwortet die Frage aus der Einleitung, ohne dass die Klasse instanziiert werden muss. Das ist die eigentliche Daseinsberechtigung von PHP is_subclass_of.
Die Beschreibung im Handbuch ist an einer Stelle bemerkenswert genau, deshalb hier im Original:
is_subclass_of(mixed $object_or_class, string $class,
bool $allow_string = true): bool
/* object_or_class - the first parameter takes an object or the class name
class - the specified class or interface to check against
allow_string - whether a class name is accepted, true since PHP 5.3.9
This function checks if the given object has the specified class
as one of its parents, or implements it. It returns false when both
names refer to the same class, which is the difference to is_a().
With allow_string set to false and a class not yet loaded, the
result is false without any error, so inheritance looks broken. */
Der vorletzte Satz enthält den Unterschied, um den es in diesem Tutorial geht, und der letzte beschreibt den stillsten Fehler, den die Funktion zu bieten hat. Beide werden weiter unten ausführlich behandelt.
Wenn instanceof nicht geht
instanceof braucht ein Objekt. PHP is_subclass_of kommt mit dem bloßen Klassennamen aus. Das klingt nach einem Detail, entscheidet aber darüber, ob sich eine Klasse prüfen lässt, bevor sie erzeugt wird.
Dazu kommt ein zweiter Unterschied, der noch häufiger überrascht: Sind geprüfte und erwartete Klasse dieselbe, liefert die Funktion false. Sie fragt nach echter Abstammung, nicht nach Zugehörigkeit. Wer den Operator und seine Einsatzfälle kennenlernen will, findet sie in PHP instanceof zum Prüfen von Objekten auf Klasse oder Interface.
is_subclass_of(), is_a() und instanceof im Vergleich
Drei Werkzeuge, dieselbe Frage, drei Ergebnisse. Nur PHP is_subclass_of antwortet in einem Fall anders als die beiden anderen. Die folgende Tabelle zeigt sie nebeneinander, geprüft wird jeweils gegen Zahlungsart.
| Geprüft wird | instanceof | is_a() | is_subclass_of() |
| Erbende Klasse | true | true | true |
| Dieselbe Klasse | true | true | false |
| Umgesetztes Interface | true | true | true |
| Nur ein Klassenname | nicht möglich | möglich | möglich |
| Eingebundener Trait | false | false | false |
Die zweite Zeile enthält den ganzen Unterschied. Wer in einem Test prüfen will, ob ein Wert von einem bestimmten Typ ist, greift zur falschen Funktion, wenn die Klasse selbst als Treffer zählen soll.
Warum PHP is_subclass_of() bei derselben Klasse false liefert
Der Name von PHP is_subclass_of sagt es bereits: Gefragt wird nach einer Unterklasse, und eine Klasse ist keine Unterklasse ihrer selbst. Im Code sieht das so aus.
<?php
class Rechnung {}
var_dump(is_subclass_of(new Rechnung(), Rechnung::class)); /* false */
var_dump(is_a(new Rechnung(), Rechnung::class)); /* true */
var_dump(new Rechnung() instanceof Rechnung); /* true */
Beide Verhaltensweisen sind sinnvoll, nur eben für verschiedene Fragen. Wer wissen will, ob ein Wert an einer Stelle verwendet werden darf, meint Zugehörigkeit und nimmt is_a(). Wer sicherstellen will, dass jemand eine Basis tatsächlich erweitert und nicht die Basis selbst eingetragen hat, meint Abstammung und ist hier richtig.
Interfaces und abstrakte Klassen prüft PHP is_subclass_of() mit
Der Funktionsname legt eine Einschränkung nahe, die es nicht gibt. Geprüft wird die gesamte Typkette, und dazu gehören Interfaces über beliebig viele Ebenen.
<?php
interface Exportierbar {}
interface Druckbar extends Exportierbar {}
abstract class Beleg implements Druckbar {}
class Gutschrift extends Beleg {}
var_dump(is_subclass_of('Gutschrift', Beleg::class)); /* true */
var_dump(is_subclass_of('Gutschrift', Druckbar::class)); /* true */
var_dump(is_subclass_of('Gutschrift', Exportierbar::class)); /* true */
Auch die abstrakte Klasse ist kein Hindernis, obwohl es von ihr niemals eine Instanz geben kann. Die Funktion wertet nur die Typkette aus und braucht dafür kein Objekt. Wie sich solche Verträge sinnvoll entwerfen lassen, zeigen PHP Interfaces als Verträge für Klassen und Abstrakte Klassen in PHP.
Traits sind keine Typen
An dieser Stelle liefert PHP is_subclass_of ein Ergebnis, das viele für einen Fehler halten.
<?php
trait Protokolliert {}
class Bestellung { use Protokolliert; }
var_dump(is_subclass_of('Bestellung', Protokolliert::class)); /* false */
/* Richtiges Werkzeug fuer diese Frage */
var_dump(in_array(Protokolliert::class, class_uses('Bestellung'), true));
/* true */
Ein Trait ist kein Typ, sondern eine Kopiervorlage für Code. Nach dem Einbinden gibt es keine Verbindung mehr, an der eine Typprüfung ansetzen könnte. Zu beachten ist dabei, dass class_uses() nur die direkt eingebundenen Traits liefert, nicht die der Elternklassen. Wozu Traits gedacht sind, erklärt PHP Traits zur Wiederverwendung von Code.
Der dritte Parameter von PHP is_subclass_of()
Dieser Parameter steht auf true und wird deshalb fast nie geschrieben. Genau das macht ihn zur Fehlerquelle, wenn er doch einmal umgestellt wird.
<?php
/* Standard: der Name wird akzeptiert und die Klasse
bei Bedarf ueber den Autoloader nachgeladen. */
var_dump(is_subclass_of('App\Zahlung\Sofort', Zahlungsart::class)); /* true */
/* Mit false: Strings werden abgelehnt. Eine noch nicht
geladene Klasse fuehrt dann zu false, ohne Fehler. */
var_dump(is_subclass_of('App\Zahlung\Sofort', Zahlungsart::class, false));
/* false, obwohl die Klasse existiert */
Der zweite Fall ist besonders unangenehm, weil nichts fehlschlägt. Es gibt keine Ausnahme und keine Warnung, nur ein false, das nach einem inhaltlichen Ergebnis aussieht. In Projekten mit automatischem Laden von Klassen, wie es PSR-4 Autoloading in PHP beschreibt, bleibt der Parameter deshalb am besten unangetastet.
Klassen aus Konfiguration sicher erzeugen
Damit lässt sich die Aufgabe aus der Einleitung vollständig lösen.
<?php
function zahlungsartErzeugen(string $klasse): Zahlungsart
{
if (!class_exists($klasse)) {
throw new InvalidArgumentException('Unbekannte Klasse: ' . $klasse);
}
if (!is_subclass_of($klasse, Zahlungsart::class)) {
throw new InvalidArgumentException($klasse . ' erweitert Zahlungsart nicht.');
}
return new $klasse();
}
$art = zahlungsartErzeugen($konfiguration['zahlungsart']);
Die Reihenfolge ist bewusst so gewählt: erst prüfen, ob es die Klasse gibt, dann die Abstammung, dann erzeugen. Bei einer Basis, die selbst eingetragen wird, greift die zweite Prüfung und verhindert den Versuch, eine abstrakte Klasse zu instanziieren.
Eine ehrliche Einordnung gehört dazu. Stammt der Klassenname aus einer Anfrage statt aus einer Konfigurationsdatei, reicht diese Prüfung nicht. Sie stellt sicher, dass die Klasse die richtige Basis hat, mehr nicht. Jede Klasse mit dieser Basis würde durchgehen, auch eine, die dort nichts zu suchen hat. Bei Eingaben von außen gehört deshalb eine ausdrückliche Liste erlaubter Werte davor.
Die ganze Hierarchie sehen
Manchmal genügt das true von PHP is_subclass_of nicht, etwa wenn Ereignisse an alle zuständigen Stellen verteilt werden sollen.
<?php
print_r(class_parents('Gutschrift'));
/* Array ( [Beleg] => Beleg ) */
print_r(class_implements('Gutschrift'));
/* Array ( [Druckbar] => Druckbar [Exportierbar] => Exportierbar ) */
Beide Funktionen liefern Arrays, deren Schlüssel den Namen entsprechen. Damit lässt sich eine Zuordnung aufbauen, die von der speziellsten zur allgemeinsten Klasse durchgeht.
flowchart TD
A[Typ pruefen] --> B{Objekt vorhanden}
B -->|ja| C[instanceof]
B -->|nein, nur Name| D{Gleiche Klasse zaehlt}
D -->|ja| E[is_a mit allow_string]
D -->|nein| F[is_subclass_of]
F --> G{Trait gesucht}
G -->|ja| H[class_uses]
Die erste Verzweigung entscheidet das meiste. Liegt ein Objekt vor, ist der Operator die einfachere Wahl, weil er zur Sprache gehört und keinen Funktionsaufruf kostet.
Fazit
PHP is_subclass_of prüft Klassen, bevor es sie gibt, und deckt dabei Elternklassen, abstrakte Basen und Interfaces gleichermaßen ab. Der eine Punkt, an dem sich die Funktion von ihren beiden Verwandten unterscheidet, ist die identische Klasse: dort steht false, weil nach Abstammung gefragt wird und nicht nach Zugehörigkeit. Traits bleiben außen vor, dafür gibt es class_uses(). Und wer den dritten Parameter unangetastet lässt, umgeht den stillsten Fehler, den diese Funktion zu bieten hat.