Use XInclude to organize Content elements in Page templates in SULU
Tags: sulu, xinclude, symfony
Published: 2023-02-23 21:33:56

With SULU, the content elements are defined with content types in the page templates. This can become confusing in a complex implementation with many page templates and content elements.

The keyword is reusability in order to avoid effort, errors and inconsistencies. For the name of the property for a text area, developers like to use bodytext, copytext or text.

A content element is, for example, an input field - like a headline - that stands alone or several input fields that are combined with a &lt;block/&gt;.

For this article SULU 2.5 is run in a Docker environment with PHP 8.2 (libxml 2.9.10).

The code examples are available on [Github](https://github.com/elevado/sulu-xinclude-article).

## Content elements integrated in the page template

Here is an example where all content elements are defined directly in the page template.

To reduce the complexity for this article, I use a page template **article.xml** with the two content elements **headline** (content type: text\_line) and **bodytext** (content type: text\_editor).

```xml
<?xml version="1.0" ?>
<template 
   xmlns="http://schemas.sulu.io/template/template"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
   <!-- ... -->
   <properties>
      <!-- ... -->
      <property name="headline" type="text_line">
         <meta>
            <title lang="en">Headline</title>
         </meta>
      </property>
      <property name="bodytext" type="text_editor">
         <meta>
            <title lang="en">Bodytext</title>
         </meta>
      </property>
   </properties>
</template>
```

## Content element in include files

To better organise the individual include files, I created the directory include within the templates directory. How you organise and name the include files is up to you.

Now the content elements are each transferred to the file **headline.xml** and **bodytext.xml** and stored in the directory include.

%kernel.project_dir%/config/templates/include/**headline.xml**

```xml
<?xml version="1.0" ?>
<properties xmlns="http://schemas.sulu.io/template/template"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <property name="headline" type="text_line">
        <meta>
            <title lang="en">Headline</title>
        </meta>
    </property>
</properties>
```
%kernel.project_dir%/config/templates/include/**bodytext.xml**

```xml
<?xml version="1.0" ?>
<properties xmlns="http://schemas.sulu.io/template/template"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <property name="bodytext" type="text_editor">
        <meta>
            <title lang="en">Bodytext</title>
        </meta>
    </property>
</properties>
```

In order to be able to use the content elements in article.xml, XInclude is used. XInclude is a recommendation of the W3C to refer to XML or text documents within XML documents in order to replace the content with the referenced file.

%kernel.project_dir%/config/templates/pages/**article.xml**

```xml
<?xml version="1.0" ?>
<template xmlns="http://schemas.sulu.io/template/template"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:xi="http://www.w3.org/2001/XInclude"
          xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <!-- ... -->
    <properties>
        <!-- ... -->
        <xi:include href="../include/headline.xml"
                    xpointer="xmlns(sulu=http://schemas.sulu.io/template/template) xpointer(/sulu:properties/sulu:property)">
            <xi:fallback>An error occurred</xi:fallback>
        </xi:include>
        <xi:include href="../include/bodytext.xml"
                    xpointer="xmlns(sulu=http://schemas.sulu.io/template/template) xpointer(/sulu:properties/sulu:property)">
            <xi:fallback>An error occurred</xi:fallback>
        </xi:include>
    </properties>
</template>
```
I do not want to go into great detail in this article about how [XML-Namespaces](https://www.w3.org/TR/xml-names/), [XInclude](https://www.w3.org/TR/xinclude/), [XPointer](https://www.w3.org/TR/xptr-framework/) and [XPath](https://www.w3.org/TR/xpath/) work, however, the principle is quite simple.

The default namespace in the include files is specified with the attribute xmlns, because only tags from SULU are used in the XML documents. The page template has the same default namespace, however, these have nothing to do with each other.

```xml
<xi:include href="%REFERENCE%" xpointer="xmlns(%PREFIX%=%URI%) xpointer(%XPATH%)" />
```

`%REFERENCE%` is the relative reference to the file to be included. Then we define the `%PREFIX%` and `%URI%` of the namespace. With XPointer it is not the prefix but the URI that is important. In the example I use **sulu**, but it could also be **banana**. For the URI we use the default namespace from the include files. For `%XPATH%` we use another XPath expression to find the element.

## All content elements in an include file

XPath gives us the possibility to find elements in the documents. We could therefore store all content elements in a document and find them with an [XPath expression](https://devhints.io/xpath) using the name of elements or properties.

%kernel.project_dir%/config/templates/pages/**elements.xml**

```xml
<?xml version="1.0"?>
<properties xmlns="http://schemas.sulu.io/template/template"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <property name="headline" type="text_line">
        <meta>
            <title lang="en">Headline</title>
        </meta>
    </property>
    <property name="bodytext" type="text_editor">
        <meta>
            <title lang="en">Bodytext</title>
        </meta>
    </property>
</properties>
```

%kernel.project_dir%/config/templates/pages/**article.xml**

```xml
<?xml version="1.0" ?>
<template xmlns="http://schemas.sulu.io/template/template"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:xi="http://www.w3.org/2001/XInclude"
          xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <!-- ... -->
    <properties>
        <!-- ... -->
        
        <!-- Example: Complete path with attribute -->
        <xi:include href="../include/elements.xml"
                    xpointer="xmlns(sulu=http://schemas.sulu.io/template/template) xpointer(/sulu:properties/sulu:property[@name = 'headline'])">
            <xi:fallback>An error occurred</xi:fallback>
        </xi:include>
        
        <!-- Example: Find element only by attribute -->
        <xi:include href="../include/elements.xml"
                    xpointer="xmlns(sulu=http://schemas.sulu.io/template/template) xpointer(//*[@name = 'bodytext'])">
            <xi:fallback>An error occurred</xi:fallback>
        </xi:include>
    </properties>
</template>
```

## Observation

In another tutorial, in preparation for this article, I saw an example with XInclude which does not work in my development environment. In the tutorial, no namespace and XML schema were defined in the file to be included and no xpointer was defined in the article.xml in `<xi:include/>`.

**Content element**

```xml
<?xml version="1.0" ?>
<property name="headline" type="text_line">
    <meta>
        <title lang="en">Headline</title>
    </meta>
</property>
```

**Page template**

```xml
<?xml version="1.0" ?>
<template xmlns="http://schemas.sulu.io/template/template"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:xi="http://www.w3.org/2001/XInclude"
          xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <!-- ... -->
    <properties>
        <!-- ... -->
        <xi:include href="../include/headline.xml"/>
    </properties>
</template>
```

The following exception is provided:

```
Uncaught PHP Exception Symfony\Component\Config\Util\Exception\XmlParsingException: "[ERROR 1871] Element 'property': This element is not expected. (in /home/wwwroot/config/templates/pages/article.xml - line 2, column 0)" at /home/wwwroot/vendor/symfony/config/Util/XmlUtils.php line 95 {"exception":"[object] (Symfony\\Component\\Config\\Util\\Exception\\XmlParsingException(code: 0): [ERROR 1871] Element 'property': This element is not expected. (in /home/wwwroot/config/templates/pages/article.xml - line 2, column 0) at /home/wwwroot/vendor/symfony/config/Util/XmlUtils.php:95)"} []
```

The exception **"[ERROR 1871] Element 'property': This element is not expected. (in /home/wwwroot/config/templates/pages/article.xml - line 2, column 0)"** is not from Symfony or SULU, but from libxml which implemented PHP.

I have not looked further into this error message or debugged it because on the one hand the XInclude declaration seems unclean and on the other hand the autocompletion of the IDE cannot be used due to the missing XML schema definition in the include files.

My suspicion is that the developer was using an older version of libxml and this might have caused it to work. This is because not all XML processors and versions of these XML processors always fully support the standard defined by the W3C.

## Fallback

I have used `<xi:fallback />` in my examples, but it is an optional element. I use it because it also helps with debugging, it is cleaner and it tells users that the include is not working.

Because XML processors do not have to provide an error message if the file cannot be included.