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.
PHP begann als eine nicht typisierte Sprache, hat aber im Laufe der Zeit immer robustere Typisierungsmechanismen eingeführt:
array
als Typ-Hintcallable als Typ-Hint?object
als Typ-Hint|-Operator&-Operator und Pure Intersection
TypesTyp-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"]);PHP unterstützt eine Vielzahl von Typen für Typ-Hints:
Traversable implementieren (seit PHP 7.1)?
verwendbarfalse bei Fehler zurückgeben können (seit PHP 8.0)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
// }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ückMit 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
];
}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.
PHP bietet zwei Modi für die Typprüfung:
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ückMit 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.
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();
}Die Verwendung von Typ-Hints und Return Types bietet verschiedene Vorteile:
Verbesserte Code-Lesbarkeit: Die Absicht des Codes wird klarer, da die erwarteten Typen explizit angegeben werden.
Frühe Fehlererkennung: Typfehler werden während der Ausführung früh erkannt, was das Debuggen erleichtert.
Bessere IDE-Unterstützung: IDEs können bessere Autovervollständigung und Codeinspektion bieten.
Selbstdokumentierender Code: Der Code dokumentiert sich teilweise selbst, was den Bedarf an zusätzlichen Kommentaren reduziert.
Verbesserte Sicherheit: Strenge Typprüfung kann bestimmte Arten von Fehlern verhindern, insbesondere im Strict Mode.
Bessere Wartbarkeit: Code mit klaren Typ-Deklarationen ist leichter zu warten und zu refaktorieren.
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;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 jetztPHP 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);
}
}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();
}Konsequente Verwendung: Verwenden Sie Typ-Hints für alle Parameter und Return Types für alle Methoden.
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.
Nullable-Typen bevorzugen: Verwenden Sie
?string statt string|null, es ist kürzer und
klarer.
Strict Types aktivieren: Nutzen Sie
declare(strict_types=1) für robustere
Typprüfungen.
Generische Arrays vermeiden: Anstatt nur
array zu verwenden, dokumentieren Sie den Inhalt mit
PHPDoc, z.B.
/** @param array<int, string> $values */.
Union Types gezielt einsetzen: Union Types machen den Code komplexer - verwenden Sie sie nur, wenn nötig.
Klare Rückgabewerte definieren: Geben Sie lieber
null zurück als false oder null
für Fehlerfälle.
Konsistente Nullability: Wenn eine Methode
null zurückgeben kann, sollten ähnliche Methoden diesem
Muster folgen.
Statt mixed spezifischere Typen
verwenden: Der Typ mixed sollte nur verwendet
werden, wenn keine spezifischere Typisierung möglich ist.
Enum-Typen nutzen: In PHP