Dependency Inversion for Neos CMS NodeType Constraints

We’ve all been there: start off with disabling all default nodes and possible auxiliary nodes in our pages‘ content collections. Once we finalize our node types we think about where we need them and explicitly enable them for the corresponding content collections. Before long we end up with something like this:

'Vendor.Package:Page':
  childNodes:
    main:
      type: 'Neos.Neos:ContentCollection'
      constraints:
        nodeTypes:
          '*': false
          'Vendor.Package:Heading': true
          'Vendor.Package:Text': true
          'Vendor.Package:Image': true
          'Vendor.Package:TwoColumn': true
          'Vendor.Package:ThreeColumn': true

And as we have elements with multiple columns, we need to define our constraints there as well. And in a container element. And every time we need a different subset of allowed node types.
Imagine we would rename a node type: we would have to refactor a lot of node type configurations which aren’t further related.

In software design we know the dependency inversion principle which tells us to rely on an API promised by interfaces rather than expecting a known set of classes implementing such interface.
By choosing which interface to implement in a class, we decide where and by which components our class can or could be used without touching the other parts of our system.

When we translate this concept to the node type definitions in Neos, we can implement inheritance with the superTypes property. „Implementing an interface“ would correspond to extending a certain node type. Since we only require the node type to be there, we just implement it as abstract. We can start by setting up a few „interfaces“:

'Vendor.Package:ContentElement':
  abstract: true
'Vendor.Package:ColumnElement':
  abstract: true

For refactoring our old structure we can simply go through all the node types previously explicitly allowed in the content collection of interest and add our „interface“ as superType, e.g.:

'Vendor.Package:Text':
  superTypes:
    'Vendor.Package:ContentElement': true
    'Vendor.Package:ColumnElement': true
    # ...
'Vendor.Package:TwoColumn':
  superTypes:
    'Vendor.Package:ContentElement': true
    # ...

Lastly we replace this chunk of node types in our content collection constraints:

'Vendor.Package:Page':
  childNodes:
    main:
      type: 'Neos.Neos:ContentCollection'
      constraints:
        nodeTypes:
          '*': false
          'Vendor.Package:ContentElement': true
          'Vendor.Package:ColumnElement': true

'Vendor.Package:TwoColumn':
  childNodes:
    column0:
      type: 'Neos.Neos:ContentCollection'
      constraints: &columnConstraints
        nodeTypes:
          '*': false
          'Vendor.Package:ContentElement': true
    column1:
      type: 'Neos.Neos:ContentCollection'
      constraints: *columnConstraints

Of course this doesn’t always make sense. Something like Vendor.Package:Carousel containing only Vendor.Package:CarouselItem nodes or node types only occuring once in a content collection otherwise requiring an additional „interface“ are definitely not worth the effort.

In most cases however we have come to a solution where we have a foreseeable amount of such „interfaces“, e.g. for the main area of a page, a section or different kinds of columns and thus don’t have to touch the respective container node types once we want to allow a node type as child.

For a more complex example imagine this use case: we want to maintain our e-mail templates in Neos. We are likely to have some custom node types. Imagine Vendor.Package:EmailTemplate.Text to extend Vendor.Package:Text as it adds some properties or default values only necessary for our e-mail templates. We can now make this node type implement its own „interface“, say Vendor.Package:EmailTemplateElement. As groups like this, that differ from our usual node type groups quite strikingly, are rather rare, we can afford to disallow such groups in our content collections. Our page definitions would look like this:

'Vendor.Package:Page':
  childNodes:
    main:
      type: 'Neos.Neos:ContentCollection'
      constraints:
        nodeTypes:
          '*': false
          'Vendor.Package:ContentElement': true
          'Vendor.Package:ColumnElement': true
          'Vendor.Package:EmailTemplateElement': false

'Vendor.Package:EmailTemplate':
  childNodes:
    main:
      type: 'Neos.Neos:ContentCollection'
      constraints:
        nodeTypes:
          '*': false
          'Vendor.Package:EmailTemplateElement': true

On the downside we can’t directly „go to declaration“ and don’t have a view on all the nodes that are allowed in a certain area anymore. Additionally when searching for files containing our „interface“, unfortunately we find not only the elements implementing said „interface“, but all node types using (or defining) it, as well.
Nonetheless we think that the cleaner more easily maintainable node definition structure outweighs that problem.

Kommentare sind geschlossen.