Kristall Tutorial – Teil 2: Autoloading

Nachdem der erste Teil eher Grundlagen behandelt hat und tendenziell ziemlich unspektakulär war, machen wir heute etwas mehr Tempo mit etwas mehr Code der auch tasächlich etwas tut. 🙂

Autoload

Der nächste Punkt auf der Liste ist das bedarfsmäßige nachladen von Klassen-Dateien, oder auch autoload genannt. Für bestmögliche flexibilität legen wir für diesen Zweck eine weitere Klasse an.

lib/Kristall/kcAutoload.php
class kcAutoload
{

    /**
     * Stores the classmap
     * @var array
     */
    protected $classmap    = array();

    /**
     * Stores the root directories to look for files
     * @var array
     */
    protected $directories = array();

    public function __construct($directories)
    {
        $this->directories = $directories;
    }

    /**
     * Creates the classmap if neccessary and returns it.
     *
     * @return array
     */
    protected function getClassMap()
    {
        // return the map if we have one
        if (!empty($this->classmap)){
                return $this->classmap;
        }

        // no map - build the map and return it
        foreach ($this->directories as $_dir){
            $it = new RecursiveDirectoryIterator(realpath($_dir));
            foreach (new RecursiveIteratorIterator($it) as $_key => $_value){
                if ($_value->isFile()){
                        $this->classmap[$_value->getFilename()] = realpath($_key);
                }
            }
        }
        return $this->classmap;
    }

    /**
     * Autoloader method to register with spl_autoload_register.
     * @param string $name
     * @internal
     */
    public function autoload($name)
    {
        $map = $this->getClassMap();

        if (isset($map[$name . '.interface.php'])){
                include_once($map[$name . '.interface.php']);
                return true;
        }

        if (isset($map[$name . '.class.php'])){
                include_once ($map[$name . '.class.php']);
                return true;
        }

        if(isset($map[$name.'.php'])){
            include_once($map[$name.'.php']);
            return true;
        }
    }
}

Dieser Autoloader erwartet im Konstruktor ein array mit Verzeichnisssen in denen er nach möglichen Kandidaten zum nachladen suchen soll. Beim erstmaligen Aufruf von autoload() werden diese Verzeichnisse rekursiv durchlaufen und daraus ein einfaches assoziatives Array mit dem Dateinamen als Schlüssel und dem vollständigen Pfad als Wert erzeugt und anschließend im Speicher vorgehalten.

Hierdurch können wir unterhalb der übergebenen Startverzeichnisse beliebig viele Dateien und Ordner anlegen ohne dass wir jedes mal die Klasse umschreiben müssen.

Autoload cache

Nachteilig bei dem jetzigen autoloader ist natürlich der Rekursive aufbau der Dateiliste, die im gegenwärtigen Zeitpunkt bei jedem Seitenaufruf ein mal gemacht wird. Während der Entwicklung macht das zwar sinn, jedoch ändern sich die Dateien und Verzeichnisse im Produktiveinsatz eher selten. Daher würde es sich anbieten, einen caching-Mechanismus einzubauen.

lib/Kristall/kcAutoload.php
    /**
     * Stores the cacheDir
     * @var string
     */
    protected $cacheDir     = '';
    /**
     *
     * @var bool
     */
    protected $useCache     = false;

    public function __construct($directories, $cacheDir = null, $useCache = false)
    {
        $this->directories = $directories;
        $this->cacheDir    = realpath($cacheDir);
        $this->useCache    = $useCache;
        if (true == $this->useCache) {
            if (empty($this->cacheDir)) {
                throw new InvalidArgumentException(sprintf(
                                'Invalid argument $cacheDir. When caching is enabled, $cacheDir may not be empty.'));
            }
            if (!file_exists($this->cacheDir)) {
                throw new InvalidArgumentException(sprintf(
                                'Invalid argument $cacheDir. "%s" is not a directory',$this->cacheDir));
            }
            if (!is_writable($this->cacheDir)) {
                throw new InvalidArgumentException(sprintf(
                                'Invalid argument $cacheDir. Directory "%s" is write protected.',$this->cacheDir));
            }
        }
    }

Der Konstruktor ist nun etwas umfangreicher geworden, da wir nun prüfen ob das caching aktiviert ist und in dem Fall eine gültigen Pfad zu einem beschreibbaren Vezeichnis erwarten.
Im Sinne der fail early Philosophie, schmeißen wir also auch sofort eine Exception und nicht erst dann wenn’s ernst wird.

lib/Kristall/kcAutoload.php
    /**
     * Tries to write the classmap to the cache directory if it is writable and the useCache property is set to true.
     */
    public function __destruct()
    {
        if ($this->useCache) {
            $file = $this->getClassMapCacheFile();
            file_put_contents($file,"classmap,true) . '; ?>');
        }
    }

    /**
     * Returns the filename for the classmap cache file
     * @return string
     */
    protected function getClassMapCacheFile()
    {
        return $this->cacheDir.DS.'classmap.php';
    }

Die Magic-Method __desctruct() wird eher selten eingesetzt, aber hier eignet sie sich hervorragend um unsere Cache-Datei anzulegen.
Ok, jetzt schreiben wir unsere classmap in eine Datei, wenn wir das caching aktivieren. Um mit der Datei auch tatsächlich zu arbeiten müssen wir die getClassMap() Methode noch etwas erweitern.

lib/Kristall/kcAutoload.php
    protected function getClassMap()
    {
        // return the map if we have one
        if (!empty($this->classmap)) {
            return $this->classmap;
        }
        // if we have no map, look for the cache file and return this if possible
        if ($this->useCache && file_exists($this->getClassMapCacheFile())) {
            $this->classmap = include($this->getClassMapCacheFile());
            return $this->classmap;
        }
        // no map - build the map and return it
        foreach ($this->directories as $_dir) {
            $it = new RecursiveDirectoryIterator(realpath($_dir));
            foreach (new RecursiveIteratorIterator($it) as $_key => $_value) {
                if ($_value->isFile()) {
                    $this->classmap[$_value->getFilename()] = realpath($_key);
                }
            }
        }
        return $this->classmap;
    }

Hervorragend, jetzt haben wir einen schicken autoloader mit optionalem caching und eine Application Klasse die unsere Konfiguration bereithält. Bringen wir die beiden zusammen.

web/index.php
ob_start();    

// ...

define('KRISTALL', realpath(LIB_DIR.DS.'Kristall'));

include(KRISTALL.DS.'kcAutoload.php');  // include the autoloader
include(KRISTALL.DS.'Kristall.php');    // load the application class

ob_end_flush();

Die Autoloader und Application Klassendateien sind die einzigen Beiden die wir von Hand per include laden müssen. Der rest wird dann automatisch bei Bedarf passieren.

lib/Kristall/Kristall.php
    /**
     * Creates and configures a new instance of Kristall and registers the autoloader
     *
     * @param array $configuration
     */
    protected function  __construct($configuration)
    {
        $this->configuration = $configuration;
        $autoloadClass       = $this->getSetting('app.autoload.class', 'kcAutoload');
        $autoloadDirectories = array_merge(array(LIB_DIR, APP_DIR),
                                           $this->getSetting('app.autoload.directories', array()));

        $autoloader = new $autoloadClass($autoloadDirectories,
                                         $this->getSetting('app.autoload.cacheDir'),
                                         $this->getSetting('app.autoload.useCache', false));

        spl_autoload_register(array($autoloader, 'autoload'));
    }

Hier passiert jetzt schon ein klein wenig mehr. Insbesondere kommt hier nun erstmals die getSetting() methode zum Einsatz sowie unsere Verzeichnis Konstanten aus der index.php.

Über getSetting(‚app.autoload.class‘, ‚kcAutolad‘) holen wir uns den Klassennamen des Autoloaders, der verwendet werden soll. Wenn in der Konfiguration keiner eingetragen ist, nehmen wir den Standard.

Die Einstiegsverzeichnisse bauen wir zusammen aus unseren Verzeichniskonstanten und allen in der Konfiguration unter app.autoload.directories eingetragenen weiteren Verzeichnissen.

Anschließend wird der autoloader initialisiert und mittels spl_autoload_register() auf den callback stapel gelegt.

Die Config Datei

Da sich die Konfiguration auf die spezifische Anwendung bezieht, kommt sie auch ins app Verzeichnis: app/config/config.php. Die Vor- und Nachteile der verschiedenen Möglichkeiten, eine Konfigurationsdatei zu schreiben sei mal dahingestellt. Man könnte hier auch mit einer XML, YAML oder INI Datei arbeiten. Ich habe mich für diesen Fall mit einem nativen php array begnügt.

app/config/config.php
return array(
    'app' => array(
        'autoload' => array(
            'cacheDir' => '',
            'useCache' => false,
        )
    )
);

Ist jetzt noch nicht wirklich umfangreich, und genau genommen würde zum jetzigen Zeitpunkt auch ein return array(); ausreichen. Die jetzige Einstellung dient eher zur Veranschaulichung wie getSetting() arbeitet.

Damit die Konfiguration nun auch tatsächlich an die Applikation übergeben wird, müssen wir nochmal die bootstrap datei anfassen.

web/index.php
// ...

$config = include(APP_DIR.DS.'config'.DS.'config.php');

include(KRISTALL.DS.'kcAutoload.php');
include(KRISTALL.DS.'Kristall.php'); 

Kristall::application($config)->run();

// ...

Damit wäre Teil 2 abgeschlossen und wir sehen uns beim nächsten Teil den Dispatcher mal genauer an.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.