Relations
Craft has a powerful engine for relating elements to one another with five relational field types. Just like other field types, relational fields can be added to any field layout, including those within nested entries.
Unlike other field types, relational fields store their data in a dedicated relations
table. In that table, Craft tracks:
- The element that is the source of the relationship;
- Which field a relationship uses;
- The site a relationship was defined;
- The element that is the target of the relationship;
- The order in which the related elements are arranged;
This allows you to design fast and powerful queries for related content, and to optimize loading of nested and related resources.
# Terminology
Each relationship consists of two elements we call the source and target:
- The source has the relational field where other elements are chosen.
- The target is the one selected by the source.
# Illustrating Relations
Suppose we have a database of Recipes (represented as a channel) and we want to allow visitors to browse other recipes that share an ingredient. To-date, ingredients have been stored as plain text along with the instructions, and users have relied on search to discover other recipes.
Let’s leverage Craft’s relations system to improve this “schema” and user experience:
- Create another channel for Ingredients.
- Create a new Entries field, with the name “Ingredients.”
- Limit the Sources option to “Ingredients” only.
- Leave the Limit field blank so we can choose however many ingredients each recipe needs.
- Add this new field to the Recipes channel’s field layout.
Now, we can attach Ingredients to each Recipe entry via the new Ingredients relation field. Each selected ingredient defines a new relationship, with the recipe as the source and the ingredient as the target.
# Using Relational Data
Relationships are primarily used within element queries, either via a relational field on an element you already have a reference to, or the relatedTo()
query parameter.
# Custom Fields
Ingredients attached to a Recipe can be accessed using the relational field’s handle. Unlike most fields (which return their stored value), relational fields return an element query, ready to fetch the attached elements in the order they were selected.
Craft has X built-in relational fields, each pointing to a different element type:
Addresses and global sets don’t have relational fields, in the traditional sense.
Eager-loading related elements does make them available directly on the source element! Don’t worry about this just yet—let’s get comfortable with the default behavior, first.
To output the list of ingredients for a recipe on the recipe’s page, you could do this (assuming the relational field handle is ingredients
):
{# Fetch related elements by calling `.all()` on the relational field: #}
{% set ingredients = entry.ingredients.all() %}
{# Check if anything came back: #}
{% if ingredients|length %}
<h3>Ingredients</h3>
<ul>
{# Loop over the results: #}
{% for ingredient in ingredients %}
<li>{{ ingredient.title }}</li>
{% endfor %}
</ul>
{% endif %}
Because entry.ingredients
is an element query, you can set additional constraints before executing it:
{# Narrow the query to only “Vegetables”: %}
{% set veggies = entry.ingredients.foodGroup('vegetable').all() %}
<h3>Vegetables</h3>
{% if veggies|length %}
<ul>
{% for veggie in veggies %}
<li>{{ veggie.title }}</li>
{% endfor %}
</ul>
{% else %}
<p>This recipe has no vegetables!</p>
{% endif %}
This query will display ingredients attached to the current recipe that also have their foodGroup
field (perhaps a Dropdown) set to vegetable
. Here’s another example using the same schema—but a different query execution method—that lets us answer a question that some cooks might have:
{% set hasMeat = entry.ingredients.foodGroup(['meat', 'poultry']).exists() %}
{% if not hasMeat %}
<span class="badge">Vegetarian</span>
{% endif %}
Each relational field type will return a different type of element query. Entries fields produce an entry query; Categories fields produce a category query; and so on.
# The relatedTo
Parameter
The relatedTo
parameter on every element query allows you to narrow results based on their relationship to an element (or multiple elements).
Any of the following can be used when setting up a relational query:
- A single element object: craft\elements\Asset (opens new window), craft\elements\Category (opens new window), craft\elements\Entry (opens new window), craft\elements\User (opens new window), or craft\elements\Tag (opens new window)
- A single element ID
- A hash with properties describing specific constraints on the relationship:
- Required:
element
,sourceElement
, ortargetElement
- Optional:
field
andsourceSite
- Required:
- An array of the above options, with an optional operator in the first position:
- The string
and
, to return relations matching all conditions rather than any; - The string
or
, to return relations that match any conditions (default behavior, can be omitted);
- The string
Chaining multiple relatedTo
parameters on the same element query will overwrite earlier ones. Use andRelatedTo
to append relational constraints.
# The andRelatedTo
Parameter
Use the andRelatedTo
parameter to join multiple sets of relational criteria together with an and
operator. It accepts the same arguments as relatedTo
, and can be supplied any number of times.
There is one limitation, here: multiple relatedTo
criteria using or
and and
operators cannot be combined.
# Simple Relationships
The most basic relational query involves passing a single element or element ID. Here, we’re looking up other recipes that use the current one’s main protein:
{# Grab the first protein in the recipe: #}
{% set protein = entry.ingredients.foodGroup('protein').one() %}
{% set similarRecipes = craft.entries()
.section('recipes')
.relatedTo(protein)
.all() %}
{# -> Recipes that share `protein` with the current one. #}
Passing an array returns results related to any of the supplied elements. This means we could expand our criteria to search for other recipes with any crossover in proteins:
{# Note the use of `.all()`, this time: #}
{% set proteins = entry.ingredients.foodGroup('protein').all() %}
{% set moreRecipes = craft.entries()
.section('recipes')
.relatedTo(proteins)
.all() %}
{# -> Recipes that share one or more proteins with the current one. #}
Passing and
at the beginning of an array returns results relating to all of the supplied items:
{% set proteins = entry.ingredients.foodGroup('protein').all() %}
{% set moreRecipes = craft.entries()
.section('recipes')
.relatedTo(['and'] | merge(proteins))
.all() %}
{# -> Recipes that also use all this recipe’s proteins. #}
This is equivalent to .relatedTo(['and', beef, pork])
, if you already had variables for beef
and pork
.
# Compound Criteria
Let’s look at how we might combine multiple relational criteria:
{# A new relational field for recipes, tracking their origins: #}
{% set origin = entry.origin.one() %}
{% set proteins = entry.ingredients.foodGroup('proteins').all() %}
{% set regionalDishes = craft.entries()
.section('recipes')
.relatedTo([
'and',
origin,
proteins,
])
.all() %}
{# -> Recipes from the same region that share at least one protein. #}
You could achieve the same result as the example above using the andRelatedTo
parameter:
{% set regionalDishes = craft.entries()
.section('recipes')
.relatedTo(origin)
.andRelatedTo(proteins)
.all() %}
These examples may return the recipe you’re currently viewing. Exclude a specific element from results with the id
param: .id(['not', entry.id])
.
# Complex Relationships
All the relatedTo
examples we’ve looked at assume that the only place we’re defining relationships between recipes and ingredients is the ingredients field. What if there were other fields on recipes that described “substitutions,” or “pairs with” and “clashes with” that might muddy our related recipes? What if an ingredient had a “featured seasonal recipe” field?
Craft lets you be specific about the location and direction of relationships when using relational params in your queries. The following options can be passed to relatedTo
and andRelatedTo
as a hash:
# Sources and Targets
- Property
- One of
element
,sourceElement
, ortargetElement
- Accepts
- Element ID, element, element query, or an array thereof
- Description
- Use
element
to get results on either end of a relational field (source or target);
- Use
- Use
sourceElement
to return elements selected in a relational field on the provided element(s);
- Use
- Use
targetElement
to return elements that have the provided element(s) selected in a relational field.
- Use
One way of thinking about the difference between sourceElement
and targetElement
is that specifying a source is roughly equivalent to using a field handle:
{% set ingredients = craft.entries()
.section('ingredients')
.relatedTo({
sourceElement: recipe,
})
.all() %}
{# -> Equivalent to `recipe.ingredients.all()`, from our very first example! #}
# Fields
- Property
field
(Optional)- Accepts
- Field handle, field ID, or an array thereof.
- Description
- Limits relationships to those defined via one of the provided fields.
Suppose we wanted to recommend recipes that use the current one’s alternate proteins—but as main ingredients, not substitutions:
{% set alternateProteins = recipe.substitutions.foodGroup('protein').all() %}
{% set recipes = craft.entries()
.section('recipes')
.relatedTo({
targetElement: alternateProteins,
field: 'ingredients',
})
.all() %}
By being explicit about the field we want the relation to use, we can show the user recipes that don’t rely on substitutions to meet their dietary needs.
# Sites
- Property
sourceSite
(Optional, defaults to main query’s site)- Accepts
- Site (opens new window) object, site ID, or site handle.
- Description
- Limits relationships to those defined from the supplied site(s).
- In most cases, you won’t need to set this explicitly. Craft’s default behavior is to look for relationships only in the site(s) that the query will return elements for.
Only use sourceSite
if you’ve designated your relational field to be translatable.
What if our recipes live on an international grocer’s website and are localized for dietary tradition? We can still provide results that make sense for a variety of cooks:
{% set proteins = recipe.ingredients.foodGroup('protein').all() %}
{% set recipes = craft.entries()
.section('recipes')
.relatedTo([
'or',
{
targetElement: proteins,
field: 'ingredients',
},
{
targetElement: proteins,
sourceSite: null,
field: 'substitutions',
},
])
.all() %}
{# -> Recipes that share regionally-appropriate proteins, *or* that can be adapted. #}
Here, we’re allowing Craft to look up substitutions defined in any site, which might imply a recipe can be adapted.
# Relations via Matrix
Relational fields on nested entries within Matrix fields are used the same way they would be on any other element type.
If you want to find elements related to a source element through a Matrix field, pass the Matrix field’s handle to the field
parameter:
{% set relatedRecipes = craft.entries()
.section('recipes')
.relatedTo({
targetElement: ingredient,
field: 'ingredients'
})
.all() %}
In this example, we’ve changed our schema a bit: ingredients are now attached to nested entries in a steps
Matrix field, so we can tie instructions and quantities or volume to each one. We still have access to all the same relational query capabilities!
We’ve also specified a field handle, to ensure that the relationships are defined through the intended field; if nested entries have multiple relational fields, it’s possible to get “false positive” results!