Created
February 25, 2011 01:58
-
-
Save ajshort/843273 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * | |
| * | |
| * @package sapphire | |
| * @subpackage manifest | |
| */ | |
| class AutoloadManifestBuilder { | |
| const TESTS_DIR = 'tests'; | |
| const LANG_DIR = 'lang'; | |
| const CONF_FILE = '_config.php'; | |
| protected $base; | |
| protected $cache; | |
| protected $classes = array(); | |
| protected $children = array(); | |
| protected $interfaces = array(); | |
| protected $implementors = array(); | |
| protected $configs = array(); | |
| /** | |
| * @return TokenisedRegularExpression | |
| */ | |
| public static function get_class_parser() { | |
| return new TokenisedRegularExpression(array( | |
| 0 => 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 => '{', | |
| )); | |
| } | |
| public function __construct($base) { | |
| $this->base = $base; | |
| $this->cache = SS_Cache::factory(__CLASS__, 'Core', array( | |
| 'automatic_serialization' => true, | |
| 'lifetime' => null | |
| )); | |
| } | |
| public function bootstrap($flush = false) { | |
| global $_CLASS_MANIFEST; | |
| global $_CLASS_CHILDREN; | |
| global $_IMPLEMENTORS; | |
| if ($flush || !$data = $this->cache->load('manifest')) { | |
| $this->rebuild(); | |
| $data = array( | |
| 'configs' => $this->configs, | |
| 'classes' => $this->classes, | |
| 'children' => $this->children, | |
| 'interfaces' => $this->interfaces, | |
| 'implementors' => $this->implementors | |
| ); | |
| $this->cache->save($data, 'manifest'); | |
| } | |
| $_CLASS_MANIFEST = $data['classes'] + $data['interfaces']; | |
| $_CLASS_CHILDREN = $data['children']; | |
| $_IMPLEMENTORS = $data['implementors']; | |
| foreach ($data['configs'] as $config) { | |
| require_once $config; | |
| } | |
| } | |
| public function rebuild() { | |
| $finder = new ManifestFileFinder(); | |
| $finder->setOptions(array( | |
| 'name_regex' => '/\.php$/', | |
| 'ignore_files' => array('index.php', 'main.php', 'cli-script.php'), | |
| 'file_callback' => array($this, 'handleFile') | |
| )); | |
| $finder->find($this->base); | |
| } | |
| 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; | |
| } | |
| } | |
| 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; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * A utility class that can recursively find files matching a set of rules | |
| * within a directory. | |
| * | |
| * The file finder can have several options set on it: | |
| * - name_regex (string): A regular expression that file basenames must match. | |
| * - accept_callback (callback): A callback that is called to accept a file. | |
| * If it returns false the item will be skipped. The callback is passed the | |
| * basename, pathname and depth. | |
| * - accept_dir_callback (callback): The same as accept_callback, but only | |
| * called for directories. | |
| * - accept_file_callback (callback): The same as accept_callback, but only | |
| * called for files. | |
| * - file_callback (callback): A callback that is called when a file i | |
| * succesfully matched. It is passed the basename, pathname and depth. | |
| * - dir_callback (callback): The same as file_callback, but called for | |
| * directories. | |
| * - ignore_files (array): An array of file names to skip. | |
| * - ignore_dirs (array): An array of directory names to skip. | |
| * - ignore_vcs (bool): Skip over commonly used VCS dirs (svn, git, hg, bzr). | |
| * This is enabled by default. | |
| * - max_depth (int): The maxmium depth to traverse down the folder tree, | |
| * default to unlimited. | |
| * | |
| * @package sapphire | |
| * @subpackage filesystem | |
| */ | |
| class SS_FileFinder { | |
| /** | |
| * @var array | |
| */ | |
| public static $vcs_dirs = array( | |
| '.git', '.svn', '.hg', '.bzr' | |
| ); | |
| /** | |
| * The default options that are set on a new finder instance. Options not | |
| * present in this array cannot be set. | |
| * | |
| * Any default_option statics defined on child classes are also taken into | |
| * account. | |
| * | |
| * @var array | |
| */ | |
| public static $default_options = array( | |
| 'name_regex' => 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, | |
| '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 (!$callback()) return false; | |
| } | |
| if ($callback = $this->getOption('accept_dir_callback')) { | |
| if (!$callback()) 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 ($callback = $this->getOption('accept_callback')) { | |
| if (!$callback()) return false; | |
| } | |
| if ($callback = $this->getOption('accept_file_callback')) { | |
| if (!$callback()) return false; | |
| } | |
| return true; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * | |
| * | |
| * @package sapphire | |
| * @subpackage manifest | |
| */ | |
| class ManifestFileFinder extends SS_FileFinder { | |
| const CONFIG_FILE = '_config.php'; | |
| const EXCLUDE_FILE = '_manifest_exclude'; | |
| const LANG_DIR = 'lang'; | |
| const TESTS_DIR = 'tests'; | |
| public static $default_options = array( | |
| 'include_themes' => false, | |
| 'ignore_tests' => true | |
| ); | |
| 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); | |
| } | |
| /** | |
| * @param string $base | |
| * @return string | |
| */ | |
| protected function getCacheKey($base) { | |
| return md5($this->base . serialize($this->options)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * | |
| * | |
| * @package sapphire | |
| * @subpackage manifest | |
| */ | |
| class TemplateManifestBuilder { | |
| const TEMPLATES_DIR = 'templates'; | |
| protected $base; | |
| protected $cache; | |
| protected $templates = array(); | |
| protected $css = array(); | |
| public function __construct($base) { | |
| $this->base = $base; | |
| $this->cache = SS_Cache::factory(__CLASS__, 'Core', array( | |
| 'automatic_serialization' => true, | |
| 'lifetime' => null | |
| )); | |
| } | |
| public function bootstrap($flush = false) { | |
| global $_TEMPLATE_MANIFEST; | |
| global $_CSS_MANIFEST; | |
| if ($flush || !$data = $this->cache->load('manifest')) { | |
| $this->rebuild(); | |
| $data = array( | |
| 'templates' => $this->templates, | |
| 'css' => $this->css, | |
| ); | |
| $this->cache->save($data, 'manifest'); | |
| } | |
| $_TEMPLATE_MANIFEST = $data['templates']; | |
| $_CSS_MANIFEST = $data['css']; | |
| } | |
| public function rebuild() { | |
| $finder = new ManifestFileFinder(); | |
| $finder->setOptions(array( | |
| 'name_regex' => '/\.c?ss$/', | |
| 'include_themes' => true, | |
| 'file_callback' => array($this, 'handleFile') | |
| )); | |
| $finder->find($this->base); | |
| } | |
| 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; | |
| } | |
| if (substr($basename, -3) == '.ss') { | |
| $type = basename(dirname($pathname)); | |
| if ($type == self::TEMPLATES_DIR) { | |
| $type = 'main'; | |
| } | |
| if ($theme) { | |
| $this->templates[substr($basename, 0, -3)]['themes'][$theme][$type] = $pathname; | |
| } else { | |
| $this->templates[substr($basename, 0, -3)][$type] = $pathname; | |
| } | |
| } else { | |
| $relPath = substr($pathname, strlen($this->base) + 1); | |
| if ($theme) { | |
| $this->css[substr($basename, 0, -4)]['themes'][$theme] = $relPath; | |
| } else { | |
| $this->css[substr($basename, 0, -4)]['unthemed'] = $relPath; | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment