diff --git a/api/RestfulService.php b/api/RestfulService.php
index dea9de7da..e3f19217d 100644
--- a/api/RestfulService.php
+++ b/api/RestfulService.php
@@ -121,68 +121,9 @@ class RestfulService extends ViewableData {
$response = unserialize($store);
} else {
- $ch = curl_init();
- $timeout = 5;
- $useragent = "SilverStripe/" . SapphireInfo::Version();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
- curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
- if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
- curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
-
- // Add headers
- if($this->customHeaders) {
- $headers = array_merge((array)$this->customHeaders, (array)$headers);
- }
-
- if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
-
- // Add authentication
- if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword");
-
- // Add fields to POST and PUT requests
- if($method == 'POST') {
- curl_setopt($ch, CURLOPT_POST, 1);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
- }
- else if($method == 'PUT') {
- $put = fopen("php://temp", 'r+');
- fwrite($put, $data);
- fseek($put, 0);
-
- curl_setopt($ch, CURLOPT_PUT, 1);
- curl_setopt($ch, CURLOPT_INFILE, $put);
- curl_setopt($ch, CURLOPT_INFILESIZE, strlen($data));
- }
+ $response = $this->curlRequest($url, $method, $data, $headers, $curlOptions);
- // Apply proxy settings
- if(is_array($this->proxy)) {
- curl_setopt_array($ch, $this->proxy);
- }
-
- // Set any custom options passed to the request() function
- curl_setopt_array($ch, $curlOptions);
-
- // Run request
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
- $responseBody = curl_exec($ch);
- $curlError = curl_error($ch);
-
- // Problem verifying the server SSL certificate; just ignore it as it's not mandatory
- if(strpos($curlError,'14090086') !== false) {
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
- $responseBody = curl_exec($ch);
- $curlError = curl_error($ch);
- }
-
- $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- if($curlError !== '' || $statusCode == 0) $statusCode = 500;
-
- $response = new RestfulService_Response($responseBody, $statusCode);
- curl_close($ch);
-
- if($curlError === '' && !$response->isError()) {
+ if(!$response->isError()) {
// Serialise response object and write to cache
$store = serialize($response);
file_put_contents($cache_path, $store);
@@ -204,7 +145,83 @@ class RestfulService extends ViewableData {
return $response;
}
-
+
+ /**
+ * Actually performs a remote service request using curl. This is used by
+ * {@link RestfulService::request()}.
+ *
+ * @param string $url
+ * @param string $method
+ * @param array $data
+ * @param array $headers
+ * @param array $curlOptions
+ * @return RestfulService_Response
+ */
+ public function curlRequest($url, $method, $data = null, $headers = null, $curlOptions = array()) {
+ $ch = curl_init();
+ $timeout = 5;
+ $useragent = 'SilverStripe/' . SapphireInfo::Version();
+
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
+ if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+
+ // Add headers
+ if($this->customHeaders) {
+ $headers = array_merge((array)$this->customHeaders, (array)$headers);
+ }
+
+ if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+
+ // Add authentication
+ if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword");
+
+ // Add fields to POST and PUT requests
+ if($method == 'POST') {
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+ } elseif($method == 'PUT') {
+ $put = fopen("php://temp", 'r+');
+ fwrite($put, $data);
+ fseek($put, 0);
+
+ curl_setopt($ch, CURLOPT_PUT, 1);
+ curl_setopt($ch, CURLOPT_INFILE, $put);
+ curl_setopt($ch, CURLOPT_INFILESIZE, strlen($data));
+ }
+
+ // Apply proxy settings
+ if(is_array($this->proxy)) {
+ curl_setopt_array($ch, $this->proxy);
+ }
+
+ // Set any custom options passed to the request() function
+ curl_setopt_array($ch, $curlOptions);
+
+ // Run request
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ $responseBody = curl_exec($ch);
+ $curlError = curl_error($ch);
+
+ // Problem verifying the server SSL certificate; just ignore it as it's not mandatory
+ if(strpos($curlError,'14090086') !== false) {
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ $responseBody = curl_exec($ch);
+ $curlError = curl_error($ch);
+ }
+
+ $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ if($curlError !== '' || $statusCode == 0) $statusCode = 500;
+
+ $response = new RestfulService_Response($responseBody, $statusCode);
+ curl_close($ch);
+
+ return $response;
+ }
+
/**
* Returns a full request url
* @param string
diff --git a/core/ClassInfo.php b/core/ClassInfo.php
old mode 100755
new mode 100644
index 09370c25a..3e23cf524
--- a/core/ClassInfo.php
+++ b/core/ClassInfo.php
@@ -12,16 +12,14 @@ class ClassInfo {
* @todo Improve documentation
*/
static function allClasses() {
- global $_ALL_CLASSES;
- return $_ALL_CLASSES['exists'];
+ return SS_ClassLoader::instance()->allClasses();
}
/**
* @todo Improve documentation
*/
static function exists($class) {
- global $_ALL_CLASSES;
- return isset($_ALL_CLASSES['exists'][$class]) ? $_ALL_CLASSES['exists'][$class] : null;
+ return SS_ClassLoader::instance()->classExists($class);
}
/**
@@ -59,54 +57,54 @@ class ClassInfo {
}
/**
- * Return the database tables linked to this class.
- * Gets an array of the current class, it subclasses and its ancestors. It then filters that list
- * to those with DB tables
+ * Returns an array of the current class and all its ancestors and children
+ * which have a DB table.
*
- * @param mixed $class string of the classname or instance of the class
+ * @param string|object $class
* @todo Move this into data object
* @return array
*/
- static function dataClassesFor($class) {
- global $_ALL_CLASSES;
- if (is_object($class)) $class = get_class($class);
-
- $dataClasses = array();
-
- if(!$_ALL_CLASSES['parents'][$class]) user_error("ClassInfo::dataClassesFor() no parents for $class", E_USER_WARNING);
- foreach($_ALL_CLASSES['parents'][$class] as $subclass) {
- if(self::hasTable($subclass)) $dataClasses[] = $subclass;
- }
-
- if(self::hasTable($class)) $dataClasses[] = $class;
+ public static function dataClassesFor($class) {
+ $result = array();
- if(isset($_ALL_CLASSES['children'][$class]))
- foreach($_ALL_CLASSES['children'][$class] as $subclass)
- {
- if(self::hasTable($subclass)) $dataClasses[] = $subclass;
+ if (is_object($class)) {
+ $class = get_class($class);
}
-
- return $dataClasses;
+
+ $classes = array_merge(
+ self::ancestry($class),
+ self::subclassesFor($class));
+
+ foreach ($classes as $class) {
+ if (self::hasTable($class)) $result[$class] = $class;
+ }
+
+ return $result;
}
-
+
/**
- * Return the root data class for that class.
- * This root table has a lot of special use in the DataObject system.
- *
- * @param mixed $class string of the classname or instance of the class
- * @return array
+ * Returns the root class (the first to extend from DataObject) for the
+ * passed class.
+ *
+ * @param string|object $class
+ * @return string
*/
- static function baseDataClass($class) {
- global $_ALL_CLASSES;
+ public static function baseDataClass($class) {
if (is_object($class)) $class = get_class($class);
- reset($_ALL_CLASSES['parents'][$class]);
- while($val = next($_ALL_CLASSES['parents'][$class])) {
- if($val == 'DataObject') break;
+
+ if (!self::is_subclass_of($class, 'DataObject')) {
+ throw new Exception("$class is not a subclass of DataObject");
+ }
+
+ while ($next = get_parent_class($class)) {
+ if ($next == 'DataObject') {
+ return $class;
+ }
+
+ $class = $next;
}
- $baseDataClass = next($_ALL_CLASSES['parents'][$class]);
- return $baseDataClass ? $baseDataClass : $class;
}
-
+
/**
* Returns a list of classes that inherit from the given class.
* The resulting array includes the base class passed
@@ -125,38 +123,43 @@ class ClassInfo {
* @param mixed $class string of the classname or instance of the class
* @return array Names of all subclasses as an associative array.
*/
- static function subclassesFor($class){
- global $_ALL_CLASSES;
- if (is_object($class)) $class = get_class($class);
-
- // get all classes from the manifest
- $subclasses = isset($_ALL_CLASSES['children'][$class]) ? $_ALL_CLASSES['children'][$class] : null;
+ public static function subclassesFor($class) {
+ $descendants = SS_ClassLoader::instance()->getManifest()->getDescendantsOf($class);
+ $result = array($class => $class);
- // add the base class to the array
- if(isset($subclasses)) {
- array_unshift($subclasses, $class);
+ if ($descendants) {
+ return $result + ArrayLib::valuekey($descendants);
} else {
- $subclasses[$class] = $class;
+ return $result;
}
-
- return $subclasses;
}
-
+
/**
- * @todo Improve documentation
+ * 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
*/
- static function ancestry($class, $onlyWithTables = false) {
- global $_ALL_CLASSES;
+ public static function ancestry($class, $tablesOnly = false) {
+ $ancestry = array();
- if(is_object($class)) $class = $class->class;
- else if(!is_string($class)) user_error("Bad class value " . var_export($class, true) . " passed to ClassInfo::ancestry()", E_USER_WARNING);
-
- $items = $_ALL_CLASSES['parents'][$class];
- $items[$class] = $class;
- if($onlyWithTables) foreach($items as $item) {
- if(!DataObject::has_own_table($item)) unset($items[$item]);
+ if (is_object($class)) {
+ $class = get_class($class);
+ } elseif (!is_string($class)) {
+ throw new Exception(sprintf(
+ 'Invalid class value %s, must be an object or string', var_export($class, true)
+ ));
}
- return $items;
+
+ do {
+ if (!$tablesOnly || DataObject::has_own_table($class)) {
+ $ancestry[$class] = $class;
+ }
+ } while ($class = get_parent_class($class));
+
+ return array_reverse($ancestry);
}
/**
@@ -164,27 +167,23 @@ class ClassInfo {
* classes and not built-in PHP classes.
*/
static function implementorsOf($interfaceName) {
- global $_ALL_CLASSES;
- return (isset($_ALL_CLASSES['implementors'][$interfaceName])) ? $_ALL_CLASSES['implementors'][$interfaceName] : false;
+ return SS_ClassLoader::instance()->getManifest()->getImplementorsOf($interfaceName);
}
/**
* Returns true if the given class implements the given interface
*/
static function classImplements($className, $interfaceName) {
- global $_ALL_CLASSES;
- return isset($_ALL_CLASSES['implementors'][$interfaceName][$className]);
+ return in_array($className, SS_ClassLoader::instance()->getManifest()->getImplementorsOf($interfaceName));
}
/**
- * Returns true if $subclass is a subclass of $parentClass.
- * Identical to the PHP built-in function, but faster.
+ * @deprecated 3.0 Please use is_subclass_of.
*/
- static function is_subclass_of($subclass, $parentClass) {
- global $_ALL_CLASSES;
- return isset($_ALL_CLASSES['parents'][$subclass][$parentClass]);
+ public static function is_subclass_of($class, $parent) {
+ return is_subclass_of($class, $parent);
}
-
+
/**
* Get all classes contained in a file.
* @uses ManifestBuilder
@@ -196,11 +195,11 @@ class ClassInfo {
* @return array
*/
static function classes_for_file($filePath) {
- $absFilePath = Director::getAbsFile($filePath);
- global $_CLASS_MANIFEST;
-
+ $absFilePath = Director::getAbsFile($filePath);
$matchedClasses = array();
- foreach($_CLASS_MANIFEST as $class => $compareFilePath) {
+ $manifest = SS_ClassLoader::instance()->getManifest()->getClasses();
+
+ foreach($manifest as $class => $compareFilePath) {
if($absFilePath == $compareFilePath) $matchedClasses[] = $class;
}
@@ -217,11 +216,11 @@ class ClassInfo {
* @return array Array of class names
*/
static function classes_for_folder($folderPath) {
- $absFolderPath = Director::getAbsFile($folderPath);
- global $_CLASS_MANIFEST;
-
+ $absFolderPath = Director::getAbsFile($folderPath);
$matchedClasses = array();
- foreach($_CLASS_MANIFEST as $class => $compareFilePath) {
+ $manifest = SS_ClassLoader::instance()->getManifest()->getClasses();
+
+ foreach($manifest as $class => $compareFilePath) {
if(stripos($compareFilePath, $absFolderPath) === 0) $matchedClasses[] = $class;
}
diff --git a/core/Core.php b/core/Core.php
old mode 100755
new mode 100644
index 997003280..a9a523547
--- a/core/Core.php
+++ b/core/Core.php
@@ -199,44 +199,39 @@ set_include_path(BASE_PATH . '/sapphire' . PATH_SEPARATOR
. BASE_PATH . '/sapphire/thirdparty' . PATH_SEPARATOR
. get_include_path());
-/**
- * Sapphire class autoloader. Requires the ManifestBuilder to work.
- * $_CLASS_MANIFEST must have been loaded up by ManifestBuilder for this to successfully load
- * classes. Classes will be loaded from any PHP file within the application.
- * If your class contains an underscore, for example, Page_Controller, then the filename is
- * expected to be the stuff before the underscore. In this case, Page.php.
- *
- * Class names are converted to lowercase for lookup to adhere to PHP's case-insensitive
- * way of dealing with them.
- */
-function sapphire_autoload($className) {
- global $_CLASS_MANIFEST;
- $lClassName = strtolower($className);
- if(isset($_CLASS_MANIFEST[$lClassName])) include_once($_CLASS_MANIFEST[$lClassName]);
- else if(isset($_CLASS_MANIFEST[$className])) include_once($_CLASS_MANIFEST[$className]);
-}
-
-spl_autoload_register('sapphire_autoload');
-
-require_once("core/ManifestBuilder.php");
-require_once("core/ClassInfo.php");
-require_once('core/Object.php');
-require_once('core/control/Director.php');
-require_once('filesystem/Filesystem.php');
-require_once("core/Session.php");
+// Include the files needed the initial manifest building, as well as any files
+// that are needed for the boostrap process on every request.
+require_once 'cache/Cache.php';
+require_once 'core/Object.php';
+require_once 'core/ClassInfo.php';
+require_once 'core/control/Director.php';
+require_once 'dev/Debug.php';
+require_once 'filesystem/FileFinder.php';
+require_once 'manifest/ClassLoader.php';
+require_once 'manifest/ClassManifest.php';
+require_once 'manifest/ManifestFileFinder.php';
+require_once 'manifest/TemplateLoader.php';
+require_once 'manifest/TemplateManifest.php';
+require_once 'manifest/TokenisedRegularExpression.php';
///////////////////////////////////////////////////////////////////////////////
// MANIFEST
-/**
- * Include the manifest
- */
-ManifestBuilder::include_manifest();
+// Regenerate the manifest if ?flush is set, or if the database is being built.
+// The coupling is a hack, but it removes an annoying bug where new classes
+// referenced in _config.php files can be referenced during the build process.
+$flush = (isset($_GET['flush']) || isset($_REQUEST['url']) && (
+ $_REQUEST['url'] == 'dev/build' || $_REQUEST['url'] == BASE_URL . '/dev/build'
+));
+$manifest = new SS_ClassManifest(BASE_PATH, false, $flush);
-/**
- * ?debugmanifest=1 hook
- */
-if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE));
+$loader = SS_ClassLoader::instance();
+$loader->registerAutoloader();
+$loader->pushManifest($manifest);
+
+SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
+ BASE_PATH, false, isset($_GET['flush'])
+));
// If this is a dev site, enable php error reporting
// This is necessary to force developers to acknowledge and fix
@@ -316,16 +311,10 @@ function getTempFolder($base = null) {
}
/**
- * Return the file where that class is stored.
- *
- * @param String $className Case-insensitive lookup.
- * @return String
+ * @deprecated 3.0 Please use {@link SS_ClassManifest::getItemPath()}.
*/
function getClassFile($className) {
- global $_CLASS_MANIFEST;
- $lClassName = strtolower($className);
- if(isset($_CLASS_MANIFEST[$lClassName])) return $_CLASS_MANIFEST[$lClassName];
- else if(isset($_CLASS_MANIFEST[$className])) return $_CLASS_MANIFEST[$className];
+ return SS_ClassLoader::instance()->getManifest()->getItemPath($className);
}
/**
diff --git a/core/ManifestBuilder.php b/core/ManifestBuilder.php
deleted file mode 100644
index 7d1fd7c11..000000000
--- a/core/ManifestBuilder.php
+++ /dev/null
@@ -1,630 +0,0 @@
-Autoloading
- *
- * Sapphire class autoloader. Requires the ManifestBuilder to work.
- * $_CLASS_MANIFEST must have been loaded up by ManifestBuilder for this to successfully load classes.
- * Classes will be loaded from any PHP file within the application. If your class contains an underscore,
- * for example, Page_Controller, then the filename is expected to be the stuff before the underscore.
- * In this case, Page.php.
- *
- * @see main.php, __autoload(), SSViewer, Requirements::themedCSS()
- * @package sapphire
- * @subpackage core
- */
-class ManifestBuilder {
-
- static $restrict_to_modules = array();
- static $extendsArray = array();
- static $classArray = array();
- static $implementsArray = array();
-
- /**
- * @var array $ignore_files Full filenames (without directory-path) which
- * should be ignored by the manifest.
- */
- public static $ignore_files = array(
- 'main.php',
- 'cli-script.php',
- 'install.php',
- 'index.php',
- );
-
- /**
- * @var array $ignore_folders Foldernames (without path) which
- * should be ignored by the manifest.
- */
- public static $ignore_folders = array(
- 'mysql',
- 'assets',
- 'shortstat',
- 'HTML',
- );
-
- /**
- * Include the manifest, regenerating it if necessary
- */
- static function include_manifest() {
- if(isset($_REQUEST['usetestmanifest'])) {
- self::load_test_manifest();
- } else {
- // The dev/build reference is some coupling but it solves an annoying bug
- if(!file_exists(MANIFEST_FILE) || (filemtime(MANIFEST_FILE) < filemtime(BASE_PATH))
- || isset($_GET['flush']) || (isset($_REQUEST['url']) && ($_REQUEST['url'] == 'dev/build'
- || $_REQUEST['url'] == BASE_URL . '/dev/build'))) {
- self::create_manifest_file();
- }
- require_once(MANIFEST_FILE);
- }
- }
-
- /**
- * Load a copy of the manifest with tests/ folders included.
- * Only loads the ClassInfo and __autoload() globals; this assumes that _config.php files are already included.
- */
- static function load_test_manifest() {
- $testManifestFile = MANIFEST_FILE . '-test';
-
- // The dev/build reference is some coupling but it solves an annoying bug
- if(!file_exists($testManifestFile)
- || (filemtime($testManifestFile) < filemtime(BASE_PATH))
- || isset($_GET['flush'])) {
-
- // Build the manifest, including the tests/ folders
- $manifestInfo = self::get_manifest_info(BASE_PATH);
- $manifest = self::generate_php_file($manifestInfo);
- if($fh = fopen($testManifestFile, 'wb')) {
- fwrite($fh, $manifest);
- fclose($fh);
- } else {
- user_error("Cannot write manifest file! Check permissions of " . MANIFEST_FILE, E_USER_ERROR);
- }
- }
-
- require($testManifestFile);
- }
-
- /**
- * Loads all PHP class files - actually opening them and executing them.
- */
- static function load_all_classes() {
- global $_CLASS_MANIFEST;
- foreach($_CLASS_MANIFEST as $classFile) require_once($classFile);
- }
-
- /**
- * Generates a new manifest file and saves it to {@link MANIFEST_FILE}.
- */
- static function create_manifest_file() {
- // Build the manifest, ignoring the tests/ folders
- $manifestInfo = self::get_manifest_info(BASE_PATH, array("tests"));
-
- $manifest = self::generate_php_file($manifestInfo);
- if($fh = fopen(MANIFEST_FILE, 'wb')) {
- fwrite($fh, $manifest);
- fclose($fh);
- } else {
- user_error("Cannot write manifest file! Check permissions of " . MANIFEST_FILE, E_USER_ERROR);
- }
- }
-
- /**
- * Turn an array produced by get_manifest_info() into the content of the manifest PHP include
- */
- static function generate_php_file($manifestInfo) {
- $output = " $globalVal) {
- $output .= "global \$$globalName;\n\$$globalName = " . var_export($globalVal, true) . ";\n\n";
- }
- foreach($manifestInfo['require_once'] as $requireItem) {
- $output .= 'require_once("' . addslashes($requireItem) . "\");\n";
- }
-
- return $output;
- }
-
-
- /**
- * Parse the $manifestInfo array, updating the appropriate globals and loading the appropriate _config files.
- */
- static function process_manifest($manifestInfo) {
- foreach($manifestInfo['globals'] as $globalName => $globalVal) {
- global $$globalName;
- $$globalName = $globalVal;
- }
- foreach($manifestInfo['require_once'] as $requireItem) {
- require_once("$requireItem");
- }
- }
-
- /**
- * Get themes from a particular directory.
- *
- * @param string $baseDir Optional: Absolute path to theme directory for testing e.g. "/Users/sharvey/Sites/test24/themes"
- * @param boolean $includeSubThemes If set to TRUE, sub-themes such as "blackcandy_blog" are included too
- * @return array Listing of theme directories
- */
- public static function get_themes($baseDir = null, $includeSubThemes = false) {
- // If no base directory specified, the default is the project root
- if(!$baseDir) $baseDir = BASE_PATH . DIRECTORY_SEPARATOR . THEMES_DIR;
- $themes = array();
- if(!file_exists($baseDir)) return $themes;
-
- $handle = opendir($baseDir);
- if($handle) {
- while(false !== ($file = readdir($handle))) {
- $fullPath = $baseDir . DIRECTORY_SEPARATOR . $file;
- if(strpos($file, '.') === false && is_dir($fullPath)) {
- $include = $includeSubThemes ? true : false;
- if(strpos($file, '_') === false) {
- $include = true;
- }
- if($include) $themes[$file] = $file;
- }
- }
- closedir($handle);
- }
- return $themes;
- }
-
- /**
- * Return an array containing information for the manifest
- * @param $baseDir The root directory to analyse
- * @param $excludedFolders An array folder names to exclude. These don't care about where the
- * folder appears in the hierarchy, so be careful
- */
- static function get_manifest_info($baseDir, $excludedFolders = array()) {
- // locate and include the exclude files
- $topLevel = scandir($baseDir);
- foreach($topLevel as $file) {
- if($file[0] == '.') continue
-
- $fullPath = '';
- $fullPath = $baseDir . '/' . $file;
-
- if(@is_dir($fullPath . '/') && file_exists($fullPath . '/_exclude.php')) {
- require_once($fullPath . '/_exclude.php');
- }
- }
-
- // Project - used to give precedence to template files
- $project = null;
-
- // Class, CSS, template manifest
- $allPhpFiles = array();
- $templateManifest = array();
- $cssManifest = array();
-
-
- if(is_array(self::$restrict_to_modules) && count(self::$restrict_to_modules)) {
- // $restrict_to_modules is set, so we include only those specified
- // modules
- foreach(self::$restrict_to_modules as $module)
- ManifestBuilder::get_all_php_files($baseDir . '/' . $module, $excludedFolders, $allPhpFiles);
- } else {
- // Include all directories which have an _config.php file but don't
- // have an _manifest_exclude file
- $topLevel = scandir($baseDir);
- foreach($topLevel as $filename) {
-
- // Skip certain directories
- if($filename[0] == '.') continue;
- if($filename == THEMES_DIR) continue;
- if($filename == ASSETS_DIR) continue;
- if(in_array($filename, $excludedFolders)) continue;
-
- if(@is_dir("$baseDir/$filename") &&
- file_exists("$baseDir/$filename/_config.php") &&
- !file_exists("$baseDir/$filename/_manifest_exclude")) {
-
- // Get classes, templates, and CSS files
- ManifestBuilder::get_all_php_files("$baseDir/$filename", $excludedFolders, $allPhpFiles);
- ManifestBuilder::getTemplateManifest($baseDir, $filename, $excludedFolders, $templateManifest, $cssManifest);
-
- // List the _config.php files
- $manifestInfo["require_once"][] = "$baseDir/$filename/_config.php";
- // Find the $project variable in the relevant config file without having to execute the config file
- if(preg_match("/\\\$project\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("$baseDir/$filename/_config.php"), $parts)) {
- eval($parts[0]);
- }
- }
- }
- }
-
- // Get themes
- if(file_exists("$baseDir/themes")) {
- $themeDirs = self::get_themes("$baseDir/themes", true);
- foreach($themeDirs as $themeDir) {
- $themeName = strtok($themeDir, '_');
- ManifestBuilder::getTemplateManifest($baseDir, THEMES_DIR . "/$themeDir", $excludedFolders, $templateManifest, $cssManifest, $themeName);
- }
- }
-
- // Build class-info array from class manifest
- $allClasses = ManifestBuilder::allClasses($allPhpFiles);
-
- // Pull the class filenames out
- $classManifest = $allClasses['file'];
- unset($allClasses['file']);
-
- // Ensure that any custom templates get favoured
- if(!$project) user_error("\$project isn't set", E_USER_WARNING);
- else if(!file_exists("$baseDir/$project")) user_error("\$project is set to '$project' but no such folder exists.", E_USER_WARNING);
- else ManifestBuilder::getTemplateManifest($baseDir, $project, $excludedFolders, $templateManifest, $cssManifest);
-
- $manifestInfo["globals"]["_CLASS_MANIFEST"] = $classManifest;
- $manifestInfo["globals"]["_ALL_CLASSES"] = $allClasses;
- $manifestInfo["globals"]["_TEMPLATE_MANIFEST"] = $templateManifest;
- $manifestInfo["globals"]["_CSS_MANIFEST"] = $cssManifest;
-
- return $manifestInfo;
- }
-
-
- /**
- * Generates a list of all the PHP files that should be analysed by the manifest builder.
- *
- * @param string $folder The folder to traverse (recursively)
- * @param array $classMap The already built class map
- */
- private static function get_all_php_files($folder, $excludedFolders, &$allPhpFiles) {
- $items = scandir($folder);
- if($items) foreach($items as $item) {
- // Skip some specific PHP files
- if(in_array($item, self::$ignore_files)) continue;
-
- // ignore hidden files and folders
- if(substr($item,0,1) == '.') continue;
-
- // ignore files without php-extension
- if(substr($item,-4) != '.php' && !@is_dir("$folder/$item")) continue;
-
- // ignore files and folders with underscore-prefix
- if(substr($item,0,1) == '_') continue;
-
- // ignore certain directories
- if(@is_dir("$folder/$item") && in_array($item, self::$ignore_folders)) continue;
-
- // ignore directories with _manifest_exlude file
- if(@is_dir("$folder/$item") && file_exists("$folder/$item/_manifest_exclude")) continue;
-
- // i18n: ignore language files (loaded on demand)
- if($item == 'lang' && @is_dir("$folder/$item") && ereg_replace("/[^/]+/\\.\\.","",$folder.'/..') == Director::baseFolder()) continue;
-
- if(@is_dir("$folder/$item")) {
- // Folder exclusion - used to skip over tests/ folders
- if(in_array($item, $excludedFolders)) continue;
-
- // recurse into directories (if not in $ignore_folders)
- ManifestBuilder::get_all_php_files("$folder/$item", $excludedFolders, $allPhpFiles);
- } else {
- $allPhpFiles[] = "$folder/$item";
- }
-
- }
- }
-
-
- /**
- * Generates the template manifest - a list of all the .ss files in the
- * application.
- *
- * See {@link SSViewer} for an overview on the array structure this class creates.
- *
- * @param String $baseDir
- * @param String $folder
- */
- private static function getTemplateManifest($baseDir, $folder, $excludedFolders, &$templateManifest, &$cssManifest, $themeName = null) {
- $items = scandir("$baseDir/$folder");
- if($items) foreach($items as $item) {
- // Skip hidden files/folders
- if(substr($item,0,1) == '.') continue;
-
- // Parse *.ss files
- if(substr($item,-3) == '.ss') {
- // Remove extension from template name
- $templateName = substr($item, 0, -3);
-
- // The "type" is effectively a subfolder underneath $folder,
- // mostly "Includes" or "Layout".
- $templateType = substr($folder,strrpos($folder,'/')+1);
- // The parent folder counts as type "main"
- if($templateType == "templates") $templateType = "main";
-
- // Write either to theme or to non-themed array
- if($themeName) {
- $templateManifest[$templateName]['themes'][$themeName][$templateType] = "$baseDir/$folder/$item";
- } else {
- $templateManifest[$templateName][$templateType] = "$baseDir/$folder/$item";
- }
-
- } else if(substr($item,-4) == '.css') {
- $cssName = substr($item, 0, -4);
- // Debug::message($item);
-
- if($themeName) {
- $cssManifest[$cssName]['themes'][$themeName] = "$folder/$item";
- } else {
- $cssManifest[$cssName]['unthemed'] = "$folder/$item";
- }
-
-
- } else if(@is_dir("$baseDir/$folder/$item")) {
- // Folder exclusion - used to skip over tests/ folders
- if(in_array($item, $excludedFolders)) continue;
-
- ManifestBuilder::getTemplateManifest($baseDir, "$folder/$item", $excludedFolders, $templateManifest, $cssManifest, $themeName);
- }
- }
- }
-
-
- /**
- * Include everything, so that actually *all* classes are available and
- * build a map of classes and their subclasses
- *
- * @param $classManifest An array of all Sapphire classes; keys are class names and values are filenames
- *
- * @return array Returns an array that holds all class relevant
- * information.
- */
- private static function allClasses($classManifest) {
- self::$classArray = array();
- self::$extendsArray = array();
- self::$implementsArray = array();
-
- // Include everything, so we actually have *all* classes
- foreach($classManifest as $file) {
- $b = basename($file);
- if($b != 'cli-script.php' && $b != 'main.php')
- self::parse_file($file);
- }
-
- $allClasses["parents"] = self::find_parents();
- $allClasses["children"] = self::find_children();
- $allClasses["implementors"] = self::$implementsArray;
-
- foreach(self::$classArray as $class => $info) {
- $allClasses['exists'][$class] = $class;
- // Class names are converted to lowercase for lookup to adhere to PHP's case-insensitive
- // way of dealing with them.
- $allClasses['file'][strtolower($class)] = $info['file'];
- }
-
- // Build a map of classes and their subclasses
- $_classes = get_declared_classes();
-
- foreach($_classes as $class) {
- $allClasses['exists'][$class] = $class;
- foreach($_classes as $subclass) {
- if(is_subclass_of($class, $subclass)) $allClasses['parents'][$class][$subclass] = $subclass;
- if(is_subclass_of($subclass, $class)) $allClasses['children'][$class][$subclass] = $subclass;
- }
- }
-
- return $allClasses;
- }
-
- /**
- * Parses a php file and adds any class or interface information into self::$classArray
- *
- * @param string $filename
- */
- private static function parse_file($filename) {
- $file = file_get_contents($filename);
-
- $implements = "";
- $extends = "";
- $class="";
-
- if($file === null) user_error("ManifestBuilder::parse_file(): Couldn't open $filename", E_USER_ERROR);
- if(!$file) return;
-
- // We cache the parse results of each file, since only a few files will have changed between flushings
- // And, although it's accurate, TokenisedRegularExpression isn't particularly fast.
- // We use an MD5 of the file as a part of the cache key because using datetime caused problems when users
- // were upgrading their sites
- $fileMD5 = md5($file);
- $parseCacheFile = TEMP_FOLDER . "/manifestClassParse-" . str_replace(array("/", ":", "\\", "."), "_", basename($filename)) . "-$fileMD5";
- if(file_exists($parseCacheFile)) {
- include($parseCacheFile);
- // Check for a bad cache file
- if(!isset($classes) || !isset($interfaces) || !is_array($classes) || !is_array($interfaces)) {
- unset($classes);
- unset($interfaces);
- }
- }
-
- // Either the parseCacheFile doesn't exist, or its bad
- if(!isset($classes)) {
- $tokens = token_get_all($file);
- $classes = (array)self::getClassDefParser()->findAll($tokens);
- $interfaces = (array)self::getInterfaceDefParser()->findAll($tokens);
-
- $cacheContent = ' T_CLASS,
- 1 => T_WHITESPACE,
- 2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'),
- 3 => T_WHITESPACE,
- 4 => T_EXTENDS,
- 5 => T_WHITESPACE,
- 6 => array(T_STRING, 'save_to' => 'extends', 'can_jump_to' => 14),
- 7 => T_WHITESPACE,
- 8 => T_IMPLEMENTS,
- 9 => 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),
- 13 => array(T_WHITESPACE, 'can_jump_to' => 10),
- 14 => array(T_WHITESPACE, 'optional' => true),
- 15 => '{',
- ));
- }
-
- /**
- * Returns a {@link TokenisedRegularExpression} object that will parse class definitions
- * @return TokenisedRegularExpression
- */
- public static function getInterfaceDefParser() {
- require_once('core/TokenisedRegularExpression.php');
-
- return new TokenisedRegularExpression(array(
- 0 => T_INTERFACE,
- 1 => T_WHITESPACE,
- 2 => array(T_STRING, 'can_jump_to' => 7, 'save_to' => 'interfaceName'),
- 3 => T_WHITESPACE,
- 4 => T_EXTENDS,
- 5 => T_WHITESPACE,
- 6 => array(T_STRING, 'save_to' => 'extends'),
- 7 => array(T_WHITESPACE, 'optional' => true),
- 8 => '{',
- ));
- }
-
-
- /**
- * Moves through self::$classArray and creates an array containing parent data
- *
- * @return array
- */
- private static function find_parents() {
- $parentArray = array();
- foreach(self::$classArray as $class => $info) {
- $extendArray = array();
-
- $parent = $info["extends"];
-
- while($parent) {
- $extendArray[$parent] = $parent;
- $parent = isset(self::$classArray[$parent]["extends"]) ? self::$classArray[$parent]["extends"] : null;
- }
- $parentArray[$class] = array_reverse($extendArray);
- }
- return $parentArray;
- }
-
- /**
- * Iterates through self::$classArray and returns an array with any descendant data
- *
- * @return array
- */
- private static function find_children() {
- $childrenArray = array();
- foreach(self::$extendsArray as $class => $children) {
- $allChildren = $children;
- foreach($children as $childName) {
- $allChildren = array_merge($allChildren, self::up_children($childName));
- }
- $childrenArray[$class] = $allChildren;
- }
- return $childrenArray;
- }
-
- /**
- * Helper function to find all children of give class
- *
- * @param string $class
- * @return array
- */
- private static function get_children($class) {
- return isset(self::$extendsArray[$class]) ? self::$extendsArray[$class] : array();
- }
-
- /**
- * Returns if the Manifest has been included
- *
- * @return Boolean
- */
- static function has_been_included() {
- global $_CLASS_MANIFEST, $_TEMPLATE_MANIFEST, $_CSS_MANIFEST, $_ALL_CLASSES;
- return (bool)!(empty($_CLASS_MANIFEST) && empty($_TEMPLATE_MANIFEST) && empty($_CSS_MANIFEST) && empty($_ALL_CLASSES));
- }
-
- /**
- * Returns a flat array with all children of a given class
- *
- * @param string $class
- * @param array $results
- */
- static function up_children($class) {
- $children = self::get_Children($class);
- $results = $children;
- foreach($children as $className) {
- $results = array_merge($results, self::up_children($className));
- }
- return $results;;
- }
-}
diff --git a/core/Object.php b/core/Object.php
old mode 100755
new mode 100644
index 2e49fbd76..24efb1c9b
--- a/core/Object.php
+++ b/core/Object.php
@@ -513,7 +513,7 @@ abstract class Object {
user_error(sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass), E_USER_ERROR);
}
- if(!ClassInfo::is_subclass_of($extensionClass, 'Extension')) {
+ if(!is_subclass_of($extensionClass, 'Extension')) {
user_error(sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass), E_USER_ERROR);
}
@@ -537,7 +537,7 @@ abstract class Object {
self::set_static($class, 'extensions', $extensions);
// load statics now for DataObject classes
- if(ClassInfo::is_subclass_of($class, 'DataObject')) {
+ if(is_subclass_of($class, 'DataObject')) {
if(is_subclass_of($extensionClass, 'DataObjectDecorator')) {
DataObjectDecorator::load_extra_statics($class, $extension);
}
diff --git a/core/Requirements.php b/core/Requirements.php
index 9e9f38728..599091a57 100644
--- a/core/Requirements.php
+++ b/core/Requirements.php
@@ -143,15 +143,24 @@ class Requirements {
}
/**
- * Register the given "themeable stylesheet" as required. See {@link Requirements_Backend::themedCSS()}
- *
- * @param $name String The identifier of the file. For example, css/MyFile.css would have the identifier "MyFile"
- * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
+ * Registers the given themeable stylesheet as required.
+ *
+ * A CSS file in the current theme path name "themename/css/$name.css" is
+ * first searched for, and it that doesn't exist and the module parameter is
+ * set then a CSS file with that name in the module is used.
+ *
+ * NOTE: This API is experimental and may change in the future.
+ *
+ * @param string $name The name of the file - e.g. "/css/File.css" would have
+ * the name "File".
+ * @param string $module The module to fall back to if the css file does not
+ * exist in the current theme.
+ * @param string $media The CSS media attribute.
*/
- static function themedCSS($name, $media = null) {
- return self::backend()->themedCSS($name, $media);
+ public static function themedCSS($name, $module = null, $media = null) {
+ return self::backend()->themedCSS($name, $module, $media);
}
-
+
/**
* Clear either a single or all requirements.
* Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}.
@@ -1044,27 +1053,22 @@ class Requirements_Backend {
return $requirements;
}
-
- /**
- * Register the given "themeable stylesheet" as required.
- * Themeable stylesheets have globally unique names, just like templates and PHP files.
- * Because of this, they can be replaced by similarly named CSS files in the theme directory.
- *
- * @param $name String The identifier of the file. For example, css/MyFile.css would have the identifier "MyFile"
- * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
- */
- function themedCSS($name, $media = null) {
- global $_CSS_MANIFEST;
-
- $theme = SSViewer::current_theme();
-
- if($theme && isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['themes'])
- && isset($_CSS_MANIFEST[$name]['themes'][$theme]))
- $this->css($_CSS_MANIFEST[$name]['themes'][$theme], $media);
- else if(isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['unthemed'])) $this->css($_CSS_MANIFEST[$name]['unthemed'], $media);
- // Normal requirements fails quietly when there is no css - we should do the same
- // else user_error("themedCSS - No CSS file '$name.css' found.", E_USER_WARNING);
+ /**
+ * @see Requirements::themedCSS()
+ */
+ public function themedCSS($name, $module = null, $media = null) {
+ $theme = SSViewer::current_theme();
+ $path = SSViewer::get_theme_folder() . "/css/$name.css";
+
+ if (file_exists(BASE_PATH . '/' . $path)) {
+ $this->css($path, $media);
+ return;
+ }
+
+ if ($module) {
+ $this->css("$module/css/$name.css");
+ }
}
function debug() {
diff --git a/core/SSViewer.php b/core/SSViewer.php
old mode 100755
new mode 100644
index 506c78e32..72a6ef8db
--- a/core/SSViewer.php
+++ b/core/SSViewer.php
@@ -182,46 +182,10 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
*
* Compiled templates are cached via {@link SS_Cache}, usually on the filesystem.
* If you put ?flush=all on your URL, it will force the template to be recompiled.
- *
- * Manifest File and Structure
- *
- * Works with the global $_TEMPLATE_MANIFEST which is compiled by {@link ManifestBuilder->getTemplateManifest()}.
- * This associative array lists all template filepaths by "identifier", meaning the name
- * of the template without its path or extension.
- *
- * Example:
- *
- * array(
- * 'LeftAndMain' =>
- * array (
- * 'main' => '/my/system/path/cms/templates/LeftAndMain.ss',
- * ),
- * 'CMSMain_left' =>
- * array (
- * 'Includes' => '/my/system/path/cms/templates/Includes/CMSMain_left.ss',
- * ),
- * 'Page' =>
- * array (
- * 'themes' =>
- * array (
- * 'blackcandy' =>
- * array (
- * 'Layout' => '/my/system/path/themes/blackcandy/templates/Layout/Page.ss',
- * 'main' => '/my/system/path/themes/blackcandy/templates/Page.ss',
- * ),
- * 'blue' =>
- * array (
- * 'Layout' => '/my/system/path/themes/mysite/templates/Layout/Page.ss',
- * 'main' => '/my/system/path/themes/mysite/templates/Page.ss',
- * ),
- * ),
- * ),
- * // ...
- * )
- *
*
* @see http://doc.silverstripe.org/themes
* @see http://doc.silverstripe.org/themes:developing
+
*
* @package sapphire
* @subpackage view
@@ -306,6 +270,30 @@ class SSViewer {
return self::current_theme() ? THEMES_DIR . "/" . self::current_theme() : project();
}
+ /**
+ * Returns an array of theme names present in a directory.
+ *
+ * @param string $path
+ * @param bool $subthemes Include subthemes (default false).
+ * @return array
+ */
+ public static function get_themes($path = null, $subthemes = false) {
+ $path = rtrim($path ? $path : THEMES_PATH, '/');
+ $themes = array();
+
+ if (!is_dir($path)) return $themes;
+
+ foreach (scandir($path) as $item) {
+ if ($item[0] != '.' && is_dir("$path/$item")) {
+ if ($subthemes || !strpos($item, '_')) {
+ $themes[$item] = $item;
+ }
+ }
+ }
+
+ return $themes;
+ }
+
/**
* @return string
*/
@@ -322,8 +310,6 @@ class SSViewer {
*
*/
public function __construct($templateList) {
- global $_TEMPLATE_MANIFEST;
-
// flush template manifest cache if requested
if (isset($_GET['flush']) && $_GET['flush'] == 'all') {
if(Director::isDev() || Director::is_cli() || Permission::check('ADMIN')) {
@@ -336,64 +322,25 @@ class SSViewer {
if(substr((string) $templateList,-3) == '.ss') {
$this->chosenTemplates['main'] = $templateList;
} else {
- if(!is_array($templateList)) $templateList = array($templateList);
-
- if(isset($_GET['debug_request'])) Debug::message("Selecting templates from the following list: " . implode(", ", $templateList));
-
- foreach($templateList as $template) {
- // if passed as a partial directory (e.g. "Layout/Page"), split into folder and template components
- if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2);
- else $templateFolder = null;
-
- // Use the theme template if available
- if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()])) {
- $this->chosenTemplates = array_merge(
- $_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()],
- $this->chosenTemplates
- );
-
- if(isset($_GET['debug_request'])) Debug::message("Found template '$template' from main theme '" . self::current_theme() . "': " . var_export($_TEMPLATE_MANIFEST[$template]['themes'][self::current_theme()], true));
- }
-
- // Fall back to unthemed base templates
- if(isset($_TEMPLATE_MANIFEST[$template]) && (array_keys($_TEMPLATE_MANIFEST[$template]) != array('themes'))) {
- $this->chosenTemplates = array_merge(
- $_TEMPLATE_MANIFEST[$template],
- $this->chosenTemplates
- );
-
- if(isset($_GET['debug_request'])) Debug::message("Found template '$template' from main template archive, containing the following items: " . var_export($_TEMPLATE_MANIFEST[$template], true));
-
- unset($this->chosenTemplates['themes']);
- }
-
- if($templateFolder) {
- $this->chosenTemplates['main'] = $this->chosenTemplates[$templateFolder];
- unset($this->chosenTemplates[$templateFolder]);
- }
- }
-
- if(isset($_GET['debug_request'])) Debug::message("Final template selections made: " . var_export($this->chosenTemplates, true));
-
+ $this->chosenTemplates = SS_TemplateLoader::instance()->findTemplates(
+ $templateList, self::current_theme()
+ );
}
if(!$this->chosenTemplates) user_error("None of these templates can be found in theme '"
. self::current_theme() . "': ". implode(".ss, ", $templateList) . ".ss", E_USER_WARNING);
-
}
/**
* Returns true if at least one of the listed templates exists
*/
- static function hasTemplate($templateList) {
- if(!is_array($templateList)) $templateList = array($templateList);
-
- global $_TEMPLATE_MANIFEST;
- foreach($templateList as $template) {
- if(strpos($template,'/') !== false) list($templateFolder, $template) = explode('/', $template, 2);
- if(isset($_TEMPLATE_MANIFEST[$template])) return true;
+ public static function hasTemplate($templates) {
+ $manifest = SS_TemplateLoader::instance()->getManifest();
+
+ foreach ((array) $templates as $template) {
+ if ($manifest->getTemplate($template)) return true;
}
-
+
return false;
}
@@ -433,76 +380,21 @@ class SSViewer {
public function exists() {
return $this->chosenTemplates;
}
-
- /**
- * Searches for a template name in the current theme:
- * - themes/mytheme/templates
- * - themes/mytheme/templates/Includes
- * Falls back to unthemed template files.
- *
- * Caution: Doesn't search in any /Layout folders.
- *
- * @param string $identifier A template name without '.ss' extension or path.
- * @return string Full system path to a template file
- */
- public static function getTemplateFile($identifier) {
- global $_TEMPLATE_MANIFEST;
-
- $includeTemplateFile = self::getTemplateFileByType($identifier, 'Includes');
- if($includeTemplateFile) return $includeTemplateFile;
-
- $mainTemplateFile = self::getTemplateFileByType($identifier, 'main');
- if($mainTemplateFile) return $mainTemplateFile;
-
- return false;
- }
-
+
/**
* @param string $identifier A template name without '.ss' extension or path
* @param string $type The template type, either "main", "Includes" or "Layout"
* @return string Full system path to a template file
*/
public static function getTemplateFileByType($identifier, $type) {
- global $_TEMPLATE_MANIFEST;
- if(self::current_theme() && isset($_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type])) {
- return $_TEMPLATE_MANIFEST[$identifier]['themes'][self::current_theme()][$type];
- } else if(isset($_TEMPLATE_MANIFEST[$identifier][$type])){
- return $_TEMPLATE_MANIFEST[$identifier][$type];
- } else {
- return false;
+ $loader = SS_TemplateLoader::instance();
+ $found = $loader->findTemplates("$type/$identifier", self::current_theme());
+
+ if ($found) {
+ return $found['main'];
}
}
-
- /**
- * Used by <% include Identifier %> statements to get the full
- * unparsed content of a template file.
- *
- * @uses getTemplateFile()
- * @param string $identifier A template name without '.ss' extension or path.
- * @return string content of template
- */
- public static function getTemplateContent($identifier) {
- if(!SSViewer::getTemplateFile($identifier)) {
- return null;
- }
-
- $content = file_get_contents(SSViewer::getTemplateFile($identifier));
- // $content = "". $content;
- // Adds an i18n namespace to all _t(...) calls without an existing one
- // to avoid confusion when using the include in different contexts.
- // Entities without a namespace are deprecated, but widely used.
- $content = ereg_replace('<' . '% +_t\((\'([^\.\']*)\'|"([^\."]*)")(([^)]|\)[^ ]|\) +[^% ])*)\) +%' . '>', '= _t(\''. $identifier . '.ss' . '.\\2\\3\'\\4) ?>', $content);
-
- // Remove UTF-8 byte order mark
- // This is only necessary if you don't have zend-multibyte enabled.
- if(substr($content, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
- $content = substr($content, 3);
- }
-
- return $content;
- }
-
/**
* @ignore
*/
diff --git a/core/control/ContentController.php b/core/control/ContentController.php
old mode 100755
new mode 100644
index a2d14f7bd..afa1de841
--- a/core/control/ContentController.php
+++ b/core/control/ContentController.php
@@ -230,7 +230,7 @@ class ContentController extends Controller {
$hasOnes = $this->dataRecord->has_one();
if(!$hasOnes) return false;
foreach($hasOnes as $hasOneName => $hasOneClass) {
- if($hasOneClass == 'WidgetArea' || ClassInfo::is_subclass_of($hasOneClass, 'WidgetArea')) {
+ if($hasOneClass == 'WidgetArea' || is_subclass_of($hasOneClass, 'WidgetArea')) {
$widgetAreaRelations[] = $hasOneName;
}
}
diff --git a/core/control/HTTPRequest.php b/core/control/HTTPRequest.php
old mode 100755
new mode 100644
index 6756709f0..c82043ab3
--- a/core/control/HTTPRequest.php
+++ b/core/control/HTTPRequest.php
@@ -348,7 +348,7 @@ class SS_HTTPRequest implements ArrayAccess {
if($varRequired && !isset($this->dirParts[$i])) return false;
$arguments[$varName] = isset($this->dirParts[$i]) ? $this->dirParts[$i] : null;
- if($part == '$Controller' && (!ClassInfo::exists($arguments['Controller']) || !ClassInfo::is_subclass_of($arguments['Controller'], 'Controller'))) {
+ if($part == '$Controller' && (!ClassInfo::exists($arguments['Controller']) || !is_subclass_of($arguments['Controller'], 'Controller'))) {
return false;
}
diff --git a/core/i18n.php b/core/i18n.php
old mode 100755
new mode 100644
index 409d12b2d..cc95e30a8
--- a/core/i18n.php
+++ b/core/i18n.php
@@ -1680,41 +1680,25 @@ class i18n extends Object {
}
/**
- * Given a file name (a php class name, without the .php ext, or a template name, including the .ss extension)
- * this helper function determines the module where this file is located
+ * Given a PHP class name, finds the module where it's located.
*
- * @param string $name php class name or template file name (including *.ss extension)
- * @return string Module where the file is located
+ * @param string $name
+ * @return string
*/
public static function get_owner_module($name) {
- // if $name is a template file
- if(substr($name,-3) == '.ss') {
- global $_TEMPLATE_MANIFEST;
- $templateManifest = $_TEMPLATE_MANIFEST[substr($name,0,-3)];
- if(is_array($templateManifest) && isset($templateManifest['themes'])) {
- $absolutePath = $templateManifest['themes'][SSViewer::current_theme()];
- } else {
- $absolutePath = $templateManifest;
- }
-
- $path = str_replace('\\','/',Director::makeRelative(current($absolutePath)));
-
- ereg('/([^/]+)/',$path,$module);
- }
- // $name is assumed to be a PHP class
- else {
- global $_CLASS_MANIFEST;
- if(strpos($name,'_') !== false) $name = strtok($name,'_');
- $name = strtolower($name); // Necessary because of r101131
- if(isset($_CLASS_MANIFEST[$name])) {
- $path = str_replace('\\','/',Director::makeRelative($_CLASS_MANIFEST[$name]));
- ereg('/([^/]+)/', $path, $module);
- }
- }
- return (isset($module)) ? $module[1] : false;
+ $manifest = SS_ClassLoader::instance()->getManifest();
+ $path = $manifest->getItemPath($name);
+ if (!$path) {
+ return false;
+ }
+
+ $path = Director::makeRelative($path);
+ $path = str_replace('\\', '/', $path);
+
+ return substr($path, 0, strpos($path, '/'));
}
-
+
/**
* Validates a "long" locale format (e.g. "en_US")
* by checking it against {@link $all_locales}.
diff --git a/core/model/DataObject.php b/core/model/DataObject.php
index 80eb7c8fe..2ca99fc1a 100755
--- a/core/model/DataObject.php
+++ b/core/model/DataObject.php
@@ -2605,8 +2605,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Get the tables to join to
$tableClasses = ClassInfo::dataClassesFor($this->class);
if(!$tableClasses) {
- if(!ManifestBuilder::has_been_included()) {
- user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR);
+ if (!DB::getConn()) {
+ throw new Exception('DataObjects have been requested before'
+ . ' a DB connection has been made. Please ensure you'
+ . ' are not querying the database in _config.php.');
} else {
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR);
}
diff --git a/core/model/DatabaseAdmin.php b/core/model/DatabaseAdmin.php
index fc62b3810..53f47da66 100644
--- a/core/model/DatabaseAdmin.php
+++ b/core/model/DatabaseAdmin.php
@@ -91,9 +91,8 @@ class DatabaseAdmin extends Controller {
increase_time_limit_to(600);
// Get all our classes
- ManifestBuilder::create_manifest_file();
- require(MANIFEST_FILE);
-
+ SS_ClassLoader::instance()->getManifest()->regenerate();
+
if(isset($_GET['returnURL'])) {
echo "
Setting up the database; you will be returned to your site shortly....
"; $this->doBuild(true); diff --git a/core/model/SiteConfig.php b/core/model/SiteConfig.php index 02fb87efd..72b8c60f1 100644 --- a/core/model/SiteConfig.php +++ b/core/model/SiteConfig.php @@ -116,7 +116,7 @@ class SiteConfig extends DataObject implements PermissionProvider { * @return array of theme directory names */ public function getAvailableThemes($baseDir = null) { - $themes = ManifestBuilder::get_themes($baseDir); + $themes = SSViewer::get_themes($baseDir); foreach(self::$disabled_themes as $theme) { if(isset($themes[$theme])) unset($themes[$theme]); } diff --git a/core/model/Versioned.php b/core/model/Versioned.php old mode 100755 new mode 100644 index e1d47c242..f564f4a50 --- a/core/model/Versioned.php +++ b/core/model/Versioned.php @@ -479,7 +479,7 @@ class Versioned extends DataObjectDecorator { */ function canBeVersioned($table) { return ClassInfo::exists($table) - && ClassInfo::is_subclass_of($table, 'DataObject') + && is_subclass_of($table, 'DataObject') && DataObject::has_own_table($table); } diff --git a/dev/CodeViewer.php b/dev/CodeViewer.php index 29390bc41..d02e27eab 100644 --- a/dev/CodeViewer.php +++ b/dev/CodeViewer.php @@ -87,7 +87,7 @@ class CodeViewer extends Controller { parent::init(); if(!Permission::check('ADMIN')) return Security::permissionFailure(); - ManifestBuilder::load_test_manifest(); + TestRunner::use_test_manifest(); } public function browse() { diff --git a/dev/ModelViewer.php b/dev/ModelViewer.php index ebf88f570..e23857ee8 100644 --- a/dev/ModelViewer.php +++ b/dev/ModelViewer.php @@ -128,11 +128,12 @@ class ModelViewer_Model extends ViewableData { } function getModule() { - global $_CLASS_MANIFEST; + $classes = SS_ClassLoader::instance()->getManifest()->getClasses(); $className = strtolower($this->className); + if(($pos = strpos($className,'_')) !== false) $className = substr($className,0,$pos); - if(isset($_CLASS_MANIFEST[$className])) { - if(preg_match('/^'.str_replace('/','\/',preg_quote(BASE_PATH)).'\/([^\/]+)\//', $_CLASS_MANIFEST[$className], $matches)) { + if(isset($classes[$className])) { + if(preg_match('/^'.str_replace('/','\/',preg_quote(BASE_PATH)).'\/([^\/]+)\//', $classes[$className], $matches)) { return $matches[1]; } } diff --git a/dev/TestRunner.php b/dev/TestRunner.php index 92b831bfc..167f02e56 100644 --- a/dev/TestRunner.php +++ b/dev/TestRunner.php @@ -79,7 +79,21 @@ class TestRunner extends Controller { if (is_string($reporter)) $reporter = new $reporter; self::$default_reporter = $reporter; } - + + /** + * Pushes a class and template manifest instance that include tests onto the + * top of the loader stacks. + */ + public static function use_test_manifest() { + SS_ClassLoader::instance()->pushManifest(new SS_ClassManifest( + BASE_PATH, true, isset($_GET['flush']) + )); + + SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest( + BASE_PATH, true, isset($_GET['flush']) + )); + } + function init() { parent::init(); @@ -102,7 +116,7 @@ class TestRunner extends Controller { * Currently excludes PhpSyntaxTest */ function all($request, $coverage = false) { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); unset($tests['FunctionalTest']); @@ -122,7 +136,7 @@ class TestRunner extends Controller { * Run test classes that should be run before build - i.e., everything possible. */ function build() { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); $tests = ClassInfo::subclassesFor('SapphireTest'); array_shift($tests); unset($tests['FunctionalTest']); @@ -138,7 +152,7 @@ class TestRunner extends Controller { * Browse all enabled test cases in the environment */ function browse() { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); self::$default_reporter->writeHeader(); self::$default_reporter->writeInfo('Available Tests', false); if(Director::is_cli()) { @@ -170,7 +184,7 @@ class TestRunner extends Controller { * Run a coverage test across all modules */ function coverageAll($request) { - ManifestBuilder::load_all_classes(); + self::use_test_manifest(); $this->all($request, true); } @@ -197,7 +211,7 @@ class TestRunner extends Controller { * Run only a single test class or a comma-separated list of tests */ function only($request, $coverage = false) { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); if($request->param('TestCase') == 'all') { $this->all(); } else { @@ -217,7 +231,7 @@ class TestRunner extends Controller { * A module is generally a toplevel folder, e.g. "mysite" or "sapphire". */ function module($request, $coverage = false) { - ManifestBuilder::load_test_manifest(); + self::use_test_manifest(); $classNames = array(); $moduleNames = explode(',', $request->param('ModuleName')); foreach($moduleNames as $moduleName) { diff --git a/filesystem/FileFinder.php b/filesystem/FileFinder.php new file mode 100644 index 000000000..d956828e8 --- /dev/null +++ b/filesystem/FileFinder.php @@ -0,0 +1,222 @@ + null, + 'accept_callback' => null, + 'accept_dir_callback' => null, + 'accept_file_callback' => null, + 'file_callback' => null, + 'dir_callback' => null, + 'ignore_files' => null, + 'ignore_dirs' => null, + 'ignore_vcs' => true, + 'min_depth' => null, + 'max_depth' => null + ); + + /** + * @var array + */ + protected $options; + + public function __construct() { + $this->options = Object::combined_static(get_class($this), 'default_options'); + } + + /** + * Returns an option value set on this instance. + * + * @param string $name + * @return mixed + */ + public function getOption($name) { + if (!array_key_exists($name, $this->options)) { + throw new InvalidArgumentException("The option $name doesn't exist."); + } + + return $this->options[$name]; + } + + /** + * Set an option on this finder instance. See {@link SS_FileFinder} for the + * list of options available. + * + * @param string $name + * @param mixed $value + */ + public function setOption($name, $value) { + if (!array_key_exists($name, $this->options)) { + throw new InvalidArgumentException("The option $name doesn't exist."); + } + + $this->options[$name] = $value; + } + + /** + * Sets several options at once. + * + * @param array $options + */ + public function setOptions(array $options) { + foreach ($options as $k => $v) $this->setOption($k, $v); + } + + /** + * Finds all files matching the options within a directory. The search is + * performed depth first. + * + * @param string $base + * @return array + */ + public function find($base) { + $paths = array(array(rtrim($base, '/'), 0)); + $found = array(); + + $fileCallback = $this->getOption('file_callback'); + $dirCallback = $this->getOption('dir_callback'); + + while ($path = array_shift($paths)) { + list($path, $depth) = $path; + + foreach (scandir($path) as $basename) { + if ($basename == '.' || $basename == '..') { + continue; + } + + if (is_dir("$path/$basename")) { + if (!$this->acceptDir($basename, "$path/$basename", $depth + 1)) { + continue; + } + + if ($dirCallback) { + call_user_func( + $dirCallback, $basename, "$path/$basename", $depth + 1 + ); + } + + $paths[] = array("$path/$basename", $depth + 1); + } else { + if (!$this->acceptFile($basename, "$path/$basename", $depth)) { + continue; + } + + if ($fileCallback) { + call_user_func( + $fileCallback, $basename, "$path/$basename", $depth + ); + } + + $found[] = "$path/$basename"; + } + } + } + + return $found; + } + + /** + * Returns TRUE if the directory should be traversed. This can be overloaded + * to customise functionality, or extended with callbacks. + * + * @return bool + */ + protected function acceptDir($basename, $pathname, $depth) { + if ($this->getOption('ignore_vcs') && in_array($basename, self::$vcs_dirs)) { + return false; + } + + if ($ignore = $this->getOption('ignore_dirs')) { + if (in_array($basename, $ignore)) return false; + } + + if ($max = $this->getOption('max_depth')) { + if ($depth > $max) return false; + } + + if ($callback = $this->getOption('accept_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + if ($callback = $this->getOption('accept_dir_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + return true; + } + + /** + * Returns TRUE if the file should be included in the results. This can be + * overloaded to customise functionality, or extended via callbacks. + * + * @return bool + */ + protected function acceptFile($basename, $pathname, $depth) { + if ($regex = $this->getOption('name_regex')) { + if (!preg_match($regex, $basename)) return false; + } + + if ($ignore = $this->getOption('ignore_files')) { + if (in_array($basename, $ignore)) return false; + } + + if ($minDepth = $this->getOption('min_depth')) { + if ($depth < $minDepth) return false; + } + + if ($callback = $this->getOption('accept_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + if ($callback = $this->getOption('accept_file_callback')) { + if (!call_user_func($callback, $basename, $pathname, $depth)) return false; + } + + return true; + } + +} \ No newline at end of file diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php old mode 100755 new mode 100644 index f46902e62..e8c458d4e --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -937,29 +937,6 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { * ################################# */ - /** - * Get part of class ancestry (even if popup is not subclassed it might be styled differently in css) - */ - function PopupClasses() { - global $_ALL_CLASSES; - - $items = array(); - $parents = isset($_ALL_CLASSES['parents'][$this->class]) ? $_ALL_CLASSES['parents'][$this->class] : null; - - if($parents) { - foreach($parents as $parent) { - if(!in_array($parent, $_ALL_CLASSES['parents']['TableListField'])) { - $items[] = $parent . '_Popup'; - } - } - } - - $items[] = $this->class . '_Popup'; - - return implode(' ', $items); - } - - /** * Returns the db-fieldname of the currently used has_one-relationship. */ diff --git a/manifest/ClassLoader.php b/manifest/ClassLoader.php new file mode 100644 index 000000000..1c1513d70 --- /dev/null +++ b/manifest/ClassLoader.php @@ -0,0 +1,85 @@ +manifests[count($this->manifests) - 1]; + } + + /** + * Pushes a class manifest instance onto the top of the stack. This will + * also include any module configuration files at the same time. + * + * @param SS_ClassManifest $manifest + */ + public function pushManifest(SS_ClassManifest $manifest) { + $this->manifests[] = $manifest; + + foreach ($manifest->getConfigs() as $config) { + require_once $config; + } + } + + /** + * @return SS_ClassManifest + */ + public function popManifest() { + return array_pop($this->manifests); + } + + public function registerAutoloader() { + spl_autoload_register(array($this, 'loadClass')); + } + + /** + * Loads a class or interface if it is present in the currently active + * manifest. + * + * @param string $class + */ + public function loadClass($class) { + if ($path = $this->getManifest()->getItemPath($class)) { + require_once $path; + } + } + + /** + * Returns true if a class or interface name exists in the manifest. + * + * @param string $class + * @return bool + */ + public function classExists($class) { + return class_exists($class, false) || $this->getManifest()->getItemPath($class); + } + +} \ No newline at end of file diff --git a/manifest/ClassManifest.php b/manifest/ClassManifest.php new file mode 100644 index 000000000..e11633d1b --- /dev/null +++ b/manifest/ClassManifest.php @@ -0,0 +1,361 @@ + T_CLASS, + 1 => T_WHITESPACE, + 2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'), + 3 => T_WHITESPACE, + 4 => T_EXTENDS, + 5 => T_WHITESPACE, + 6 => array(T_STRING, 'save_to' => 'extends', 'can_jump_to' => 14), + 7 => T_WHITESPACE, + 8 => T_IMPLEMENTS, + 9 => 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), + 13 => array(T_WHITESPACE, 'can_jump_to' => 10), + 14 => array(T_WHITESPACE, 'optional' => true), + 15 => '{', + )); + } + + /** + * @return TokenisedRegularExpression + */ + public static function get_interface_parser() { + return new TokenisedRegularExpression(array( + 0 => T_INTERFACE, + 1 => T_WHITESPACE, + 2 => array(T_STRING, 'can_jump_to' => 7, 'save_to' => 'interfaceName'), + 3 => T_WHITESPACE, + 4 => T_EXTENDS, + 5 => T_WHITESPACE, + 6 => array(T_STRING, 'save_to' => 'extends'), + 7 => array(T_WHITESPACE, 'optional' => true), + 8 => '{', + )); + } + + /** + * 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; + + $this->cache = SS_Cache::factory('SS_ClassManifest', 'Core', array( + 'automatic_serialization' => true, + 'lifetime' => null + )); + $this->cacheKey = $this->tests ? 'manifest_tests' : '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']; + } 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; + } + + /** + * 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' + ); + + // Reset the manifest so stale info doesn't cause errors. + foreach ($reset as $reset) { + $this->$reset = array(); + } + + $finder = new ManifestFileFinder(); + $finder->setOptions(array( + 'name_regex' => '/\.php$/', + 'ignore_files' => array('index.php', 'main.php', 'cli-script.php'), + 'ignore_tests' => !$this->tests, + 'file_callback' => array($this, 'handleFile') + )); + $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 + ); + $this->cache->save($data, $this->cacheKey); + } + } + + public function handleFile($basename, $pathname, $depth) { + if ($depth == 1 && $basename == self::CONF_FILE) { + $this->configs[] = $pathname; + return; + } + + $classes = null; + $interfaces = 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']) && isset($data['interfaces']) + && is_array($data['classes']) && is_array($data['interfaces']) + ); + + if ($valid) { + $classes = $data['classes']; + $interfaces = $data['interfaces']; + } + } + + if (!$classes) { + $tokens = token_get_all($file); + $classes = self::get_class_parser()->findAll($tokens); + $interfaces = self::get_interface_parser()->findAll($tokens); + + $cache = array('classes' => $classes, 'interfaces' => $interfaces); + $this->cache->save($cache, $key, array('fileparse')); + } + + foreach ($classes as $class) { + $name = $class['className']; + $extends = isset($class['extends']) ? $class['extends'] : null; + $implements = isset($class['interfaces']) ? $class['interfaces'] : null; + + if (array_key_exists($name, $this->classes)) { + throw new Exception(sprintf( + 'There are two files containing the "%s" class: "%s" and "%s"', + $name, $this->classes[$name], $pathname + )); + } + + $this->classes[strtolower($name)] = $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; + } + } + } + + foreach ($interfaces as $interface) { + $this->interfaces[strtolower($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(); + } + } + +} \ No newline at end of file diff --git a/manifest/ManifestFileFinder.php b/manifest/ManifestFileFinder.php new file mode 100644 index 000000000..63e8e2969 --- /dev/null +++ b/manifest/ManifestFileFinder.php @@ -0,0 +1,65 @@ + false, + 'ignore_tests' => true, + 'min_depth' => 1 + ); + + public function acceptDir($basename, $pathname, $depth) { + // Skip over the assets directory in the site root. + if ($depth == 1 && $basename == ASSETS_DIR) { + return false; + } + + // Skip over any lang directories in the top level of the module. + if ($depth == 2 && $basename == self::LANG_DIR) { + return false; + } + + // If we're not in testing mode, then skip over the tests directory in + // the module root. + if ($this->getOption('ignore_tests') && $depth == 2 && $basename == self::TESTS_DIR) { + return false; + } + + // Ignore any directories which contain a _manifest_exclude file. + if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) { + return false; + } + + // Only include top level module directories which have a configuration + // _config.php file. However, if we're in themes mode then include + // the themes dir without a config file. + $lackingConfig = ( + $depth == 1 + && !($this->getOption('include_themes') && $basename == THEMES_DIR) + && !file_exists($pathname . '/' . self::CONFIG_FILE) + ); + + if ($lackingConfig) { + return false; + } + + return parent::acceptDir($basename, $pathname, $depth); + } + +} \ No newline at end of file diff --git a/manifest/TemplateLoader.php b/manifest/TemplateLoader.php new file mode 100644 index 000000000..10226f733 --- /dev/null +++ b/manifest/TemplateLoader.php @@ -0,0 +1,95 @@ +manifests[count($this->manifests) - 1]; + } + + /** + * @param SS_TemplateManifest $manifest + */ + public function pushManifest(SS_TemplateManifest $manifest) { + $this->manifests[] = $manifest; + } + + /** + * @return SS_TemplateManifest + */ + public function popManifest() { + return array_pop($this->manifests); + } + + /** + * Attempts to find possible candidate templates from a set of template + * names and a theme. + * + * The template names can be passed in as plain strings, or be in the + * format "type/name", where type is the type of template to search for + * (e.g. Includes, Layout). + * + * @param string|array $templates + * @param string $theme + * @return array + */ + public function findTemplates($templates, $theme = null) { + $result = array(); + + foreach ((array) $templates as $template) { + $found = false; + + if (strpos($template, '/')) { + list($type, $template) = explode('/', $template, 2); + } else { + $type = null; + } + + if ($candidates = $this->getManifest()->getTemplate($template)) { + if ($theme && isset($candidates['themes'][$theme])) { + $found = $candidates['themes'][$theme]; + } else { + unset($candidates['themes']); + $found = $candidates; + } + + if ($found) { + if ($type && isset($found[$type])) { + $found = array('main' => $found[$type]); + } + + $result = array_merge($found, $result); + } + } + } + + return $result; + } + +} \ No newline at end of file diff --git a/manifest/TemplateManifest.php b/manifest/TemplateManifest.php new file mode 100644 index 000000000..d1e29dace --- /dev/null +++ b/manifest/TemplateManifest.php @@ -0,0 +1,154 @@ +base = $base; + $this->tests = $includeTests; + + $this->cacheKey = $this->tests ? 'manifest_tests' : 'manifest'; + $this->forceRegen = $forceRegen; + + $this->cache = SS_Cache::factory('SS_TemplateManifest', 'Core', array( + 'automatic_serialization' => true, + 'lifetime' => null + )); + } + + /** + * Returns a map of all template information. The map is in the following + * format: + * + *
+ * array(
+ * 'moduletemplate' => array(
+ * 'main' => '/path/to/module/templates/Main.ss'
+ * ),
+ * 'include' => array(
+ * 'include' => '/path/to/module/templates/Includes/Include.ss'
+ * ),
+ * 'page' => array(
+ * 'themes' => array(
+ * 'blackcandy' => array(
+ * 'main' => '/path/to/theme/Page.ss'
+ * 'Layout' => '/path/to/theme/Layout/Page.ss'
+ * )
+ * )
+ * )
+ * )
+ *
+ *
+ * @return array
+ */
+ public function getTemplates() {
+ if (!$this->inited) {
+ $this->init();
+ }
+
+ return $this->templates;
+ }
+
+ /**
+ * Returns a set of possible candidate templates that match a certain
+ * template name.
+ *
+ * This is the same as extracting an individual array element from
+ * {@link SS_TemplateManifest::getTemplates()}.
+ *
+ * @param string $name
+ * @return array
+ */
+ public function getTemplate($name) {
+ if (!$this->inited) {
+ $this->init();
+ }
+
+ $name = strtolower($name);
+
+ if (array_key_exists($name, $this->templates)) {
+ return $this->templates[$name];
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Regenerates the manifest by scanning the base path.
+ *
+ * @param bool $cache
+ */
+ public function regenerate($cache = true) {
+ $finder = new ManifestFileFinder();
+ $finder->setOptions(array(
+ 'name_regex' => '/\.ss$/',
+ 'include_themes' => true,
+ 'ignore_tests' => !$this->tests,
+ 'file_callback' => array($this, 'handleFile')
+ ));
+ $finder->find($this->base);
+
+ if ($cache) {
+ $this->cache->save($this->templates, $this->cacheKey);
+ }
+
+ $this->inited = true;
+ }
+
+ public function handleFile($basename, $pathname, $depth) {
+ if (strpos($pathname, $this->base . '/' . THEMES_DIR) === 0) {
+ $start = strlen($this->base . '/' . THEMES_DIR) + 1;
+ $theme = substr($pathname, $start);
+ $theme = substr($theme, 0, strpos($theme, '/'));
+ $theme = strtok($theme, '_');
+ } else {
+ $theme = null;
+ }
+
+ $type = basename(dirname($pathname));
+ $name = strtolower(substr($basename, 0, -3));
+
+ if ($type == self::TEMPLATES_DIR) {
+ $type = 'main';
+ }
+
+ if ($theme) {
+ $this->templates[$name]['themes'][$theme][$type] = $pathname;
+ } else {
+ $this->templates[$name][$type] = $pathname;
+ }
+ }
+
+ protected function init() {
+ if (!$this->forceRegen && $data = $this->cache->load($this->cacheKey)) {
+ $this->templates = $data;
+ $this->inited = true;
+ } else {
+ $this->regenerate();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/core/TokenisedRegularExpression.php b/manifest/TokenisedRegularExpression.php
similarity index 97%
rename from core/TokenisedRegularExpression.php
rename to manifest/TokenisedRegularExpression.php
index 0fc65bcc0..dba58e22a 100644
--- a/core/TokenisedRegularExpression.php
+++ b/manifest/TokenisedRegularExpression.php
@@ -6,7 +6,7 @@
* @package sapphire
* @subpackage core
*/
-class TokenisedRegularExpression extends Object {
+class TokenisedRegularExpression {
/**
* The regular expression definition
*/
@@ -14,7 +14,6 @@ class TokenisedRegularExpression extends Object {
function __construct($expression) {
$this->expression = $expression;
- parent::__construct();
}
function findAll($tokens) {
diff --git a/tests/ClassInfoTest.php b/tests/ClassInfoTest.php
index 81ba1dd8f..53d261ef4 100644
--- a/tests/ClassInfoTest.php
+++ b/tests/ClassInfoTest.php
@@ -4,12 +4,18 @@
* @subpackage tests
*/
class ClassInfoTest extends SapphireTest {
-
+
+ public function testExists() {
+ $this->assertTrue(ClassInfo::exists('Object'));
+ $this->assertTrue(ClassInfo::exists('ClassInfoTest'));
+ $this->assertTrue(ClassInfo::exists('stdClass'));
+ }
+
function testSubclassesFor() {
$this->assertEquals(
ClassInfo::subclassesFor('ClassInfoTest_BaseClass'),
array(
- 0 => 'ClassInfoTest_BaseClass',
+ 'ClassInfoTest_BaseClass' => 'ClassInfoTest_BaseClass',
'ClassInfoTest_ChildClass' => 'ClassInfoTest_ChildClass',
'ClassInfoTest_GrandChildClass' => 'ClassInfoTest_GrandChildClass'
),
@@ -33,10 +39,62 @@ class ClassInfoTest extends SapphireTest {
// 'ClassInfo::classes_for_folder() returns additional classes not matching the filename'
// );
}
-
+
+ /**
+ * @covers ClassInfo::baseDataClass()
+ */
+ public function testBaseDataClass() {
+ $this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_BaseClass'));
+ $this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_ChildClass'));
+ $this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GrandChildClass'));
+
+ $this->setExpectedException('Exception');
+ ClassInfo::baseDataClass('DataObject');
+ }
+
+ /**
+ * @covers ClassInfo::ancestry()
+ */
+ public function testAncestry() {
+ $ancestry = ClassInfo::ancestry('SiteTree');
+ $expect = ArrayLib::valuekey(array(
+ 'Object',
+ 'ViewableData',
+ 'DataObject',
+ 'SiteTree'
+ ));
+ $this->assertEquals($expect, $ancestry);
+
+ $ancestry = ClassInfo::ancestry('SiteTree', true);
+ $this->assertEquals(array('SiteTree' => 'SiteTree'), $ancestry);
+
+ $this->setExpectedException('Exception');
+ ClassInfo::ancestry(42);
+ }
+
+ /**
+ * @covers ClassInfo::dataClassesFor()
+ */
+ public function testDataClassesFor() {
+ $expect = array(
+ 'ClassInfoTest_BaseDataClass' => 'ClassInfoTest_BaseDataClass',
+ 'ClassInfoTest_HasFields' => 'ClassInfoTest_HasFields'
+ );
+
+ $classes = array(
+ 'ClassInfoTest_BaseDataClass',
+ 'ClassInfoTest_NoFields',
+ 'ClassInfoTest_HasFields'
+ );
+
+ foreach ($classes as $class) {
+ $this->assertEquals($expect, ClassInfo::dataClassesFor($class));
+ }
+ }
+
}
-class ClassInfoTest_BaseClass {
+class ClassInfoTest_BaseClass extends DataObject {
}
@@ -47,4 +105,11 @@ class ClassInfoTest_ChildClass extends ClassInfoTest_BaseClass {
class ClassInfoTest_GrandChildClass extends ClassInfoTest_ChildClass {
}
-?>
\ No newline at end of file
+
+class ClassInfoTest_BaseDataClass extends DataObject {
+ public static $db = array('Title' => 'Varchar');
+}
+class ClassInfoTest_NoFields extends ClassInfoTest_BaseDataClass {}
+class ClassInfoTest_HasFields extends ClassInfoTest_NoFields {
+ public static $db = array('Description' => 'Varchar');
+}
diff --git a/tests/FullTestSuite.php b/tests/FullTestSuite.php
index 77e2dc6f9..a1b04e06b 100644
--- a/tests/FullTestSuite.php
+++ b/tests/FullTestSuite.php
@@ -47,7 +47,7 @@ class FullTestSuite {
* @return Array
*/
public static function get_all_tests() {
- ManifestBuilder::load_test_manifest();
+ TestRunner::use_test_manifest();
$tests = ClassInfo::subclassesFor('SapphireTest');
array_shift($tests);
diff --git a/tests/ManifestBuilderTest.fixture.inc b/tests/ManifestBuilderTest.fixture.inc
deleted file mode 100644
index 4b0955ae1..000000000
--- a/tests/ManifestBuilderTest.fixture.inc
+++ /dev/null
@@ -1,111 +0,0 @@
- <<