Wie funktioniert php Typisierung
1. Die grundlegenden PHP-Typen
2. Typisierung bei Funktionen
3. declare(strict_types=1)
4. Rückgabetypen
5. void
6. Nullable Types
7. Union Types
8. Union Types mit Logik
9. mixed
10. Arrays typisieren
11. Klassen als Typen
12. Interfaces
13. Warum Interfaces so wichtig sind
14. Intersection Types
15. Union vs. Intersection
16. never
17. object
18. self, parent und static
19. Properties typisieren
20. Konstruktoren typisieren
21. readonly
22. Eine realistischere Anwendung
23. Ein Beispiel mit Union + Interface
24. Typisierung als „Vertrag“
25. Die wichtigsten Typen auf einen Blick
Die wichtigsten Typen sind:
string
int
float
bool
array
object
null
Beispiele:
$name = "Anna"; // string
$age = 25; // int
$price = 19.99; // float
$active = true; // bool
$tags = ["php", "web"]; // array
$user = new User(); // object
$value = null; // null
PHP ist dabei standardmäßig dynamisch typisiert.
Das bedeutet:
$value = "Hallo";
$value = 123;
$value = true;
Eine Variable kann also zu unterschiedlichen Zeitpunkten unterschiedliche Typen enthalten.
Hier wird Typisierung besonders interessant.
function add(int $a, int $b): int
{
return $a + $b;
}
Die Typen stehen hier an drei Stellen:
int $a int $b
↓ ↓
↑
Rückgabetyp
Also:
$a muss int sein
$b muss int sein
die Funktion muss int zurückgeben
Beispiel:
$result = add(10, 20);
echo $result; // 30
Das solltest du unbedingt verstehen.
Am Anfang einer PHP-Datei kannst du schreiben:
<?php
declare(strict_types=1);
Dann:
und:
add(10, 20); // ✅
add("10", 20); // ❌ TypeError
Ohne strict_types kann PHP bei bestimmten Funktionsaufrufen versuchen, Werte automatisch umzuwandeln.
Mit:
sagst du:
Verwende die angegebenen Typen strikt.
Für moderne PHP-Projekte ist strict_types=1 eine sehr gute Gewohnheit.
Du kannst nicht nur Parameter, sondern auch den Rückgabewert typisieren.
function getName(): string
return "Anna";
Das hier wäre falsch:
return 123;
Mit strict_types=1 führt das zu einem TypeError.
Manchmal soll eine Funktion nichts zurückgeben.
Dafür gibt es void.
function sayHello(string $name): void
echo "Hallo $name";
Die Funktion macht etwas, aber sie liefert keinen Wert zurück.
sayHello("Anna");
ist sinnvoll.
Aber:
$result = sayHello("Anna");
$result enthält keinen sinnvollen Rückgabewert.
Ein wichtiger Unterschied:
function foo(): void
// kein return
ist etwas anderes als:
function foo(): null
return null;
void bedeutet:
Diese Funktion gibt keinen Wert zurück.
Was ist, wenn ein Wert entweder ein string oder null sein darf?
Dafür gibt es:
?string
function getUserName(?int $userId): ?string
if ($userId === null) {
↓
string oder null
Dasselbe könnte man auch mit einem Union Type schreiben:
string|null
?string ist im Grunde die Kurzschreibweise dafür.
Jetzt kommen wir zu einem wichtigen fortgeschrittenen Konzept.
Ein Union Type bedeutet:
Ein Wert darf Typ A oder Typ B sein.
function printId(int|string $id): void
echo $id;
Jetzt sind beide erlaubt:
printId(123);
printId("ABC-123");
printId(true); // ❌
Denn bool gehört nicht zur Union.
Du kannst auch mehrere Typen kombinieren:
function process(int|string|float $value): void
// ...
oder
Wenn eine Funktion mehrere Typen akzeptiert, musst du häufig unterscheiden, welchen Typ du tatsächlich bekommen hast.
Dafür gibt es beispielsweise is_string() und is_int():
function formatValue(int|string $value): string
if (is_int($value)) {
return "ID: " . $value;
return "Name: " . $value;
Jetzt:
echo formatValue(123);
// ID: 123
echo formatValue("Anna");
// Name: Anna
Das nennt man Type Checking bzw. Type Narrowing.
Jetzt kommt ein sehr spezieller Typ:
mixed
mixed bedeutet im Grunde:
Dieser Wert kann praktisch jeden PHP-Typ haben.
function dumpValue(mixed $value): void
var_dump($value);
Jetzt ist alles erlaubt:
dumpValue("Hallo");
dumpValue(123);
dumpValue(12.5);
dumpValue(true);
dumpValue(["a", "b"]);
dumpValue(null);
mixed entspricht konzeptionell ungefähr:
int|string|float|bool|array|object|null|...
Es ist also sehr flexibel.
mixed sollte nicht einfach überall verwendet werden.
Schlecht:
function calculate(mixed $value): mixed
Hier weiß niemand mehr richtig, was die Funktion erwartet oder zurückgibt.
Besser:
function calculate(float $price, int $quantity): float
return $price * $quantity;
Je präziser deine Typen, desto besser.
mixed ist vor allem sinnvoll, wenn eine Funktion tatsächlich mit beliebigen Typen arbeiten soll.
Hier gibt es eine Besonderheit.
Du kannst schreiben:
function getTags(): array
return ["php", "mysql", "html"];
Aber array sagt nur:
Ich bekomme irgendein Array.
Nicht:
Ich bekomme ein Array aus Strings.
Dafür werden häufig PHPDoc-Typen verwendet:
/**
* @return string[]
*/
Damit können IDEs und statische Analysewerkzeuge verstehen:
string[]
Array mit Strings
Man kann auch komplexere Formen beschreiben:
* @param array<string, int> $scores
function calculateScores(array $scores): int
return array_sum($scores);
Key → string
Value → int
Zum Beispiel:
$scores = [
"Anna" => 90,
"Max" => 80,
];
Jetzt wird Typisierung richtig mächtig.
Angenommen:
class User
public function __construct(
public string $name
) {}
Dann können wir schreiben:
function greet(User $user): string
return "Hallo " . $user->name;
Jetzt erwartet die Funktion ein User-Objekt.
$user = new User("Anna");
echo greet($user);
greet("Anna"); // ❌
Das ist enorm nützlich, weil du damit ausdrücken kannst:
Diese Funktion arbeitet ausschließlich mit User-Objekten.
Jetzt kommen wir zu einem der wichtigsten Konzepte in objektorientiertem PHP.
Ein Interface beschreibt einen Vertrag.
interface PaymentMethod
public function pay(float $amount): bool;
Jede Klasse, die PaymentMethod implementiert, muss eine pay()-Methode besitzen, die float akzeptiert und bool zurückgibt.
class CreditCardPayment implements PaymentMethod
public function pay(float $amount): bool
echo "Bezahle $amount € per Kreditkarte";
return true;
Und:
class PaypalPayment implements PaymentMethod
echo "Bezahle $amount € mit PayPal";
Jetzt kommt der interessante Teil:
function checkout(PaymentMethod $payment): void
$payment->pay(100);
Du kannst jetzt beide Klassen übergeben:
checkout(new CreditCardPayment());
checkout(new PaypalPayment());
Warum funktioniert das?
Weil beide Klassen den Vertrag
PaymentMethod
erfüllen.
Das ist Polymorphie.
Stell dir einen Online-Shop vor.
Du möchtest nicht:
function checkout(CreditCardPayment $payment)
weil du dann nur Kreditkarte akzeptierst.
Stattdessen:
function checkout(PaymentMethod $payment)
Damit ist der Code unabhängig von der konkreten Implementierung.
Heute:
CreditCardPayment
Morgen:
PaypalPayment
Übermorgen:
BitcoinPayment
Solange die Klasse
implements PaymentMethod
verwendet, funktioniert sie.
Das ist ein zentraler Bestandteil von Dependency Inversion und generell sauberem OOP-Design.
Jetzt wird es etwas fortgeschrittener.
A|B
also:
A oder B
Ein Intersection Type bedeutet:
A&B
A und B
interface Loggable
public function log(): void;
interface Cacheable
public function cache(): void;
Eine Klasse:
class User implements Loggable, Cacheable
public function log(): void
echo "Logging";
public function cache(): void
echo "Caching";
Jetzt können wir schreiben:
function process(Loggable&Cacheable $object): void
$object->log();
$object->cache();
Loggable
UND
Cacheable
Das Objekt muss beide Interfaces erfüllen.
process(new User()); // ✅
Eine Klasse, die nur Loggable implementiert:
class Logger implements Loggable
public function log(): void {}
kann nicht übergeben werden:
process(new Logger()); // ❌
weil Cacheable fehlt.
Das ist ein Punkt, den man sich gut merken sollte:
bedeutet:
function foo(Cat|Dog $animal): void
Akzeptiert:
Cat
Dog
function foo(Loggable&Cacheable $object): void
Akzeptiert nur Objekte, die beides sind.
never ist ein sehr spezieller Rückgabetyp.
Er bedeutet:
Diese Funktion wird niemals normal zurückkehren.
Zum Beispiel eine Funktion, die das Programm beendet:
function abort(string $message): never
throw new RuntimeException($message);
Danach geht die normale Programmausführung nicht weiter.
function getUser(): User
if (!userExists()) {
abort("User nicht gefunden");
return new User("Anna");
PHP bzw. statische Analysewerkzeuge können verstehen:
abort()
kehrt niemals zurück
deshalb muss hier kein User zurückgegeben werden
Ein anderes Beispiel:
function redirect(string $url): never
header("Location: $url");
exit;
Die Funktion endet entweder durch exit oder würde anderweitig nicht normal zurückkehren.
Das ist wichtig:
function logMessage(): void
echo "Hallo";
Die Funktion kehrt normal zurück, nur eben ohne Rückgabewert.
Dagegen:
function crash(): never
throw new Exception();
Die Funktion kehrt niemals normal zurück.
Es gibt auch den Typ:
function inspect(object $value): void
Das akzeptiert grundsätzlich Objekte.
Aber häufig ist eine konkretere Typisierung besser:
function inspect(User $user): void
statt:
function inspect(object $user): void
Denn bei User weiß dein Programm wesentlich mehr darüber, was mit dem Objekt gemacht werden kann.
Bei Klassen gibt es noch spezielle Typen.
public function save(): self
// speichern
return $this;
self bedeutet die aktuelle Klasse.
Man kann dann beispielsweise schreiben:
$user->save()->save();
static ist etwas subtiler und bezieht sich auf die tatsächlich aufgerufene Klasse.
Das wird besonders bei Vererbung und Fluent APIs relevant.
Typisierung funktioniert auch bei Klassen-Eigenschaften:
public string $name;
public int $age;
public bool $active;
$user = new User();
$user->name = "Anna";
$user->age = 25;
$user->active = true;
Ein falscher Wert:
$user->age = "25";
kann mit strikter Typisierung zu einem TypeError führen.
Sehr häufig sieht moderner PHP-Code so aus:
public string $name,
public int $age,
public bool $active
$user = new User(
name: "Anna",
age: 25,
active: true
);
Das ist eine sehr kompakte Schreibweise.
In modernen PHP-Versionen kannst du zusätzlich readonly verwenden:
public readonly int $id,
Danach:
$user->id = 123; // ❌
Die Property kann nach ihrer Initialisierung nicht mehr verändert werden.
Das ist besonders praktisch für Datenobjekte.
Jetzt kombinieren wir mehrere Konzepte.
Wir definieren:
Eine Kreditkarte:
class CreditCard implements PaymentMethod
echo "Kreditkarte: $amount €";
PayPal:
class Paypal implements PaymentMethod
echo "PayPal: $amount €";
Unser Checkout:
function checkout(
PaymentMethod $payment,
float $amount
): bool {
return $payment->pay($amount);
checkout(new CreditCard(), 49.99);
checkout(new Paypal(), 49.99);
Der Checkout weiß überhaupt nicht, wie bezahlt wird.
Er weiß nur:
Ich bekomme eine PaymentMethod.
Diese besitzt pay().
Das ist einer der großen Vorteile von Typisierung + Interfaces.
Manchmal möchtest du mehrere Möglichkeiten erlauben:
function printUserId(int|string $id): void
Oder bei Objekten:
interface Admin {}
interface Customer {}
Dann könnte man beispielsweise sagen:
function process(Admin|Customer $user): void
Das Objekt muss entweder Admin oder Customer erfüllen.
Admin&Customer
müsste es hingegen beides gleichzeitig sein.
Eine sehr hilfreiche Denkweise ist:
Lies das nicht nur als Syntax.
Lies es als Vertrag:
„Gib mir einen float und einen int, und ich garantiere dir einen float zurück.“
Bei einem Interface:
lautet der Vertrag:
„Jede PaymentMethod kann einen Betrag bezahlen und sagt mir mit bool, ob es funktioniert hat.“
Bei never:
function abort(): never
„Diese Funktion wird niemals normal zurückkehren.“
Bei void:
function log(): void
lautet er:
„Diese Funktion kehrt normal zurück, liefert aber keinen Wert.“
Typ
Bedeutung
Ganzzahl
Kommazahl
Text
true / false
Array
beliebiges Objekt
kein Wert
praktisch jeder Typ
void
kein Rückgabewert
never
kehrt niemals normal zurück
User
konkretes Objekt der Klasse User
Objekt, das dieses Interface erfüllt