PHP Autoloader für das Sessionproblem

Der PHP Autoloader dient nicht allein der Bequemlichkeit. Es gibt beim Deserialisieren das Problem mit unvollständigen Klassen. Dies trifft insbesondere dann auf, wenn eine Klassendefinition während dem Deserialisieren der Session fehlt. Wenn die Klassendefinition fehlt erstellt PHP ein Objekt der Klasse __PHP_Incomplete_Class. Damit fehlen diesem entarteten Objekt sämtliche Methoden. Um diesen Programmierfehler zu vermeiden muss vor Sesssionbeginn die Klassendefinition bekannt sein.

Die folgenden Beispiele wollen das Problem konkret mit Code belegen und Möglichkeiten zeigen diesen Fehler zu vermeiden. Dazu werden wir ein Objekt der Klasse MyClass, welche in der Datei MyClass.php definiert ist, aus der Session auslesen.

<?php

class MyClass
{

    private
    
/**
     * @var int
     */
    
$_counter 0;

    
/**
     * @return int
     */
    
public function increment()
    {
        return 
$this->_counter++;
    }
    
}

Die folgenden Codebeispiele werden das Gleiche machen:

  1. Es wird falls das Objekt $_SESSION['myObject'] noch nicht existiert eine neue Instanz in die Session geschrieben.
  2. Dann wird der Klassenname des in der Session gespeicherten Objekts ausgegeben.
  3. Abschließend wird die Methode MyClass::increment() von dem Objekt aufgerufen und das Ergebnis ausgegeben.

Bei der ersten Ausführung laufen alle Beispiele korrekt ab und liefern folgende Ausgabe:

MyClass 0

In dieser Ausführung haben sie lediglich ein neues Objekt in die Session geschrieben und mit diesem vollständigen Objekt weitergearbeitet.

Eine erneute Ausführung des Scripts sollte erwartungsgemäß das vorher gespeicherte Objekt laden und dann folgende Ausgabe liefern:

MyClass 1

__PHP_Incomplete_Class - So geht's nicht.

Folgender Code started eine Session (dies kann auch implizit durch session.auto_start=true geschehen). Wenn das Objekt myObject noch nicht exisiert wird seine Klassendefinition geladen und eine neue Instanz in die Session hinterlegt.

<?php

session_start
();

if (! isset(
$_SESSION['myObject'])) {
    require_once 
dirname(__FILE__) . '/MyClass.php';
    
$_SESSION['myObject'] = new MyClass();

}
echo 
get_class($_SESSION['myObject']),   "\n";
echo 
$_SESSION['myObject']->increment(), "\n";

Dieser Code führt bei seiner zweiten Ausführung mit einem Fatal error zum Abbruch:

__PHP_Incomplete_Class Fatal error: main(): The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "MyClass" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in fail.php on line 11

Da die Klasse MyClass nur definiert wird, wenn noch kein Objekt in der Session existiert, kennt PHP beim Deserialisieren die Klassendefinition noch nicht. PHP wird statt MyClass eine Instanz der Klasse __PHP_Incomplete_Class erstellen. Dieses unvollständige Objekt kennt nicht die Semantik der Methode increment().

PHP sagt in seiner Fehlermeldung bereits wie man diesen Fehler reparieren kann. Man muss dafür sorgen, dass die Klassendefinition vor dem Sessionbeginn bekannt ist. Die beiden nächsten Beispiele zeigen Möglichkeiten auf dies zu gewähren.

require 'MyClass.php' vor session_start()

Um keine unvollständige Objekte zu erhalten muss man vor session_start() dafür sorgen dass die Klassendefinition bekannt ist. In unserem Beispiel machen wir das in dem wir das require_once dirname(__FILE__) . '/MyClass.php' aus dem If-Block rausziehen und vor dem session_start() stellen.

<?php

require_once dirname(__FILE__) . '/MyClass.php';

session_start();

if (! isset(
$_SESSION['myObject'])) {
    
$_SESSION['myObject'] = new MyClass();

}
echo 
get_class($_SESSION['myObject']),   "\n";
echo 
$_SESSION['myObject']->increment(), "\n";

Ein zweiter Aufruf dieses Beispiels liefert die erwartete Ausgabe:

MyClass 1

Dieses Beispiel ist sehr einfach konstruiert. Man kann sich aber auch einen Anwendungsfall vorstellen, bei dem man selber gar nicht weiß welche Klassen gerade in der Session enthalten sind. Ein Warenkorb mit etlichen Produktklassen wäre ein solches Szenario. Der Inhalt der Session kann eine beliebige Menge aus den möglichen Produktklassen sein. Dieses Problem kann man noch lösen in dem man einfach profilaktisch die Klassendefinitionen aller Produktklassen lädt. Dabei fällt auf, dass der Speicher mit unnötigen Klassendefinitionen belegt wird und das Laden dieser unnötigen Dateien auch Zeit in Anspruch nimmt. Dieses Effizienzproblem wird das letzte Beispiel lösen.

PHP Autoloader - Klassendefinition auf Anfrage

Die Fehlermeldung aus dem ersten Beispiel hat uns bereits gesagt, dass ein Autoloader auch eine Lösung für das Problem ist. Folgendes Beispiel verzichtet auf das Laden der Klassendefinition und überlässt dies stattdessen dem PHP Autoloader.

<?php

require dirname(__FILE__) . '/autoloader/Autoloader.php';

session_start();

if (! isset(
$_SESSION['myObject'])) {
    
$_SESSION['myObject'] = new MyClass();

}
echo 
get_class($_SESSION['myObject']),   "\n";
echo 
$_SESSION['myObject']->increment(), "\n";

Auch hier haben wir wieder die erwartete Ausgabe beim zweiten Ausführen:

MyClass 1

Der Unterschied zur vorherigen Lösung ist, dass ausschließlich die Klassendefinitionen geladen werden, welche tatsächlich für die Session benötigt werden. Das macht in dieser Beispielanwendung effektiv keinen Unterschied. In beiden Lösungen wird nur die Klasse MyClass geladen. In dem angedachten Anwendungsfall mit dem Warenkorb macht diese Lösung einen erheblichen Unterschied. Die Autoloaderlösung lädt tatsächlich nur die Produktklassen welche sich in dem Warenkorb befinden.