-
-
Save dvessel/852446 to your computer and use it in GitHub Desktop.
<?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; | |
} | |
} |
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.
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.
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.
It's in my d.o sandbox. http://drupal.org/sandbox/dvessel/1122312
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.