all-in-one-event-calendar/lib/bootstrap/loader.php
2019-07-25 14:11:00 +02:00

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';
}
}