15 Magische Methoden

Magische Methoden in PHP sind spezielle Methoden, die mit einem doppelten Unterstrich (__) beginnen und automatisch ausgeführt werden, wenn bestimmte Ereignisse im Lebenszyklus eines Objekts auftreten. Diese Methoden ermöglichen es, das Verhalten von Objekten auf fortgeschrittene Weise anzupassen und erweitern die Funktionalität von Klassen erheblich.

15.1 Überblick über magische Methoden

PHP definiert eine Reihe magischer Methoden, die unterschiedliche Funktionen erfüllen:

Methode Beschreibung
__construct() Konstruktor, wird bei der Objekterstellung aufgerufen
__destruct() Destruktor, wird bei der Objektzerstörung aufgerufen
__call() Wird aufgerufen, wenn eine nicht erreichbare Methode aufgerufen wird
__callStatic() Wird aufgerufen, wenn eine nicht erreichbare statische Methode aufgerufen wird
__get() Wird aufgerufen, wenn auf eine nicht erreichbare Eigenschaft zugegriffen wird
__set() Wird aufgerufen, wenn einer nicht erreichbaren Eigenschaft ein Wert zugewiesen wird
__isset() Wird aufgerufen, wenn isset() auf eine nicht erreichbare Eigenschaft angewendet wird
__unset() Wird aufgerufen, wenn unset() auf eine nicht erreichbare Eigenschaft angewendet wird
__sleep() Wird vor der Serialisierung aufgerufen
__wakeup() Wird nach der Deserialisierung aufgerufen
__serialize() Wird für die Serialisierung (ab PHP 7.4) aufgerufen
__unserialize() Wird für die Deserialisierung (ab PHP 7.4) aufgerufen
__toString() Wird aufgerufen, wenn das Objekt als String verwendet wird
__invoke() Wird aufgerufen, wenn das Objekt wie eine Funktion aufgerufen wird
__set_state() Wird für var_export() verwendet
__clone() Wird nach der Objektklonung aufgerufen
__debugInfo() Wird von var_dump() aufgerufen (ab PHP 5.6)

Lassen Sie uns jede dieser Methoden im Detail untersuchen.

15.2 Konstruktor und Destruktor

Die Methoden __construct() und __destruct() markieren den Beginn bzw. das Ende des Lebenszyklus eines Objekts.

15.2.1 __construct()

Der Konstruktor wird automatisch aufgerufen, wenn ein neues Objekt erstellt wird. Er wird typischerweise für die Initialisierung von Objekteigenschaften verwendet:

class Person {
    private string $name;
    private int $alter;
    
    public function __construct(string $name, int $alter = 30) {
        $this->name = $name;
        $this->alter = $alter;
        echo "Neue Person erstellt: {$this->name}, {$this->alter} Jahre alt\n";
    }
}

$person = new Person("Max Mustermann", 25); // Ausgabe: Neue Person erstellt: Max Mustermann, 25 Jahre alt

15.2.2 __destruct()

Der Destruktor wird aufgerufen, wenn alle Referenzen auf ein Objekt entfernt wurden oder wenn das Skript beendet wird:

class Verbindung {
    private $verbindungsHandle;
    
    public function __construct(string $server) {
        echo "Verbindung zu {$server} hergestellt\n";
        $this->verbindungsHandle = "HANDLE_" . rand(1000, 9999);
    }
    
    public function __destruct() {
        echo "Verbindung {$this->verbindungsHandle} wird geschlossen\n";
        // Aufräumlogik, z.B. Datenbanktransaktionen beenden, Dateien schließen usw.
    }
}

function testVerbindung() {
    $verbindung = new Verbindung("beispiel.de");
    // Verbindung wird verwendet...
    // Am Ende der Funktion wird der Destruktor automatisch aufgerufen
}

testVerbindung();
// Ausgabe: 
// Verbindung zu beispiel.de hergestellt
// Verbindung HANDLE_XXXX wird geschlossen

Der Destruktor ist besonders nützlich für das Freigeben von Ressourcen oder das Aufräumen, bevor ein Objekt endgültig entfernt wird.

15.3 Eigenschaftszugriff: __get, __set, __isset und __unset

Diese Methoden ermöglichen die Überwachung und Kontrolle des Zugriffs auf Objekteigenschaften, insbesondere auf Eigenschaften, die nicht direkt zugänglich sind.

15.3.1 __get()

Wird aufgerufen, wenn auf eine nicht zugängliche Eigenschaft zugegriffen wird:

class User {
    private array $daten = [
        'name' => 'Unbekannt',
        'email' => 'keine@beispiel.de',
        'rolle' => 'Gast'
    ];
    
    public function __get(string $name) {
        if (array_key_exists($name, $this->daten)) {
            return $this->daten[$name];
        }
        
        $klassenmethode = 'get' . ucfirst($name);
        if (method_exists($this, $klassenmethode)) {
            return $this->$klassenmethode();
        }
        
        throw new Exception("Eigenschaft {$name} existiert nicht");
    }
    
    private function getVollständigerName(): string {
        return $this->daten['name'] . " (" . $this->daten['rolle'] . ")";
    }
}

$user = new User();
echo $user->name; // Ausgabe: Unbekannt
echo $user->vollständigerName; // Ausgabe: Unbekannt (Gast)

15.3.2 __set()

Wird aufgerufen, wenn einer nicht zugänglichen Eigenschaft ein Wert zugewiesen wird:

class User {
    private array $daten = [
        'name' => 'Unbekannt',
        'email' => 'keine@beispiel.de',
        'rolle' => 'Gast'
    ];
    
    private array $erlaubteEigenschaften = ['name', 'email', 'rolle'];
    
    public function __set(string $name, $wert) {
        if (in_array($name, $this->erlaubteEigenschaften)) {
            // Hier könnte auch Validierung stattfinden
            $this->daten[$name] = $wert;
        } else {
            throw new Exception("Die Eigenschaft {$name} kann nicht gesetzt werden");
        }
    }
}

$user = new User();
$user->name = "Max Mustermann"; // Setzt $daten['name'] = "Max Mustermann"
$user->passwort = "geheim"; // Wirft Exception: Die Eigenschaft passwort kann nicht gesetzt werden

15.3.3 __isset()

Wird aufgerufen, wenn isset() oder empty() für eine nicht zugängliche Eigenschaft verwendet wird:

class Konfiguration {
    private array $einstellungen = [
        'debug' => true,
        'cache' => false,
        'timeout' => 30
    ];
    
    public function __isset(string $name): bool {
        return array_key_exists($name, $this->einstellungen);
    }
}

$config = new Konfiguration();
var_dump(isset($config->debug)); // Ausgabe: bool(true)
var_dump(isset($config->unbekannt)); // Ausgabe: bool(false)

15.3.4 __unset()

Wird aufgerufen, wenn unset() für eine nicht zugängliche Eigenschaft verwendet wird:

class Konfiguration {
    private array $einstellungen = [
        'debug' => true,
        'cache' => false,
        'timeout' => 30
    ];
    
    public function __isset(string $name): bool {
        return array_key_exists($name, $this->einstellungen);
    }
    
    public function __unset(string $name): void {
        if (array_key_exists($name, $this->einstellungen)) {
            unset($this->einstellungen[$name]);
        }
    }
}

$config = new Konfiguration();
unset($config->debug); // Entfernt 'debug' aus dem $einstellungen-Array

15.4 Methodenaufruf: __call und __callStatic

Diese Methoden werden aufgerufen, wenn nicht existierende oder nicht zugängliche Methoden aufgerufen werden.

15.4.1 __call()

Wird aufgerufen, wenn eine nicht zugängliche Instanzmethode aufgerufen wird:

class ServiceProxy {
    private object $service;
    
    public function __construct(object $service) {
        $this->service = $service;
    }
    
    public function __call(string $name, array $argumente) {
        if (method_exists($this->service, $name)) {
            // Delegiert den Aufruf an den zugrunde liegenden Service
            return call_user_func_array([$this->service, $name], $argumente);
        }
        
        throw new Exception("Die Methode {$name} existiert nicht");
    }
}

class UserService {
    public function findById(int $id): array {
        return ['id' => $id, 'name' => 'Benutzer ' . $id];
    }
}

$proxy = new ServiceProxy(new UserService());
$user = $proxy->findById(123); // Ruft UserService::findById(123) auf

15.4.2 __callStatic()

Wird aufgerufen, wenn eine nicht zugängliche statische Methode aufgerufen wird:

class Model {
    protected static array $finderMethoden = ['findByName', 'findByEmail', 'findByRole'];
    
    public static function __callStatic(string $name, array $argumente) {
        // Prüft, ob der Methodenname ein bekannter Finder ist
        if (in_array($name, static::$finderMethoden)) {
            $feldName = strtolower(substr($name, 7)); // Entfernt "findBy" vom Methodennamen
            $wert = $argumente[0] ?? null;
            
            return static::findWhere($feldName, $wert);
        }
        
        throw new Exception("Die statische Methode {$name} existiert nicht");
    }
    
    protected static function findWhere(string $feld, $wert): array {
        echo "Suche nach {$feld} = {$wert}\n";
        // In einer realen Anwendung würde hier eine Datenbankabfrage erfolgen
        return ['id' => 1, $feld => $wert];
    }
}

$benutzer = Model::findByName("Max"); // Ausgabe: Suche nach name = Max

15.5 Serialisierung: __sleep, __wakeup, __serialize und __unserialize

Diese Methoden kontrollieren, wie ein Objekt serialisiert und deserialisiert wird.

15.5.1 __sleep()

Wird aufgerufen, bevor ein Objekt serialisiert wird. Es sollte ein Array mit den Namen der zu serialisierenden Eigenschaften zurückgeben:

class Verbindung {
    private string $server;
    private string $benutzer;
    private string $passwort;
    private $handle;
    
    public function __construct(string $server, string $benutzer, string $passwort) {
        $this->server = $server;
        $this->benutzer = $benutzer;
        $this->passwort = $passwort;
        $this->verbinden();
    }
    
    private function verbinden() {
        $this->handle = "HANDLE_" . rand(1000, 9999);
        echo "Verbindung hergestellt: {$this->handle}\n";
    }
    
    public function __sleep(): array {
        // Nur diese Eigenschaften werden serialisiert
        // $handle wird ausgelassen, da es nicht sinnvoll ist, einen Verbindungshandle zu serialisieren
        return ['server', 'benutzer', 'passwort'];
    }
}

$verbindung = new Verbindung("db.beispiel.de", "admin", "geheim");
$serialisiert = serialize($verbindung);

15.5.2 __wakeup()

Wird aufgerufen, nachdem ein Objekt deserialisiert wurde. Es wird häufig verwendet, um Ressourcen wiederherzustellen oder Verbindungen neu zu öffnen:

class Verbindung {
    private string $server;
    private string $benutzer;
    private string $passwort;
    private $handle;
    
    public function __construct(string $server, string $benutzer, string $passwort) {
        $this->server = $server;
        $this->benutzer = $benutzer;
        $this->passwort = $passwort;
        $this->verbinden();
    }
    
    private function verbinden() {
        $this->handle = "HANDLE_" . rand(1000, 9999);
        echo "Verbindung hergestellt: {$this->handle}\n";
    }
    
    public function __sleep(): array {
        return ['server', 'benutzer', 'passwort'];
    }
    
    public function __wakeup(): void {
        // Stellt die Verbindung nach der Deserialisierung wieder her
        $this->verbinden();
    }
}

$verbindung = new Verbindung("db.beispiel.de", "admin", "geheim");
$serialisiert = serialize($verbindung);
$neueVerbindung = unserialize($serialisiert); // Ruft __wakeup() auf und stellt die Verbindung wieder her

15.5.3 __serialize() und __unserialize() (ab PHP 7.4)

Diese neueren Methoden bieten mehr Kontrolle über den Serialisierungsprozess:

class Benutzer {
    private string $name;
    private string $email;
    private string $passwort;
    
    public function __construct(string $name, string $email, string $passwort) {
        $this->name = $name;
        $this->email = $email;
        $this->passwort = $passwort;
    }
    
    public function __serialize(): array {
        // Passwort wird vor der Serialisierung entfernt
        return [
            'name' => $this->name,
            'email' => $this->email,
            // passwort wird absichtlich ausgelassen
        ];
    }
    
    public function __unserialize(array $data): void {
        $this->name = $data['name'];
        $this->email = $data['email'];
        $this->passwort = ''; // Setzt das Passwort zurück
    }
}

15.6 Weitere nützliche magische Methoden

15.6.1 __toString()

Wird aufgerufen, wenn ein Objekt in einen String umgewandelt wird:

class Adresse {
    private string $straße;
    private string $hausnummer;
    private string $plz;
    private string $ort;
    
    public function __construct(string $straße, string $hausnummer, string $plz, string $ort) {
        $this->straße = $straße;
        $this->hausnummer = $hausnummer;
        $this->plz = $plz;
        $this->ort = $ort;
    }
    
    public function __toString(): string {
        return "{$this->straße} {$this->hausnummer}, {$this->plz} {$this->ort}";
    }
}

$adresse = new Adresse("Hauptstraße", "42", "12345", "Musterstadt");
echo $adresse; // Ausgabe: Hauptstraße 42, 12345 Musterstadt

15.6.2 __invoke()

Ermöglicht es, ein Objekt wie eine Funktion aufzurufen:

class Kalkulator {
    private float $ergebnis = 0;
    
    public function __invoke(float $a, float $b, string $operation = '+'): float {
        switch ($operation) {
            case '+':
                $this->ergebnis = $a + $b;
                break;
            case '-':
                $this->ergebnis = $a - $b;
                break;
            case '*':
                $this->ergebnis = $a * $b;
                break;
            case '/':
                if ($b === 0.0) {
                    throw new Exception("Division durch Null");
                }
                $this->ergebnis = $a / $b;
                break;
            default:
                throw new Exception("Unbekannte Operation: {$operation}");
        }
        
        return $this->ergebnis;
    }
    
    public function getErgebnis(): float {
        return $this->ergebnis;
    }
}

$rechner = new Kalkulator();
echo $rechner(5, 3, '+'); // Ausgabe: 8
echo $rechner(10, 2, '/'); // Ausgabe: 5

Die __invoke()-Methode ist besonders nützlich für Callable-Objekte, die als Callbacks verwendet werden können:

// Sortieren eines Arrays mit einem Callable-Objekt
class Sortierer {
    private string $feld;
    private string $richtung;
    
    public function __construct(string $feld, string $richtung = 'ASC') {
        $this->feld = $feld;
        $this->richtung = strtoupper($richtung);
    }
    
    public function __invoke(array $a, array $b): int {
        $vergleich = $a[$this->feld] <=> $b[$this->feld];
        return $this->richtung === 'ASC' ? $vergleich : -$vergleich;
    }
}

$daten = [
    ['id' => 3, 'name' => 'Charlie'],
    ['id' => 1, 'name' => 'Alice'],
    ['id' => 2, 'name' => 'Bob']
];

// Nach Namen aufsteigend sortieren
usort($daten, new Sortierer('name'));
print_r($daten);

// Nach ID absteigend sortieren
usort($daten, new Sortierer('id', 'DESC'));
print_r($daten);

15.6.3 __clone()

Wird aufgerufen, nachdem ein Objekt geklont wurde. Dies ist nützlich, um tiefe Kopien von Objekten zu erstellen:

class Post {
    public string $titel;
    public object $autor;
    public array $kommentare;
    
    public function __construct(string $titel, object $autor) {
        $this->titel = $titel;
        $this->autor = $autor;
        $this->kommentare = [];
    }
    
    public function __clone() {
        // Erstellt eine tiefe Kopie des Autor-Objekts
        $this->autor = clone $this->autor;
        
        // Erstellt neue Kopien der Kommentare
        $alteKommentare = $this->kommentare;
        $this->kommentare = [];
        
        foreach ($alteKommentare as $kommentar) {
            $this->kommentare[] = clone $kommentar;
        }
    }
}

class Autor {
    public string $name;
    
    public function __construct(string $name) {
        $this->name = $name;
    }
}

class Kommentar {
    public string $text;
    
    public function __construct(string $text) {
        $this->text = $text;
    }
}

$autor = new Autor("Max Mustermann");
$post = new Post("Magische Methoden in PHP", $autor);
$post->kommentare[] = new Kommentar("Toller Artikel!");

// Klonen erstellt eine tiefe Kopie
$kopie = clone $post;
$kopie->autor->name = "Erika Musterfrau"; // Ändert nur den Namen in der Kopie
$kopie->kommentare[0]->text = "Interessant!"; // Ändert nur den Kommentar in der Kopie

echo $post->autor->name; // Ausgabe: Max Mustermann
echo $kopie->autor->name; // Ausgabe: Erika Musterfrau
echo $post->kommentare[0]->text; // Ausgabe: Toller Artikel!
echo $kopie->kommentare[0]->text; // Ausgabe: Interessant!

15.6.4 __debugInfo()

Die __debugInfo()-Methode wird von var_dump() aufgerufen und gibt an, welche Informationen angezeigt werden sollen:

class APIClient {
    private string $apiKey;
    private string $baseUrl;
    private array $letzteAnfrage;
    private array $letzteAntwort;
    
    public function __construct(string $apiKey, string $baseUrl) {
        $this->apiKey = $apiKey;
        $this->baseUrl = $baseUrl;
        $this->letzteAnfrage = [];
        $this->letzteAntwort = [];
    }
    
    public function __debugInfo(): array {
        return [
            'baseUrl' => $this->baseUrl,
            'apiKey' => '***versteckt***', // Versteckt den API-Schlüssel
            'letzteAnfrage' => $this->letzteAnfrage,
            'letzteAntwort' => $this->letzteAntwort
        ];
    }
}

$client = new APIClient("geheimer-api-schlüssel", "https://api.beispiel.de");
var_dump($client); // Zeigt angepasste Debug-Informationen an, ohne den API-Schlüssel preiszugeben

15.6.5 __set_state()

Die statische Methode __set_state() wird von var_export() verwendet:

class Punkt {
    public float $x;
    public float $y;
    
    public function __construct(float $x, float $y) {
        $this->x = $x;
        $this->y = $y;
    }
    
    public static function __set_state(array $properties): self {
        return new self($properties['x'], $properties['y']);
    }
}

$punkt = new Punkt(1.5, 2.5);
$exportiert = var_export($punkt, true);
echo $exportiert . PHP_EOL;

// Ausgabe: Punkt::__set_state(array('x' => 1.5, 'y' => 2.5))

// Der exportierte Code kann evaluiert werden, um das Objekt zu rekonstruieren
$neuerPunkt = eval("return " . $exportiert . ";");
echo "X: {$neuerPunkt->x}, Y: {$neuerPunkt->y}"; // Ausgabe: X: 1.5, Y: 2.5

15.7 Praxisbeispiel: Ein aktives Datensatzmodell (Active Record)

Im folgenden Beispiel werden mehrere magische Methoden verwendet, um ein einfaches aktives Datensatzmodell zu implementieren:

class Model {
    protected array $attribute = [];
    protected array $geändert = [];
    protected bool $neu = true;
    
    // Konstruktor zum Laden von Datensätzen aus der Datenbank
    public function __construct(array $daten = []) {
        foreach ($daten as $key => $value) {
            $this->attribute[$key] = $value;
        }
        
        if (isset($this->attribute['id'])) {
            $this->neu = false;
        }
    }
    
    // Eigenschaftszugriff über magische Methoden
    public function __get(string $name) {
        return $this->attribute[$name] ?? null;
    }
    
    public function __set(string $name, $wert) {
        $this->attribute[$name] = $wert;
        $this->geändert[$name] = true;
    }
    
    public function __isset(string $name): bool {
        return isset($this->attribute[$name]);
    }
    
    public function __unset(string $name): void {
        unset($this->attribute[$name]);
        $this->geändert[$name] = true;
    }
    
    // Methoden zum Speichern und Laden
    public function speichern(): bool {
        if ($this->neu) {
            // INSERT-Operation simulieren
            echo "INSERT INTO " . static::getTabelle() . " (" 
                . implode(", ", array_keys($this->attribute)) 
                . ") VALUES ('" 
                . implode("', '", array_values($this->attribute)) 
                . "')" . PHP_EOL;
                
            // Simuliert das Setzen einer ID
            $this->attribute['id'] = rand(1, 1000);
            $this->neu = false;
        } else {
            // UPDATE-Operation simulieren
            $updates = [];
            foreach ($this->geändert as $key => $value) {
                if (isset($this->attribute[$key])) {
                    $updates[] = "{$key} = '{$this->attribute[$key]}'";
                }
            }
            
            if (!empty($updates)) {
                echo "UPDATE " . static::getTabelle() . " SET " 
                    . implode(", ", $updates) 
                    . " WHERE id = {$this->attribute['id']}" . PHP_EOL;
            }
        }
        
        $this->geändert = [];
        return true;
    }
    
    public static function finden(int $id): ?self {
        echo "SELECT * FROM " . static::getTabelle() . " WHERE id = {$id}" . PHP_EOL;
        // Simuliert einen Datensatz aus der Datenbank
        return new static([
            'id' => $id,
            'name' => "Datensatz {$id}",
            'erstellt' => date('Y-m-d H:i:s')
        ]);
    }
    
    // Dynamische Finder-Methoden
    public static function __callStatic(string $name, array $argumente) {
        if (strpos($name, 'findeVon') === 0) {
            $feld = lcfirst(substr($name, 8)); // Entfernt "findeVon" vom Methodennamen
            $wert = $argumente[0] ?? null;
            
            echo "SELECT * FROM " . static::getTabelle() . " WHERE {$feld} = '{$wert}'" . PHP_EOL;
            // Simuliert einen Datensatz aus der Datenbank
            return new static([
                'id' => rand(1, 1000),
                $feld => $wert,
                'erstellt' => date('Y-m-d H:i:s')
            ]);
        }
        
        throw new Exception("Die statische Methode {$name} existiert nicht");
    }
    
    // Methode zum Konvertieren in einen String
    public function __toString(): string {
        $klasse = get_class($this);
        $id = $this->attribute['id'] ?? 'neu';
        return "{$klasse} #{$id}";
    }
    
    // Debug-Informationen
    public function __debugInfo(): array {
        return [
            'id' => $this->attribute['id'] ?? null,
            'attribute' => $this->attribute,
            'geändert' => array_keys($this->geändert),
            'neu' => $this->neu
        ];
    }
    
    // Tabellennamen ableiten (kann in Unterklassen überschrieben werden)
    protected static function getTabelle(): string {
        $klassenName = static::class;
        $teile = explode("\\", $klassenName);
        return strtolower(end($teile)) . 's';
    }
}

// Beispiel für eine konkrete Modellklasse
class Benutzer extends Model {
    protected static function getTabelle(): string {
        return 'benutzer';
    }
}

// Verwendung des Modells
$benutzer = new Benutzer(['name' => 'Max Mustermann', 'email' => 'max@beispiel.de']);
$benutzer->speichern(); // INSERT

$benutzer->email = 'neuemail@beispiel.de';
$benutzer->speichern(); // UPDATE

$gefundenerBenutzer = Benutzer::finden(42); // SELECT mit WHERE id = 42
$emailBenutzer = Benutzer::findeVonEmail('test@beispiel.de'); // SELECT mit WHERE email = '...'

echo $gefundenerBenutzer; // Benutzer #42
var_dump($emailBenutzer); // Debug-Informationen dank __debugInfo()

Dieses Beispiel demonstriert, wie magische Methoden zusammen verwendet werden können, um eine elegante API für den Datenbankzugriff zu schaffen. Es verwendet: