MASSIVE ART_Daniel Mathis_640x640circle
Daniel Mathis
Sulu Core developer and gaming enthusiast.
@Prokyon4

PHP in Sulu: How To Write A Custom Controller To Render Any Content

The ideal content management system (CMS) would be able to handle any kind of content out of the box. Unfortunately, such an ideal system could never exist. Have you ever seen a restaurant that serves every meal imaginable? Sulu CMS approaches the challenge of serving any type of content by being incredibly extensible through templates and, if necessary, PHP code. In this article, we are going to show you how to implement dynamic content using a typical PHP-based customization in Sulu: a custom controller.

Check out other options for using code to customize Sulu in PHP in Sulu: The Power Of Code.

Custom controllers in Sulu

If you’re new to Symfony or Sulu, a controller is a component in the Model-View-Controller architecture.

  • A model represents structured data, such as a blog page, a calendar, or an address book. Sulu refers to models as entities.
  • A view defines how data is rendered at the frontend. Sulu uses Twig templates to build views, or full React apps in headless mode.
  • A controller connects models to views. A controller typically receives a request from a view and responds with data taken from a model.

Controllers are the starting points for including custom business logic in your CMS application. Controllers can access entities, invoke custom services, call external APIs, and combine all the results into one for the view to render.

When to use a custom controller

Sulu’s standard functionality can build a fully functional website, but there are some use cases that require a custom controller:

  • Deliver automated content. Content isn’t always static. Sometimes, you need to dynamically generate content from frequently updated data. For example, a product catalog that displays the number of items in stock. If standard features like the Automation bundle or target groups do not match the use case, a custom controller can provide highly tailored automation.
  • Fetch additional data from the database. Imagine external data synched into your system that requires special treatment. A custom controller can fetch that data for further processing.
  • Provide static forms. Typically, a content manager would set up a static form using a page XML and a Twig template. However, if you have specialized forms for a template, such as a wizard or a product configurator, you might want to implement that form using a custom controller.

Sample scenario: What’s the current moon phase?

With the above concepts and use cases in mind, let’s go through a tiny sample project to build our own custom controller. The goal is to display an image of the current phase of the moon on your web page. This information can be calculated on the fly and, therefore, is an ideal case for a custom controller.

The controller:

  1. Is invoked by a “moon” template
  2. Calculates the current moon phase,
  3. Passes the calculated data to the template to render an image of the calculated moon phase.

Step 1: Set up a new Sulu project

We’ll start by setting up a local development environment. You’ll need the standard ingredients:

Using Docker is optional, but it’s a great way of quickly and conveniently spinning up a database. The following steps assume that you use Docker.

We roughly follow the steps in Getting Started. Refer to the docs if you need more details about each step.

Create the project

The project’s starting point is the `sulu/skeleton` project template. Start a shell or prompt window, `cd` into a suitable directory, and run Composer:

composer create-project sulu/skeleton custom-controller

If prompted to install Docker config, respond with ‘yes’.

Start the database

Now we’ll start a database instance.

1. Change into the custom-controller directory and start the containers:

cd custom-controller
docker-compose up

This process will run in the foreground as long as the database is running. (When you are finished with this tutorial, hit Ctrl+C in this terminal session to power down the Docker compose stack.)

2. To continue with the project, open a second shell. Alternatively, you can run docker-compose up -d, and also call docker-compose logs in the same directory to see the log output.

Now you have a MySQL instance running and listening on port 3306. (The compose file also spawns a mailing service that you can ignore for this project.)

Initialize the system

The database is not yet populated. Run the following command in custom-controller to set up tables and data:

php bin/adminconsole sulu:build dev

Start the CMS

Now let’s test the installation.

1. Start up the web server with the following command:

php -S localhost:8000 -t public/ config/router.php

(Feel free to use another port if port 8000 is already occupied on your computer.)

2. Open http://localhost:8000/admin in your browser and log in as admin/admin. You should now see the admin UI.

Step 2: Create a custom controller

Now we can start creating a custom controller.

First, create a new file in your editor or IDE.

src/Controller/Website/MoonPhaseController.php

The controller is quite straightforward; below is the complete class. Let’s walk through the code step by step. The numbers in parentheses refer to the respective comments in the code.

<?php

declare(strict_types=1);

namespace App\Controller\Website; // (1) 

use Sulu\Bundle\WebsiteBundle\Controller\WebsiteController; // (2)
use Sulu\Component\Content\Compat\StructureInterface;
use Symfony\Component\HttpFoundation\Response;

class MoonPhaseController extends WebsiteController
{
	public function indexAction(StructureInterface $structure, $preview = false, $partial = false): Response  // (3)
	{
		$response = $this->renderStructure(
			$structure,
			[],
			$preview,
			$partial
		);

		return $response;
	}

	protected function getAttributes($attributes, ?StructureInterface $structure = null, $preview = false)  // (4) 
	{
		$attributes = parent::getAttributes($attributes, $structure, $preview);
		$attributes['moonphase'] = $this->lunarPhase();

		return $attributes;
	}

	protected function lunarPhase()  // (5)
	{
		$year = date('Y');
		$month = date('n');
		$day = date('j');
		if ($month < 4) {
			$year = $year - 1;
			$month = $month + 12;
		}
		$days_y = 365.25 * $year;
		$days_m = 30.42 * $month;
		$julian = $days_y + $days_m + $day - 694039.09;
		$julian = $julian / 29.53;
		$phase = intval($julian);
		$julian = $julian - $phase;
		$phase = round($julian * 8 + 0.5);
		if ($phase == 8) {
			$phase = 0;
		}
		$phase_symbols = array('🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘');
		return $phase_symbols[$phase];
	}
}
The moon phase controller

We start by declaring the namespace (1). Sulu distinguishes between the visitor-facing web site and the admin UI, hence we choose the Website namespace.

The MoonPhaseController extends Sulu’s WebsiteController (2) and makes use of Sulu’s StructureInterface and Symfony’s Response types.

The controller exposes the indexAction() method (3). A controller that implements a custom action must make use of renderStructure() to craft the HTTP response.

The getAttributes() method (4) is called by the parent WebsiteController class during the rendering process. It allows the child controller to add or modify the attributes passed to the view. In our scenario, it adds a moonphase attribute with the value returned by the lunarPhase method (5). This moonphase attribute gets exposed to the Twig template in the frontend, as we’ll see later.

Finally, method lunarPhase() calculates the current phase of the moon. (Source: StackOverflow under a CC-BY-SA license. For the visual appeal, we changed the function to return images instead of the text description of the phase.)

Step 3: Integrate the controller with the CMS

At this point, the controller is functioning, but it is neither reachable from the web, nor does it take care of rendering the content it creates. To invoke the controller from the web, we will create a page template to define the page structure and the controller method to be called when opening the page. For rendering, we will create a Twig template.

Create a new page template “moon.xml”

To create a page template:

  1. Cd into config/templates/pages.
  2. Make a copy of default.xml. Name it moon.xml.
  3. Change the <key> tag to moon.
  4. Change the <view> tag to pages/moon.
  5. Change the controller path to App\Controller\Website\MoonPhaseController.
  6. Adjust the property titles accordingly.

Remove the property named “article” from the template because our page is not an article.

Here is the full content of moon.xml, with the changes mentioned above highlighted. Note the <controller> tag that connects the page to the controller’s indexAction() method.

<?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">

	<key>moon</key>
	<view>pages/moon</view>
	<controller>App\Controller\Website\MoonPhaseController::indexAction</controller>
	<cacheLifetime>604800</cacheLifetime>
	<meta>
		<title lang="en">Moon Phase</title>
		<title lang="de">Mondphase</title>
	</meta>
	<properties>
		<property name="title" type="text_line" mandatory="true">
			<meta>
				<title lang="en">Moon Phase</title>
				<title lang="de">Mondphase</title>
			</meta>
			<params>
				<param name="headline" value="true"/>
			</params>

			<tag name="sulu.rlp.part"/>
		</property>

		<property name="url" type="resource_locator" mandatory="true">
			<meta>
				<title lang="en">Moon Phase Resourcelocator</title>
				<title lang="de">Mondphasen-Adresse</title>
			</meta>

			<tag name="sulu.rlp"/>
		</property>

	</properties>
</template>
The page template
Create a Twig template

For the frontend template, go to templates/pages and make a copy of default.html.twig. Name the copy moon.html.twig.

This Twig template takes over the content rendering. Add the following code to the file:

{% extends "base.html.twig" %}

{% block content %}
	<section style="background-color:#000000">
		<h2 style="color:#ffffff">Current Moon Phase</h2>
		<p style="font-size:10em; text-align:center;">
			{{ moonphase|raw }}
			<br/>&nbsp;
		</p>
	</section>
{% endblock %}
The Twig template

The placeholder {{ moonphase|raw }} receives the moonphase attribute from the controller. Apart from that, the template does not do much except create a night sky background and inflate the moon phase emoji to a whopping 10em text size!

Now the coding part is done.

Step 4: Set up the example webspace in the admin UI

Finally, we can create a simple site with a home page and a moon phase page.

  1. First, clear the caches.
  2. Open https://localhost:8000/admin in your browser and log in.
  3. Open the side menu, then click on Webspaces. You will see the example.com webspace.
  4. Hover over the column to the right of the example.com column. A button with a plus sign will display on top of the column.
  5. Click the button to create a new page.

6. Click the drop-down menu labeled “Default.” You will see the entry named “Moon Phase” that Sulu created from our moon.xml template.

7. Select the “Moon Phase” menu item.

The UI will switch to the moon.xml template.

8. Enter a title for the page, and set the resource locator to “moon”.

9. Save and publish the page.

10. Test it out! Open http://localhost:8000/moon and see the current moon phase.

Your new hobby: creating custom controllers

Now you have entered the world of custom controllers in Sulu, and creating a controller is probably easier than you might have thought. Use your imagination (and your customer’s functional specifications document) to create a Sulu that the world has never seen.

If you would like to learn more about Sulu, try it out or get in contact with us.