Hoe we door asynchrone communicatie beter samenwerken: ontdek ons experiment

Hoe we zorgen voor betere developer experience in Drupal

Werken met PHP en Drupal zijn bij Wieni core business, zeker als het gaat over web frameworks voor content management. Met onze permanente ontwikkeling van eigen modules tillen we de developer experience naar een hoger niveau. Lees hier meer over onze powersetup.

It's complicated

Drupal zit in het Wieni DNA. Van Drupal 6 (maart 2009) langs Drupal 7 (januari 2011) tot 8 (september 2016), en sinds oktober 2020 ook de eerste Drupal 9.

De introductie van Drupal 8, waarbij afgestapt werd van een custom functional codebase naar een op Symfony geënte codebase, zorgde voor heel wat deining in de community: contrib modules hadden voor het eerst een complete rewrite nodig (functional naar object oriented), scripts die voor contentmigratie zouden moeten zorgen deden het niet zoals zou moeten, de teneur naar meer enterprise-oriented functionaliteiten ipv de typerende homebrew stijl werd niet door iedereen gesmaakt, etc.

Veel ontwikkelaars, die de extra kost (en de steile leercurve) van een upgrade van D7 naar D8 niet konden verantwoorden of zelf niet meer akkoord gingen met de richting die Drupal uitging, zijn afgezakt (pun intended) naar forks (zoals backdropcms.org) of andere populaire CMS’en die meer inzetten op backwards compatibility, zoals Wordpress.

Ondanks de nieuwe Symfony fundering schortte er nog vanalles aan Drupal 8 als modern CMS: de gedateerde en weinig gebruiksvriendelijke interface, functionaliteiten als het beheer van media en het (automatisch) uitvoeren van updates zijn lang afwezig geweest. Op deze vlakken is er de voorbije jaren aan de hand van strategische initiatieven hard gewerkt aan verschillende van deze pijnpunten, en met resultaat! Het nieuwe backend-thema Claro en het bijhorende design system zit reeds in Drupal sinds versie 8.8.0 en het nieuwe front-end-thema Olivero is op moment van schrijven volop in ontwikkeling.

Wieni is al sinds eerste release van Drupal 8 een grote fan. De overstap naar Symfony heeft wonderen gedaan voor de schaalbaarheid en de developer experience van onze projecten. Het heeft ons ook toegestaan om websites als Vier, DNS Belgium, UZ Leuven en Bruzz op te zetten en te blijven uitbreiden.

Drupal 8, what’s the big deal?

Zowat elke website die we de voorbije jaren bij Wieni hebben gemaakt is aangedreven door Drupal 8, en daar zijn verschillende redenen voor!

Entity API

Het paradepaardje van Drupal 8 is de Entity API. Een entiteit is een heel abstract gegeven, maar in Drupal is het een manier om data te modelleren. Elke entity heeft een type (de entity type) en een bundle, een onderverdeling van entity types. Een entiteit kan velden hebben, een manier om kenmerken te koppelen. En veld kan simpele tekst zijn, maar bv. ook een verwijzing naar andere entiteiten. Een voorbeeld van een entiteit is een artikel: zijn type zal waarschijnlijk Node zijn, een ingebouwd entity type. De naam van de bundle zal Artikel zijn, en normaal gezien heeft het ook een reeks aan velden: auteur (een verwijzing naar een gebruiker-entiteit), aanmaakdatum (een datum en tijd-veld), titel (een tekstveld), enzovoort.

Een groot voordeel van deze abstracte API is dat het out-of-the-box heel eenvoudig is om zaken aan te passen en toe te voegen. Een veld toevoegen, aanpassen op welke manier een entiteit wordt opgeslagen, code schrijven die wordt uitgevoerd elke keer een bepaald soort entiteit wordt opgeslagen of verwijderd, ... Het is allemaal mogelijk.

Een ander voordeel is dat heel wat CMS-gerelateerde functionaliteiten standaard beschikbaar zijn voor deze entities. Zo heb je na het maken van een nieuwe entity type - zonder amper iets van code te schrijven - elementen zoals formulieren om ze toe te voegen, te bewerken of te verwijderen, overzichten met paginering, filters en links naar de verschillende formulieren, een uitgebreide interface voor de admin om velden te beheren, ...

Abstracties die zo ver gaan als deze remmen vaak meer af dan voordeel op te leveren. Bij de Drupal Entity API is dit echter niet het geval. Kudos!

Config API

Een nadeel van veel frameworks is dat configuratie samen met content in de database bijgehouden wordt. Dat maakt het onmogelijk om aanpassingen bij te houden in version control en eventueel automatisch te deployen. Voor dat probleem heeft Drupal 8 een oplossing: de Config API. Configuratie wordt bijgehouden in de database, maar kan op een heel eenvoudige manier geëxporteerd worden naar YAML-formaat in één environment, om vervolgens in andere environments opnieuw te importeren. Er wordt ook telkens een mooie diff getoond om duidelijk te maken wat er juist gewijzigd wordt. Zoals alle andere Drupal API’s is ook deze heel open van aard, waardoor er al verschillende third-party uitbreidingen ontstaan zijn. Eentje daarvan is de Configuration Split module. Die zorgt ervoor dat je afsplitsingen kunt maken van de hoofdconfiguratie, die je dan naar wens kan in- of uitschakelen. Een praktisch voorbeeld is dat je zo bepaalde modules alleen kan inschakelen op lokale environments, of dat je op lokale environments meer gedetailleerde foutboodschappen toont dan op productie.

Translation API

Voor een framework als Drupal dat wereldwijd gebruikt wordt, is de mogelijkheid om een website in verschillende talen aan te bieden geen overbodige luxe. Daarvoor heeft Drupal 8 de Interface Translation-module: een manier om tekst die in code staat vertaalbaar te maken via een interface in de backend van de website.

Vertalingen worden niet bijgehouden aan de hand van zelf verzonnen keys, zoals in frameworks als Laravel, maar aan de hand van de tekst zelf in de standaardtaal van de website. Dit brengt zowel voor- als nadelen met zich mee. Handig is dat je niet bezig moet zijn met zelf keys te verzinnen telkens je een zin wil vertalen. Een groot nadeel is dan weer dat wanneer je in een eerder vertaalde zin een woordje aanpast, de key dus ook is aangepast, en je de reeds geschreven vertalingen kwijt bent. Er wordt echter nagedacht om een soort van hybride systeem te voorzien, iets tussen key- en string-based vertalingen.

Naast eenvoudige tekst is het ook mogelijk om entities te vertalen met de Content Translation module, en om tekst die bewaard wordt in config te vertalen met de Configuration Translation module. Dit alles zorgt ervoor dat in Drupal 8 quasi alles volledig vertaalbaar is.

Drush

In Drupal beheer je alles via de grafische UI: zowel inhoud als configuratie. Dat maakt het CMS toegankelijk voor iedereen. Als productieve developer is zo’n interface echter niet voldoende. Een command line interface (CLI) is een onmisbare tool voor elk modern framework.

Wat betreft Drupal CLI’s zijn er twee grote spelers: Drupal Console en Drush. Vroeger was het grote verschil dat Drupal Console gebouwd was op een moderne, object oriented manier en op basis van Symfony Console terwijl Drush nog vast hing aan zijn functionele, Drupal 7-stijl codebase. Sinds Drush 9 in januari 2018 verscheen, is dat verschil echter volledig weg, aangezien ook Drush nu gebouwd is met Symfony Console en met OOP en dependency injection in gedachten. Vandaag is het enige verschil de ingebouwde commands die beide tools aanbieden: Drupal Console biedt een hele reeks commands aan om code scaffolding te genereren, iets wat Drush niet kan.

wmscaffold

Bij Wieni gebruiken we enkel Drush. Om het gemis van de Drupal Console code scaffolding generator op te vangen, bouwden we onze eigen oplossing: wmscaffold. Het is geen 1 op 1 vervanging voor Drupal Console: wmscaffold bevat vooral functionaliteiten die voor ons bij Wieni het meest waardevol zijn. Zo kun je eenvoudig model klasses genereren voor wmmodel (inclusief getters!) en controllers voor wmcontroller, maar ook node types en velden aanmaken of verwijderen. Het handige is dat deze commands door elkaar getriggerd worden met events: als je een node type maakt, wordt er automatisch een bijhorende model en controller class aangemaakt. Wanneer je vervolgens een veld toevoegt aan dat node type, wordt er automatisch een getter toegevoegd aan de model. Een overzicht van alle commands vind je in de Github repo.

… maar niet alles is rozengeur en maneschijn

Dé expertise van Wieni is het ontwerpen van websites op maat van de klant. Dat zijn vaak content intensieve websites, maar ook webapps waarvoor meer business logic nodig is. De specialiteit van Drupal is een CMS te zijn voor iedereen, met of zonder kennis van programmeren. Daar botst het soms. Voor veel specifieke functionaliteiten vind je bestaande modules die een kant-en-klare oplossing bieden, maar vaak hebben wij of de klant heel specifieke eisen die niet passen binnen die kant-en-klare oplossingen. Dan heb je twee opties: beginnen knutselen rond die bestaande oplossing, maar dan heb je het nadeel dat je vaak niet zo’n propere en schaalbare code aan het schrijven bent, of je eigen implementatie schrijven. Dat is een afweging die we regelmatig moeten maken en die dan ook vaak resulteert in een zelfgeschreven module.

Views

Een goed voorbeeld is Views, een module die heel lang een contrib-project is geweest en sinds oktober 2012 deel is van Drupal Core . Het is een module om via een UI gefilterde lijsten met entities te kunnen maken. Met andere woorden een veredelde query builder. Heel goed voor eenvoudige sites met weinig speciale vereisten, maar van zodra je die wel hebt, moet je gaan aankloppen bij contrib modules en krijg je te maken met de eerder beschreven situaties.

Om die reden is in (bijna) al onze Drupal sites de Views module uitgeschakeld. Voor overzichten in de backend gebruiken we onze eigen wmentity_overview, en voor het ophalen van entities voor de frontend gebruiken we entity of database queries. Zo kunnen we veel sneller nieuwe, complexere features toevoegen en moeten we onze weg niet meer zoeken in de complexe Views UI.

Render arrays

Een van de belangrijkste relikwieën van Drupal 7 - die nog steeds veel gebruikt wordt in Drupal 8 en 9 - zijn de render arrays, de basisblokken van elke Drupal-pagina. Een render array is een associatieve array die voldoet aan de standaarden en gegevensstructuren die worden gebruikt in het theme rendering system van Drupal.

Het grote voordeel van dit systeem is dat de pagina die doorheen de request wordt opgebouwd, wordt bijgehouden als gestructureerde data die eenvoudig aangepast/overschreven kan worden door andere code. Op het einde van de request wordt deze grote array omgezet (of gerenderd) naar HTML en naar de eindgebruiker gestuurd.

Dit systeem werkt goed in verschillende situaties, maar er is één essentiële situatie waar we dit systeem volledig omzeilen: frontend theming, of het opbouwen en themen van de publiek zichtbare site. Hier zijn we meer fan van het klassieke MVC-model: de model is een toegankelijke voorstelling van de gegevens die uit de database worden opgehaald. Een controller verzamelt de data die nodig is om de pagina op te bouwen en geeft die ten slotte door aan de view, die op basis daarvan de pagina opbouwt. Om dit klassieke model te kunnen toepassen in context van Drupal entities hebben we een reeks modules ontwikkeld.

wmmodel

Het voorstellen van data uit de database aan de hand van een klasse is iets wat out-of-the-box ondersteund wordt door Drupal en zijn Entity API. Het enige nadeel is dat klasses enkel gedefinieerd worden op entity type-niveau. Het gevolg? Wanneer je als developer bijvoorbeeld een nieuwe bundle maakt op het Node entity type (een node type), is het niet mogelijk om daar je eigen klasse aan te koppelen en dus zelf getter/setter/helper methodes te definiëren. Daardoor zitten Twig templates vaak vol met complexe selectors om data op te halen uit velden en logica die eigenlijk niet in de template zou moeten leven. Daarom hebben wij wmmodel ontwikkeld, een module die de mogelijkheid biedt om met een simpele annotation een klasse toe te wijzen aan een bepaalde bundle.

mymodule/src/Entity/Model/Node/Page.php

<?php

namespace Drupal\mymodule\Entity\Model\Node;

use Drupal\node\Entity\Node;
use Drupal\wmmodel\Entity\Interfaces\WmModelInterface;

/**
 * @Model(
 *     entity_type = "node",
 *     bundle = "page",
 * )
 */
class Page extends Node implements WmModelInterface
{
    use WmModel;
}

wmcontroller

Het hierboven beschreven probleem met entity models bestaat ook voor controllers: er is 1 controller voor alle node types. Als je per node type andere data wilt voorzien, gaat die logica dus ergens anders moeten staan, bv. in procedurale preprocess-functies. De wmcontroller module zorgt ervoor dat je per node type een aparte controller kan definiëren. Het voorziet ook een manier om eenvoudig een Twig template te renderen, zonder daarvoor het Drupal theming systeem te gebruiken.

mymodule/src/Controller/Node/ArticleController.php

<?php

namespace Drupal\mymodule\Controller\Node;

use Drupal\mymodule\Entity\Node\Article;
use Drupal\wmcontroller\Controller\ControllerBase;

class ArticleController extends ControllerBase
{
    public function show(Article $article)
    {
        return $this->view(
            'article.detail',
            ['article' => $article],
        );
    }
}

mytheme/templates/article/detail.html.twig

{# @var article \Drupal\mymodule\Entity\Model\Node\Article #}

<h1>{{ article.getTitle() }}</h1>

{% for paragraph in article.getParagraphs() %}
    {%
        include '@thing/article/paragraph/small.html.twig'
        with {paragraph: paragraph} only
    %}
{% endfor %}

wmpresenter

In een ideale wereld bevatten model klasses alleen maar methodes om data op te halen uit of aan te passen aan de entiteit in kwestie. In praktijk schrijf je echter vaak logica die bepaalt hoe de data juist getoond wordt in de UI, bv. een getFullName methode die voornaam, achternaam en eventuele tussenvoegsels samenvoegt. Die kan terecht in de presenter: een klasse die de model wrapt, dus waar je alle getters op kunt callen die op de model staan, en waar je nog extra methodes op kunt zetten die helpen met de presentatie. Een groot voordeel: op deze klassen kan je dependency injection gebruiken, op models niet.

mymodule/src/Entity/Presenter/Node/Page.php

<?php

namespace Drupal\mymodule\Entity\Presenter\Node;

use Drupal\wmcontroller\Entity\AbstractPresenter;
use Drupal\mymodule\Entity\Model\Node\Page as PageModel;
use Drupal\mymodule\Service\RandomService;

/**
 * @mixin PageModel
 * @property PageModel $entity
 */
class Page extends AbstractPresenter
{
    protected RandomService $randomService;

    public function construct(RandomService $randomService)
    {
        $this->randomService = $randomService;
    }
    
    public function getFullTitle(): string
    {
        return sprintf('%s, %s', $this->getTitle(), $this->getSuffix());
    }

    public function getSomethingComplex(): array
    {
        return $this->randomService->getSomethingComplex($this->entity);
    }
}

mytheme/templates/page.detail.html.twig

{# @var page \Drupal\mymodule\Entity\Presenter\Node\Page #}

<h1>{{ page.fullTitle }}</h1>

 

Tot slot

Onze jarenlange ervaring met Drupal 8 én de set aan open sourced modules die we hebben ontwikkeld om ons het leven eenvoudiger te maken, zorgen ervoor dat we heel tevreden zijn met onze workflow en erg efficiënt kunnen werken. Neem zeker een kijkje op ons Githubprofiel om onze andere open source modules te leren kennen.

Onze zoektocht naar een nog betere developer experience in Drupal is echter nog niet voorbij. We streven elke dag naar betere manieren om te coderen in Drupal 8. Niets houdt ons tegen om te verbeteren. We zijn immers niet perfect: zo hebben we geen node previews, geen dynamic page caches, zijn we nog steeds aan het experimenteren met automated testing, etc.

We geloven dat we met genoeg motivatie en passie in het team een veel aangenamere codebase kunnen bouwen, ook in Drupal. Zo is het ons onlangs ook gelukt om Symfony autowiring te beginnen gebruiken in onze custom modules.

Heb je een vraag over onze workflow? Heb je iets gezien wat je wenkbrauwen deed fronsen? Of wil je graag ons backend team komen versterken? Aarzel niet om ons een berichtje te sturen!

Wieni en Drupal

  • Onze modules zijn open source en terug te vinden op Github. Issues, pull requests en andere vormen van feedback zijn altijd welkom.
  • Voor een overzicht van onze contributions aan andere Drupal-projecten kan je terecht op het Drupal-profiel van Wieni en van Dieter.

Meer weten over onze development frameworks?

Daarvoor kan je terecht bij Dieter. Hij is Backend Developer bij Wieni, wat inhoudt dat je door middel van Drupal 8 en Symfony de achterliggende basis legt voor alle websites. Contacteer hem op dieter@wieni.be.

Over Dieter