silverstripe-framework/core/ClassInfo.php

348 lines
9.8 KiB
PHP

<?php
/**
* Provides introspection information about the class tree.
*
* It's a cached wrapper around the built-in class functions. SilverStripe uses
* class introspection heavily and without the caching it creates an unfortunate
* performance hit.
*
* @package framework
* @subpackage core
*/
class ClassInfo {
/**
* Wrapper for classes getter.
*/
public static function allClasses() {
return SS_ClassLoader::instance()->getManifest()->getClasses();
}
/**
* Returns true if a class or interface name exists.
*
* @param string $class
* @return bool
*/
public static function exists($class) {
return class_exists($class, false) || interface_exists($class, false) || SS_ClassLoader::instance()->getItemPath($class);
}
/**
* Cache for {@link hasTable()}
*/
private static $_cache_all_tables = array();
/**
* @var Array Cache for {@link ancestry()}.
*/
private static $_cache_ancestry = array();
/**
* @todo Move this to SS_Database or DB
*/
public static function hasTable($class) {
// Cache the list of all table names to reduce on DB traffic
if(empty(self::$_cache_all_tables) && DB::is_active()) {
self::$_cache_all_tables = DB::get_schema()->tableList();
}
return !empty(self::$_cache_all_tables[strtolower($class)]);
}
public static function reset_db_cache() {
self::$_cache_all_tables = null;
self::$_cache_ancestry = array();
}
/**
* Returns the manifest of all classes which are present in the database.
*
* @param string $class Class name to check enum values for ClassName field
* @param boolean $includeUnbacked Flag indicating whether or not to include
* types that don't exist as implemented classes. By default these are excluded.
* @return array List of subclasses
*/
public static function getValidSubClasses($class = 'SiteTree', $includeUnbacked = false) {
if(is_string($class) && !class_exists($class)) return null;
$class = self::class_name($class);
$classes = DB::get_schema()->enumValuesForField($class, 'ClassName');
if (!$includeUnbacked) $classes = array_filter($classes, array('ClassInfo', 'exists'));
return $classes;
}
/**
* Returns an array of the current class and all its ancestors and children
* which require a DB table.
*
* @param string|object $class
* @todo Move this into data object
* @return array
*/
public static function dataClassesFor($class) {
if(is_string($class) && !class_exists($class)) return null;
$result = array();
$class = self::class_name($class);
$classes = array_merge(
self::ancestry($class),
self::subclassesFor($class)
);
foreach ($classes as $class) {
if (DataObject::has_own_table($class)) $result[$class] = $class;
}
return $result;
}
/**
* Returns the root class (the first to extend from DataObject) for the
* passed class.
*
* @param string|object $class
* @return string
*/
public static function baseDataClass($class) {
if(is_string($class) && !class_exists($class)) return null;
$class = self::class_name($class);
if (!is_subclass_of($class, 'DataObject')) {
throw new InvalidArgumentException("$class is not a subclass of DataObject");
}
while ($next = get_parent_class($class)) {
if ($next == 'DataObject') {
return $class;
}
$class = $next;
}
}
/**
* Returns a list of classes that inherit from the given class.
* The resulting array includes the base class passed
* through the $class parameter as the first array value.
*
* Example usage:
* <code>
* ClassInfo::subclassesFor('BaseClass');
* array(
* 'BaseClass' => 'BaseClass',
* 'ChildClass' => 'ChildClass',
* 'GrandChildClass' => 'GrandChildClass'
* )
* </code>
*
* @param mixed $class string of the classname or instance of the class
* @return array Names of all subclasses as an associative array.
*/
public static function subclassesFor($class) {
if(is_string($class) && !class_exists($class)) return null;
//normalise class case
$className = self::class_name($class);
$descendants = SS_ClassLoader::instance()->getManifest()->getDescendantsOf($class);
$result = array($className => $className);
if ($descendants) {
return $result + ArrayLib::valuekey($descendants);
} else {
return $result;
}
}
/**
* Convert a class name in any case and return it as it was defined in PHP
*
* eg: self::class_name('dataobJEct'); //returns 'DataObject'
*
* @param string|object $nameOrObject The classname or object you want to normalise
*
* @return string The normalised class name
*/
public static function class_name($nameOrObject) {
if (is_object($nameOrObject)) {
return get_class($nameOrObject);
} elseif (!self::exists($nameOrObject)) {
Deprecation::notice(
'4.0',
"ClassInfo::class_name() passed a class that doesn't exist. Support for this will be removed in 4.0",
Deprecation::SCOPE_GLOBAL
);
return $nameOrObject;
}
$reflection = new ReflectionClass($nameOrObject);
return $reflection->getName();
}
/**
* Returns the passed class name along with all its parent class names in an
* array, sorted with the root class first.
*
* @param string $class
* @param bool $tablesOnly Only return classes that have a table in the db.
* @return array
*/
public static function ancestry($class, $tablesOnly = false) {
if(is_string($class) && !class_exists($class)) return null;
$class = self::class_name($class);
$lClass = strtolower($class);
$cacheKey = $lClass . '_' . (string)$tablesOnly;
$parent = $class;
if(!isset(self::$_cache_ancestry[$cacheKey])) {
$ancestry = array();
do {
if (!$tablesOnly || DataObject::has_own_table($parent)) {
$ancestry[$parent] = $parent;
}
} while ($parent = get_parent_class($parent));
self::$_cache_ancestry[$cacheKey] = array_reverse($ancestry);
}
return self::$_cache_ancestry[$cacheKey];
}
/**
* @return array A self-keyed array of class names. Note that this is only available with Silverstripe
* classes and not built-in PHP classes.
*/
public static function implementorsOf($interfaceName) {
return SS_ClassLoader::instance()->getManifest()->getImplementorsOf($interfaceName);
}
/**
* Returns true if the given class implements the given interface
*/
public static function classImplements($className, $interfaceName) {
return in_array($className, self::implementorsOf($interfaceName));
}
/**
* Get all classes contained in a file.
* @uses ManifestBuilder
*
* @todo Doesn't return additional classes that only begin
* with the filename, and have additional naming separated through underscores.
*
* @param string $filePath Path to a PHP file (absolute or relative to webroot)
* @return array
*/
public static function classes_for_file($filePath) {
$absFilePath = Director::getAbsFile($filePath);
$matchedClasses = array();
$manifest = SS_ClassLoader::instance()->getManifest()->getClasses();
foreach($manifest as $class => $compareFilePath) {
if($absFilePath == $compareFilePath) $matchedClasses[] = $class;
}
return $matchedClasses;
}
/**
* Returns all classes contained in a certain folder.
*
* @todo Doesn't return additional classes that only begin
* with the filename, and have additional naming separated through underscores.
*
* @param string $folderPath Relative or absolute folder path
* @return array Array of class names
*/
public static function classes_for_folder($folderPath) {
$absFolderPath = Director::getAbsFile($folderPath);
$matchedClasses = array();
$manifest = SS_ClassLoader::instance()->getManifest()->getClasses();
foreach($manifest as $class => $compareFilePath) {
if(stripos($compareFilePath, $absFolderPath) === 0) $matchedClasses[] = $class;
}
return $matchedClasses;
}
private static $method_from_cache = array();
public static function has_method_from($class, $method, $compclass) {
$lClass = strtolower($class);
$lMethod = strtolower($method);
$lCompclass = strtolower($compclass);
if (!isset(self::$method_from_cache[$lClass])) self::$method_from_cache[$lClass] = array();
if (!array_key_exists($lMethod, self::$method_from_cache[$lClass])) {
self::$method_from_cache[$lClass][$lMethod] = false;
$classRef = new ReflectionClass($class);
if ($classRef->hasMethod($method)) {
$methodRef = $classRef->getMethod($method);
self::$method_from_cache[$lClass][$lMethod] = $methodRef->getDeclaringClass()->getName();
}
}
return strtolower(self::$method_from_cache[$lClass][$lMethod]) == $lCompclass;
}
/**
* Returns the table name in the class hierarchy which contains a given
* field column for a {@link DataObject}. If the field does not exist, this
* will return null.
*
* Note: In 3.x and below this method may return 'DataObject'. From 4.0 onwards
* null will be returned if a field is not a member of the object.
*
* @param string $candidateClass
* @param string $fieldName
*
* @return string
*/
public static function table_for_object_field($candidateClass, $fieldName) {
if(!$candidateClass
|| !$fieldName
|| !class_exists($candidateClass)
|| !is_subclass_of($candidateClass, 'DataObject')
) {
return null;
}
//normalise class name
$candidateClass = self::class_name($candidateClass);
$exists = self::exists($candidateClass);
// Short circuit for fixed fields
$fixed = DataObject::config()->fixed_fields;
if($exists && isset($fixed[$fieldName])) {
return self::baseDataClass($candidateClass);
}
// Find regular field
while($candidateClass && $candidateClass != 'DataObject' && $exists) {
if( DataObject::has_own_table($candidateClass)
&& DataObject::has_own_table_database_field($candidateClass, $fieldName)
) {
break;
}
$candidateClass = get_parent_class($candidateClass);
$exists = $candidateClass && self::exists($candidateClass);
}
if(!$candidateClass || !$exists) {
return null;
}
return $candidateClass;
}
}