Using the Adapter Pattern in components

By Paul,

June 2019

Web Dev
Finding that your components are getting bloated?

You may or may not have heard of the Adapter Pattern. It's a well established software design pattern that solves problems where classes with incompatible interfaces need to work together.

To quote Wikipedia:

Wikipedia

If that sounds a bit complicated then here's a simpler analogy:

If you've ever been to the US you'll know that their plug sockets use 2 pins whereas UK plug sockets use 3 pins. In order to use your UK appliances over there you need a 3-to-2 pin plug adapter.

In this analogy, the plugs on the UK appliances don't care where the electricity is coming from - all they need to function correctly is the correct amount of electricity. Linking back to the Wikipedia description, in this example the adapter allows the interface of an existing class (2 pin plug) to be used as another interface (3 pin plug).

Animation of plug socket being overloaded with too many adapters

We recently worked on a CMS-driven website that was very component-heavy. Many of these components needed to receive data from various different elements within the CMS. As the website build progressed we soon found that our components were either getting bloated with too much logic, or that we were ending up repeating chunks of logic across multiple templates.

By switching to use the adapter pattern in our components we were able to separate this logic into adaptercomponents that we could use in different situations across the website. This helped eliminate repetition and bloat, and gave each component a clear separation of responsibilities going forward.

A Craft CMS example

Imagine we have a list of Craft CMS entries that we are looping through in the template. We need to render a card component for each of these entries. We already have a card.twig component, so let's pass the entry element to it.

Since the entry element we're getting from the CMS is a pretty complex data object we'll need to add some logic to the card.twig component to extract the data we need before we display it. Ok, no big deal.

A bit further into the website build we realise that we also now need to display a product category element as a card. As before, we can pass the category element from the CMS to the card.twig component and add some more logic to it to extract the relevant data we need from the category. Now that the card.twig component can receive two types of data that have differing structures (entry and category) we'll also need to add some more logic to it to determine which type of data we're working with.

At this point it starts to become obvious that something isn't right. The logic in our card.twig component is already getting pretty complicated and we're only passing two different types of data to it. As the website build goes on it's likely that we'll also have other elements from the CMS that need to be rendered as a card and that's only going to make the problem worse.

Ideally, the card.twig component should be completely oblivious to where the data is coming from. All it should care about is that it gets the absolute minimum amount of data necessary to render successfully.

So how should we pass the data from various elements in Craft to the card.twig component? This is where the adapter pattern comes into play. If, for example, we need to render an entry from our news section into the card.twig component, we can create a card--news-entry.twig adapter component that we include first:

index.twig

{% for entry in craft.entries.section('news') %}
	
	{% include '_partials/card--news-entry.twig' with {entry: entry} only %}

{% endfor %}

We pass the entry element from Craft to card—news-entry.twig. This adapter component then takes this entry element, extracts the relevant data from it and then passes that data to our card.twig component.

card--news-entry.twig

{#
REQUIRED VARS
entry (craft\elements\Entry)
#}

{% set cardData = {
	image: entry.imageField.one().getUrl(),
	title: entry.title,
	description: entry.description
} %}

{% include '_partials/card.twig' with cardData only %}

The card.twig component receives the data it needs from the card--news-entry.twig adapter component and can render successfully. All through this process our card.twig component has been unconcerned about where its data is coming from. All it cares about is that it receives the image, title and description variables needed to render successfully.

card.twig

{#
REQUIRED VARS
image (string) - Url of image
title (string)
description (string)
#}

<div class="card">

	<img src="{{ image }}" class="card__image">

	<h2 class="card__title">{{ title }}</h2>

	<div class="card__description">
		
		{{ description }}

	</div>

</div>

If, in the future, we needed to render a card component for a news category, we could use the same pattern and create a card--news-category.twig adapter component that contains the logic for extracting the necessary data from the category element before passing it off to our card.twig component.

card--news-category.twig

{#

REQUIRED VARS

category (craft\elements\Category)

#}

{% set cardData = {

image: category.featureImage.one().getUrl(),

title: category.title,

description: category.summary

} %}

{% include '_partials/card.twig' with cardData only %}

Couldn't actually think of a good ending for this article so....BRAN STARK?

Image of the character Bran Stark from Game Of Thrones sitting in a wheelchair