Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.
A relationship definition is a typed link between definitions with a minimum and maximum. Read it forwards from the owning record's relationships, and backwards with a query on the owning side, never by walking inverse relationships.
Context
Context. An enrolment belongs to a course; an integration has categories. A relationship definition on the owning definition names the target definitions, a minimum (enforced on save) and a maximum (empty for unlimited). Targets are always other custom entity definitions; a link to a page, a user or a product is a record area instead. Reading forwards is cheap. Reading backwards from a popular record must be a query, because the inverse collection loads every row that points at it.
Steps
On the enrolment definition, add a relationship definition: identifier course, allowed definitions Course, minimum 1, maximum 1.
Authors pick the course on each enrolment; the API sets it by record id.
Forwards: loop getAllRelationships() and match the identifier, or use the shorthand $enrolment->course in element content.
Backwards: query enrolments with the relationship as criteria, limited.
Template code
Template code
{* forwards, on the enrolment page *}
{foreach $enrolment->getAllRelationships() as $rel}
{if $rel->getDefinition()->getIdentifier() == 'course'}{$course = $rel->getRecord()}{/if}
{/foreach}
{if !empty($course)}<p>Course: <a href="{$course->getFullPath()}">{$course->getName()|escape}</a></p>{/if}
{* backwards, on the course page: the latest twenty enrolments *}
{$enrolments = $_query->findCustomEntityRecordsBy('enrolment', [
'relationships.definition.identifier' => 'course',
'relationships.records.id' => $course->getId()
], ['created' => 'DESC'], 20)}
<p>{$enrolments|count} recent enrolments</p>
{# forwards #}
{% set course = null %}
{% for rel in enrolment.getAllRelationships() %}
{% if rel.getDefinition().getIdentifier() == 'course' %}{% set course = rel.getRecord() %}{% endif %}
{% endfor %}
{% if course %}<p>Course: <a href="{{ course.getFullPath() }}">{{ course.getName()|e }}</a></p>{% endif %}
{# backwards #}
{% set enrolments = _query.findCustomEntityRecordsBy('enrolment', {
'relationships.definition.identifier': 'course',
'relationships.records.id': course.getId()
}, {'created': 'DESC'}, 20) %}
<p>{{ enrolments|length }} recent enrolments</p>
Why this and not that
Not a text area holding the course's id: no picker for authors, no integrity, no reverse lookup. Not getInverseRelationships() on the course: on a course with thousands of enrolments it exhausts memory. Not isset or is defined on the shorthand: the relationship property is invisible to both, which is why the loop guards instead. getAllRelationships() returns one entry per definition including empty ones; getRelationships() only those with records.