Connect external databases to a TYPO3 CMS via OpenAPI

Experience shows that customers and developers (sometimes) have a different focus on a project: Customers often focus more on the website content and want to create landing pages, change imprint data or make similar content adjustments. On the other hand, as a developer, I focus more on the data, its structures and logical connections, and how these can be represented in a domain model.

This challenge came up in a project for a customer from the travel industry. They already have a system for maintaining travel data that is tailored precisely to their requirements. Now, however, they would like to expand the system for the output (website) with the flexibility of a CMS so that more individual travel offers can be created and displayed in the frontend.

However, this is content that does not follow a clear line and cannot be forced into structures. So the question arose: Do I create a page object that can hold any content?

But of course content management systems exist for this exact purpose. Replicating such a system would be nonsense, since there are now many different providers on the market who provide sophisticated solutions for this problem - such as TYPO3, for example.

So our customer was faced with the following question: should their proprietary software be extended to also include CMS functionalities? Or should they switch to a CMS that is tailored to their needs? 

Here’s how we implemented the customer’s requirements

Step 1: Provide data with OpenAPI

Instead of accessing the travel management system database directly, I chose to provide the data via a REST API.

Why? Because it has the following advantages:

  1. Read and write permissions can be defined precisely
  2. Reverse-proxy caching with Varnish reduces the database load
  3. Data can be requested by various clients (on server and client side)
  4. Future application can easily be connected via the interface

With OpenAPI Specification, a clear API defined within reasonable time. Code generators can be used to generate an API server and API clients in the preferred programming language.

I chose PHP for the server-side language and PHP and JavaScript for the clients. To do this, I first used the existing database structure to generate a configuration file for OpenAPI.

docker-compose exec apache-php bin/console api:openapi:export --spec-version=3 --yaml > openapi.yml

 

When generating the frontend, a "living" API documentation is generated, which lists the available endpoints with their parameters. But that's not all! In this documentation, example requests to the API can be executed directly and the response can be evaluated.

This documentation is also automatically integrated into GitLab:

Step 2: Data output with plugins and Vue.js

With TYPO3 plugins

Server-side generation of the output is sufficient for less dynamic outputs. The generated PHP API client is used in the TYPO3 plugins we create. By adding parameters, we were able to offer configuration and filter options in the plugins to the editor. For example, the editor can filter for river or sea cruises when outputting trips.

With Vue.js

The speed of travel portals is an important point for users. They want to be able to quickly discover and book suitable offers. Constantly reloading the page can quickly spoil the fun of booking.

For a high website speed and an optimal user experience, this project required the business logic being executed directly in the user's browser. Frameworks such as Angular, React, Vue.js or Svelte are ideal for such tasks. Due to the two-way data binding, data management and its mapping are synchronous. In the extended search for trips, this gave me the advantage that I could adjust the interdependent filter options again and again without having to worry about the synchronicity of the data with the DOM.

Step 3: Use Vue.js as plugin or content element

I put the generated Vue.js application into a TYPO3 content element. This has the advantage that configurations can be passed to the Vue.js app through TYPO3. This was realized with data attributes:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://typo3.org/ns/TYPO3/Fluid/ViewHelpers"
      xmlns:flux="http://typo3.org/ns/FluidTYPO3/Flux/ViewHelpers"
      xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
      data-namespace-typo3-fluid="true">
<f:layout name="Content" />
<f:section name="Configuration">
    <flux:form id="Banner">
        <flux:field.select
            name="preselectCruiseType"
            default="0"
            items="{
                0: {0: 'ocean', 1: 'ocean'},
                1: {0: 'river', 1: 'river'}
            }"/>
      […]
    </flux:form>
</f:section>
<f:section name="Preview">
  […]
</f:section>
<f:section name="Main">
    <f:format.raw>
        <script type="text/javascript">
            window.addEventListener('load', () => {
                let element = document.createElement('script');
                element.onload = function() {
                    window.finder();
                }
                element.src = '/path/to/vuejsapp/dist/app.js';
                document.head.appendChild(element);
            });
        </script>
    </f:format.raw>
    <app id="app"
         data-action="{f:uri.page(pageUid: settings.page.search)}"
         data-type="Banner"
         data-url-api="{settings.url.api}"
         data-preselect-cruise-type="{preselectCruiseType}">
    </app>
</f:section>
</html>

 

Vue.js and image cropping

With GraphicsMagick, TYPO3 offers a way to easily compress or crop images. This happens on the server side, i.e. before the page is rendered and sent to the client.

But how should we deal with client-side applications that keep requesting new assets?

The previous solution for said customer was to pre-render all of these images and store them under specific paths. However, I decided to use an image cropping service in order to be able to react more flexibly to project requirements. The original image is given to the service with parameters for width, height and file format. The reduced, optimized, and possibly cropped image is then returned by the service.

There are some SaaS providers and open source projects. At the time the project started, thumbor was our tool of choice. imgproxy.net is also recommended. In principle, however, both work the same:

  1. An image proxy service is started
  2. The URL of the original, high-resolution image is sent to this service as a GET parameter
  3. Additions are made (e.g. the desired target size, the image format, whether the image should be cropped or not, etc.)
  4. The service generates an image according to the specifications and stores it in its own cache directory.

Finally, the service returns the generated image in the desired file format.

Optionally you can prevent the service from being used by unauthorized people, which of course is highly recommended. Otherwise, too much load may be generated and copyrights may be violated. This is accomplished with a SALT. This allows only our client application to fetch cropped images for specific image sizes, allowing separate development of frontend and backend.

Here’s an example:

  1. Within our Vue.js application, we want to crop an image to a size of 350 x 230 pixels
  2. I get the path to the original image from the API. Here is an example path: https://placekitten.com/1440/1200 
  3. The SALT prevents the service from being abused. It is part of the URL and looks something like this: o_tAXuF0nsTgwUk9YL_T4VxxxxY=
  4. Now I send the path and the necessary parameters to the image crop service and get the cropped image back
<template>
    <picture>
        <source :media="cropSettings.sizes.lg.mediaBp" :srcset="`${cropSettings.server}/${cropObject[cropSettings.sizes.lg.pixels]}/${cropSettings.sizes.lg.pixels}/${imagePath}`">
        <source :media="cropSettings.sizes.sm.mediaBp" :srcset="`${cropSettings.server}/${cropObject[cropSettings.sizes.sm.pixels]}/${cropSettings.sizes.sm.pixels}/${imagePath}`">
        <img
            :class="classes"
            :id="id"
            :alt="alt"
            :src="`${cropSettings.server}/${cropObject[cropSettings.sizes[cropSettings.default].pixels]}/${cropSettings.sizes[cropSettings.default].pixels}/${imagePath}`"
        >
    </picture>
</template>
<script>
import { cropSettings } from '../../data/defaults'
export default {
    props: {
        id: {
            type: String,
            required: true
        },
        classes: {
            type: String,
            default: ''
        },
        alt: {
            type: String,
            required: true
        },
        imagePath: {
            type: String,
            required: true
        },
        cropObject: {
            type: Object,
            required: true
        },
        cropServer: {
            type: String
        }
    },
    data () {
        return {
            cropSettings: {
                server: this.cropServer, // config from data attribute
                sizes: {
                    lg: { pixels: '600x395', mediaBp: '(max-width: 768px)' },
                    sm: { pixels: '350x230', mediaBp: '(min-width: 769px)' }
                },
                default: 'sm'
            }
        }
    }
}
</script>

 

This is the resulting HTML:

<img src=”https://media.example.de/o_tAX0F0nsTgwUk9YL_T4VxxxxY=/350x230/rps.example.de/files/media_gallery/size_x/Liegestuhl_3_1.jpg” />

 

What could be improved?

A powerful image crop service could also be used on the server side. My first idea is to extend the Fluid ViewHelper  f:image in such a way that it sends the external images with appropriate parameters to the crop service instead of GraphicsMagick.

Infrastructure and added value for the customer

The customer can continue to maintain the data of their trips in the backend. With TYPO3 they can precisely control the output of the data and supplement it with non-structured data.

However, the flexibility gained causes greater complexity of the infrastructure.

Summary

What did we learn from this customer project? Not everything has to be done from scratch. OpenAPI makes it easy to connect modern applications to existing systems via interfaces. With reactive frameworks like Vue.js it is possible to create a better user experience than with server-side rendered websites.

Share article:

New Blogposts

By Caroline Kuhn

Website launch of the dialogue platform for MDF AG

With the launch of the dialogue platform lej-nachbarn.de, a community solution for direct…

Read more
By Christoph Aßmann
Deutsche Post & DHL Shipping Magento 2 Extension Update - part 1

In the last quarter of 2021, Netresearch published two notable releases of the Deutsche Post & DHL…

Read more
By Christoph Aßmann
Deutsche Post & DHL Shipping Magento 2 Extension Update - part 2

Following version 2.4.0 in the beginning of the year, Netresearch published another feature-rich…

Read more
By Sebastian Koschel
Improved performance after customization of Usercentrics service

After customizing Usercentrics' Smart Data Protector with our own development, we were able to…

Read more