An introduction to relations and reverse relations in Craft CMS, with examples

Craft is a powerful tool to manage relations between your content. Craft doesn't have a single "relationship" field, but several relational field types. These include Entries, Assets, Users, Categories, and Tags.

Let's pretend we are an agency and we want to create a relationship between our Projects which appear in our Project Gallery and our Services which appear on our Services page. We will do this by creating two channels 'Projects' and 'Services'.

In our Projects Channel we will create an Entries field called 'Related Services' where we can select one or more Services that were provided for that Project. In our template, we will loop through the 'Related Services' Entries field to display all of our Project's related 'Services':

{# Project Template: templates/projects/_entry #}
{% for service in entry.servicesEntriesField %}
  {{ service.title }}
{% endfor %}

Our Entries field behaves just like all of the other relational fields (Assets, Categories, Tags, Users) and we can interact with it in the same way that we interact with any other fields that have an array of data in our templates, such as: Checkbox, Dropdown, Multi-select, and Radio field options; Matrix field blocks; and Table rows. While our related entries may be an Entries field here, we can treat our entries the same as we would if we were looping through a Channel and Structure Section.

{% for entry in entry.servicesEntriesField %}
  {# We can interact with the entries in our Entries Field loop #}
  {{ entry.title }}
  {{ entry.url }}
{% endfor %}

{% for entry in craft.entries('projects') %}
  {# The same way we interact with entries as we loop through our Sections #}
  {{ entry.title }}
  {{ entry.url }}
{% endfor %}

If we combine those two snippets with our example:

{# We will update the Section `entry` variable to be called 
   `project` so it's easier for us to identify in our loop #}
{% for project in craft.entries('projects') %}

  {# Our Project Entry information #}
  {{ project.title }}
  {{ project.url }}

  {# We'll also update the Field `entry` variable to be called 
     `services` so it's easier for us to identify in our nested loop #}
  {% for service in entry.servicesEntriesField %}

    {# Our Services Entry information #}
    {{ service.title }}
    {{ service.url }}

  {% endfor %}

{% endfor %}

Displaying reverse relations #

Now let's say we wanted to do the opposite: display all of our 'Projects' on our individual 'Services' page. In our current example, the 'Related Services' Entries field is in our Projects channel, so we can't just loop through the field like we did before. Our Services Section does not have a Projects Entries field. And we don't want to have to add and maintain this data in two places. So what do we do?

In this situation, we can dynamically retrieve the information we need in our template using the relatedTo() parameter. Let's take a look at how we do this:

{# Services Template: templates/services/_entry
   In this template `entry` is one of our Service Entries #}
{% set relatedProjects = craft.entries.section('projects').relatedTo(entry) %}

{% for service in relatedProjects %}
  {{ service.title }}
{% endfor %}

Let's take a closer look at what this code is doing. On its own, the craft.entries tag says: Get me all Entries in the Projects section.

craft.entries.section('projects')

When we add the relatedTo() parameter, the full tag reads: Get me all Entries in the Projects section, but, only if they are have a relationship with the specific Service that we are identifying in the relatedTo parameter. Remember, in our example, entry is one of our Service Entries.

craft.entries.section('projects').relatedTo(entry)

We then assign those results to a variable we created called relatedProjects.

{% set relatedProjects = craft.entries.section('projects').relatedTo(entry) %}

And loop through those results just like we would any array of Entries we interact with in Craft:

{% for project in relatedProjects %}
  {{ project.title }}
{% endfor %}

Again, it's important to note that while we may be accessing our Entries in several different ways, all of entries we are interacting with in the examples above are EntryModels. We can treat every EntryModel (on a single page, in a for loop, in a relational field, or as a reverse relationship) just like we would any other EntryModel in the system.

To review the EntryModel's we've seen above:

  • In our Projects and Service Sections with their own URLs entry is an EntryModel. This is also true for Sections that are Singles.
  • Looping through the entries in our Projects Section, craft.entries('projects') is an array of EntryModels. Each time we loop through an array we access an individual EntryModel.
  • Our Entries relational field entry.servicesEntriesField is an array of EntryModels.
  • Our reverse relationship craft.entries.section('projects').relatedTo(entry) is an array of EntryModels.

Combining several different types of relations on a single page #

If you want to try to push these concepts a bit further, here's one more, largely academic, example about how we can combine all of these different scenarios into one template. For the sake of exploration in this next example, we will assume we have a Singles page in which we will display both our Project content with related Service information. On top of this, within our Services information, we will also create a reverse relationship back to our Projects.

{# Our Singles Section EntryModel can be accessed with the `entry` variable #}
{{ entry.title }}
{{ entry.description }}

{# We will grab all of the EntryModels for our Projects Section using the
   craft.entries('projects') and identify each EntryModel as we loop through
   them with the `project` variable name #}
{% for project in craft.entries('projects') %}

  {# Our Project Entry information #}
  {{ project.title }}
  {{ project.url }}

  {# We will grab all of the EntryModels for our related Services via the
     Services Entries Field `entry.servicesEntriesField` and identify each 
     EntryModel as we loop through them with the `service` variable name #}
  {% for service in entry.servicesEntriesField %}

    {# Our Services Entry information #}
    {{ service.title }}
    {{ service.url }}

    {# Within our Services Field loop, we can now create a relationship back
       to the projects.  We updated our `entry` variable in the relatedTo() 
       parameter to the `service` variable that represents the EntryModel of
       each service entry in our services loop.  #}
    {% set relatedProjects = craft.entries.section('projects').relatedTo(service) %}

    {# Since we have already used the `project` variable above and we are still
       within a loop where we can access that `project variable`, we want to
       make sure to name the variable that refers to the Project EntryModel in 
       this loop differently.  We'll call it `nestedProject`. #}
    {% for nestedProject in relatedProjects %}
      {{ nestedProject.title }}
      {{ nestedProject.url }}
    {% endfor %}

  {% endfor %}

{% endfor %}

Don't miss any Craft tips, tricks, and community updates