Jump to content
MoritzLost

Utility function to recursively search through repeaters

Recommended Posts

This doesn't warrant a full tutorial, but I wrote a little function that will recursively search through a repeater field to find the first non-empty field matching a list of fields to look for. I needed something like this to generate a fallback for SEO fields (og:image, og:description et c.). Here it is:

<?php
namespace ProcessWire;

/**
 * Find the first non-empty field out of a list of fields in a repeater or repeater matrix field.
 * Will recursively search through nested repeaters until it finds a non-empty
 * field.
 *
 * @param RepeaterPageArray $repeater The field to search through.
 * @param array $fields A list of fields the function should look for.
 * @param array|null $allowed_repeater_types All the Repeater Matrix types the function will check. Leave empty to allow all.
 * @param array|null $allowed_repeater_fields All the repeater fields the function will check recursively. Leave empty to allow all.
 * @return void
 */
function firstRecursiveRepeaterMatch(
    RepeaterPageArray $repeater,
    array $fields,
    ?array $allowed_repeater_types = null,
    ?array $allowed_repeater_fields = null
) {
    // iterate over the items of the repeater
    foreach ($repeater as $current) {
        // if the function is currently inside a repeater matrix field,
        // skip this item if it isn't one of the allowed types, unless
        // allowed_repeater_types is empty (all types allowed)
        if (
            $current instanceof RepeaterMatrixPage
            && is_array($allowed_repeater_types)
            && !in_array($current->type, $allowed_repeater_types)
        ) {
            continue;
        }
        // get all fields of the current item
        foreach ($current->getFields() as $field) {
            $name = $field->name;
            // if the current field is another repeater, check it recursively
            $fieldtype_class = $field->getFieldType()->className();
            if ($fieldtype_class === 'FieldtypeRepeater' || $fieldtype_class === 'FieldtypeRepeaterMatrix') {
                // continue with the next item if the field name isn't one of the
                // allowed repeater fields, unless allowed_repeater_fields empty (null)
                if (
                    is_array($allowed_repeater_fields)
                    && !in_array($name, $allowed_repeater_fields)
                ) {
                    continue;
                }
                $deep_search = firstRecursiveRepeaterMatch(
                    $current->get($name),
                    $fields,
                    $allowed_repeater_types,
                    $allowed_repeater_fields
                );
                // if the deep search inside the repeater
                // finds something, the function ends here
                if ($deep_search !== null) {
                    return $deep_search;
                }
            }
            // if the current field name is one of the requested
            // fields, return it's value if it isn't empty
            if (in_array($name, $fields)) {
                $value = $current->get($name);
                if (
                    // check for empty values
                    !empty($value)
                    // if the value is any wirearray, check
                    // if it has at least one item
                    && (!$value instanceof WireArray || $value->count() > 0)
                ) {
                    return $value;
                }
            }
        }
    }
    // if the function reaches this point, there is no match in the entire tree
    return null;
}

Can be used like this:

$seo_description = firstRecursiveRepeaterMatch(
	// content sections fields, a repeater matrix with multiple types
	$page->sections,

	// look for the first non-empty instance of any of those fields
	['body', 'html_basic', 'html_full'],

	// only check sections of the following types
	['section_text', 'section_columns', 'section_accordion'],

	// only check the following nested repeaters recursively
	['columns', 'accordion']
);

Wanted to share because I thought it could be useful to others. It should be easy to adjust the matching condition from non-emtpy fields to some other condition, depending on the use case ...

  • Like 8
  • Thanks 1

Share this post


Link to post
Share on other sites

Thanks a lot for this, it might be useful in the future 🙂

Could you please point me where I can learn about the question mark on this line?

?array $allowed_repeater_types = null,

I know it should be something about php 7.*, but I cannot find anything due to the fact that the search string "? php" leads, obviously, to wrong results on google 🙂

Thanks!

  • Like 3

Share this post


Link to post
Share on other sites

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Noel Boss
      I use a PageTable field to make edits to children of pages more intuitive…
      To register the hooks, insert the following Snippet inside your init function in your module (or add it to your init.php file):
      /** * Initialize the module. * * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called * when ProcessWire's API is ready. As a result, this is a good place to attach hooks. */ public function init() { // Prefill pageTable field $this->wire()->addHookBefore('InputfieldPageTable::render', $this, 'addChildrenToPageTableFieldsHook'); $this->wire()->addHookBefore('InputfieldPageTableAjax::checkAjax', $this, 'addChildrenToPageTableFieldsHook'); } Then, add this hook method:
      /** * Fill pagetable fields with children before editing…. * * @param HookEvent $event */ public function addChildrenToPageTableFieldsHook(HookEvent $event) { $field = $event->object; // on ajax, the first hook has no fieldname if (!$field->name) { return; } // get the edited backend page $editID = $this->wire('input')->get->int('id'); if (!$editID && $this->wire('process') instanceof WirePageEditor) { $editID = $this->wire('process')->getPage()->id; } $page = wire('pages')->get($editID); // disable output formating – without this, the ajax request will not populate the field $page->of(false); // you could also insert a check to only do this with sepcific field names… // $page->set($field->name, $page->children('template=DesiredTemplate')); // just specific templates $page->set($field->name, $page->children); } Now whenever there is a page-table field on your page, it gets populated with the children
×
×
  • Create New...