Netgen's Site API for eZ Platform

by Petar Španja -
Netgen's Site API for eZ Platform

Netgen's Site API is a separate layer on top of the existing eZ Platform Repository API, designed to make developing websites easier, faster, and less error-prone.

When eZ Publish was rewriten into version 5, its Repository API was designed for general purpose. That means not just for building websites, but also for CMS administration interface and applications. While I think it did turn out well, the fact that it was not designed for developing websites first is obvious. That was recognized early and discussed often. A dedicated Site API on top of Repository API was proposed several times, but it was always rejected, and for a good reason: we didn't want the overhead of another API layer and it's better to make Repository API work for us in the site context.

Recently I became more involved with working on projects, starting to use the product instead of develop it. Developers losing the perspective of the user's point of view is a well known problem. While I was aware of it, I still find it funny how quickly I came to the conclusion that a dedicated Site API deserves another look - doing simple stuff just took too much work.

Probably the biggest problem I found when working on multilingual sites is having to take special care to always work with correct Content translation, the one that should be displayed on a particular siteaccess. If you are coming from eZ Publish Legacy Stack, you're probably already aware of that because previously you did not have to worry about it at all. The Content was always transparently loaded in the correct translation, you just had to fetch and use it.

Closely related to that, since single Content object can contain fields in multiple languages, you either need to know the exact translation you want or use a special helper service to do that for you. The problem I find with such helper services is that, while they solve one problem, they bring a number of their own which might not be immediately apparent. You need to know there is a helper, you need to inject it, know how to use it, etc. It all takes thinking, time, and lines of code. And, as you should know, there is a well-established correlation between lines of code and number of bugs.

Version 5 also introduced the possibility of Content existing without Locations. The idea is to enable organizing Content in a way alternative to the Location tree, mainly through tags. However, for most websites Location tree still is and will remain the main way of organizing Content. That means you often need to fetch Content through Location, but as Content is not aggregated in the Location, it takes a separate step. Having a dedicated object that would represent a Content on a Location would remove that need.

There is also a problem of identifiers not being directly available on some of the key domain objects. That is so by design, in order to simplify cache invalidation. The main one is ContentType identifier missing from ContentInfo. It needs to be fetched through loading ContentType separately through ContentType ID, which is available. By now almost everyone implements a dedicated helper service to do this.

Another problem is FieldType identifier not being available on the Field object. Common use case here is checking whether the Field value is empty or not which is done through the FieldType service, fetched through the FieldType identifier. The identifier is found in the FieldDefinition and that means loading ContentType again.

All of the above might not seem like too much, but if it's all encountered daily in the developer's work, it amounts to a significant detriment to the developer's experience. One might attempt to solve these problems in the Repository API, but I think this has several downsides. First, I'm not convinced it can be done as elegantly as with a dedicated API. Secondly, it also means we would make Repository API more complex while we should rather strive to do the opposite.

For these reasons, we decided that a dedicated Site API is indeed warranted. While it does bring some overhead, it also brings clarity as we can keep the Repository API simple and at the same time be free with adding additional stuff to the Site API. Whether that is the correct choice, it' remains to be proved. We already started to use it on our new projects and so far developer feedback was very positive. Here's how we did it.

First thing to know about it is that Site API is read-only. We implement only two services: LoadService and FindService. LoadService is used to load the object by ID while FindService finds objects by existing Repository Search API. Both services will transparently load the correct translation. The rule is: what shouldn't be displayed can't be loaded.

The services will return dedicated Site API domain objects which closely correspond to the Repository domain objects. These are:

  • Content
  • ContentInfo
  • Field
  • Location
  • Node

You'll notice that there is no VersionInfo object. That is so because there is no need for it in the site context, you only need published versions there.

Content contains Fields for single translation only, so it is not necessary to know what that translation is nor to use a special helper to select the Field in the correct translation.

ContentInfo contains full Repository ContentType object and Content name in the translation that is loaded. That means no helper needed to get the name in the correct translation.

Field contains FieldType identifier and isEmpty() method to easily check if the Field's value is empty. It also aggregates Repository's FieldDefinition, so the name and description in the correct translation are available.

Location contains Site API ContentInfo object.

Node is actually a Location object, additionally aggregating full Content object.

All of the domain objects aggregate their corresponding Repository object for your convenience.

Some examples in PHP:

/** @var \Netgen\EzPlatformSiteApi\API\Site $site */
$loadService = $site->getLoadService();
$location = $loadService->loadLocation(42);

$node = $loadService->loadNode($location->parentLocationId);

if (!$node->content->getField('image')->isEmpty()) {
    // do something
}
/** @var \Netgen\EzPlatformSiteApi\API\Site $site */
$findService = $site->getFindService();

$query = new LocationQuery(...);
$result = $findService->findNodes($query);

if ($result->totalCount > 0) {
    /** @var \Netgen\EzPlatformSiteApi\API\Values\Node $node */
    $node = $result->searchHits[0]->valueObject;

    if (!$node->invisible && !$node->content->getField('image')->isEmpty()) {
        // do something
    }
}

And Twig:

<h1>{{ content.name }} [{{ content.contentInfo.contentTypeIdentifier }}]</h1>
<h2>{{ content.fields.title.value.text }}</h2>
<h3>{{ content.fields['sub_title'].value.text }}</h3>

{% set imageAlias = ng_image_alias( content.fields.image, 'big' ) %}

{% for identifier, field in content.fields %}
    <h4>Field '{{ identifier }}' in Content #{{ field.content.id }}</h4>

    {% if not field.isEmpty() %}
        {{ ng_render_field( field ) }}
    {% else %}
        <p>Field of type '{{ field.fieldTypeIdentifier }}' is empty.</p>
    {% endif %}
{% endfor %}

And that's it for now. It's open source, so for more details take a look at the code and do let us know what you think.

Comments

This site uses cookies. Some of these cookies are essential, while others help us improve your experience by providing insights into how the site is being used.

For more detailed information on the cookies we use, please check our Privacy Policy.

  • Necessary cookies enable core functionality. The website cannot function properly without these cookies, and can only be disabled by changing your browser preferences.