PHP Autoloader-Bibliothek

1676
ircmaxell

Im Grunde hatte ich diese Klasse vor einiger Zeit geschrieben, um das automatische Laden unserer lokalen Bibliotheken zu erleichtern.

Die Voraussetzung ist, dass alles nach Paketen in mehrere Ebenen von Unterpaketen aufgeteilt wird. Klassen werden mit CamelCasing benannt. So wird eine Klasse Namen seines Paket im Zusammenhang wie folgt: PackageSubpackageSubpackageName. Jetzt kann jedes Paket über paketspezifische Schnittstellen verfügen, die isPackageNamefür Schnittstellen, Ausnahmen von PackageNameExceptionusw. definiert sind. Ich habe versucht, es für die Wiederverwendung flexibel genug zu machen.

/**
 * A class for lazy-loading other classes
 *
 * This class enables lazy-loading of php classes.  The benefit of this is
 * three-fold.  First, there is a memory benefit, since not all classes are
 * loaded until they are needed.  Second, there is a time benefit, since not all
 * classes are loaded.  Third, it produces cleaner code, since there is no need
     * to litter files with require_once() calls.
 *
 * @category Libraries
 * @package  Libraries
 * @author   Me
 */
abstract class Loader
{

    /**
     * @var array An array of class to path mappings
     */
    protected static $classes = array();

    /**
     * @var boolean Has the loader been initialized already
     */
    protected static $initialized = false;

    /**
     * @var array An array of auto-search paths
     */
    protected static $namedPaths = array(
        'exception',
        'interface',
        'iterator',
    );

    /**
     * @var array An array of include paths to search
     */
    protected static $paths = array(
        PATH_LIBS,
    );

    /**
     * Tell the auto-loader where to find an un-loaded class
     *
     * This can be used to "register" new classes that are unknown to the
     * system.  It can also be used to "overload" a class (redefine it
     * elsewhere)
     *
     * @param string $class The class name to overload
     * @param string $path  The path to the new class
     *
     * @throws InvalidArgumentException Upon an Invalid path submission
     * @return void
     */
    public static function _($class, $path)
    {
        $class = strtolower($class);
        if (!file_exists($path)) {
            throw new InvalidArgumentException('Invalid Path Specified');
        }
        self::$classes[$class] = $path;
    }

    /**
     * Add a path to the include path list
     *
     * This adds a path to the list of paths to search for an included file.
     * This should not be used to overload classes, since the default include
     * directory will always be searched first.  This can be used to extend
     * the search path to include new parts of the system
     *
     * @param string $path The path to add to the search list
     *
     * @throws InvalidArgumentException when an invalid path is specified
     * @return void
     */
    public static function addPath($path)
    {
        if (!is_dir($path)) {
            throw new InvalidArgumentException('Invalid Include Path Added');
        }
        $path = rtrim($path, DS);
        if (!in_array($path, self::$paths)) {
                self::$paths[] = $path;
        }
    }

    /**
     * Add a path to the auto-search paths (for trailing extensions)
     *
     * The path should end with an 's'.  Default files should not.
     *
     * @param string $path The name of the new auto-search path
     *
     * @return void
     */
    public static function addNamedPath($path)
    {
        $path = strtolower($path);
        if (substr($path, -1) == 's') {
            $path = substr($path, 0, -1);
        }
        if (!in_array($path, self::$namedPaths)) {
            self::$namedPaths[] = $path;
        }
    }

    /**
     * Initialize and register the autoloader.
     *
     * This method will setup the autoloader.  This should only be called once.
     *
     * @return void
     */
    public static function initialize()
    {
        if (!self::$initialized) {
            self::$initialized = true;
            spl_autoload_register(array('Loader', 'load'));
        }
    }

    /**
     * The actual auto-loading function.
     *
     * This is automatically called by PHP whenever a class name is used that
     * doesn't exist yet.  There should be no need to manually call this method.
     *
     * @param string $class The class name to load
     *
     * @return void
     */
    public static function load($class)
    {
        $className = strtolower($class);
        if (isset(self::$classes[$className])) {
            $file = self::$classes[$className];
        } else {
            $file = self::findFile($class);
        }
        if (file_exists($file)) {
            include_once $file;
        }
    }

    /**
     * Find the file to include based upon its name
     *    
     * This splits the class name by uppercase letter, and then rejoins them
     * to attain the file system path.  So FooBarBaz will be turned into
     * foo/bar/baz.  It then searches the include paths for that chain.  If baz
     * is a directory, it searches that directory for a file called baz.php.
     * Otherwise, it looks for baz.php under the bar directory.
     *
     * @param string $class The name of the class to find
     *
     * @return string The path to the file defining that class
     */
    protected static function findFile($class)
    {
        $regex = '#([A-Z]{1}[a-z0-9_]+)#';
        $options = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE;
        $parts = preg_split($regex, $class, null, $options);

        $subpath = '';
        $file = strtolower(end($parts));
        $test = strtolower(reset($parts));
        if ($test == 'is') {
            array_shift($parts);
            return self::findNamedFile($class, $parts, 'interface');
        }
        foreach ($parts as $part) {
            $subpath .= DS . strtolower($part);
        }    
        foreach (self::$paths as $path) {
            $newpath = $path . $subpath;
            if (is_file($newpath . '.php')) {
                return $newpath . '.php';
            } elseif (is_file($newpath . DS . $file . '.php')) {
                return $newpath . DS . $file . '.php';
            }
        }
        if (in_array($file, self::$namedPaths)) {
            //Get rid of the trailing part
            array_pop($parts);
            return self::findNamedFile($class, $parts, $file);
        }    
        return '';
    }

    /**
     * Find a file for named directories (interfaces, exceptions, iterators, etc)
     *
     * @param string $class The class name of the exception to find
     * @param array  $parts The parts of the class name pre-split
     * @param string $name  The name of the named directory to search in
     *    
     * @return string The found path, or '' if not found
     */
    protected static function findNamedFile($class, array $parts, $name)
    {    
        if (empty($parts)) {
            return '';
        }
        $name = strtolower($name);
        //Add a trailing s, since individual files are not plural
        $filename = $name;
        $name .= 's';
        //Try the global path first
        $subpath = DS . $name . DS . strtolower(implode('', $parts)) . '.php';
        foreach (self::$paths as $path) {
            $newpath = $path . $subpath;
            if (is_file($newpath)) {
                return $newpath;
            }
        }
        //Try to build a full sub path for package specific named files
        $package = array_shift($parts);
        $subpath = DS . strtolower($package) . DS . $name . DS;
        if (!empty($parts)) {
            $subpath .= strtolower(implode('', $parts)) . '.php';
        } else {
            $subpath .= $filename . '.php';
        }
        foreach (self::$paths as $path) {
            $newpath = $path . $subpath;
            if (is_file($newpath)) {
                return $newpath;
            }
        }
        return '';
    }
}

Es ist auch vollständig getestet.

Was sind deine Gedanken? Ist es überkomplex?

Antworten
24

4 Antworten auf die Frage

14
Wade Tandy

Das erste Problem, das ich sehe, ist, dass es viele Fälle gibt, in denen jemand eine Klasse mit mehr als einem Wort im Namen (DataMapper) erstellen möchte, und der von Ihnen bereitgestellte Autoloader dies nicht zulässt. Ich würde empfehlen, ein anderes Zeichen zu verwenden, um die Paketnamen zu begrenzen. Das Zend Framework verwendet Package_SubPackage_SubPackage_Class und das funktioniert sehr gut.

Abgesehen davon bin ich nicht sicher, was Ihre spezifischen Gründe für das Schreiben Ihres eigenen Autoloaders sind (ob für Produktion, Schulung usw.), aber wenn Sie planen, ihn für die Produktion zu verwenden, würde ich die Zend_Loader-Klasse von empfehlen Das Zend Framework, wie es unterstützt, vollständig getestet und ständig weiterentwickelt wird. Sie können die Kurzanleitung hier lesen

Nun, es ist persönliche Präferenz, aber ich kann es nicht ertragen, _ in Klassennamen zu verwenden. Ich finde es schwieriger zu tippen und zerbricht es zu sehr. Bei mehreren Wörtern in einem Namen ist dies ein gültiger Punkt. Und warum benutze ich Zend nicht? Nun, es ist persönliche Präferenz. Ich habe es in der Vergangenheit ausprobiert und habe mich nicht darum gekümmert. Ich kann es verwenden, aber es reibt mich nur in die falsche Richtung ... Danke für die Einsicht, obwohl ... ircmaxell vor 10 Jahren 4
Um zu klären, als ich über Zend sprach, habe ich nicht das gesamte Framework empfohlen, sondern nur die Autoloader-Klasse, die völlig entkoppelt von allem anderen in zf verwendet werden kann. Wade Tandy vor 10 Jahren 2
`HiMyNameIsRobertAndIWorkLongHoursSadFace` vs.` Hi_My_Name_Is_Robert_And_I_Work_Long_Hours_Sad_Face`, ich glaube, das ist der Grund für `_` RobertPitt vor 10 Jahren 2
@RobertPitt: Ich würde argumentieren, dass wenn Ihr Klassenname wirklich so lang ist, Sie Ihr Verpackungssystem umgestalten oder überdenken müssen ... ircmaxell vor 10 Jahren 0
8
edorian

Einige Punkte, die ich gefunden habe:

Die Klasse ist abstractals seltsam markiert, da ich herausfand, dass sie nur statische Methodenaufrufe hat, und da sie "self ::" für statische Aufrufe verwendet, denke ich, gibt es keine sinnvolle Möglichkeit, die Klasse irgendwie zu erweitern. (Mit der LSB-Ausgabe).

Ich sehe kein großes Problem mit der Klasse "alle statisch" und ich gehe davon aus, dass sie in Ihr Projekt passt. (Sie haben keinen eindeutigen Bootstrap und möchten / benötigen nicht mehrere Instanzen dieser Klasse.)


Das include_once $file;ist mir ein bisschen seltsam, da der "_once" -Teil nicht nötig sein sollte. Wenn Sie den Loader jedoch zu einem späteren Zeitpunkt im Projekt geschrieben haben, können Sie feststellen, wo möglicherweise Probleme auftreten, wenn Klassen zweimal geladen werden.

Normalerweise würde ich sagen, dass Sie PHP nicht dazu zwingen müssen, sich zu erinnern, ob es bereits eine Datei berührt hat (und einen teuren Datenträger ()), da die Ladefunktion nur einmal für jede zuvor unbekannte Klasse aufgerufen wird.


Alles in allem finde ich das Verhältnis von Code und Nutzen gut und es ist nicht zu komplex.

Alternativen

Die anstehenden "Standards" und libs werden bereits erwähnt, daher weise ich auf eine andere Art des automatischen Ladens hin, die "besser ist" und weniger aufdringlich ist (weniger Code in Ihrer Anwendung erforderlich).

Sie PHP Autoload Builderscannen Ihre Codebasis und stellen eine Datei mit einer großen Array-Zuordnung für alle Klassen (Schnittstellen usw.) bereit, die Sie nur in Ihren Bootstrap einbeziehen müssen. Sie kann erneut ausgeführt werden, um neue Klassen aufzunehmen, oder die resultierende Datei kann von Hand bearbeitet werden. (Einige Leute bauen Werkzeuge um sie herum, sodass sie sich automatisch in der Entwicklung neu erstellen, wenn eine Klasse nicht gefunden wird).

5
RobertPitt

Wenn es um Autoloader geht, neige ich dazu, sie mit jedem meiner Projekte kompatibel zu machen. Daher würde ich immer die besten Codierstandards wie Zend einhalten.

Es gibt einen Vorschlag, der das Layout von Klassen, Verzeichnisstruktur und Namespaces angibt, in denen Autoloader sehr gut funktionieren.

Im Folgenden werden die zwingenden Anforderungen beschrieben, die für die Interoperabilität des Autoloaders einzuhalten sind.

  • Verpflichtend:

    • Ein vollständig qualifizierter Namespace und eine Klasse muss die folgende Struktur haben:

      \<Vendor Name>\(<Namespace>\)*<Class Name>
      
    • Jeder Namespace muss einen Namespace der obersten Ebene ("Vendor Name") haben.
    • Jeder Namespace kann beliebig viele Sub-Namespaces haben.
    • Jedes Namespace-Trennzeichen wird DIRECTORY_SEPARATORbeim Laden aus dem Dateisystem in ein a konvertiert .
    • Jedes " " Zeichen im CLASS NAME wird in einDIRECTORY_SEPARATOR " " umgewandelt . Das Zeichen " " hat keine besondere Bedeutung im Namespace.
    • Der vollständig qualifizierte Namespace und die Klasse werden beim Laden aus dem Dateisystem mit ".php" ergänzt.
    • Alphabetische Zeichen in Anbieternamen, Namespaces und Klassennamen können aus einer beliebigen Kombination von Klein- und Großbuchstaben bestehen.
  • Beispiele:

  • \Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
  • \Symfony\Core\Request => /path/to/project/lib/vendor/Symfony/Core/Request.php
  • \Zend\Acl => /path/to/project/lib/vendor/Zend/Acl.php
  • \Zend\Mail\Message => /path/to/project/lib/vendor/Zend/Mail/Message.php

Wenn Sie den Autoloader mit dem oben genannten Verfahren erstellen, können Sie sicher um Ihre Projekte herumbewegen, je nachdem, ob Sie Namespaces haben oder nicht.

@Referenz

Es gibt eine sehr schöne Klasse, die ich in rund 6 Projekten verwende, und ich finde, dass dies perfekt ist, und Sie sollten lernen und sehen, was Sie damit machen können.

Klassenverbindung

Eine Beispielanwendung wäre wie folgt:

$classLoader = new SplClassLoader('Doctrine\Common', '/libs/doctrine');
$classLoader->register();

$classLoader = new SplClassLoader('Ircmexell\MyApplication', 'libs/internal');
$classLoader->register();
2
Jamal

Ich habe ungefähr ein Jahr lang einen sehr einfachen Autoloader mit gemeinsamen PHP-Dateien root für mein Projekt und alle enthaltenen Bibliotheken (Zend, Rediska usw.) verwendet.

Das Stammverzeichnis meines Projekts enthält die Verzeichnisse / app und / external.

Alle Bibliotheken in / external sind vollständig aus svn / git ausgecheckt, und dann mache ich einen Symlink für ihren PHP-Code in / app.

Zum Beispiel für PHPExcel:

pwd 
/var/www/project/app
ls -lah PHPE*
PHPExcel -> ../external/PHPExcel/PHPExcel
PHPExcel.php -> ../external/PHPExcel/PHPExcel.php

Dann füge ich so etwas in meine index.php-Datei ein:

set_include_path (PATH . 'app');
require 'somepath/Autoloader.php';
Autoloader::registerAutoload ();

Es erlaubt mir, eine häufig zu verwenden (es ist voll kompatibel mit Zend, Rediska, PHPExcel und vielen anderen Bibliotheken) und ist ein sehr einfacher Autoloader für alle Bibliotheken.

class Autoloader
{

    public static function registerAutoload ()
    {
        spl_autoload_register (array (__CLASS__, 'autoload'));
    }

    /**
     * @static
     * @param string $class
     * @return void
     */
    public static function autoload ($class)
    {
        require str_replace (array ('_', '\\'), '/', $class) . '.php';
    }

}