525 lines
17 KiB
PHP
525 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Autoloader Class
|
|
*
|
|
* This class is responsible for loading all the requested class of the
|
|
* system
|
|
*
|
|
* @author Time.ly Network, Inc.
|
|
* @since 2.0
|
|
* @package Ai1EC
|
|
* @subpackage Ai1EC.Loader
|
|
*/
|
|
class Ai1ec_Loader {
|
|
|
|
/**
|
|
* @var string Used to specify new instances every time.
|
|
*/
|
|
CONST NEWINST = 'n';
|
|
|
|
/**
|
|
* @var string Used to specify to treat as singleton.
|
|
*/
|
|
CONST GLOBALINST = 'g';
|
|
|
|
/**
|
|
* @var array Map of files to be included
|
|
*/
|
|
protected $_paths = array();
|
|
|
|
/**
|
|
* @var bool Set to true when internal state is changed
|
|
*/
|
|
protected $_modified = false;
|
|
|
|
/**
|
|
* @var array Map of files already included
|
|
*/
|
|
protected $_included_files = array();
|
|
|
|
/**
|
|
* @var string The prefix used for the classes
|
|
*/
|
|
protected $_prefix = null;
|
|
|
|
/**
|
|
* @var string Base path to plugins core directory
|
|
*/
|
|
protected $_base_path = null;
|
|
|
|
/**
|
|
* @var array Registered folders.
|
|
*/
|
|
protected $_registered = array();
|
|
|
|
/**
|
|
* load method
|
|
*
|
|
* Load given class, via `require`, into memory
|
|
*
|
|
* @param string $class Name of class, which needs to be loaded
|
|
*
|
|
* @return Ai1ec_Loader Instance of self for chaining
|
|
*/
|
|
public function load( $class ) {
|
|
if ( isset( $this->_paths[$class] ) ) {
|
|
$this->include_file( $this->_paths[$class]['f'] );
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Method which actually includes required file.
|
|
*
|
|
* The PHP language construct used is `require` and not a `require_once`,
|
|
* as this is internal method, which shall guard itself against incidents
|
|
* that may occur during loading classes more than once.
|
|
* During include additional callbacks may be fired to include related
|
|
* files, i.e. speed-up further requires.
|
|
*
|
|
* @param string $file Name of file to include
|
|
*
|
|
* @return Ai1ec_Loader Instance of self for chaining
|
|
*/
|
|
public function include_file( $file ) {
|
|
if ( ! isset( $this->_included_files[$file] ) ) {
|
|
$this->_included_files[$file] = true;
|
|
require $file;
|
|
}
|
|
return $this->_included_files[$file];
|
|
}
|
|
|
|
/**
|
|
* collect_classes method
|
|
*
|
|
* Method to extract classes list from filesystem.
|
|
* Returned array contains names of class, as keys, and file entites as
|
|
* value, where *entities* means either a file name
|
|
* - {@see self::match_file()} for more.
|
|
*
|
|
* @return array Map of classes and corresponding file entites
|
|
*/
|
|
public function collect_classes( $path = null, $folder_name = AI1EC_PLUGIN_NAME ) {
|
|
// extension inject theit own base path
|
|
$path = ( null === $path ) ? $this->_base_path : $path;
|
|
$names = $this->_locate_all_files( $path, $folder_name );
|
|
$names = $this->_process_reflections( $names );
|
|
$this->_cache( $path, $names );
|
|
$this->_paths = array_merge( $this->_paths, $names );
|
|
return $names;
|
|
}
|
|
|
|
/**
|
|
* Read/write cached classes map.
|
|
*
|
|
* If no entries are provided - acts as cache reader.
|
|
*
|
|
* @param array $entries Entries to write [optional=null]
|
|
*
|
|
* @return bool|array False on failure, true on success in writer
|
|
* mode, cached entry in reader mode on success
|
|
*/
|
|
protected function _cache( $path, array $entries = null ) {
|
|
$cache_file = $this->_get_cache_file_path( $path );
|
|
if ( $entries ) {
|
|
if (
|
|
is_file( $cache_file ) &&
|
|
! is_writable( $cache_file ) ||
|
|
! is_writable( dirname( $cache_file ) )
|
|
) {
|
|
return false;
|
|
}
|
|
ksort( $entries, SORT_STRING );
|
|
$content = array(
|
|
'0registered' => $this->_registered,
|
|
'1class_map' => $entries,
|
|
);
|
|
$content = var_export( $content, true );
|
|
$content = $this->_sanitize_paths( $content, $path );
|
|
$content = '<?php return ' . $content . ';';
|
|
$this->_modified = false;
|
|
if (
|
|
false === file_put_contents( $cache_file, $content, LOCK_EX )
|
|
) { // LOCK_EX is not supported on all hosts (streams)
|
|
return (bool)file_put_contents( $cache_file, $content );
|
|
}
|
|
return true;
|
|
}
|
|
if ( ! is_file( $cache_file ) ) {
|
|
return false;
|
|
}
|
|
$cached = ( require $cache_file );
|
|
$this->_registered[$cache_file] = true;
|
|
return $cached['1class_map'];
|
|
}
|
|
|
|
/**
|
|
* Gets the way classes must be instanciated.
|
|
*
|
|
* Retrieves from annotations the way classes must be retrieved.
|
|
* Possible values are
|
|
* - new: a new instance is instantiated every time
|
|
* - global: treat as singleton
|
|
* - classname.method: a factory is used, specify it in that order
|
|
* The default if nothing is specified is global.
|
|
*
|
|
* @param ReflectionClass $class
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _get_instantiator( ReflectionClass $class ) {
|
|
$doc = $class->getDocComment();
|
|
preg_match_all(
|
|
'#^\s\*\s@instantiator\s+(.*)$#im',
|
|
$doc,
|
|
$annotations
|
|
);
|
|
$instantiator = '';
|
|
if ( isset( $annotations[1][0] ) ) {
|
|
$instantiator = rtrim( $annotations[1][0] );
|
|
}
|
|
return $this->_convert_instantiator_for_map( $instantiator );
|
|
}
|
|
|
|
/**
|
|
* Check if the registry must be injected in the constructor.
|
|
* By convention the registry will always be the first parameter.
|
|
*
|
|
* @param ReflectionClass $class The class to check
|
|
*
|
|
* @return boolean true if the registry must be injected, false if not.
|
|
*/
|
|
protected function _inject_registry( ReflectionClass $class ) {
|
|
$contructor = $class->getConstructor();
|
|
if ( null !== $contructor ) {
|
|
foreach ( $contructor->getParameters() as $param ) {
|
|
$param_class = $param->getClass();
|
|
if ( $param_class instanceof ReflectionClass ) {
|
|
$name = $param_class->getName();
|
|
if ( 'Ai1ec_Registry_Object' === $name ) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update the classmap with Reflection informations.
|
|
*
|
|
* @param array $names The class map.
|
|
*
|
|
* @return array The classmap with instantiator.
|
|
*/
|
|
protected function _process_reflections( array $names ) {
|
|
$this->_paths = array_merge( $this->_paths, $names );
|
|
spl_autoload_register( array( $this, 'load' ) );
|
|
foreach ( $names as $classname => &$data ) {
|
|
try {
|
|
$class = new ReflectionClass( $data['c'] );
|
|
$data['i'] = $this->_get_instantiator( $class );
|
|
if ( $this->_inject_registry( $class ) ) {
|
|
$data['r'] = 'y';
|
|
}
|
|
} catch ( ReflectionException $excpt ) { // unreachable class
|
|
$data['i'] = self::NEWINST;
|
|
}
|
|
}
|
|
return $names;
|
|
}
|
|
|
|
/**
|
|
* Converts the long form to the short form where applicable.
|
|
*
|
|
* @param string $instantiator
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _convert_instantiator_for_map( $instantiator ) {
|
|
if ( empty( $instantiator ) || 'global' === $instantiator ) {
|
|
return self::GLOBALINST;
|
|
}
|
|
if ( 'new' === $instantiator ) {
|
|
return self::NEWINST;
|
|
}
|
|
return $instantiator;
|
|
}
|
|
|
|
/**
|
|
* _locate_all_files method
|
|
*
|
|
* Scan file system, given path, recursively, to search for files and
|
|
* extract `class` names from them.
|
|
*
|
|
* @param string $path File system path to scan
|
|
*
|
|
* @return array Map of classes and corresponding files
|
|
*/
|
|
protected function _locate_all_files( $path, $folder_name ) {
|
|
$class_list = array();
|
|
$directory = opendir( $path );
|
|
while ( false !== ( $entry = readdir( $directory ) ) ) {
|
|
if ( is_null( $entry ) || '.' === $entry{0} || 'tests' === $entry || strpos( strtolower( $entry ), 'icalcreator' ) !== false ) {
|
|
continue;
|
|
}
|
|
$local_path = $path . DIRECTORY_SEPARATOR . $entry;
|
|
$base_path = substr( $local_path, strlen( $this->_base_path ) );
|
|
|
|
if ( is_dir( $local_path ) ) {
|
|
$class_list += $this->_locate_all_files( $local_path, $folder_name );
|
|
} else {
|
|
$class_list += $this->_extract_classes( $local_path, $folder_name );
|
|
}
|
|
}
|
|
closedir( $directory );
|
|
return $class_list;
|
|
}
|
|
|
|
/**
|
|
* _extract_classes method
|
|
*
|
|
* Extract names of classes from given file.
|
|
* So far only files ending in `.php` are processed and regular expression
|
|
* is used instead of `token_get_all` to increase parsing speed.
|
|
*
|
|
* @param string $file Name of file to scan
|
|
*
|
|
* @return array List of classes in file
|
|
*/
|
|
protected function _extract_classes( $file, $folder_name ) {
|
|
$class_list = array();
|
|
if ( '.php' === strrchr( $file, '.' ) ) {
|
|
$tokens = token_get_all( file_get_contents( $file ) );
|
|
for ( $i = 2, $count = count( $tokens ); $i < $count; $i++ ) {
|
|
if (
|
|
T_CLASS === $tokens[$i - 2][0] ||
|
|
T_INTERFACE === $tokens[$i - 2][0] &&
|
|
T_WHITESPACE === $tokens[$i - 1][0] &&
|
|
T_STRING === $tokens[$i][0]
|
|
) {
|
|
$names = $this->_generate_loader_names(
|
|
$tokens[$i][1],
|
|
$file,
|
|
$folder_name
|
|
);
|
|
foreach ( $names as $name ) {
|
|
$class_list[$name] = array(
|
|
'f' => $file,
|
|
'c' => $tokens[$i][1],
|
|
);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
return $class_list;
|
|
}
|
|
|
|
/**
|
|
* Generate path name abbreviation.
|
|
*
|
|
* @param string $name Path name particle.
|
|
*
|
|
* @return string Abbreviated path name.
|
|
*/
|
|
public function path_name_shortening( $name ) {
|
|
return strtoupper( $name[0] );
|
|
}
|
|
|
|
/**
|
|
* _sanitize_paths method
|
|
*
|
|
* Sanitize paths before writing to cache file.
|
|
* Make sure, that constants and absolute paths are used independently
|
|
* of system used, thus making file cross-platform generatable.
|
|
*
|
|
* @param string $content Output to be written to cache file.
|
|
* @param string $base_path Base path to use if not default.
|
|
*
|
|
* @return string Modified content, with paths replaced
|
|
*/
|
|
protected function _sanitize_paths(
|
|
$content,
|
|
$base_path = null
|
|
) {
|
|
$local_ds = '/';
|
|
$ai1ec_path = $this->_base_path;
|
|
$const_name = 'AI1EC_PATH';
|
|
if ( null !== $base_path ) {
|
|
$ai1ec_path = $base_path;
|
|
$const_name = implode( array_map(
|
|
array( $this, 'path_name_shortening' ),
|
|
explode( '-', basename( $base_path ) )
|
|
) ) . '_PATH';
|
|
$const_name = str_replace( 'AIOEC', 'AI1EC', $const_name );
|
|
}
|
|
if ( '\\' === DIRECTORY_SEPARATOR ) {
|
|
$local_ds = '\\\\';
|
|
$ai1ec_path = str_replace( '\\', '\\\\', $ai1ec_path );
|
|
}
|
|
$content = str_replace(
|
|
'\'' . $ai1ec_path . $local_ds,
|
|
$const_name . ' . DIRECTORY_SEPARATOR . \'',
|
|
$content
|
|
);
|
|
$content = str_replace(
|
|
$local_ds,
|
|
'\' . DIRECTORY_SEPARATOR . \'',
|
|
$content
|
|
);
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Generate all the alternatives name that the loaded recognize.
|
|
*
|
|
* For example:
|
|
* The class Ai1ec_Html_Helper can be loaded as
|
|
* - html.helper ( the path to the file )
|
|
* - Ai1ec_Html_Helper ( needed by Autoload )
|
|
*
|
|
* @param $class string the original name of the class.
|
|
* @param $file string the file
|
|
*
|
|
* @return array An array of strings with the availables names.
|
|
*/
|
|
protected function _generate_loader_names( $class, $file, $folder_name ) {
|
|
$names = array( $class );
|
|
// Remove the extension.
|
|
$file = substr( $file, 0, strrpos( $file , '.' ) );
|
|
$file = strtr( $file, array( '//' => '/' ) );
|
|
// Get just the meaningful data.
|
|
$relative_path_position = strrpos( // offset of base directory
|
|
$file,
|
|
DIRECTORY_SEPARATOR . $folder_name . DIRECTORY_SEPARATOR
|
|
);
|
|
$file = substr(
|
|
$file,
|
|
strpos( // cut to app|lib|vendor|...
|
|
$file,
|
|
DIRECTORY_SEPARATOR,
|
|
$relative_path_position + strlen( $folder_name ) + 2
|
|
)
|
|
);
|
|
$names[] = str_replace(
|
|
DIRECTORY_SEPARATOR,
|
|
'.',
|
|
trim( $file, DIRECTORY_SEPARATOR )
|
|
);
|
|
return $names;
|
|
}
|
|
|
|
/**
|
|
* Translate the key to the actual class name if any
|
|
*
|
|
* @param $key string Key requested to initialize
|
|
*
|
|
* @return array|null Array of the class, or null if none is found
|
|
*/
|
|
public function resolve_class_name( $key ) {
|
|
if ( ! isset( $this->_paths[$key] ) ) {
|
|
return null;
|
|
}
|
|
return $this->_paths[$key];
|
|
}
|
|
|
|
/**
|
|
* Update cache if object was modified
|
|
*
|
|
* @return void Destructor does not return
|
|
*/
|
|
public function __destruct() {
|
|
if ( $this->_modified ) {
|
|
$this->_cache( $this->_paths );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience wrapper to detect internal extension file path.
|
|
*
|
|
* @param string $path Absolute path to extension base directory.
|
|
*
|
|
* @return bool Success loading extension classes.
|
|
*/
|
|
public function register_extension_map( $path ) {
|
|
return $this->register_map( $this->_get_cache_file_path( $path ) );
|
|
}
|
|
|
|
/**
|
|
* Register external class map to use in loading sequence
|
|
*
|
|
* @param string $file Path to class map
|
|
*
|
|
* @return bool Success loading it
|
|
*/
|
|
public function register_map( $file ) {
|
|
if (
|
|
isset( $this->_registered[$file] ) && (
|
|
! defined( 'AI1EC_DEBUG' ) ||
|
|
! AI1EC_DEBUG
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
if ( ! is_file( $file ) ) {
|
|
return false;
|
|
}
|
|
$entries = ( require $file );
|
|
foreach ( $entries['1class_map'] as $class_name => $properties ) {
|
|
$this->_paths[$class_name] = $properties;
|
|
}
|
|
$this->_registered[$file] = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* Initialize the loader creating the map of available classes, if the
|
|
* AI1EC_DEBUG constants is true the list is regenerated
|
|
*
|
|
* @throws Exception if the map is invalid
|
|
*
|
|
* @return void Constructor does not return
|
|
*/
|
|
public function __construct( $base_path ) {
|
|
$this->_base_path = $base_path;
|
|
$this->_prefix = explode( '_', __CLASS__ );
|
|
$this->_prefix = $this->_prefix[0];
|
|
$class_map = $this->_cache( $base_path );
|
|
if (
|
|
! is_array( $class_map ) ||
|
|
defined( 'AI1EC_DEBUG' ) && AI1EC_DEBUG
|
|
) {
|
|
if ( ! defined( 'AI1EC_DEBUG' ) || ! AI1EC_DEBUG ) {
|
|
// using generic `Ai1ec_Exception` as others are, potentially,
|
|
// not resolved at this time.
|
|
throw new Ai1ec_Exception(
|
|
'Generated class map is invalid: ' .
|
|
var_export( $class_map, true ) .
|
|
'. Please delete lib/bootstrap/loader-map.php (if it exists), make ' .
|
|
'sure lib/bootstrap/ is writable by the web server, and enable ' .
|
|
'debug mode by setting AI1EC_DEBUG to true (then back to false ' .
|
|
'when done).'
|
|
);
|
|
}
|
|
$class_map = $this->collect_classes();
|
|
}
|
|
$this->_paths = $class_map;
|
|
}
|
|
|
|
/**
|
|
* Method to get cache file path given path to plugin.
|
|
*
|
|
* @param string $path Path to plugin directory.
|
|
*
|
|
* @return string Absolute path to loader cache file.
|
|
*/
|
|
protected function _get_cache_file_path( $path ) {
|
|
return $path . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR .
|
|
'bootstrap' . DIRECTORY_SEPARATOR . 'loader-map.php';
|
|
}
|
|
|
|
}
|