Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 04 · Custom entities

Parent and child records

Records can form trees, either among the records of one definition or as typed parent and child definitions. This page covers both, what a save enforces, what follows the parent, and how to render a tree safely.

Who this is for. Content modellers deciding whether a record owns other records, and developers rendering nested records. After this page you can choose between a hierarchy and a relationship, set up either kind of hierarchy, and render a tree without risking runaway recursion.

Two kinds of hierarchy

A record can have a parent record and children. Which records may be its parent depends on its definition, and a definition uses one of two shapes:

ShapeSet up byA record's parent must beExample
Records of one type nesting among themselvesA definition with no parent definitionA record of the same definition, or noneRegions with sub-regions
Typed parent and childA definition that names a parent definition (Defining a custom entity)A record of exactly that parent definition, alwaysTests within an enrolment

A save enforces the shape. A record of a definition with no parent definition is refused when its parent is a record of another type. A record of a child definition is refused with The parent must be provided when it has no parent, and with The parent belongs to the wrong entity when its parent is of another definition.

What follows the parent

  • The site. When the definitions are sited, a child takes its parent's site when the parent is set. Changing the site of an existing record changes it for the record's whole subtree, all the way down.
  • The order. When the definition is positioned, records are ordered among the children of one parent (and within one site, when sited), not across the whole definition.
  • Not the URL. The detail route is built from a definition's record path alone, with no nesting under the parent's path. Give every child definition a record path of its own: without one, its records have a nested URL that does not resolve.

Adding children in the admin

What the admin offers on a parent record depends on how many child definitions its definition has:

  • One child definition: a single + Add <child> action, which creates the child with this record as its parent, such as + Add Test on an enrolment.
  • Two or more: a list of create links, one for each child definition the user may create, each with this record as the parent.
  • None: neither; records of the definition nest among themselves instead.

A hierarchy or a relationship

Use a hierarchy when the child cannot exist without its parent and is edited from it: a test does not exist without the enrolment it belongs to. Use a relationship when both sides stand on their own and the link, not ownership, is what you model: an enrolment and a course both exist independently, so an enrolment relates to its course (Relationships between records).

Rendering a tree

getChildren() returns a record's children. A nested structure renders with a recursive function in Smarty or a macro in Twig, called with the root record, such as $region on a region's detail page:

Template code templates/regions/show.html
{function name=recordTree node=null depth=0}
  <li>{$node->getName()|escape}
    {if $depth < 5 && $node->getChildren()->count() > 0}
      <ul>{foreach $node->getChildren() as $child}{recordTree node=$child depth=$depth+1}{/foreach}</ul>
    {/if}
  </li>
{/function}
<ul>{recordTree node=$region}</ul>
{% macro recordTree(node, depth) %}
  <li>{{ node.getName()|e }}
    {% if depth < 5 and node.getChildren()|length > 0 %}
      <ul>{% for child in node.getChildren() %}{{ _self.recordTree(child, depth + 1) }}{% endfor %}</ul>
    {% endif %}
  </li>
{% endmacro %}
<ul>{{ _self.recordTree(region, 0) }}</ul>
Always bound the depth

Nothing in the platform limits how deep authors can nest records. A recursive template without a depth guard, like the depth < 5 above, follows whatever an author builds.

Go deeper

Was this page helpful?

Your answer goes to the docs team.