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 Mar 3, 2011

Definitely and this class can be used for pretty much any type of array. I was also thinking about how it could implement more complex select string so it behaves a little more like advanced css selectors.

It's not a new idea but this would be so nice. Might work on it later.

<?php

  $elements = new RendElements($page);

  // This one is easily doable. Check if a value is empty or not for a given key.
  // Could check for arrays, strings, booleans.
  // Would work like the :empty pseudo selector.
  // http://www.w3.org/TR/css3-selectors/#empty-pseudo

  $elements('key:empty, key:not(:empty)');

  // Like CSS attribute selectors. This would work on string elements.
  // http://www.w3.org/TR/css3-selectors/#attribute-selectors

  $elements('key=value, key^=value, key*=value, key$=value');

  // Structural pseudo selectors.
  // http://www.w3.org/TR/css3-selectors/#structural-pseudos

  $elements('key:first-child, key:nth-child(odd), key:last-child');

?>

@dvessel
Copy link
Author

dvessel commented Mar 3, 2011

I just want to note something here. There is a SPL (standard php library) iterator filter class named RecursiveFilterIterator which I tried to use but it's not reliable when combined with the RecursiveIteratorIterator class. If I can get it to work, this should be even faster and I won't need the depth index which scans through every array key the first time it's run. There would be no need for the 'refresh' method either.

The problem is that the ->key() and ->getDepth() methods don't work when combining the two. If anyone has ideas on a workaround, I'd love to hear it. It seems like a bug to me at this point.

@dvessel
Copy link
Author

dvessel commented Mar 9, 2011

It wasn't a bug. I just didn't know what I was doing. I created a custom class just to do filtering. Some of this would need to be rewritten to take advantage of it but I know for a fact that it's blazing fast.

https://gist.github.com/861520

I'll revise it on another Gist.

@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