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:

  1. Create another channel for Ingredients.
  2. Create a new Entries field, with the name “Ingredients.”
  3. Limit the Sources option to “Ingredients” only.
  4. Leave the Limit field blank so we can choose however many ingredients each recipe needs.
  5. 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 five built-in relational fields, each of which establishes links to a different element type:

Additionally, the link field uses the relations system to store references to assets, categories, and entries.

Addresses and global sets don’t have relational fields, in the traditional sense—the former are managed as nested elements, and the latter exist as static, singleton elements that can be queried by handle using the value of a dropdown field.

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:

See complex relationships, below, for detailed information about the capabilities of hash and array syntaxes.

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.

# The notRelatedTo and andNotRelatedTo Parameters

Find entries that aren’t related to one or more elements with the not* relational query methods. This can be combined with the relatedTo parameter.



 
 


{% set ketoRecipes = craft.entries()
  .section('recipes')
  .relatedTo(fish)
  .notRelatedTo(gluten)
  .all() %}

# 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, or targetElement
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 sourceElement to return elements selected in a relational field on the provided element(s);
  • Use targetElement to return elements that have the provided element(s) selected in a relational field.

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.

The field param does not honor handles overridden in a field layout. Craft doesn’t know until elements are actually loaded which field layouts are relevant.

# 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!