Вход Регистрация
Файл: framework/core/manifest/ClassManifest.php
Строк: 738
<?php
/**
 * A utility class which builds a manifest of all classes, interfaces and some
 * additional items present in a directory, and caches it.
 *
 * It finds the following information:
 *   - Class and interface names and paths.
 *   - All direct and indirect descendants of a class.
 *   - All implementors of an interface.
 *   - All module configuration files.
 *
 * @package framework
 * @subpackage manifest
 */
class SS_ClassManifest {

    const 
CONF_FILE '_config.php';
    const 
CONF_DIR '_config';

    protected 
$base;
    protected 
$tests;
    protected 
$cache;
    protected 
$cacheKey;

    protected 
$classes      = array();
    protected 
$roots        = array();
    protected 
$children     = array();
    protected 
$descendants  = array();
    protected 
$interfaces   = array();
    protected 
$implementors = array();
    protected 
$configs      = array();
    protected 
$configDirs   = array();

    
/**
     * @return TokenisedRegularExpression
     */
    
public static function get_class_parser() {
        return new 
TokenisedRegularExpression(array(
            
=> T_CLASS,
            
=> T_WHITESPACE,
            
=> array(T_STRING'can_jump_to' => array(714), 'save_to' => 'className'),
            
=> T_WHITESPACE,
            
=> T_EXTENDS,
            
=> T_WHITESPACE,
            
=> array(T_STRING'save_to' => 'extends[]''can_jump_to' => 14),
            
=> T_WHITESPACE,
            
=> T_IMPLEMENTS,
            
=> T_WHITESPACE,
            
10 => array(T_STRING'can_jump_to' => 14'save_to' => 'interfaces[]'),
            
11 => array(T_WHITESPACE'optional' => true),
            
12 => array(',''can_jump_to' => 10'save_to' => 'interfaces[]'),
            
13 => array(T_WHITESPACE'can_jump_to' => 10),
            
14 => array(T_WHITESPACE'optional' => true),
            
15 => '{',
        ));
    }

    
/**
     * @return TokenisedRegularExpression
     */
    
public static function get_namespaced_class_parser() {
        return new 
TokenisedRegularExpression(array(
            
=> T_CLASS,
            
=> T_WHITESPACE,
            
=> array(T_STRING'can_jump_to' => array(816), 'save_to' => 'className'),
            
=> T_WHITESPACE,
            
=> T_EXTENDS,
            
=> T_WHITESPACE,
            
=> array(T_NS_SEPARATOR'save_to' => 'extends[]''optional' => true),
            
=> array(T_STRING'save_to' => 'extends[]''can_jump_to' => array(616)),
            
=> T_WHITESPACE,
            
=> T_IMPLEMENTS,
            
10 => T_WHITESPACE,
            
11 => array(T_NS_SEPARATOR'save_to' => 'interfaces[]''optional' => true),
            
12 => array(T_STRING'can_jump_to' => array(1116), 'save_to' => 'interfaces[]'),
            
13 => array(T_WHITESPACE'optional' => true),
            
14 => array(',''can_jump_to' => 11'save_to' => 'interfaces[]'),
            
15 => array(T_WHITESPACE'can_jump_to' => 11),
            
16 => array(T_WHITESPACE'optional' => true),
            
17 => '{',
        ));
    }

    
/**
     * @return TokenisedRegularExpression
     */
    
public static function get_namespace_parser() {
        return new 
TokenisedRegularExpression(array(
            
=> T_NAMESPACE,
            
=> T_WHITESPACE,
            
=> array(T_NS_SEPARATOR'save_to' => 'namespaceName[]''optional' => true),
            
=> array(T_STRING'save_to' => 'namespaceName[]''can_jump_to' => 2),
            
=> array(T_WHITESPACE'optional' => true),
            
=> ';',
        ));
    }

    
/**
     * @return TokenisedRegularExpression
     */
    
public static function get_interface_parser() {
        return new 
TokenisedRegularExpression(array(
            
=> T_INTERFACE,
            
=> T_WHITESPACE,
            
=> array(T_STRING'save_to' => 'interfaceName')
        ));
    }

    
/**
     * Create a {@link TokenisedRegularExpression} that extracts the namespaces imported with the 'use' keyword
     *
     * This searches symbols for a `use` followed by 1 or more namespaces which are optionally aliased using the `as`
     * keyword. The relevant matching tokens are added one-by-one into an array (using `save_to` param).
     *
     * eg: use NamespaceClassName as Alias, OtherNamespaceClassName;
     *
     * @return TokenisedRegularExpression
     */
    
public static function get_imported_namespace_parser() {
        return new 
TokenisedRegularExpression(array(
            
=> T_USE,
            
=> T_WHITESPACE,
            
=> array(T_NS_SEPARATOR'save_to' => 'importString[]''optional' => true),
            
=> array(T_STRING'save_to' => 'importString[]''can_jump_to' => array(28)),
            
=> array(T_WHITESPACE'save_to' => 'importString[]'),
            
=> array(T_AS'save_to' => 'importString[]'),
            
=> array(T_WHITESPACE'save_to' => 'importString[]'),
            
=> array(T_STRING'save_to' => 'importString[]'),
            
=> array(T_WHITESPACE'optional' => true),
            
=> array(',''save_to' => 'importString[]''optional' => true'can_jump_to' => 2),
            
10 => array(T_WHITESPACE'optional' => true'can_jump_to' => 2),
            
11 => ';',
        ));
    }

    
/**
     * Constructs and initialises a new class manifest, either loading the data
     * from the cache or re-scanning for classes.
     *
     * @param string $base The manifest base path.
     * @param bool   $includeTests Include the contents of "tests" directories.
     * @param bool   $forceRegen Force the manifest to be regenerated.
     * @param bool   $cache If the manifest is regenerated, cache it.
     */
    
public function __construct($base$includeTests false$forceRegen false$cache true) {
        
$this->base  $base;
        
$this->tests $includeTests;

        
$cacheClass defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE 'ManifestCache_File';

        
$this->cache = new $cacheClass('classmanifest'.($includeTests '_tests' ''));
        
$this->cacheKey 'manifest';

        if (!
$forceRegen && $data $this->cache->load($this->cacheKey)) {
            
$this->classes      $data['classes'];
            
$this->descendants  $data['descendants'];
            
$this->interfaces   $data['interfaces'];
            
$this->implementors $data['implementors'];
            
$this->configs      $data['configs'];
            
$this->configDirs   $data['configDirs'];
        } else {
            
$this->regenerate($cache);
        }
    }

    
/**
     * Returns the file path to a class or interface if it exists in the
     * manifest.
     *
     * @param  string $name
     * @return string|null
     */
    
public function getItemPath($name) {
        
$name strtolower($name);

        if (isset(
$this->classes[$name])) {
            return 
$this->classes[$name];
        } elseif (isset(
$this->interfaces[$name])) {
            return 
$this->interfaces[$name];
        }
    }

    
/**
     * Returns a map of lowercased class names to file paths.
     *
     * @return array
     */
    
public function getClasses() {
        return 
$this->classes;
    }

    
/**
     * Returns a lowercase array of all the class names in the manifest.
     *
     * @return array
     */
    
public function getClassNames() {
        return 
array_keys($this->classes);
    }

    
/**
     * Returns an array of all the descendant data.
     *
     * @return array
     */
    
public function getDescendants() {
        return 
$this->descendants;
    }

    
/**
     * Returns an array containing all the descendants (direct and indirect)
     * of a class.
     *
     * @param  string|object $class
     * @return array
     */
    
public function getDescendantsOf($class) {
        if (
is_object($class)) {
            
$class get_class($class);
        }

        
$lClass strtolower($class);

        if (
array_key_exists($lClass$this->descendants)) {
            return 
$this->descendants[$lClass];
        } else {
            return array();
        }
    }

    
/**
     * Returns a map of lowercased interface names to file locations.
     *
     * @return array
     */
    
public function getInterfaces() {
        return 
$this->interfaces;
    }

    
/**
     * Returns a map of lowercased interface names to the classes the implement
     * them.
     *
     * @return array
     */
    
public function getImplementors() {
        return 
$this->implementors;
    }

    
/**
     * Returns an array containing the class names that implement a certain
     * interface.
     *
     * @param  string $interface
     * @return array
     */
    
public function getImplementorsOf($interface) {
        
$interface strtolower($interface);

        if (
array_key_exists($interface$this->implementors)) {
            return 
$this->implementors[$interface];
        } else {
            return array();
        }
    }

    
/**
     * Returns an array of paths to module config files.
     *
     * @return array
     */
    
public function getConfigs() {
        return 
$this->configs;
    }

    
/**
     * Returns an array of module names mapped to their paths.
     *
     * "Modules" in SilverStripe are simply directories with a _config.php
     * file.
     *
     * @return array
     */
    
public function getModules() {
        
$modules = array();

        if(
$this->configs) {
            foreach(
$this->configs as $configPath) {
                
$modules[basename(dirname($configPath))] = dirname($configPath);
            }
        }

        if(
$this->configDirs) {
            foreach(
$this->configDirs as $configDir) {
                
$path preg_replace('//_config$/'''dirname($configDir));
                
$modules[basename($path)] = $path;
            }
        }

        return 
$modules;
    }

    
/**
     * Used to set up files that we want to exclude from parsing for performance reasons.
     */
    
protected function setDefaults()
    {
        
$this->classes['sstemplateparser'] = FRAMEWORK_PATH.'/view/SSTemplateParser.php';
        
$this->classes['sstemplateparseexception'] = FRAMEWORK_PATH.'/view/SSTemplateParser.php';
    }

    
/**
     * Completely regenerates the manifest file.
     *
     * @param bool $cache Cache the result.
     */
    
public function regenerate($cache true) {
        
$reset = array(
            
'classes''roots''children''descendants''interfaces',
            
'implementors''configs''configDirs'
        
);

        
// Reset the manifest so stale info doesn't cause errors.
        
foreach ($reset as $reset) {
            
$this->$reset = array();
        }

        
$this->setDefaults();

        
$finder = new ManifestFileFinder();
        
$finder->setOptions(array(
            
'name_regex'    => '/^(_config.php|[^_].*.php)$/',
            
'ignore_files'  => array('index.php''main.php''cli-script.php''SSTemplateParser.php'),
            
'ignore_tests'  => !$this->tests,
            
'file_callback' => array($this'handleFile'),
            
'dir_callback' => array($this'handleDir')
        ));
        
$finder->find($this->base);

        foreach (
$this->roots as $root) {
            
$this->coalesceDescendants($root);
        }

        if (
$cache) {
            
$data = array(
                
'classes'      => $this->classes,
                
'descendants'  => $this->descendants,
                
'interfaces'   => $this->interfaces,
                
'implementors' => $this->implementors,
                
'configs'      => $this->configs,
                
'configDirs'   => $this->configDirs
            
);
            
$this->cache->save($data$this->cacheKey);
        }
    }

    public function 
handleDir($basename$pathname$depth) {
        if (
$basename == self::CONF_DIR) {
            
$this->configDirs[] = $pathname;
        }
    }

    
/**
     * Find a the full namespaced declaration of a class (or interface) from a list of candidate imports
     *
     * This is typically used to determine the full class name in classes that have imported namesapced symbols (having
     * used the `use` keyword)
     *
     * NB: remember the '\' is an escaped backslash and is interpreted as a single 
     *
     * @param string $class The class (or interface) name to find in the candidate imports
     * @param string $namespace The namespace that was declared for the classes definition (if there was one)
     * @param array $imports The list of imported symbols (Classes or Interfaces) to test against
     *
     * @return string The fully namespaced class name
     */
    
protected function findClassOrInterfaceFromCandidateImports($class$namespace ''$imports = array()) {

        
//normalise the namespace
        
$namespace rtrim($namespace'\');

        //by default we'
ll use the $class as our candidate
        $candidateClass 
$class;

        if (!
$class) {
            return 
$candidateClass;
        }
        
//if the class starts with a  then it is explicitly in the global namespace and we don't need to do
        // anything else
        
if (substr($class01) == '\') {
            $candidateClass = substr($class, 1);
            return $candidateClass;
        }
        //if there'
s a namespace, starting assumption is the class is defined in that namespace
        if (
$namespace) {
            
$candidateClass $namespace '\' . $class;
        }

        if (empty($imports)) {
            return $candidateClass;
        }

        //normalised class name (PHP is case insensitive for symbols/namespaces
        $lClass = strtolower($class);

        //go through all the imports and see if the class exists within one of them
        foreach ($imports as $alias => $import) {
            //normalise import
            $import = trim($import, '
\');

            //if there is no string key, then there was no declared alias - we'
ll use the main declaration
            
if (is_int($alias)) {
                
$alias strtolower($import);
            } else {
                
$alias strtolower($alias);
            }

            
//exact match? Then it's a class in the global namespace that was imported OR it's an alias of
            // another namespace
            // or if it ends with the ClassName then it's the class we are looking for
            
if ($lClass == $alias
                
|| substr_compare(
                    
$alias,
                    
'\' . $lClass,
                    strlen($alias) - strlen($lClass) - 1,
                    // -1 because the $lClass length is 1 longer due to 
                    strlen($alias)
                ) === 0
            ) {
                $candidateClass = $import;
                break;
            }
        }
        return $candidateClass;
    }

    /**
     * Return an array of array($alias => $import) from tokenizer'
s tokens of a PHP file
     
*
     * 
NB: If there is no alias we don't set a key to the array
     *
     * @param array $tokens The parsed tokens from tokenizer'
s parsing of a PHP file
     
*
     * @return array 
The array of imports as (optional$alias => $import
     
*/
    protected function 
getImportsFromTokens($tokens) {
        
//parse out the imports
        
$imports self::get_imported_namespace_parser()->findAll($tokens);

        
//if there are any imports, clean them up
        // imports come to us as array('importString' => array([array of matching tokens]))
        // we need to join this nested array into a string and split out the alias and the import
        
if (!empty($imports)) {
            
$cleanImports = array();
            foreach (
$imports as $import) {
                if (!empty(
$import['importString'])) {
                    
//join the array up into a string
                    
$importString implode(''$import['importString']);
                    
//split at , to get each import declaration
                    
$importSet explode(','$importString);
                    foreach (
$importSet as $importDeclaration) {
                        
//split at ' as ' (any case) to see if we are aliasing the namespace
                        
$importDeclaration preg_split('/s+ass+/i'$importDeclaration);
                        
//shift off the fully namespaced import
                        
$qualifiedImport array_shift($importDeclaration);
                        
//if there are still items in the array, it's the alias
                        
if (!empty($importDeclaration)) {
                            
$cleanImports[array_shift($importDeclaration)] = $qualifiedImport;
                        }
                        else {
                            
$cleanImports[] = $qualifiedImport;
                        }
                    }
                }
            }
            
$imports $cleanImports;
        }
        return 
$imports;
    }

    public function 
handleFile($basename$pathname$depth) {
        if (
$basename == self::CONF_FILE) {
            
$this->configs[] = $pathname;
            return;
        }

        
$classes    null;
        
$interfaces null;
        
$namespace null;
        
$imports null;

        
// The results of individual file parses are cached, since only a few
        // files will have changed and TokenisedRegularExpression is quite
        // slow. A combination of the file name and file contents hash are used,
        // since just using the datetime lead to problems with upgrading.
        
$file file_get_contents($pathname);
        
$key  preg_replace('/[^a-zA-Z0-9_]/''_'$basename) . '_' md5($file);

        if (
$data $this->cache->load($key)) {
            
$valid = (
                isset(
$data['classes']) && is_array($data['classes'])
                && isset(
$data['interfaces']) && is_array($data['interfaces'])
                && isset(
$data['namespace']) && is_string($data['namespace'])
                && isset(
$data['imports']) && is_array($data['imports'])
            );

            if (
$valid) {
                
$classes $data['classes'];
                
$interfaces $data['interfaces'];
                
$namespace $data['namespace'];
                
$imports $data['imports'];
            }
        }

        if (!
$classes) {
            
$tokens     token_get_all($file);

            
$classes self::get_namespaced_class_parser()->findAll($tokens);

            
$namespace self::get_namespace_parser()->findAll($tokens);

            if(
$namespace) {
                
$namespace implode(''$namespace[0]['namespaceName']);
            } else {
                
$namespace '';
            }

            
$imports $this->getImportsFromTokens($tokens);

            
$interfaces self::get_interface_parser()->findAll($tokens);

            
$cache = array(
                
'classes' => $classes,
                
'interfaces' => $interfaces,
                
'namespace' => $namespace,
                
'imports' => $imports
            
);
            
$this->cache->save($cache$key);
        }

        foreach (
$classes as $class) {
            
$name $class['className'];
            if (
$namespace) {
                
$namespace rtrim($namespace'\');
                $name = $namespace . '
\' . $name;
            }
            $extends = isset($class['
extends']) ? implode('', $class['extends']) : null;
            $implements = isset($class['
interfaces']) ? $class['interfaces'] : null;

            if ($extends) {
                $extends = $this->findClassOrInterfaceFromCandidateImports($extends, $namespace, $imports);
            }

            if (!empty($implements)) {
                //join all the tokens
                $implements = implode('', $implements);
                //split at comma
                $implements = explode('
,', $implements);
                //normalise interfaces
                foreach ($implements as &$interface) {
                    $interface = $this->findClassOrInterfaceFromCandidateImports($interface, $namespace, $imports);
                }
                //release the var name
                unset($interface);
            }

            $lowercaseName = strtolower($name);
            if (array_key_exists($lowercaseName, $this->classes)) {
                throw new Exception(sprintf(
                    '
There are two files containing the "%s" class: "%s" and "%s"',
                    $name, $this->classes[$lowercaseName], $pathname
                ));
            }

            $this->classes[$lowercaseName] = $pathname;

            if ($extends) {
                $extends = strtolower($extends);

                if (!isset($this->children[$extends])) {
                    $this->children[$extends] = array($name);
                } else {
                    $this->children[$extends][] = $name;
                }
            } else {
                $this->roots[] = $name;
            }

            if ($implements) {
                foreach ($implements as $interface) {
                    $interface = strtolower($interface);

                    if (!isset($this->implementors[$interface])) {
                        $this->implementors[$interface] = array($name);
                    } else {
                        $this->implementors[$interface][] = $name;
                    }
                }
            }
        }

        $interfaceBase = '';
        if ($namespace) {
            $interfaceBase = $namespace . '
\';
        }
        foreach ($interfaces as $interface) {
            $this->interfaces[strtolower($interfaceBase . $interface['
interfaceName'])] = $pathname;
        }
    }

    /**
     * Recursively coalesces direct child information into full descendant
     * information.
     *
     * @param  string $class
     * @return array
     */
    protected function coalesceDescendants($class) {
        $result = array();
        $lClass = strtolower($class);

        if (array_key_exists($lClass, $this->children)) {
            $this->descendants[$lClass] = array();

            foreach ($this->children[$lClass] as $class) {
                $this->descendants[$lClass] = array_merge(
                    $this->descendants[$lClass],
                    array($class),
                    $this->coalesceDescendants($class)
                );
            }

            return $this->descendants[$lClass];
        } else {
            return array();
        }
    }

}
Онлайн: 0
Реклама