Integrating Netgen Layouts with content source like Contentful, eZ Platform, Sylius or other — part 1

By Ivo Lukač -

Netgen Layouts is a Symfony based Layout Management framework and UI that enables operating web sites with little or no development. Its UI enables daily changes to be efficient and usable even to non-developers. In order to make this happen developers need to build the site based on Layouts, implement blocks and other components, integrate with content source(s). Layouts is not a CMS, it can fetch and show content from other systems, so it needs to be integrated with at least one content source (backend).

In this blog post series, I will try to show how to integrate Layouts with a desired content source. I will give you many examples and provide links to documentation pages and source code. If you are Symfony or PHP developer the task should not be difficult. Creating a backend integration requires typical OOP knowledge and basic knowledge of dependency injection. 

Prerequisites

There are already good instructions on installing Layouts in several ways, depending on your project. It can be tightly integrated with existing Symfony-based web solutions (such as eZ Platform CMS or Sylius eCommerce). It can also be used standalone and loosely integrated with any system(s) with a REST/SOAP interface (such as Contentful headless CMS, etc.).

In this blog post, we will presume Layouts are installed and ready to be used. We will also presume there is no integration available for the content source you have and that you need to build it. We will also presume you understand the basic concepts and the architecture.

Have a model

Before starting the integration work you need to make sure you have a model implementation of your content. As the content can come from different systems, local or remote, via various APIs, it is valuable to have an easy way to fetch the content from the PHP code in your Layouts integration. Let’s list some of the model types you could have:

  • something basic like Doctrine ORM for local database data
  • a PHP API of a local application (like eZ Platform CMS)
  • an SDK that wraps a remote API (like Contentful)
  • a generated model based on specification formats like OpenAPI or Swagger (using JanePHP for generating)
  • wrapper around data like XML
  • or manually built model

Whatever the case is, you should have one class that represents one kind of content items you want to fetch in Layouts and show in your grids and lists.

In some cases you will have a model but it will not be adequate so you will wrap original classes with your own classes. For example, we have a wrapper class for the Contentful integration to implement routing and caching. In this case, the original entity is encapsulated and available in templates. 

When building the proof of concept RSS integration using “dg/rss-php” I wrapped it into an RSSItem class:

class RSSItem {
  private $type;
  private $id;
  private $title;
  private $author;
  private $timestamp;
  private $intro;
  private $link;

  public function __construct(\SimpleXMLElement $xml, string $type) {
    $this->type = $type;

    if ($type == 'rss') {
      $this->id = md5($xml->link.$xml->timestamp);
      $this->title = (string) $xml->title;
      $this->timestamp = (string) $xml->timestamp;
      $this->intro = (string) $xml->description;
      $this->link = (string) $xml->link;
    } elseif ($type == 'atom') {
      $this->id = (string) $xml->id;
      $this->title = (string) $xml->title;
      $this->timestamp = (string) $xml->timestamp;
      $this->intro = (string) $xml->summary;
      $this->link = (string) $xml->link->href;
      $this->author = (string) $xml->author->name; 
    }
  }

  public function getId(): string {
    return $this->id;
  }

  public function getTitle(): string {
    return $this->title;
  }

  public function getTimestamp(): string {
    return $this->timestamp;
  }

  public function getAuthor(): string {
    return $this->author;
  }

  public function getIntro(): string {
    return $this->intro;
  }

  public function getLink(): string {
    return $this->link;
  }

  public function getType(): string {
    return $this->type;
  }
}

This way Layouts don’t care if the item came from an Atom or RSS feed, data is normalised.

Have a service

Usually it is worthwhile to have a Symfony service with all fetching functions in one place. It reduces the amount of code and it is easy to inject it where it's needed. Maybe you already have one provided (like eZ LocationService or Sylius ProductRepositoryInterface) or you can create one (like for Contentful integration).

Define the Value Type

Before doing any coding declare a Value Type. It is just an identifier that is used to connect the code you write with Layouts configurations. Declaring it is simple, check the example for the RSS Item:

netgen_layouts:
    value_types:
        rss_value_type:
            name: 'RSS Item'
            manual_items: false

We disabled the manual items option as we will only make a dynamic query for the RSS integration. If you would like to be able to pick items from your content source manually, omit that configuration line (default is true). We will show how to implement the manual picking in the following posts.

Start with the Value Converter

Once you declare your value type, the next step is to make a Value Converter. It is a simple class to convert backend-specific data to the generic item which is used by Layouts. It also provides the original model object.

You need to implement the \Netgen\Layouts\Item\ValueConverterInterface class. Few examples:

Similarly, while making the RSS integration I created a value converter for the above-mentioned RSSItem model object:

final class RSSValueTypeConverter implements ValueConverterInterface {
  public function supports($object): bool {
    return $object instanceof RSSItem;
  }

  public function getValueType($object): string {
    return 'rss_value_type';
  }

  public function getId($object): string {
    return $object->getId();
  }

  public function getRemoteId($object): string {
    return $object->getId();
  }

  public function getName($object): string {
    return $object->getTitle();
  }

  public function getIsVisible($object): bool {
    return true;
  }

  public function getObject($object): object {
    return $object;
  }
}

Then you need to register this class in the service container and tag it as netgen_layouts.cms_value_converter. Take note of the fact that we don’t have a Remote ID concept in the RSS integration so we just return the ID, but in your case you might have it. Also, the RSS item is always visible, in other cases the original object might have some kind of visibility property you want to pass to Layouts.

More detailed documentation on Value Converters can be found on our documentation site.

Make the linking possible

To be able to generate links to your items we don’t need to do a lot. We just need to implement the \Netgen\Layouts\Item\ValueUrlGeneratorInterface.

Example for the RSSItem:

final class RSSValueTypeUrlGenerator implements ValueUrlGeneratorInterface {
  public function generate($object): ?string {
    return (string) $object->getLink();
  }
}

Then register the class and tag it with netgen_layouts.cms_value_url_generator and the value type identifier. More details on our documentation site

Add a few templates

There are at least 2 templates to create - one for the Layouts admin application, one for the front web site. The internal one is just linking to the original page and showing the item type:

<div class="rss-item">
  <p><a href="{{ nglayouts_item_path(item) }}" target="_blank">{{ item.name }}</a></p>
  <div class="value-type">
    <p>RSS Item</p>
  </div>
</div>

The external one is a bit more detailed:

<article class="view-type view-type-{{ view_type }}">
  <h2 class="title"><a href="{{ nglayouts_item_path(item) }}" target="_blank" rel="noopener noreferrer">{{ item.name }}</a></h2>
  <div class="info">
    {% if item.object.author %}
      <a href="#" class="article-author">{{ item.object.author }}</a>
    {% endif %}
    <time>{{ item.object.timestamp|date('j M Y') }}</time>
  </div>
</article>

Note how we use fields from the RSSItem object encapsulated in the generic item as $item.object. You can have more than one item view type, but we will discuss this in the following posts.

Configuration and other details can be found here on our documentation site.

Finally, the Value Loader

If you are planning to implement the manual picking then you need the value loader as well. If you just want to have dynamic fetching you can skip this step.

The Value Loader is responsible for loading the original model object with the ID or Remote ID. It needs to implement the Netgen\Layouts\Item\ValueLoaderInterface and be registered to the service container with the netgen_layouts.cms_value_loader tag.

Examples from the existing integrations:

As you will notice, all of these don’t do much, just use the function from the injected service. That function does all the work for fetching the item from the content source and hiding all the complexity in the service.  

More info on our documentation site

Conclusion

With the steps covered in this post, you might feel you didn’t do much, but the focus was on doing the groundwork and preparing for more exciting things. In the next post, we will cover the dynamic fetching of data by implementing a custom query type.

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.