Filtering Compendium

This guide will give you an overview about the many versatile ways of filtering collections of pages, files, users, and languages that are available in Kirby. It is sort of a compendium of many examples taken from solutions in the forum.

Filtering by visible/invisible

In Kirby, pages can be either visible (folder with prepended number/date) or invisible. Kirby has two handy built-in methods to filter pages according to their visibilty status:

$collection = page('projects')->children()->visible();
$collection = page('projects')->children()->invisible();

Check out the docs: $pages->visible() and $pages->invisible. Please note that visibleand invisible are just flags for filtering collections. An invisible folder is still accessible via its URL.

Filter page collections by a single field

Often, you only want to display all pages that have a certain value in a special field, e.g. a category or a tags field. For this task, you can use the filterBy() method:

// filter a collection by a value in a single value field
$collection = page('projects')->children()->visible()->filterBy('category', 'webdesign');

// filter a collection by a value in a field with a comma separated list of values
$collection = page('projects')->children()->visible()->filterBy('tags', 'webdesign', ',');

Note the use of the delimiter in the second example to get a value from a comma separated list. If you use other delimiters in your fields, you can, of course, change the delimiter.

For more information on using tags dynamically to filter blog posts, see Filtering content by tag

Filtering using filter operators

With the above example, you only get results that fit a single value. You can fine tune your filter results by using the operator parameter in the filterBy() method:

Pages

// get all pages with a date after now
$collection = page('blog')->children()->filterBy('date', '>', time());

// get all pages with a title that starts with "A" or a number
$collection = page('blog')->children()->filterBy('title', '<', 'B');

Files

// get all images that contain a string
$images = page('blog')->images()->filterBy('filename', '*=', 'cover-');

// get all file types exept images
$files = page('blog')->images()->filterBy('type', '!=', 'image');

For a full documentation of the available filter operators see the docs.

Filtering by methods

You cannot only filter by custom field methods in your content files but also by methods of a pages', files', users' collection. Here are some examples:

Pages

// filter by page template
$collection = page('projects')->children()->visible()->filterBy('template', 'blogarticle');

// filter by intended template
$collection = page('projects')->children()->visible()->filterBy('intendedTemplate', 'blogarticle');

// filter by depth (this is the same as ($site->grandchildren())
$collection = $site->index()->filterBy('depth', 2);

// filter by modified date
$collection = $site->index()->filterBy('modified', '>', strtotime('2016-04-30'));

Files

// filter by file extension
$images = $page->images()->filterBy('extension', 'png');

// filter by type
$files = $page->files()->filterBy('type', 'document');

Users

// filter by role
$users = $site->users()->filterBy('role', 'editor');

Filter collections by more than one field/method

You can also string together multiple filterBy() methods to narrow down your filter results (using AND logic):

Pages

// fetch all events with a template that is neither "concert" nor "exhibition"
$collection = page('events')->children()->filterBy('template', '!=', 'concert')->filterBy('template', '!=', 'exhibition');

You can, of course, achieve the same result with a filter with callback:

$collection = page('events')->children()->filter(function($p) {
  return $p->template() != 'concert' && $p->template() != 'exhibition';
});

2.3.2 +

You can also use the new not in filter:

// fetch all events with a template that is neither "concert" nor "exhibition"
$collection = page('events')->children()->filterBy('template', 'not in', ['concert', 'exhibition']);

Files

// fetch all files that are neither videos nor images
$files = $page->images()->filterBy('type', '!=', 'video')->filterBy('type', '!=', 'image');

Users

// fetch all users that are neither admins nor editors
$files = $site->users()->filterBy('role', '!=', 'admin')->filterBy('role', '!=', 'editor');

Create custom collection filters

In addition to the predifined filter methods mentioned above, you can also define your own custom collection filters for filtering pages, files, users or languages.

The following example filters pages by language.

Add the filter to a filters.php file in /site/plugins:

// Anything that matches a given language attribute

collection::$filters['languageIs'] = function($collection, $field, $value) {

  foreach($collection->data as $key => $item) {
    if(collection::extractValue($item->content()->language(), $field) != $value) {
      unset($collection->$key);
    }
  }

  return $collection;

};

You can then use this filter in your templates like this:

$articles = $page->children()->visible()->filterBy('code', 'languageIs', 'en');

You can find more on this in the docs.

Filter by multiple values in an array

In this example we have an array of locations (e.g. from a post request) and want to fetch all stores with a location field value of one the array items.

// $selections is an array with the values of the selected checkboxes
$locations = array('New York', 'Paris', 'Tokyo');
$stores = $pages->filter(function($p) use($locations)  {
  // only $p that return true in here will be part of $stores
  return in_array($child->locations(), $selections);
});

In this example, $child->locations() only contains a single value, so we can use in_array() here.

In the this example we fetch all blog posts with one of the tags of the current page. The resulting collection is the shuffled and three pages are returned.

<?php
// get an array of the tags of the current page
$tags = $page->tags()->split(',');

// filter the blog posts with a filter with callback
$children = page('blog')->children()->visible();
$relatedPages = $children->filter(function($child) use($tags) {
  if(array_intersect($child->tags()->split(','), $tags)) {
    return $child;
  }
});
?>
<ul>
  <!-- shuffle and limit the results -->
  <?php foreach($relatedPages->not($page)->shuffle()->limit(3) as $related): ?>
    <li><a href='<?= $related->url(); ?>'><?= $related->title(); ?></a></li>
  <?php endforeach ?>
</ul>

Note that here we compare two arrays, so we have to use array_intersect() instead.

Fun with filtering by date

Since the date field is a bit special, I'll cover this here a bit more extensively.

Filtering from - to

For a simple from - to filtering of dates, you can string together two filterBy() methods as seen above:

// fetch all blog post of 2015 using beginning and end dates
$collection = page('blog')->children()->visible()->filterBy('date', '>', strtotime('2015-01-01'))->filterBy('date', '<', strtotime('2015-12-31'))

If you just happen to have the year as a variable, you can use a filter with callback:

$year = '2015';
$collection = $pages->find('blog')->children()->visible()->filter(function($p) use($year) {
  return $p->date('Y') === $year;
});

Custom date fields

Events often use custom startdate and enddate date fields. The filterBy() method currently does not work with custom date fields. To filter events with either their ending or beginning in the future, you can use a filter with a callback:

$events = page('events')
          ->children()
          ->visible()
          ->filter(function($child) {
            return $child->date(null, 'startdate') > time() || $child->date(null, 'enddate') > time();
          });

Note that the date method here uses two parameters. The first is the format parameter, the second is the name of the custom field.

With just two little changes to the above code, you can fetch all events that are happening right now:

$events = page('events')
          ->children()
          ->visible()
          ->filter(function($child) {
            return $child->date(null, 'startdate') < time() && $child->date(null, 'enddate') > time();
          });

Using filters with forms (e.g. select fields)

Let's suppose we had a clothing shop and wanted to give our potential customers a chance to filter our products by several categories. Here we use three select fields to filter by color, brand, and size. We submit the form via Javascript. We use the following form in our products.php template. (We could of course generate this form programmatically, but let's keep it simple.)

<form id="filters" method="post">
  <!-- give the select the name of the category to filter by -->
  <select name="color" onchange="this.form.submit()">
    <option selected value="">Select a color</option>

    <!-- let's fill the options with our colors -->
    <?php foreach($color as $item): ?>
      <!-- we don't want empty items -->
      <?php if(!$item) continue ?>

      <!-- set the option to selected if selected -->
      <option<?php e(isset($data['color']) && $data['color'] == $item, ' selected') ?> value="<?= $item ?>"><?= $item?></option>
    <?php endforeach ?>
  </select>
  <select name="brand" onchange="this.form.submit()">
    <option selected value="">Select a brand</option>

    <?php foreach($brand as $item): ?>
      <?php if ($item == "") continue; ?>
      <option<?php e(isset($data['brand']) && $data['brand'] == $item, ' selected') ?> value="<?= $item ?>"><?= $item ?></option>
    <?php endforeach ?>
  </select>
  <select name="size" onchange="this.form.submit()">
    <option selected value="">Select a size</option>

    <?php foreach($size as $item): ?>
      <?php if($item == "") continue; ?>
      <option<?php e(isset($data['size']) && $data['size'] == $item, ' selected') ?> value="<?= $item ?>"><?= $item ?></option>
    <?php endforeach ?>
  </select>
</form>

Now that we have our form ready, let's get into the logic. In our products.php controller we filter our products if there is a post request:

<?php

return function($site, $pages, $page) {

  $color = $page->children()->pluck('color', null, true);
  $brand = $page->children()->pluck('brand', null, true);
  $size = $page->children()->pluck('size', null, true);
  $keys = array('color', 'brand', 'size');

  // return all children if nothing is selected
  $projects = $page->children()->visible();

  // if there is a post request, filter the projects collection
  if(r::is('POST') && $data = get()) {
    $projects = $page->children()->visible()->filter(function($child) use($keys, $data) {

      // loop through the post request
      foreach($data as $key => $value) {

        // only act if the value is not empty and the key is valid
        if($value && in_array($key, $keys)) {

          // return false if the child page's category and value don't match
          if(!$match = $child->$key() == $value) {
            return false;
          }
        }
      }

      // otherwise return the child page
      return $child;

    });
  }
  return compact('products', 'color', 'brand', 'size', 'data');
};

Filtering using the search method

Another option of filtering collections is by using the search() method. There is a complete tutorial on how to include a search to your site in the docs.

You can find an example of how to use the search() method with a multi-select form in this forum post.