Created
March 3, 2011 06:43
-
-
Save dvessel/852446 to your computer and use it in GitHub Desktop.
RendElements Class
This file contains 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 | |
/** | |
* Example uses from the page hook when working with a render array. | |
* | |
* $elements = new RendElements($page); | |
* | |
* Returns a reference to the header. 'header' is the select string. | |
* | |
* $header = $elements('header'); | |
* | |
* Creates a new object member and forms a reference to the found items. | |
* The select string (first parameter) works like a simple decendent selector. | |
* The second parameter used to label the member. | |
* | |
* $elements->newMember('content #attributes', 'content_attributes'); | |
* | |
* Each item is keyed to the path leading to it in the array structure. | |
* $attr_path will look like this: 'content > search_form > #attributes'. | |
* | |
* foreach ($elements->content_attributes as $attr_path => &$attribute) { | |
* | |
* Since $attribute is referenced, the modifications will be reflected | |
* in the original $page array. | |
* | |
* $attributes['class'][] = 'extra-content-class'; | |
* } | |
* | |
* Object members can be unset as expected. This will only remove the reference. | |
* Original data is still intact. | |
* | |
* unset($elements->content_attributes); | |
* | |
* To debug your selection, use the ->dumpPaths() method. | |
* | |
* $elements->dumpPaths('class'); | |
* | |
* Multiple selections are possible by separating them with a comma. | |
* | |
* $elements->newMember('header, sidebar_left, content, sidebar_right, footer', 'regions'); | |
* | |
* Use the standard Drupal API functions on each element. | |
* | |
* foreach ($elements->regions as &$region) { | |
* print render($region); | |
* } | |
* | |
* Or invoke it with a method callback to let it act on the searched items. | |
* This is done through magic methods. The only internal public methods are | |
* newMember, dumpPaths and refresh. Any other method call must have a | |
* corresponding function callback. Drupal core provides render, hide and show | |
* for render arrays and they can be used here. | |
* | |
* Other possible callbacks include var_dump, krumo, etc... As long as the | |
* callback accepts a single parameter - the found sub-elements, it should | |
* work. Be cautious of the type of value being passed to the callback. | |
* Depending on the select string, the found items can be of any type. | |
* | |
* print $elements->render('header, sidebar_left, content, sidebar_right, footer'); | |
* | |
* The returned value becomes a string by default. To return an array of values, | |
* add a second parameter of FALSE. | |
* | |
* print var_dump($elements->array_keys('#attributes', FALSE)); | |
* | |
* To work on a copy of the found elements instead of a reference, a third | |
* parameter can be set to FALSE. This will prevent the original $page array | |
* from being modified. | |
* | |
* print $elements->render('content', TRUE, FALSE); | |
* | |
* The instance holds an index for speedy access but it may become invalid if | |
* internal elements are heavily modified. Use the ->refresh() method to clear | |
* it. This should be avoided especially on massive arrays. | |
* | |
* $elements->refresh(); | |
* | |
* When working with huge arrays, it might be easier to break it off into chunks | |
* by instantiating a specific sub-section. | |
* | |
* $content = new RendElements($page['content']); | |
* | |
* Do not do the following. Due to heavy recursions, it can kill your site. | |
* | |
* $content = new RendElements($elements('content')); | |
* | |
*/ | |
class RendElements { | |
protected $elements; | |
protected $index; | |
public function __construct(array &$elements){ | |
$this->elements = &$elements; | |
$this->index = new stdClass(); | |
} | |
public function __invoke($search = '', $reference = TRUE) { | |
return $this->executeSearch($search, $reference); | |
} | |
public function __call($callback, $args) { | |
// Default arguments | |
// - search = '' | |
// - return string = TRUE | |
// - reference = TRUE | |
foreach (array('', TRUE, TRUE) as $i => $arg) { | |
$args[$i] = isset($args[$i]) ? $args[$i] : $arg; | |
} | |
return $this->callback($callback, $args[0], $args[1], $args[2]); | |
} | |
public function newMember($search = '', $name) { | |
$this->$name = $this->executeSearch($search); | |
return !empty($this->$name); | |
} | |
public function dumpPaths($search = '') { | |
print var_dump(array_keys($this->executeSearch($search))); | |
} | |
public function refresh() { | |
$this->index->refresh = TRUE; | |
} | |
protected function callBack($callback, $search, $return_string = TRUE, $reference = TRUE) { | |
$sub_elements = $this->executeSearch($search, $reference); | |
$results = array(); | |
foreach ($sub_elements as $path => &$sub_element) { | |
$results[$path] = $callback($sub_element); | |
} | |
return $return_string ? implode("\n", $results) : $results; | |
} | |
protected function executeSearch($search, $reference = TRUE) { | |
$found_items = array(); | |
if ($search === '') { | |
return array('::base element::' => &$this->elements); | |
} | |
foreach ($this->searchStringToArray($search) as $search_set) { | |
foreach ($this->locate($search_set) as $key => $paths) { | |
$inner_ref = &$this->elements; | |
foreach ($paths as $path) { | |
$inner_ref = &$inner_ref[$path]; | |
} | |
if ($reference) { | |
$found_items[$key] = &$inner_ref; | |
} | |
else { | |
$found_items[$key] = $inner_ref; | |
} | |
} | |
} | |
return $found_items; | |
} | |
protected function searchStringToArray($search) { | |
$search_sets = array(); | |
foreach (explode(',', $search) as $i => $set) { | |
foreach (preg_split("/[\s]+/", $set, -1, PREG_SPLIT_NO_EMPTY) as $sub_set) { | |
$search_sets[$i][] = is_numeric($sub_set) ? (int) $sub_set : $sub_set; | |
} | |
} | |
return $search_sets; | |
} | |
protected function locate($search_set) { | |
$element = array_pop($search_set); | |
$locations = $this->index($element) ? $this->index->elements[$element] : array(); | |
// Filter by parent search items. Last element popped. | |
if (!empty($search_set)) { | |
foreach (array_keys($locations) as $path) { | |
$_path = $path; | |
foreach ($search_set as $sub_path) { | |
if (($_path = strstr($_path, $sub_path)) === FALSE) { | |
unset($locations[$path]); | |
break; | |
} | |
} | |
} | |
} | |
return $locations; | |
} | |
protected function index($element) { | |
/* PROCESS DEPTH MAP -------------------------------------------------- */ | |
// Refresh depth map? | |
if (!empty($this->index->refresh) && isset($this->index->depths)) { | |
unset($this->index->depths); | |
} | |
// First build a depth map for all elements if it doesn't exist. | |
// This will help speed up processing when building the index. | |
if (empty($this->index->depths)) { | |
$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($this->elements), RecursiveIteratorIterator::SELF_FIRST); | |
// Get the max depth for each element. | |
for ($depth_index = array(), $it->next(); ($k = $it->key()) !== NULL; $it->next()) { | |
$d = $it->getDepth(); | |
if (!isset($depth_index[$k]) || $depth_index[$k] < $d) { | |
$depth_index[$k] = $d; | |
} | |
} | |
$this->index->depths = $depth_index; | |
} | |
// If the element still doesn't exist, return false and do nothing. | |
if (!isset($this->index->depths[$element])) { | |
$this->index->refresh = FALSE; | |
return FALSE; | |
} | |
/* PROCESS INDEX ------------------------------------------------------ */ | |
// Refresh index? | |
if (!empty($this->index->refresh) && isset($this->index->elements[$element])) { | |
unset($this->index->elements[$element]); | |
} | |
// If the index for the element does not exist, search for and build it. | |
if (!isset($this->index->elements[$element])) { | |
// New iterator... | |
$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($this->elements), RecursiveIteratorIterator::SELF_FIRST); | |
// ...with limited depth. | |
$it->setMaxDepth($this->index->depths[$element]); | |
// $element is the last search item. Scan for it while building a path that leads to it. | |
for ($cursor = array(), $it->next(); ($key = $it->key()) !== NULL; $it->next()) { | |
$cursor[$it->getDepth()] = $key; | |
// Search match! | |
if ($element === $key) { | |
// Ensure path index has the right depth. | |
$cursor = array_splice($cursor, 0, $it->getDepth() + 1); | |
$this->index->elements[$element][implode(' > ', $cursor)] = $cursor; | |
} | |
} | |
} | |
$this->index->refresh = FALSE; | |
return TRUE; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I got a lot of what I posted above down in another version. A demo can be seen here: http://vimeo.com/22153875
It'll be released soon.