Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Searching Craft CMS Matrix Blocks
With a little custom Behavior, we can easily query for Elements such as Entries in Craft CMS based on the contents of the Matrix Blocks they contain
Matrix blocks in Craft CMS are fantastically flexible, allowing you to create highly customized free-form block editors for your content authors.
However, the more content you put into Matrix blocks, the more you may need to search on the content they contain.
The more you put into the Matrix, the more you’ll want to get out
While Craft CMS offers Matrix block queries to facilitate querying them, some common patterns can be a little complicated.
For instance, a common situation is you need to query for Elements such as Entries, Orders, etc. based on the content inside of Matrix blocks that they contain.
In this article, we’ll learn how to do just that, so read on!
Link Hot Fudge Sundae
We’ll leverage the same example used in the Custom Matrix Block Peer Validation Rules article, with a Sundae Builder that allows you to build sundaes by adding scoops and toppings as matrix blocks.
Let’s have a quick look at the setup in the Craft CMS CP for our Sundae Builder.
First, we have a Sundae section that has a single field added to it, a Matrix block field named Sundae Builder:
We then have two block types in our Sundae Builder.
First, we have the Scoop block with a field named Flavor that lets you add a scoop of ice cream, and pick a flavor:
Next, we have a Topping block with a field named Kind that lets you add a topping to the sundae:
While the example presented here is relatively simple, we can expand on it to do some pretty amazing things.
Link The Problem
So with this setup, what we want to be able to do is find Entries in the Sundae section, but we want to filter them based on the content in the Sundae Builder Matrix blocks.
For example, maybe we need to find all Entries in the Sundae section where the sundae was made with a scoop of chocolate ice cream.
There’s no straightforward way to do this in Craft CMS, but Brandon Kelly posted a solution on a GitHub issue that we’re basing ours on.
The basic idea is to query the Matrix blocks first, then use the results to filter our Element query
So what we do is find the Matrix blocks that match our criteria, get their ownerIds (the id of the Element that contains the Matrix field), and then use the result to filter our Element query.
While this is all well and good if we’re fluent in PHP, we’d like a solution that works in Twig as well. In addition, a way for it to be a bit more generalized would be aces.
Link Oh, Behave!
What we’re going to do is make queries filtered by Matrix criteria globally available to all Element queries, in both PHP and Twig.
We’ll do this by utilizing Yii2 Behaviors, which allow us to dynamically extend the functionality of existing PHP classes.
For more on Behaviors, check out the Extending Craft CMS with Validation Rules and Behaviors article.
We’ll be adding this functionality via a Module, discussed in depth in the Enhancing a Craft CMS 3 Website with a Custom Module article.
Every website I create has a Module baked into it called Site Module.
Even if your Craft CMS website doesn’t need a custom Module immediately, it’s nice to have the scaffolding there in place for future needs.
So let’s have a look at the full MatrixCriteriaBehavior that we’re using (don’t worry, we’ll break it down later):
<?php
namespace modules\sitemodule\behaviors;
use Craft;
use craft\elements\db\ElementQuery;
use craft\elements\db\ElementQueryInterface;
use craft\elements\MatrixBlock;
use craft\events\CancelableEvent;
use yii\base\Behavior;
/**
* @author nystudio107
* @package SiteModule
* @since 1.0.0
*/
class MatrixCriteriaBehavior extends Behavior
{
// Constants
// =========================================================================
const NO_MATCHING_MATRIX_CRITERIA = 'no-matching-matrix-criteria';
// Public Properties
// =========================================================================
/**
* @var string the Matrix field to use
*/
public $matrixFieldHandle;
/**
* @var array the criteria for the Matrix query
*/
public $matrixCriteria;
// Public Methods
// =========================================================================
/**
* @inheritDoc
*/
public function events()
{
return [
ElementQuery::EVENT_BEFORE_PREPARE => function($event) {
$this->applyMatrixCriteriaParams($event);
},
];
}
/**
* Limit the ElementQuery to elements that match the passed in Matrix criteria
*
* @param string $matrixFieldHandle the handle of the Matrix field to match the criteria in
* @param array $matrixCriteria the criteria for the MatrixBlock query
* @return ElementQueryInterface
*/
public function matrixCriteria(string $matrixFieldHandle, array $matrixCriteria): ElementQueryInterface
{
$this->matrixFieldHandle = $matrixFieldHandle;
$this->matrixCriteria = $matrixCriteria;
/* @var ElementQueryInterface $elementQuery */
$elementQuery = $this->owner;
return $elementQuery;
}
// Private Methods
// =========================================================================
/**
* Apply the 'matrixFieldHandle' & 'matrixCriteria' params to select the ids
* of the elements that own matrix blocks that match, and then add them to the
* id parameter of the ElementQuery
*
* @param CancelableEvent $event
*/
private function applyMatrixCriteriaParams(CancelableEvent $event): void
{
if (!$this->matrixFieldHandle || empty($this->matrixCriteria)) {
return;
}
/* @var ElementQueryInterface $elementQuery */
$elementQuery = $this->owner;
// Get the id of the matrix field from the handle
$matrixField = Craft::$app->getFields()->getFieldByHandle($this->matrixFieldHandle);
if ($matrixField === null) {
return;
}
// Set up the matrix block query
$matrixQuery = MatrixBlock::find();
// Mix in any criteria for the matrix block query
Craft::configure($matrixQuery, $this->matrixCriteria);
// Get the ids of the elements that contain matrix blocks that match the matrix block query
$ownerIds = $matrixQuery
->fieldId($matrixField->id)
->select('matrixblocks.ownerId')
->orderBy(null)
->distinct()
->column();
// If the original query's `id` is not empty, use the intersection
if (!empty($elementQuery->id)) {
$originalIds = $elementQuery->id;
if (!is_array($originalIds)) {
$originalIds = [(int)$originalIds];
}
$ownerIds = array_intersect($originalIds, $ownerIds);
}
// Ensure the parent query returns nothing if no ids were found
if (empty($ownerIds)) {
$ownerIds = null;
$elementQuery->uid = self::NO_MATCHING_MATRIX_CRITERIA;
}
// Add them to the original query that was passed in
$elementQuery->id($ownerIds);
}
}
This is a Behavior, located in our Site Module’s modules/sitemodule/src/behaviors/ directory, that we will attach to the craft\elements\db\ElementQuery class, which is what you’re using every time you do something like:
{% set orders = craft.entries
.section('sundaes')
.all()
%}
This Behavior does the following:
- Adds two new properties to the ElementQuery class: $matrixFieldHandle & $matrixCriteria to hold the new parameters our MatrixCriteria needs
- Adds a new method to the ElementQuery class called matrixCriteria() that lets you specify the Matrix field handle and Matrix criteria to filter the Element query by
- Listens for the ElementQuery::EVENT_BEFORE_PREPARE event that the ElementQuery triggers before the query parameters you’ve specified are converted into a database query
- When that event is triggered, it finds any Matrix blocks that match the Matrix field handle and Matrix criteria you’ve specified, and adds them to the id query parameter of the Element query
This allows you to then do a query like this:
{% set orders = craft.entries
.section('sundaes')
.matrixCriteria('sundaeBuilder', {
'type': 'scoop',
'flavor': 'chocolate'
})
.all()
%}
Or the same MatrixCriteria query in PHP:
use craft\elements\Entry;
$orders = Entry::find()
->section('sundaes')
->matrixCriteria('sundaeBuilder', [
'type' => 'scoop',
'flavor' => 'chocolate'
])
->all();
Both of these query the sundaes Section for Entries that have a Matrix field with the handle sundaeBuilder for Matrix blocks of the type scoop, which have the custom field flavor set to chocolate.
This “just works” because the Behavior has been added to the built-in Craft ElementQuery! 🎉
You can pass anything into the second parameter that you normally would in a Matrix Block query, including custom fields.
It’s just using array syntax rather than object syntax, so that you don’t have to create a Query object to pass in.
N.B.: We could also have listened for the ElementQuery::EVENT_AFTER_PREPARE event, and modified the $elementQuery->subQuery instead, to accomplish the same thing (and it might even lead to a bit cleaner of an implementation, too).
Link Getting Attached
The only other thing we need to do in order to make the MatrixCriteriaBehavior work is attach it to ElementQuery objects.
We do that dynamically in our Site Module’s init() method like so:
use modules\sitemodule\behaviors\MatrixCriteriaBehavior;
use craft\elements\db\ElementQuery;
use craft\events\DefineBehaviorsEvent;
// Add the MatrixCriteriaBehavior behavior to ElementQuery objects
Event::on(
ElementQuery::class,
ElementQuery::EVENT_DEFINE_BEHAVIORS,
function(DefineBehaviorsEvent $event) {
$event->sender->attachBehaviors([
MatrixCriteriaBehavior::class,
]);
}
);
Link End of Query
You can use this Behavior right out of the box to start doing queries based on the contents of Matrix blocks.
In fact, you could include this Behavior as a default on every site you make, so the functionality is there waiting for you.
If someone wants to turn this into a plugin to make the barrier of entry lower, feel free to do so!
But more importantly, hopefully, it has also given you insight into how you might extend ElementQuery objects, or other parts of Craft CMS in wonderful ways using Behaviors.
Happy querying!