Skip to content

Instantly share code, notes, and snippets.

@ajshort
Created February 25, 2011 01:58
Show Gist options
  • Select an option

  • Save ajshort/843273 to your computer and use it in GitHub Desktop.

Select an option

Save ajshort/843273 to your computer and use it in GitHub Desktop.
<?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;
}
}
}
<?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;
}
}
<?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));
}
}
<?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