-
-
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; | |
| } | |
| } |
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');
?>
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
yeah, this is pretty powerful ...
I was looking how to leverage http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_attributes/7 more ...
seeing if I could augment / add classes to the actual drupal elements ... but it looks like this Class handles that very well.