Filter by multiple related subpages

Hej folks im stuck …

I am trying to get all articles based on pages field “rubriken” which is set in the article as well as inside a block to filter results.

[…]

<!-- rubriken by block -->
<?php if ($block->rubriken()->isNotEmpty()): ?>
  <?php $filter2 = $block->rubriken()->toPages(); ?>
<?php endif ?>

<!-- rubriken of all articles -->
<?php if ($block->rubriken()->isNotEmpty()): ?>
  <?php
  $articles = $articles->filter(function($child) use($filter2) {
    $kats = $child->rubriken()->toPages(); 
    return $child->rubriken()->toPages()->has($filter2);
  });
  ?>
<?php endif ?>

By changing the field type to “multiselect” and using “split()” instead of “toPages()” it works correctly.
But I need this as a Pages-Field to give the backend-user the ability to choose between parentpages (whole category) or subpages (subcategory).

Thanks in advance.

This returns a collection of pagesm not a single page.

has(), however, expects either a page object or a page ID.

Assuming that you $filter2 is actually supposed to be a pages collection, you will have to adapt your filter code:

$articles = $articles->filter(fn ($child)  => $child->rubriken()->toPages()->intersects($filter2);
1 Like

Thank you kindly –> this is working as expected.
However … it is not taking care of the parent of “rubriken” :frowning:

[1] Let’s say my collection is called ‘articles’.
Every article has a pages-field called ‘rubriken’. The blueprint of ‘rubriken’ is allowing subpages.

[2] The second collection is said ‘rubriken’. The two entries here are ‘forms’ and ‘colors’.

[3] Inside of ‘forms’ we got ‘round’, ‘squared’ and inside of ‘colors’ we have ‘blue’, ‘red’ and so on

[4] For my article I am now choosing ‘rubrik’ → ‘blue’.

[6] If I am now trying to filter the results by selecting only the parent ‘colors’ it wont work.

[edit]
I’am pretty sure I’ll have to make “filter3” pointing to the parent, but is it possible to intersect by two filters to get this working?

Well, of course not. Because you have chosen the child in your articles rubriken field, not the parent.

Well, you can always use an OR condition:

$articles = $articles->filter(fn ($child) => $child->rubriken()->toPages()->intersects($rubriken) || $child->rubriken()->toPages()->intersects($somethingElse));
1 Like

Strange. Sorry, maybe you could have a look at this again…

<?php if ($block->rubriken()->isNotEmpty()): ?>
<!-- filter set in block  -->
  <?php $filter = $block->rubriken()->toPages(); ?>
<!--compare with articles -->
  <?php $articles = $articles->filter(fn ($child) => $child->rubriken()->toPages()->intersects($filter) || $child->rubriken()->parents()->toPages()->intersects($filter)); ?>
<?php endif ?>

I am manually setting $filter using a page-field connected to ‘rubriken’.

If I am choosing a subpage like ‘blue’ the return works fine, but if I choose lets say the parent page ‘color’ as $filter it does not include articles having ‘blue’ in their rubriken-field although it is clearly a color. :slight_smile:

This doesn’t make sense at all. $child->rubriken() returns a field objects, and parents() is not a method you can call on a field object.

Let’s think this through, so that you understand the problem.

  1. In your articles’s rubriken field, you have stored the value blue which is a category that is a child of another parent category page.
  2. The $block->rubriken(), however, returns the parent page, color. Obviously, color is not a value that is stored in your article. So rather than filtering your pages by color, you would have to pass all colors as arguments.
  3. That means, if $filter contains a parent page like color or form, you would have to replace this value with all the children of color/form (or add them, not sure see below)

What is not quite clear to me if something like form or color will ever be stored in the articles (if so, we need the or statement, otherwise we don’t, but just pass the children)

Or your would have to get the parent pages of the already converted pages inside the filter.

What I also wonder: will $filter always contains one or more main categories like formor color or also just subcategories or even a mix?

Hej thank you much for helping out further!

You analyzed this correctly:
Because I don’t want the user having to also add the parent everytime he adds a subpage of a parent rubrik to his article, $filter should be able to contain a single or multiple parents selected to get only articles with its children or multiple children of multiple parents without the parents itself. (mix)

So basically you could go like: Use any form(parent) & color blue(child).

This should work. Create a plugin with the following code:

<?php

Kirby::plugin('texnixe/category-filter', [
    'pageMethods' => [
        'hasCategory' => function (string $fieldName, Kirby\Cms\Pages $filterCategories) {
            $selectedCategories = $this->{$fieldName}()->toPages();
            $mainCategories     = $filterCategories->filterby('hasChildren', true);

            if ($selectedCategories->intersects($filterCategories)) {
                return true;
            }

            foreach ($selectedCategories as $category) {
                return $mainCategories->has($category->parent());
            }
        },

    ],
]);

Then in your block snippet:

$filter     = $block->rubriken()->toPages();
// make sure to pass the correct fieldname your use in your articles ass first parameter
$articles = $articles->filter(fn ($child) =>  $child->hasCategory('rubrik', $filter));
1 Like

Thanks again.

If my article is set to ‘round’ → filtering by ‘form’ works, but if it also has ‘blue’ (and if this blue is sorted first) it wont show up.

So for example filtering by parent ‘form’ will only show me articles, having a subpage of ‘form’ in the first place. Same behavior by using ‘color’ as $filter…

Ok, the method returns too early.

Kirby::plugin('texnixe/category-filter', [
    'pageMethods' => [
        'hasCategory' => function (string $fieldName, Kirby\Cms\Pages $filterCategories) {
            $selectedCategories = $this->{$fieldName}()->toPages();
            $mainCategories     = $filterCategories->filterby('hasChildren', true);
            $result = false;
            if ($selectedCategories->intersects($filterCategories)) {
                return true;
            }

            foreach ($selectedCategories as $category) {
                if ($mainCategories->has($category->parent())) {
                  return true;
                }
            }
          
            return false;
        },
    ],
]);
1 Like

Hey texnixe,

maybe I can also use your plugin to achieve a list of active “rubriken” grouped by the parents, like:

Active-Rubriken:

Parent-Rubrik-1-Title (2)

  • Children-Rubrik-1-Title
  • Children-Rubrik-1-Title

Parent-Rubrik-2-Title (1)

  • Children-Rubrik-2-Title

Would this be the way?

$all_rubriken = $pages->listed()->template('rubriken');
$active_rubriken = $articles->rubriken()->toPages();
$all_active_rubriken = $all_rubriken->filter(fn ($child) =>  $child->hasCategory('rubriken', $active_rubriken));

<?php foreach ($all_active_rubriken as $ar): ?>
  <?php echo $ar->parent()->title() ?>
  <?php foreach ($ar as $arc): ?>
      <?php echo $arc->title() ?>
  <?php endforeach ?>
<?php endforeach ?>

:slight_smile:

I guess this doesn’t work as expected?

Does this return the right collection? Could you please post your page structure in the file system of the categories (with text file names visible).

This cannot possibly work, because you cannot call a field name on a collection ($articles), but only on a single page. Although I can’t tell how this variable is defined.

It helps if you always provide your variable definitions, otherwise it’s just guesswork.

Hey thanks for feedback,
no – as you said this does not work …

Ok, i’m gonna start with the page-structure of my categories, right now I have:

content/farben/rubriken.de.txt
and content/farben/1_rot/rubrik.de.txt

or

content/form/rubriken.de.txt
and content/form/1_rund/rubrik.de.txt

And here is my whole query for understanding:

<!--
░░░░░░░░░░░░░░░░░░░░░░░
Q U E R Y
░░░░░░░░░░░░░░░░░░░░░░░
-->

<!-- collection via block -->
<?php $filter1 = $block->grid()->first(); ?>

<!-- activate collection (f1) -->
<?php if ($filter1->isNotEmpty()): ?>
  <?php  $articles = page($filter1); ?>
<?php else: ?>
  <?php $articles = $site->pages(); ?>
<?php endif ?>
<?php $articles = $articles->children()->listed(); ?>

<!-- rubrikcheck -->
<?php if ($block->rubriken()->isNotEmpty()): ?>
  <?php $rubrikcheck = true ?>
<?php else: ?>
  <?php $rubrikcheck = false ?>
<?php endif ?>

<!-- filter by rubriken inside block -->
<?php if ($rubrikcheck = true): ?>
  <?php $filter = $block->rubriken()->toPages(); ?>
  <!-- compare by plugin  -->
  <?php $articles = $articles->filter(fn ($child) =>  $child->hasCategory('rubriken', $filter)); ?>
<?php endif ?>

<!-- Settings -->
<?php $pagination = $block->paginate()->toInt(); ?>

<?php if ($block->limit()->toInt() > 0): ?>
<?php $limit = $block->limit()->toInt(); ?>
<?php else: ?>
<?php $limit = 99999999; ?>
<?php endif ?>

<!-- activate w settings -->
<?php
$articles = $articles
->sortBy(function ($page){ return $page->creationdate()->toDate();}, 'desc')
->limit($limit)
->paginate($pagination);
?>

<!-- NEW NEW NEW: GROUP BY PARENT? -->

<?php if ($rubrikcheck = true): ?>
<?php if ($articles->count() > 0): ?>

<?php
$all_rubriken = $pages->template('rubriken');
var_dump($all_rubriken);
$active_rubriken = $articles->rubriken();
var_dump($active_rubriken);
$all_active_rubriken = $all_rubriken->filter(fn ($child) =>  $child->hasCategory('rubriken', $active_rubriken));
var_dump($all_active_rubriken);
?>

<?php foreach ($all_active_rubriken as $ar): ?>
  <?php echo $ar->parent()->title() ?>
  <?php foreach ($ar as $arc): ?>
      <?php echo $arc->title() ?>
  <?php endforeach ?>
<?php endforeach ?>

<?php endif ?>
<?php endif ?>

What is this?

This will return the main categories only like form, farben etc, but not the subcategories like red etc. Is that intended?

This is a select field with the following reference in the blueprint:

query: site.children.template(‘sammlung’)

This is the folder-structure for this:
content/testsammlung/sammlung.de.txt
and content/testsammlung/1_test-custom/sammlung-custom.de.txt

Each sammlung-custom contains a pages field with the following query in the blueprint:
site.children.template(‘rubriken’)

To get a selection of all categories used in $articles:

$allUsedCategories = new Pages();
$fields = $articles->pluck('rubriken', null));
foreach($fields as $field) {
    $cats = $field->toPages();
    $allUsedCategories->add($cats);
}

Once you have those, you can group them.

Hej I am getting this message:
Block error: "You must pass a Pages or Page object or an ID of an existing page to the Pages collection" in block type: "grid"

Maybe change it to “toPages()”?

$allUsedCategories = new Pages();
$fields = $articles->pluck('rubriken', null));
foreach($fields as $field) {
    $cats = $field->toPages();
    $allUsedCategories->add($cats);
}

Of course, was a copy/paste error.

Perfect. With the following code it seems to work.

<?php if ($rubrikcheck = true): ?>
<?php if ($articles->count() > 0): ?>

<?php
$allUsedCategories = new Pages();
$fields = $articles->pluck('rubriken', null);
foreach($fields as $field) {
    $cats = $field->toPages();
    $allUsedCategories->add($cats);
}; ?>

<?php $callback = function($p) {return $p->parent() }; ?>
<?php $groupedItems = $allUsedCategories->group($callback); ?>

<?php foreach($groupedItems as $rub => $itemsPerRub): ?>
  <h2><?= $rub ?></h2>
  <ul>
    <?php foreach($itemsPerRub as $item) : ?>
    <li><?= $item->title() ?></li>
    <?php endforeach; ?>
  </ul>
<?php endforeach ?>

The only (and I swear last) problem is, that this is giving me every used category of $articles. I would like to narrow this down more to only the ones which are also selected via $filter at the beginning:

<?php $filter = $block->rubriken()->toPages(); ?>

////

That’s why I have tried to use another filter() on $allUsedCategories

<!-- Filter by Block again -->
<?php $allUsedCategories = $allUsedCategories->filter(fn ($child) => $child->has($filter)); ?>
<?php var_dump($filter); ?><br>
<?php var_dump($allUsedCategories); ?>

And the dumps are:

object(Kirby\Cms\Pages)#485 (1) { [0]=> string(9) "form/rund" } 
object(Kirby\Cms\Pages)#523 (2) { [0]=> string(11) "farben/blau" [1]=> string(9) "form/rund" }

Should this not be a match here?