Skip to content

Instantly share code, notes, and snippets.

@dvessel
Created March 3, 2011 06:43
Show Gist options
  • Save dvessel/852446 to your computer and use it in GitHub Desktop.
Save dvessel/852446 to your computer and use it in GitHub Desktop.
RendElements Class
<?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;
}
}
@dvessel
Copy link
Author

dvessel commented Apr 9, 2011

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.

@dvessel
Copy link
Author

dvessel commented Apr 10, 2011

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment