Testing nested HTML Components with the test implementation

In the previous article, I settled on a solution to test data passed to the template renderer for an HTML component.

There's one caveat with the test implementation. The data of nested components gets json_encoded multiple times.

Let me demonstrate.

(As a reminder) we had these two interfaces:

interface Component
{
public function render(): string;
}

interface TemplateRenderer
{
public function render(string $templateName, array $data = []): string;
}

And the test implementation for the TemplateRenderer is this:

class JsonEncodeTemplateRenderer implements TemplateRenderer
{
public function render(string $templateName, array $data = []): string
{
return json_encode($data);
}
}

(The TemplateRenderer is passed as a dependency in the constructor of the Component.)

If we have a component that has a child component, and that one has a child component, like so:

var_dump($component([
'foo' => 'foo',
'bar' => $component([
'baz' => 'baz',
'qux' => $component([
'quux' => 'quux',
]),
]),
]));

we end up with a JSON encoded string like this:

string(79) "{"foo":"foo","bar":"{\"baz\":\"baz\",\"qux\":\"{\\\"quux\\\":\\\"quux\\\"}\"}"}

It's not ideal because we used assertEquals to compare arrays. If we apply the json_decode as we did, we will get an array that still contains JSON encoded values:

array(2) {
  ["foo"]=>
  string(3) "foo"
  ["bar"]=>
  string(41) "{"baz":"baz","qux":"{\"quux\":\"quux\"}"}"
}

The solution

I was never that thrilled with decoding the data in the tests anyway, so I created a "custom" assertion that does all the "heavy-lifting", and recursively decodes the string.

The "custom" assertion is as simple as it can be:

protected function assertTemplateRendererDataEquals(array $expected, string $actual, string $message = ''): void
{
$this->assertEquals(
$expected,
$this->decodeEncodedTemplateRendererData($actual),
$message
);
}

It just wraps the "native" assertEquals and defers the work to a private method:

 private function decodeEncodedTemplateRendererData(mixed $data): mixed
{
if (!is_string($data) && !is_array($data)) {
return $data;
}

if (is_string($data)) {
try {
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
} catch (\Exception) {
return $data;
}
}

if (is_array($data)) {
foreach ($data as $key => $value) {
$data[$key] = $this->decodeEncodedTemplateRendererData($value);
}
}

return $data;
}

And with all this in place, in the tests, I can simply do this and forget about the complexity:

$this->assertTemplateRendererDataEquals(
[
'foo' => 'foo',
'bar' => [
'baz' => ...
]
],
$component->render()
);

The code to create those components "on-the-fly":

$component = fn(array $data = []): string => (new class(
new JsonEncodeTemplateRenderer(),
$data
) implements Component {
public function __construct(
readonly private TemplateRenderer $templateRenderer,
readonly private array $data,
) {
}

public function render(): string
{
return $this->templateRenderer->render(
'index.php',
$this->data,
);
}
})->render();

Testing HTML Components

For a WordPress (classic) theme using get_template_part generally provides a good level of organization. But for a large, enterprise-level website, I organize my server-side rendered UI elements into what I call "HTML Components". It's somewhat inspired by React.

Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.

Since we prefer OOP for this project, there's an interface for the components:

interface Component
{
public function render(): string;
}

A component could be as simple as this one, but typically there's more to it:

class Infobox implements Component
{
public function render() {
return '<div class="infobox">...</div>';
}
}

There's a template renderer to avoid mixin HTML in the PHP class and achieve a better separation of concerns:

interface TemplateRenderer
{
public function render(string $templateName, array $data = []): string;
}

Besides passing the data to the template files, components can retrieve icons, derive state from props (React jargon), etc., do what's needed.

Here's a stripped-down but "real" component:

class Component {
use ResourcesDirTemplateFileTrait;

public function __construct(
readonly private Props $props,
readonly private TemplateRenderer $templateRenderer,
readonly private Svg\Loader $svgLoader
) {
}

public function render(): string
{
return $this->templateRenderer->render(
$this->templateFile(),
[
TemplateArgs::TITLE => $this->props->title,
TemplateArgs::CONTENT => $this->props->content,
TemplateArgs::DEFAULT_IS_OPEN => json_encode(!$this->props->title),
TemplateArgs::ICON_TOGGLE => $this->props->title
? $this->svgLoader->byName('arrow-caret-down')
: '',
]
);
}
}

While these components do not contain "too much logic", I strive to provide tests for them.

Testing

When testing these components, I'm interested in two things. First, if the components are rendering the output (HTML) I expect. Second, if the data passed to the template files is what it should be.

Testing the output also tests the data indirectly. There's an overlap between the two types of tests, but they can't be exchanged, and they focus on different aspects.

I made the rule to always write tests "for the HTML", but only test the data directly for more "complex" components.

"Snapshot" testing

PHPUnit had the assertEqualXMLStructure method, which made it easy to compare two DOM structures. There are plans to reintroduce it.

Until then, I'm comparing two "HTML" strings. This has its shortcomings. For example, the order of the attributes has to match; spacing can cause problems, etc.

// This fails
$this->assertEquals(
'<div class="infobox" id="infobox-alpha"></div>',
$infobox->render() // <div class="infobox-alpha" class="infobox">...</div>
);

To overcome this, I have a custom assertion method that does some normalization before comparing the strings:

class InfoboxHtmlSnapshotTest extends HtmlSnapshotTestCase
{
public function testDefaultOutput()
{
$infobox = new Infobox(...);

$this->assertHtmlEquals(
file_get_contents(__DIR__ . '/snapshots/default.html'),
$infobox->render()
);
}
}

I'm doing something similar to what Jest calls snapshot testing but the "snapshots", in my case, are created and maintained manually.

Testing the $data with mocking

My initial approach for testing the passed data to the template renderer was mocking it and setting up expectations.

public function testNoIconAndIsOpenWhenNoTitle()
{
$templateRendererMock = $this->createMock(TemplateRenderer::class);

$templateRendererMock->expects($this->once())
->method('render')
->with(
$this->stringContains('index.php'),
[
'title' => '',
'content' => 'In 2009, the International Air Transport Association (IATA) ...',
'initialIsOpen' => 'true',
'iconToggle' => '',
]
);

$infobox = new Infobox(
...,
$templateRendererMock,
...
);

$infobox->render();
}

This approach started to be messy when I created components that rendered child components.

In those cases, the render method was called multiple times. Switching and using the withConsecutive turned out to be hard to reason about and follow.

Using a test implementation

The contract of the TemplateRenderer had to be satisfied, so there was no way around returning something else than a string.

But the option to create a test implementation for the template renderer was open. So that's what I did.

$customImplTemplateRenderer = new class implements TemplateRenderer
{
public function render(string $templateName, array $data = []): string;
{
return json_encode($data);
}
};

This implementation simply returns the data in a JSON encoded string format, which, once decoded, allows for good old array comparison:

$infobox = new Infobox(
...,
$customImplTemplateRenderer,
...
);

$this->assertEquals(
[
'title' => '',
'content' => 'In 2009, the International Air Transport Association (IATA) ...',
'initialIsOpen' => 'true',
'iconToggle' => '',
],
json_decode($infobox->render(), true)
);

When nesting HTML Components, it's just a matter of comparing a multidimensional array, and there's no need for writing the boilerplate for creating a mock.

ELS, product pricing based on pricing policy rules

Electrosystems (ELS) specializes in importing, developing, and distributing electronic security systems in the B2B sector. They have been selling the most advanced technological products at competitive prices in Greece since 2007.

Background

Since it's a long-standing company, they have a robust, established workflow with an ERP system at its core to manage customers and orders.

ELS has an online shop that is used only for creating preliminary orders without asking for payment. The finalization of orders happens offline.

Since they have various partnership deals, their customers get a customized pricing offer for the products. When customers log in to the shop, they see those prices. (They do now!)

Problem

While the previous team of developers was able to set up WooCommerce and the data importing and synchronization, they failed to implement the pricing logic.

The ERP has the option to expose the customer and product data. It does not offer an endpoint to retrieve a product price based on a customer, only the underlyinushg raw rules data that is used for the different discount policies.

The partial and failed pricing policy implementation created more confusion for the customers than helped them.

With customers complaining about seeing different prices than in their final offers, ELS wasted countless hours in ad-hoc verifications and debugging that seemed to go nowhere.

Mistrust and high response time started to characterize the communication with the original developers.

Solution

...

Inpsyde Modularity Properties

If you are building a WordPress plugin, you typically need to reference specific values repeatedly—for example, the version or base path. The same is true for a theme or a library.

A pretty common approach is to define these as constants. There are boilerplates that suggest this approach.

If you have a "main" plugin class, you might have these "constants" as properties too.

This approach immediately introduces duplication because some of the values are typically also found in the Plugin Header, like the version. Hack together a release script and problem solved(?!).

The various Propreties classes of Inpsyde Modularity deal with this topic. They centralize the "constants" in one place, but it also conveniently sets them up.

Instead of defining typical constants one by one:

define('PLUGIN_NAME_VERSION', '1.0.0');
define('PLUGIN_NAME_PATH', __FILE__);

you can do this:

use Inpsyde\Modularity\Properties\PluginProperties;

$pluginPropreties = PluginProperties::new('/path/to/plugin-main-file.php');

and it will parse the plugin's header and set up the properties. You can then access the values by calling methods on the object:

$pluginPropreties->version(); // version of the plugin
$pluginPropreties->name(); // name of the plugin

If you are using Modularity for a theme, you have the ThemeProperties class:

$themeProperties = ThemeProperties::new('/path/to/theme-directory/');

For completeness, there are two more classes, the LibraryPropreties and the BasePropreties.

The BasePropreties doesn't parse any file; it expects you to pass some values manually to it. The LibraryProperties parses a composer.json file. More about these two somewhere else.

For more detailed information, check the documentation.

Next up the ...

Get an SSL certificate for the PHP Apache Docker image variant with Certbot

When prototyping or hacking on something with PHP, I use the PHP Docker image variant that includes Apache.

Even if it's a toy project, I get an SSL certificate for its domain when I put it online. Here's yet another step-by-step instruction on how to do it. It just focuses on the absolute minimal.


We are going to have a Dockerfile with the following:

FROM php:8.1-apache

RUN a2enmod ssl

CMD ["apache2-foreground"]

This is equal to the docker container run php:8.1-apache with the difference that it enables the ssl module. By default, Apache does not have that module enabled.

To build and tag the image:

docker image build -t app .

Let's follow the convention and put our main index.php file in the public folder.

We need two folders related to the process of getting the certificates. One for the actual certificates (certs), the one it's more like a temporary folder (data).

So far, we have this:

.
├── Dockerfile
├── public/index.php
├── letsencrypt/certs/
├── letsencrypt/data/

We also need to customize the default Apache configuration.

The configuration before the certificate will look different than the one after we have the certificates. Instead of modifying the contents of the config file, we can prepare them in advance:

.
├── apache2/000-no-ssl-default.conf
├── apache2/000-default.conf

Here's the content of the 000-no-ssl-default.conf:

<VirtualHost *:80>
    DocumentRoot /var/www/html/public

    Alias /.well-known/acme-challenge /var/www/letsencrypt/data/.well-known/acme-challenge
</VirtualHost>

80 is the default port for HTTP. We set the root to the public instead of the default /var/www/html/ that Apache recommends.

The other part is creating an "alias", which is saying when the /.well-known/acme-challenge URL is loaded, then load whatever is located at the specified path.

With all this, we can start the container with the bind mounts:

docker container run \
-d \
-p 80:80 \
-v ${PWD}/public:/var/www/html/public \
-v ${PWD}/apache2/000-no-ssl-default.conf:/etc/apache2/sites-enabled/000-default.conf \
-v ${PWD}/letsencrypt/:/var/www/letsencrypt \
app

If we don't have a domain, we can get a subdomain free from FreeDNS and point it to the server's IP address.

The next step is getting a certificate issued by Let's Encrypt with Certbot.

Let's Encrypt is a free, automated, and open certificate authority (CA), run for the public's benefit.

Certbot is a free, open source software tool for automatically using Let's Encrypt certificates on manually-administrated websites to enable HTTPS.

docker container run \
-it \
--rm \
-v ${PWD}/letsencrypt/certs:/etc/letsencrypt \
-v ${PWD}/letsencrypt/data:/data/letsencrypt \
certbot/certbot certonly \
--webroot \
--webroot-path=/data/letsencrypt \
-d your-domain.com \
--dry-run

This command might look especially long and complicated, but until the certbot/certbot part it is just the most common Docker flags, and after that are specific flags for the tool.

Here are the relevant pages from the docs:

We bind mount the letsencrypt folders to both containers. The reason is that the Certbot has to have access to write to it, and the Apache has to have access to read from it.

We run it first with the --dry-run to ensure everything runs fine before issuing the certificates. Repeatedly asking for the certificate and running into problems when doing it will ban the domain for several days.

We will be asked to confirm a few things; we just need to follow the instructions. If all is good, we can rerun it without the --dry-run.

We should now see a bunch of files and folders in the /letsencrypt/certs/ folder.

Now that we have the certificates, we should use the 000-default.conf. After stopping the current container, we can start it again but this time with:

docker container run \
-d \
-p 80:80 \
-p 443:443 \
-v ${PWD}/public:/var/www/html/public \
-v ${PWD}/apache2/000-default.conf:/etc/apache2/sites-enabled/000-default.conf \
-v ${PWD}/letsencrypt/:/var/www/letsencrypt \
app

The content of the config can be this simple:

<VirtualHost *:80>
    Redirect / https://your-domain.com/
</VirtualHost>
    
<VirtualHost *:443>
    DocumentRoot /var/www/html/public

    SSLEngine on
    SSLCertificateFile "/var/www/letsencrypt/certs/live/your-domain.com/fullchain.pem"
    SSLCertificateKeyFile "/var/www/letsencrypt/certs/live/your-domain.com/privkey.pem"
</VirtualHost>

We leave the 80 accessible, but we redirect all requests to the HTTPS version. In the HTTPS section, we specified the paths for the certificates.