The discussions and debates in this forum are definitely helping my code even though I have been snippy at times, for which I apologize. I recently put the finishing touches on an autoloader that handles namespaces well, though it leaves some of the structural details to the user.
The only feature I can think to add is a regular expression match to extract the class name from the file name in the event that the file name isn’t exactly equal to the class name. For example, some folks do File.class.php or even File.interface.php or even both in the same project, so a regular expression could strip the ‘.class’ out of the file name to arrive at the class name.
There are two classes here, one per code block.
Again, thank all of you for helping in my progress.
The Factory is responsible for Path assembly. It is used by the framework if there is no cache file, or if the cache file doesn’t have a Loader.
<?php
namespace Gazelle;
class LoaderFactory {
/**
* Public function flags
*/
const FILE_EXTENSION = 0;
const NAMESPACE_EXTENSION = 1;
/**
* The extensions of files we parse into our system.
* You can empty this, but all files will be parsed in if you do.
* @var array
*/
protected static $fileExtensions = array ('php');
/**
* The extensions of directories we treat as namespaces.
* You can empty this, if you do all directories will be treated as namespaces.
* @var array
*/
protected static $namespaceExtensions = array('ns');
/**
* The class of the autoloader we return. It must either be Gazelle\\Loader or a child class.
* @var unknown_type
*/
protected static $loader = "Gazelle\\\\Loader";
/**
* Get a new loader class. Make sure you've loaded the one you want, this
* factory doesn't do that for you. This function can be overloaded. Three
* types of parameters are accepted in any number
*
* Loader Object: When passed it will be unregistered from the PHP autoload service
* and its classes merged into the new path collection. The position of
* loader objects in the argument list is only significant in this manner:
* Paths that preceded the object can be overwritten by the object paths,
* those that follow will be overwrite the object paths.
* String: A string that isn't a path to a file will be set as the top namespace for all
* paths that follow in the argument list until the next string is encountered.
* File Path: Each path passed will be parsed and added to the last top namespace specified.
*
* The path construction presumes filename = class name, and the class names are the keys
* of the resulting path array. If you aren't careful then in a complicated argument list
* you can have a collision. Sometimes though this is desirable, such as when a project
* wants to map its own copy of a class list to the autoload map overwriting a framework
* default.
*
* It's a flexible system but take care, as with flexibility comes ample opportunity to
* shoot yourself in the foot.
*/
public static function getLoader() {
$collection = array();
$top = '';
foreach (func_get_args() as $path ) {
if (is_object($path) && $path instanceof Loader ) {
$collection = array_merge($collection, $path->classes);
spl_autoload_unregister( array( $path, 'load') );
} else if (is_string($path)) {
if (is_dir($path) || is_file($path)) { // Parse the path into our class hive.
if (empty($top)) {
$collection = array_merge($collection, self::parsePath( $path ));
} else {
if (isset($collection[$top])) {
$collection[$top] = array_merge( $collection[$top], self::parsePath( $path ));
} else {
$collection[$top] = self::parsePath( $path );
}
}
} else { // Any other string given to the system switches the top level namespace
$top = $path;
}
} else {
throw new \\Exception ("Illegal Path {$path}");
}
}
$loader = new self::$loader ( $collection );
spl_autoload_register(array($loader, 'load'), true);
return $loader;
}
/**
* Set class that is used to handle autoload requests.
* @param string $loader
*/
public static function setLoaderClass( $loader ) {
assert(is_string($loader) && !empty($loader) && class_exists( $loader, false) );
self::$loader = $loader;
}
/**
* Add an extension to those we use to identify php files and directory namespaces.
* @param string $type file | namespace
* @param integer $name
*/
public static function addExtension( $name, $type = self::FILE_EXTENSION ) {
assert(is_string($name));
switch ($type) {
case self::FILE_EXTENSION: self::$fileExtensions[] = $name; break;
case self::NAMESPACE_EXTENSION: self::$namespaceExtensions[] = $name; break;
}
}
/**
* Remove an extension from the list of those we use to identify php files and directory namespaces.
* @param string $name
* @param integer $type
*/
public static function removeExtension( $name, $type = self::FILE_EXTENSION ) {
assert(is_string($name));
switch ($type) {
case self::FILE_EXTENSION:
$key = array_search($name, self::$fileExtensions);
if ( $key !== false ) {
unset(self::$fileExtensions[$key]);
}
break;
case self::NAMESPACE_EXTENSION:
$key = array_search($name, self::$namespaceExtensions);
if ( $key !== false ) {
unset(self::$namespaceExtensions[$key]);
}
break;
}
}
/**
* Parse a file path into our class hive.
*
* @param string $path
*/
protected static function parsePath( $path ) {
$return = array();
if (is_file($path)) {
return self::parseFile( $path );
}
if (!is_dir($path)) {
// Since we're the loader we can't reliably use the framework version of Exception.
throw new \\Exception("Invalid Path {$path}");
}
$files = glob($path . '/*');
if ( $files && !empty( $files ) ) {
foreach ($files as $file) {
if (!is_readable($file)) {
continue;
}
if (is_dir($file)) {
// If no namespace extensions, all directories are treated as namespaces.
if (count(self::$namespaceExtensions) == 0 || in_array( pathinfo( $file, PATHINFO_EXTENSION ), self::$namespaceExtensions )) {
$return[pathinfo( $file, PATHINFO_FILENAME )] = self::parsePath( $file );
} else {
$return = array_merge($return, self::parsePath( $file ));
}
} else {
$return = array_merge( $return, self::parseFile($file) );
}
}
}
return $return;
}
protected static function parseFile( $file ) {
if (count(self::$fileExtensions) == 0 || in_array( pathinfo( $file, PATHINFO_EXTENSION ), self::$fileExtensions )) {
return array( pathinfo($file, PATHINFO_FILENAME) => $file );
} else {
return array();
}
}
}
If I pulled out the massive commenting it would be under 100 lines lastcraft
The Loader itself is 65 lines even with comments.
<?php
namespace Gazelle;
/**
* Gazelle Autoload System.
* @author Michael
*/
class Loader {
/**
* Hive of class paths organized by namespace.
* @var array
*/
protected $classes = array();
/**
* CONSTRUCT.
*/
public function __construct( array $paths = array() ) {
$this->classes = $paths;
}
/**
* When unserialized we relink with the PHP autoload service.
*/
public function __wakeup() {
spl_autoload_register(array($this, 'load'), true);
}
/**
* Class hive is externally readable, but not writable.
* @param string $var
*/
public function __get( $var ) {
if ($var == 'classes') {
return $this->classes;
} else {
throw new FatalException("No access to var {$var}");
}
}
/**
* Responder to the PHP Autoload service.
* @param $path
*/
public function load( $path ) {
$haystack = $this->classes;
$needles = explode("\\\\", $path);
do {
$needle = array_shift($needles);
if (!isset($haystack[$needle])) {
return false;
} else if (is_array($haystack[$needle])) {
$haystack = $haystack[$needle];
continue;
} else if (file_exists($haystack[$needle])) {
require_once ( $haystack[$needle] );
return true;
}
} while (count($needles) > 0);
return false;
}
}
EDIT: Some use cases.
// Most basic case, point to a class directory and recurse down slurping up all php files.
$loader = LoaderFactory::getLoader( $testDir.'/classes' );
// Appending files to a loader involves passing the old loader back into the factory.
$loader = LoaderFactory::getLoader( $loader, $testDir.'/classes' );
// Or if we like the paths we have and want to make sure we keep them.
$loader = LoaderFactory::getLoader( $testDir.'/classes', $loader );
// Now up to this point we haven't dealt with namespaces. Say all of the files in the
// classes directory go to myNamespace..
$loader = LoaderFactory::getLoader( 'MyNamespace', $testDir.'/classes' );
// One limitation is we can't do subnamespaces off the bat. The Factory assumption
// is that directories that form namespaces are suffixed with ".ns". But if we
// want all directories to be treated as namespaces.
LoaderFactory::removeExtension( 'ns', LoaderFactory::NAMESPACE_EXTENSION );
// The very first directory is still the 'MyNamespace' root here.
$loader = LoaderFactory::getLoader( 'MyNamespace', $testDir.'/classes' );
// If we want we can load up two namespaces in one go.
$loader = LoaderFactory::getLoader( 'MyNamespace', $testDir.'/classes', 'OtherNamespace', $testDir.'/moreClasses' );
Questions, comments, concerns and other feedback welcomed, and I’ll try to behave myself. My only lingering doubt is that the factory links up the loader to PHP’s autoload system for you, and the loader links itself up when it wakes up from the serialized state.