16 Type Hints und Return Types

Type Hints (Typdeklarationen) und Return Types (Rückgabetypen) sind wichtige Funktionen in PHP, die seit PHP 5 eingeführt und in späteren Versionen kontinuierlich erweitert wurden. Diese Funktionen verbessern die Codequalität, erhöhen die Lesbarkeit, ermöglichen bessere IDE-Unterstützung und helfen, Fehler früher zu erkennen.

16.1 Entwicklung der Typisierung in PHP

PHP begann als eine nicht typisierte Sprache, hat aber im Laufe der Zeit immer robustere Typisierungsmechanismen eingeführt:

16.2 Grundlegende Typ-Hints für Parameter

Typ-Hints für Parameter legen fest, welchen Typ ein Parameter haben muss:

function addUser(string $name, int $age, array $roles): void {
    // Code zur Benutzerverarbeitung
    echo "Benutzer $name ($age Jahre) mit Rollen: " . implode(', ', $roles);
}

// Korrekte Verwendung
addUser("Max Mustermann", 30, ["Admin", "Editor"]);

// Fehler: Argument 2 ($age) must be of type int, string given
// addUser("Max Mustermann", "30", ["Admin", "Editor"]);

16.3 Verfügbare Typdeklarationen

PHP unterstützt eine Vielzahl von Typen für Typ-Hints:

16.3.1 Skalare Typen (seit PHP 7.0)

16.3.2 Verbundtypen

16.3.3 Klassentypen

16.3.4 Spezielle Typen

16.4 Return Types

Return Types geben an, welchen Typ eine Funktion oder Methode zurückgibt:

function getUserName(int $userId): string {
    // Code zum Abrufen des Benutzernamens
    return "Benutzer_" . $userId;
}

// Korrekte Verwendung
$name = getUserName(123); // Gibt "Benutzer_123" zurück

// Führt zu einem TypeError
// function getUserNameError(int $userId): string {
//     return $userId; // Fehler: int zurückgegeben, string erwartet
// }

16.5 Nullable Types

Seit PHP 7.1 können Typen als nullable deklariert werden, indem ein Fragezeichen (?) vor den Typ gesetzt wird:

function findUser(int $userId): ?array {
    // Wenn der Benutzer nicht gefunden wird, null zurückgeben
    if ($userId <= 0) {
        return null;
    }
    
    // Gibt Benutzerdaten zurück
    return [
        'id' => $userId,
        'name' => 'Benutzer_' . $userId
    ];
}

$user = findUser(42); // Gibt Array zurück
$user = findUser(-1); // Gibt null zurück

16.6 Union Types (seit PHP 8.0)

Mit Union Types können mehrere mögliche Typen für einen Parameter oder Rückgabewert angegeben werden:

function process(string|int $value): int|float {
    if (is_string($value)) {
        return strlen($value);
    }
    
    return $value * 2.5;
}

$result1 = process("Hallo"); // Gibt 5 zurück (Länge des Strings)
$result2 = process(10);      // Gibt 25.0 zurück (10 * 2.5)

Union Types sind besonders nützlich bei Funktionen, die unterschiedliche Typen verarbeiten oder zurückgeben können:

function fetchData(int $id): array|false|null {
    if ($id <= 0) {
        return null; // Ungültige ID
    }
    
    // Simulieren eines fehlgeschlagenen Datenabrufs
    if ($id > 1000) {
        return false; // Fehler bei der Datenabfrage
    }
    
    // Erfolgreicher Abruf
    return [
        'id' => $id,
        'name' => 'Datensatz ' . $id
    ];
}

16.7 Intersection Types (seit PHP 8.1)

Intersection Types verlangen, dass ein Wert mehrere Interfaces gleichzeitig implementiert:

interface Renderable {
    public function render(): string;
}

interface Cacheable {
    public function getCacheKey(): string;
}

class View implements Renderable, Cacheable {
    private string $template;
    private array $data;
    
    public function __construct(string $template, array $data) {
        $this->template = $template;
        $this->data = $data;
    }
    
    public function render(): string {
        return "Rendering template: {$this->template}";
    }
    
    public function getCacheKey(): string {
        return md5($this->template . json_encode($this->data));
    }
}

// Funktion, die einen Parameter benötigt, der beide Interfaces implementiert
function renderAndCache(Renderable&Cacheable $view): string {
    $cacheKey = $view->getCacheKey();
    // Prüfen, ob die Ansicht im Cache ist
    $cached = false; // Im realen Code: Abfrage aus dem Cache-System
    
    if ($cached) {
        return "Aus Cache: " . $cacheKey;
    }
    
    $output = $view->render();
    // Im realen Code: Speichern im Cache
    return $output;
}

$view = new View('user/profile', ['id' => 42]);
echo renderAndCache($view);

Intersection Types können nur mit Interfaces oder Traits kombiniert werden, nicht mit Klassen.

16.8 Type Checking Modes

PHP bietet zwei Modi für die Typprüfung:

16.8.1 Coercive Mode (Standard)

Im Standardmodus versucht PHP, Werte automatisch in den erwarteten Typ umzuwandeln:

function add(int $a, int $b): int {
    return $a + $b;
}

$result = add("5", "10"); // Wird zu add(5, 10) umgewandelt und gibt 15 zurück

16.8.2 Strict Mode

Mit dem Strict Mode werden keine automatischen Typumwandlungen durchgeführt:

declare(strict_types=1); // Muss die erste Anweisung in der Datei sein

function multiply(int $a, int $b): int {
    return $a * $b;
}

// Fehler: Argument 1 ($a) must be of type int, string given
// $result = multiply("5", 10);

Die strict_types-Deklaration gilt nur für die Datei, in der sie definiert ist, und beeinflusst, wie Funktionsaufrufe innerhalb dieser Datei überprüft werden.

16.9 Praktisches Beispiel: Datenbankabstraktion

Hier ist ein praktisches Beispiel für die Verwendung von Typ-Hints und Return Types in einer einfachen Datenbankabstraktionsklasse:

interface DatabaseAdapter {
    public function query(string $sql, array $params = []): array;
    public function insert(string $table, array $data): int;
    public function update(string $table, array $data, string $where, array $params = []): int;
    public function delete(string $table, string $where, array $params = []): int;
    public function lastInsertId(): ?int;
}

class MySQLAdapter implements DatabaseAdapter {
    private PDO $pdo;
    
    public function __construct(string $host, string $database, string $username, string $password) {
        $this->pdo = new PDO(
            "mysql:host={$host};dbname={$database};charset=utf8mb4",
            $username,
            $password,
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            ]
        );
    }
    
    public function query(string $sql, array $params = []): array {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }
    
    public function insert(string $table, array $data): int {
        $columns = implode(', ', array_keys($data));
        $placeholders = implode(', ', array_fill(0, count($data), '?'));
        
        $sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(array_values($data));
        
        return $stmt->rowCount();
    }
    
    public function update(string $table, array $data, string $where, array $params = []): int {
        $setClauses = [];
        foreach (array_keys($data) as $column) {
            $setClauses[] = "{$column} = ?";
        }
        
        $sql = "UPDATE {$table} SET " . implode(', ', $setClauses) . " WHERE {$where}";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(array_merge(array_values($data), $params));
        
        return $stmt->rowCount();
    }
    
    public function delete(string $table, string $where, array $params = []): int {
        $sql = "DELETE FROM {$table} WHERE {$where}";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        
        return $stmt->rowCount();
    }
    
    public function lastInsertId(): ?int {
        $id = $this->pdo->lastInsertId();
        return $id ? (int)$id : null;
    }
}

class UserRepository {
    private DatabaseAdapter $db;
    
    public function __construct(DatabaseAdapter $db) {
        $this->db = $db;
    }
    
    public function findById(int $id): ?array {
        $results = $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
        return $results[0] ?? null;
    }
    
    public function findByEmail(string $email): ?array {
        $results = $this->db->query("SELECT * FROM users WHERE email = ?", [$email]);
        return $results[0] ?? null;
    }
    
    public function create(string $name, string $email, string $password): int {
        $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
        
        $this->db->insert('users', [
            'name' => $name,
            'email' => $email,
            'password' => $hashedPassword,
            'created_at' => date('Y-m-d H:i:s')
        ]);
        
        return $this->db->lastInsertId() ?? 0;
    }
    
    public function update(int $id, array $data): bool {
        $rowCount = $this->db->update('users', $data, 'id = ?', [$id]);
        return $rowCount > 0;
    }
    
    public function delete(int $id): bool {
        $rowCount = $this->db->delete('users', 'id = ?', [$id]);
        return $rowCount > 0;
    }
}

// Verwendung
try {
    $db = new MySQLAdapter('localhost', 'myapp', 'username', 'password');
    $userRepo = new UserRepository($db);
    
    // Neuen Benutzer erstellen
    $userId = $userRepo->create('Max Mustermann', 'max@example.com', 'sicheres_passwort');
    
    // Benutzer suchen
    $user = $userRepo->findById($userId);
    
    // Benutzer aktualisieren
    $updated = $userRepo->update($userId, ['name' => 'Maximilian Mustermann']);
    
    // Benutzer löschen
    $deleted = $userRepo->delete($userId);
} catch (Exception $e) {
    echo "Fehler: " . $e->getMessage();
}

16.10 Vorteile starker Typisierung in PHP

Die Verwendung von Typ-Hints und Return Types bietet verschiedene Vorteile:

  1. Verbesserte Code-Lesbarkeit: Die Absicht des Codes wird klarer, da die erwarteten Typen explizit angegeben werden.

  2. Frühe Fehlererkennung: Typfehler werden während der Ausführung früh erkannt, was das Debuggen erleichtert.

  3. Bessere IDE-Unterstützung: IDEs können bessere Autovervollständigung und Codeinspektion bieten.

  4. Selbstdokumentierender Code: Der Code dokumentiert sich teilweise selbst, was den Bedarf an zusätzlichen Kommentaren reduziert.

  5. Verbesserte Sicherheit: Strenge Typprüfung kann bestimmte Arten von Fehlern verhindern, insbesondere im Strict Mode.

  6. Bessere Wartbarkeit: Code mit klaren Typ-Deklarationen ist leichter zu warten und zu refaktorieren.

16.10.1 Property Type Declarations (seit PHP 7.4)

Seit PHP 7.4 können auch Klassenattribute mit Typen versehen werden:

class Product {
    public int $id;
    public string $name;
    public float $price;
    public ?string $description;
    private array $categories = [];
    
    public function __construct(int $id, string $name, float $price, ?string $description = null) {
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
        $this->description = $description;
    }
    
    public function addCategory(string $category): void {
        $this->categories[] = $category;
    }
    
    public function getCategories(): array {
        return $this->categories;
    }
}

$product = new Product(1, "Smartphone", 499.99);
$product->addCategory("Elektronik");
$product->addCategory("Mobile Geräte");

// Fehler: Typed property Product::$price must be float, int assigned
// $product->price = 500;

16.11 Typed Properties mit Spätinitialisierung (seit PHP 7.4)

In einigen Fällen werden Eigenschaften erst nach der Objektkonstruktion initialisiert. Mit PHP 7.4 kann der Uninitialized State mit dem speziellen Schlüsselwort unset überprüft werden:

class Configuration {
    private string $apiKey;
    private string $apiSecret;
    
    public function setCredentials(string $apiKey, string $apiSecret): void {
        $this->apiKey = $apiKey;
        $this->apiSecret = $apiSecret;
    }
    
    public function isConfigured(): bool {
        return isset($this->apiKey, $this->apiSecret);
    }
    
    public function getApiKey(): string {
        if (!isset($this->apiKey)) {
            throw new RuntimeException("API-Schlüssel nicht konfiguriert");
        }
        return $this->apiKey;
    }
}

$config = new Configuration();
// $key = $config->getApiKey(); // Würde eine Exception werfen

$config->setCredentials("mein_api_key", "mein_api_secret");
$key = $config->getApiKey(); // Funktioniert jetzt

16.12 Constructor Property Promotion (seit PHP 8.0)

PHP 8.0 führte Constructor Property Promotion ein, eine Kurzschreibweise für die Definition von Eigenschaften und deren Initialisierung im Konstruktor:

// PHP < 8.0
class Point {
    private float $x;
    private float $y;
    private float $z;
    
    public function __construct(float $x, float $y, float $z) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

// PHP >= 8.0 mit Constructor Property Promotion
class Point {
    public function __construct(
        private float $x,
        private float $y,
        private float $z
    ) {}
    
    public function getDistance(): float {
        return sqrt($this->x * $this->x + $this->y * $this->y + $this->z * $this->z);
    }
}

16.13 Komplexeres Beispiel: Benutzerverwaltungssystem

Hier ist ein komplexeres Beispiel, das verschiedene Typisierungsfunktionen in einem Benutzerverwaltungssystem demonstriert:

declare(strict_types=1);

enum UserRole: string {
    case ADMIN = 'admin';
    case MANAGER = 'manager';
    case USER = 'user';
    case GUEST = 'guest';
}

enum UserStatus: string {
    case ACTIVE = 'active';
    case INACTIVE = 'inactive';
    case BANNED = 'banned';
    case PENDING = 'pending';
}

interface UserRepositoryInterface {
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function save(User $user): bool;
    public function delete(User $user): bool;
    public function findAll(int $limit = 10, int $offset = 0): array;
}

class User {
    private ?int $id;
    private string $name;
    private string $email;
    private string $passwordHash;
    private array $roles = [];
    private UserStatus $status;
    private ?DateTime $lastLogin = null;
    
    public function __construct(
        string $name,
        string $email,
        array $roles = [UserRole::USER],
        UserStatus $status = UserStatus::PENDING,
        ?int $id = null
    ) {
        $this->name = $name;
        $this->email = $email;
        $this->roles = $roles;
        $this->status = $status;
        $this->id = $id;
    }
    
    public function getId(): ?int {
        return $this->id;
    }
    
    public function setId(int $id): self {
        $this->id = $id;
        return $this;
    }
    
    public function getName(): string {
        return $this->name;
    }
    
    public function setName(string $name): self {
        $this->name = $name;
        return $this;
    }
    
    public function getEmail(): string {
        return $this->email;
    }
    
    public function setEmail(string $email): self {
        $this->email = $email;
        return $this;
    }
    
    public function setPassword(string $password): self {
        $this->passwordHash = password_hash($password, PASSWORD_DEFAULT);
        return $this;
    }
    
    public function verifyPassword(string $password): bool {
        return isset($this->passwordHash) && password_verify($password, $this->passwordHash);
    }
    
    public function getRoles(): array {
        return $this->roles;
    }
    
    public function addRole(UserRole $role): self {
        if (!in_array($role, $this->roles)) {
            $this->roles[] = $role;
        }
        return $this;
    }
    
    public function removeRole(UserRole $role): self {
        $this->roles = array_filter($this->roles, fn($r) => $r !== $role);
        return $this;
    }
    
    public function hasRole(UserRole $role): bool {
        return in_array($role, $this->roles);
    }
    
    public function getStatus(): UserStatus {
        return $this->status;
    }
    
    public function setStatus(UserStatus $status): self {
        $this->status = $status;
        return $this;
    }
    
    public function getLastLogin(): ?DateTime {
        return $this->lastLogin;
    }
    
    public function recordLogin(): self {
        $this->lastLogin = new DateTime();
        return $this;
    }
    
    public function isActive(): bool {
        return $this->status === UserStatus::ACTIVE;
    }
}

class UserService {
    private UserRepositoryInterface $repository;
    
    public function __construct(UserRepositoryInterface $repository) {
        $this->repository = $repository;
    }
    
    public function registerUser(string $name, string $email, string $password): ?User {
        // Prüfen, ob der Benutzer bereits existiert
        if ($this->repository->findByEmail($email)) {
            return null; // Benutzer existiert bereits
        }
        
        $user = new User($name, $email);
        $user->setPassword($password)
             ->setStatus(UserStatus::PENDING);
        
        $saved = $this->repository->save($user);
        
        return $saved ? $user : null;
    }
    
    public function activateUser(User $user): bool {
        $user->setStatus(UserStatus::ACTIVE);
        return $this->repository->save($user);
    }
    
    public function loginUser(string $email, string $password): ?User {
        $user = $this->repository->findByEmail($email);
        
        if (!$user || !$user->isActive() || !$user->verifyPassword($password)) {
            return null;
        }
        
        $user->recordLogin();
        $this->repository->save($user);
        
        return $user;
    }
    
    public function promoteToAdmin(User $user): bool {
        $user->addRole(UserRole::ADMIN);
        return $this->repository->save($user);
    }
    
    public function banUser(User $user): bool {
        $user->setStatus(UserStatus::BANNED);
        return $this->repository->save($user);
    }
    
    /**
     * @return User[]
     */
    public function getActiveUsers(int $limit = 10): array {
        $allUsers = $this->repository->findAll($limit);
        return array_filter($allUsers, fn(User $user) => $user->isActive());
    }
}

// Beispielimplementierung eines Repositories
class MySQLUserRepository implements UserRepositoryInterface {
    private PDO $connection;
    
    public function __construct(PDO $connection) {
        $this->connection = $connection;
    }
    
    public function findById(int $id): ?User {
        $stmt = $this->connection->prepare("SELECT * FROM users WHERE id = :id");
        $stmt->execute(['id' => $id]);
        
        $userData = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$userData) {
            return null;
        }
        
        return $this->createUserFromData($userData);
    }
    
    public function findByEmail(string $email): ?User {
        $stmt = $this->connection->prepare("SELECT * FROM users WHERE email = :email");
        $stmt->execute(['email' => $email]);
        
        $userData = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$userData) {
            return null;
        }
        
        return $this->createUserFromData($userData);
    }
    
    public function save(User $user): bool {
        if ($user->getId() === null) {
            return $this->insert($user);
        } else {
            return $this->update($user);
        }
    }
    
    public function delete(User $user): bool {
        if ($user->getId() === null) {
            return false;
        }
        
        $stmt = $this->connection->prepare("DELETE FROM users WHERE id = :id");
        return $stmt->execute(['id' => $user->getId()]);
    }
    
    public function findAll(int $limit = 10, int $offset = 0): array {
        $stmt = $this->connection->prepare("SELECT * FROM users LIMIT :limit OFFSET :offset");
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();
        
        $users = [];
        while ($userData = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $users[] = $this->createUserFromData($userData);
        }
        
        return $users;
    }
    
    private function createUserFromData(array $data): User {
        $roles = json_decode($data['roles'], true);
        $mappedRoles = array_map(fn($role) => UserRole::from($role), $roles);
        
        $user = new User(
            $data['name'],
            $data['email'],
            $mappedRoles,
            UserStatus::from($data['status']),
            (int)$data['id']
        );
        
        // Passwort setzen
        $reflectionProperty = new ReflectionProperty(User::class, 'passwordHash');
        $reflectionProperty->setAccessible(true);
        $reflectionProperty->setValue($user, $data['password_hash']);
        
        // Last login setzen, wenn vorhanden
        if ($data['last_login']) {
            $lastLogin = new ReflectionProperty(User::class, 'lastLogin');
            $lastLogin->setAccessible(true);
            $lastLogin->setValue($user, new DateTime($data['last_login']));
        }
        
        return $user;
    }
    
    private function insert(User $user): bool {
        $stmt = $this->connection->prepare("
            INSERT INTO users (name, email, password_hash, roles, status, last_login)
            VALUES (:name, :email, :password_hash, :roles, :status, :last_login)
        ");
        
        $result = $stmt->execute([
            'name' => $user->getName(),
            'email' => $user->getEmail(),
            'password_hash' => $this->getPasswordHash($user),
            'roles' => json_encode(array_map(fn($role) => $role->value, $user->getRoles())),
            'status' => $user->getStatus()->value,
            'last_login' => $user->getLastLogin()?->format('Y-m-d H:i:s')
        ]);
        
        if ($result) {
            $user->setId((int)$this->connection->lastInsertId());
        }
        
        return $result;
    }
    
    private function update(User $user): bool {
        $stmt = $this->connection->prepare("
            UPDATE users
            SET name = :name,
                email = :email,
                password_hash = :password_hash,
                roles = :roles,
                status = :status,
                last_login = :last_login
            WHERE id = :id
        ");
        
        return $stmt->execute([
            'name' => $user->getName(),
            'email' => $user->getEmail(),
            'password_hash' => $this->getPasswordHash($user),
            'roles' => json_encode(array_map(fn($role) => $role->value, $user->getRoles())),
            'status' => $user->getStatus()->value,
            'last_login' => $user->getLastLogin()?->format('Y-m-d H:i:s'),
            'id' => $user->getId()
        ]);
    }
    
    private function getPasswordHash(User $user): string {
        $reflectionProperty = new ReflectionProperty(User::class, 'passwordHash');
        $reflectionProperty->setAccessible(true);
        return $reflectionProperty->getValue($user) ?? '';
    }
}

// Beispielverwendung
try {
    $pdo = new PDO('mysql:host=localhost;dbname=userdb', 'username', 'password');
    $repository = new MySQLUserRepository($pdo);
    $userService = new UserService($repository);
    
    // Neuen Benutzer registrieren
    $user = $userService->registerUser('Max Mustermann', 'max@example.com', 'sicheres_passwort');
    
    // Benutzer aktivieren
    if ($user) {
        $userService->activateUser($user);
        echo "Benutzer aktiviert: " . $user->getName();
    }
    
    // Benutzer einloggen
    $loggedInUser = $userService->loginUser('max@example.com', 'sicheres_passwort');
    
    if ($loggedInUser) {
        echo "Erfolgreich eingeloggt: " . $loggedInUser->getName();
        echo "Letzter Login: " . $loggedInUser->getLastLogin()->format('Y-m-d H:i:s');
    }
} catch (Exception $e) {
    echo "Fehler: " . $e->getMessage();
}

16.14 Best Practices für Type Hints und Return Types

  1. Konsequente Verwendung: Verwenden Sie Typ-Hints für alle Parameter und Return Types für alle Methoden.

  2. Dokumentation ergänzen: Auch wenn Typen viel über die Verwendung aussagen, ist eine gute PHPDoc-Dokumentation immer noch wertvoll, besonders für komplexere Typen oder Collections.

  3. Nullable-Typen bevorzugen: Verwenden Sie ?string statt string|null, es ist kürzer und klarer.

  4. Strict Types aktivieren: Nutzen Sie declare(strict_types=1) für robustere Typprüfungen.

  5. Generische Arrays vermeiden: Anstatt nur array zu verwenden, dokumentieren Sie den Inhalt mit PHPDoc, z.B. /** @param array<int, string> $values */.

  6. Union Types gezielt einsetzen: Union Types machen den Code komplexer - verwenden Sie sie nur, wenn nötig.

  7. Klare Rückgabewerte definieren: Geben Sie lieber null zurück als false oder null für Fehlerfälle.

  8. Konsistente Nullability: Wenn eine Methode null zurückgeben kann, sollten ähnliche Methoden diesem Muster folgen.

  9. Statt mixed spezifischere Typen verwenden: Der Typ mixed sollte nur verwendet werden, wenn keine spezifischere Typisierung möglich ist.

  10. Enum-Typen nutzen: In PHP