eZ Publish 5: Custom tags with Symfony services

How to render a custom tag for XML Text using your Symfony services and templates

One of my favorite features of eZ Publish is the ability to create nice-looking content with various nicely formatted blocks. That makes reading more interesting for the user. Due to storing content as XML it is possible to present information however you want. Along with a big number of standard tags like paragraph, image or table you are free to create your own custom tags: an embedded YouTube video, Google Maps, a sllideshare presentation, a source code block with syntax highlighting, QR-code etc. This article will show you how to use Symfony services to render custom tags.

eZ Publish 5 uses XSL stylesheets to render XML tags. On the one hand it is a very logical approach because XSLT is the a best ever known tool ever to transform XML data into HTML or something else. But on the from other hand XSL is not a sophisticated programming language. While it might be easy to implement YouTube embedded HTML code using XSL style sheet, generating, but a QR-code with XSL would probably be much less fun.

Moreover, sometimes we already have twig templates in a project and wish to reuse them. Luckily we have the possibilityan to render custom tags without XSL. There is the concept of pre-converter services instead.
Assume we’d like to display information about a webshop product in a custom tag.

First of all configure a XML tag name and its attributes. Unfortunately it still requires editing  of a legacy configuration file:
content.ini.append.php

[product]
CustomAttributes[]=product_id

Here “product” is a custom tag name and “product_id” is an product identifier which is used to fetch product data from the product catalog.

Then register a pre-converter service:

services.yml

demo.ezxml.pre_converter:
       class: Acme\DemoBundle\XmlText\ProductPreConverterService
       arguments: [@demo.product.provider, @templating]
       tags:
           -  { name: ezpublish.ezxml.converter }

Here we have two injected services: product.provider to provide data about a webshop product and the templating engine to render it in HTML. The tag ezpublish.ezxml.converter defines a service as pre-converter.

Main method of a pre-converter that executes the job:

ProductPreConverterService.php

 

public function convert(DOMDocument $xmlDoc)
{
       $renderedHtml = '';
       $xpath = new DOMXpath($xmlDoc);
       /** @var \DOMNodeList $customTags */
       $customTags = $xpath->query(".//custom[@name='product']");
       /** @var \DOMElement $customTag */
       foreach ($customTags as $customTag) {
           $productId = $customTag->getAttribute('custom:product_id');
           $product = $this->productProvider->getProductData($productId);
           $html = $this->templatingEngine->render(
               'AcmeDemoBundle:CustomTag:product.html.twig',
               $product
           );
           $this->replaceCustomTagWithHtmlText($customTag, $html);
       }
}

 

If the product provider is implemented as a controller, it is possible to do the same job even without the templating engine:

ProductPreConverterService.php

 

  public function convert(DOMDocument $xmlDoc)
   {
       $renderedHtml = '';
       $xpath = new DOMXpath($xmlDoc);
       /** @var \DOMNodeList $customTags */
       $customTags = $xpath->query(".//custom[@name='product']");
       /** @var \DOMElement $customTag */
       foreach ($customTags as $customTag) {
           $productId = $customTag->getAttribute('custom:product_id');
           $actionResponse = $this->productProvider->showProductAction($productId);
           $html = $actionResponse->getContent();
           $this->replaceCustomTagWithHtmlText($customTag, $html);
       }
   }

 

The only trick here is the method replaceCustomTagWithHtmlText.
It may happen that a twig template renders non valid XHTML code. In this case our pre-converter will fail with generating the XML document. To solve this problem there is the following method that works equally good with HTML and XHTML:

ProductPreConverterService.php

  private function replaceCustomTagWithHtmlText(DOMElement $customTag, $htmlText)
   {
       $xmlDocument = $customTag->ownerDocument;
       $htmlDocument = new DOMDocument();
       // It is important to add xml declaration to import data in UTF-8
       $htmlDocument->loadHTML('' . $htmlText);
       $xpath = new DOMXpath($htmlDocument);
       $htmlBody = $xpath->query('/html/body')->item(0);
       $importedNode = $xmlDocument->importNode($htmlBody, true);
       $fragment = $xmlDocument->createDocumentFragment();
       while ($importedNode->childNodes->length > 0) {
           $fragment->appendChild($importedNode->childNodes->item(0));
       }
       $customTag->parentNode->replaceChild($fragment, $customTag);
   }

 

In most cases using XSL style sheets for rendering custom tags is a good solution. But if your templates are complex and it is not possible or desirable to use XSL, you can use the approach described above.

— Andrey Astakhov, silver.solutions development team

2 thoughts on “eZ Publish 5: Custom tags with Symfony services

  1. Hi gggeek
    That’s a really tricky case.

    The only idea I have is to try to use listener instead of custom controller
    (see eZ\Bundle\EzPublishCoreBundle\EventListener\ViewControllerListener).

  2. Nice one, thanks!

    A further question: what about the case where you need to get back at the location Id?
    This is a bit of a pain to store in the custom attributes of the xml tag, as you might need to have the id of the current location displayed in there (opposed to the current location when editing)?

    One workaround I can think of is to display the current node usinga custom controller, in that controller save the current location id, then inject the controller as service into the preconverter, and grab the data from there. But it feels a bit hackish.

    Any other solutions?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Sicherheitsfrage * Time limit is exhausted. Please reload CAPTCHA.