WordPress is een CMS dat al lang bestaat, maar is ook de meestgebruikte. Dankzij de traditie van het ondersteunen van verouderde PHP versies en legacy code, worden nog niet alle moderne programmeergewoonten geïmplementeerd, waar WordPress abstractie een voorbeeld van is.

Zo zou het bijvoorbeeld veel beter zijn om de codebase van de WordPress core op te splitsen in packages die beheerd kunnen worden via Composer. Of om WordPress classes automatisch te laden via de bestandspaden.

Dit artikel beschrijft hoe je handmatig WordPress code kan abstraheren en abstracte WordPress pluginfuncties kan gebruiken.

Problemen met het integreren van WordPress en PHP tools

Dankzij de verouderde architectuur komen we soms problemen tegen wanneer we WordPress integreren met tooling van PHP codebases, zoals de statische analyser PHPStan, de library voor unit testing PHPUnit, en de namespace-scoping library PHP-Scoper. Denk bijvoorbeeld eens aan de volgende gevallen:

De WordPress code binnen onze projecten is meestal maar een klein deel van de totale code, omdat er in een project ook code zit die onafhankelijk werkt van het onderliggende CMS. Maar alleen maar omdat er wel een klein beetje WordPress code in zit, kan een project niet goed meer integreren met bepaalde tools.

Hierdoor kan het soms beter zijn om een project op te splitsen in verschillende packages. Hierbij hebben sommige de WordPress code, en andere alleen de bedrijfsspecifieke code met “vanilla” PHP en geen WordPress code. Op die manier wordt die tweede soort dan niet getroffen door bovenstaande problemen, en kunnen die perfect integreren met de gewenste tools.

Wat is abstractie van code?

De abstractie van code verwijdert vastgelegde dependencies uit de code, waardoor je packages krijgt die met elkaar samenwerken via zogenoemde contracten. Deze packages kunnen vervolgens voor verschillende toepassingen met verschillende stacks gebruikt worden, waardoor je ze zo effectief mogelijk kan inzetten. Het resultaat van codeabstractie is een mooie modulaire codebase op basis van de volgende principes:

  1. Code voor interfaces, niet voor implementaties.
  2. Maken van packages en ze distribueren via Composer.
  3. Alles samenvoegen via dependency injectie.
Wil je meer weten over WordPress code abstractie? 👩‍💻 Alles van de best practices tot aanbevolen plugins kan je hier lezen ⬇️Click to Tweet

Code voor interfaces, niet voor implementaties

Code voor interfaces, of het programmeren voor interfaces, is de methode waarbij je contracten gebruiken om delen van je code met elkaar te laten samenwerken en communiceren. Een contract is in feite een PHP interface (of in een andere programmeertaal) waarmee gedefinieerd wordt wat de beschikbare functies zijn, inclusief de verwachte inputs en outputs.

Een interface bepaalt wat het doel van de functionaliteit is, zonder uit te leggen hoe de functionaliteit praktisch geïmplementeerd wordt. Door functionaliteit via interfaces te gebruiken kan onze toepassing onafhankelijke stukken code gebruiken die een bepaald doel bereiken, zonder dat het uitmaakt hoe dat precies gedaan wordt. Op die manier hoeft de toepassing niet aangepast te worden om aan te sluiten op een ander stuk code dat hetzelfde doel bereikt, bijvoorbeeld wanneer er een andere provider komt.

Voorbeeld van contracten

De onderstaande code gebruikt Symfony’s contract CacheInterface en het PHP Standard Recommendation (PSR) contract CacheItemInterface voor het implementeren van caching functionaliteit:

use Psr\Cache\CacheItemInterface;
use Symfony\Contracts\Cache\CacheInterface;

$value = $cache->get('my_cache_key', function (CacheItemInterface $item) {
    $item->expiresAfter(3600);
    return 'foobar';
});

$cache implementeert CacheInterface, die de methode get definieert om een object uit de cache op te halen. Door deze functionaliteit te openen via het contract, maakt het voor de toepassing niet uit waar de cache precies is. Of dit nou in het geheugen is, op schijf, in de database of het netwerk, of nog heel ergens anders, dat maakt allemaal niet uit. Maar het moet wel de functie uit kunnen voeren. CacheItemInterface definieert de methode expiresAfter om te bepalen hoe lang een item in de cache bewaard moet worden. De toepassing kan deze methode gebruiken zonder dat het uitmaakt wat het object in de cache precies is, het draait alleen maar om hoe lang het in de cache moet zijn.

Programmeren voor interfaces in WordPress

Aangezien het nu draait om de abstractie van WordPress code, zal het resultaat zijn dat de toepassing geen direct gebruik maakt van WordPress code, maar uitsluitend via een interface. Zo heeft de WordPress functie get_posts deze “signature”, een handtekening, oftewel een combinatie van inputs en outputs:

/**
 * @param array $args
 * @return WP_Post[]|int[] Array of post objects or post IDs.
 */
function get_posts( $args = null )

In plaats van deze methode direct te gebruiken, kunnen we er gebruik van maken via het contract Owner\MyApp\Contracts\PostsAPIInterface:

namespace Owner\MyApp\Contracts;

interface PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[];
}

Hierbij zie je dat de WordPress functie get_posts objecten van de class WP_Post kan terugsturen, die specifiek zijn voor WordPress. Bij abstractie van code moeten we dergelijke vastgelegde dependencies zien te verwijderen. De methode get_posts in het contract stuurt objecten van het type PostInterface terug, waardoor je de class WP_Post kan gebruiken zonder deze expliciet te noemen. De class PostInterface moet toegang geven tot alle methoden en eigenschappen van WP_Post:

namespace Owner\MyApp\Contracts;

interface PostInterface
{
  public function get_ID(): int;
  public function get_post_author(): string;
  public function get_post_date(): string;
  // ...
}

Door deze strategie toe te passen kunnen we beter leren begrijpen waar WordPress in onze stack past. In plaats van WordPress te zien als de toepassing zelf, waarmee we thema’s en plugins installeren, kunnen we het gaan zien als een dependency binnen de toepassing, die net zo goed vervangbaar is als andere onderdelen. Alhoewel we in de praktijk WordPress natuurlijk niet zullen vervangen, is het conceptueel gezien wel vervangbaar.

Het maken en distribueren van packages

Composer is een packagemanager voor PHP. Hiermee kunnen PHP toepassingen packages (stukken code) ophalen uit een repository, en die installeren als dependencies, oftewel afhankelijkheden. Om de toepassing los te koppen van WordPress, moeten we de code in twee soorten packages onderverdelen: packages met WordPress code en packages met de zakelijke toepassing en zonder WordPress code.

Als laatste stap voegen we alle packages toe als dependencies in de toepassing, en installeren we die via de Composer. Aangezien de tooling toegepast kan worden op de packages met de bedrijfsspecifieke code, moeten deze het grootste deel van de code van de toepassing bevatten: hoe hoger het percentage hoe beter. Een mooie doelstelling is om te proberen om 90% van de totale code in die packages te hebben.

Extractie van WordPress code naar packages

Doorgaand met het eerdere voorbeeld, worden de contracten PostAPIInterface en PostInterface toegevoegd aan de packages met de bedrijfsspecifieke code, en andere packages zullen de WordPress implementatie van deze contracten bevatten. Om aan de eisen van PostInterface te voldoen, maken we een PostWrapper class die alle eigenschappen van een WP_Post object ophaalt:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostInterface;
use WP_Post;

class PostWrapper implements PostInterface
{
  private WP_Post $post;
  
  public function __construct(WP_Post $post)
  {
    $this->post = $post;
  }

  public function get_ID(): int
  {
    return $this->post->ID;
  }

  public function get_post_author(): string
  {
    return $this->post->post_author;
  }

  public function get_post_date(): string
  {
    return $this->post->post_date;
  }

  // ...
}

Bij het implementeren van PostAPI moeten we de objecten van WP_Post omzetten naar PostWrapper, aangezien de methode get_posts antwoordt met PostInterface[]:

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\PostAPIInterface;
use WP_Post;

class PostAPI implements PostAPIInterface
{
  public function get_posts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Convert WP_Post[] to PostWrapper[]
    return array_map(
      function (WP_Post|int $post) {
        if ($post instanceof WP_Post) {
          return new PostWrapper($post);
        }
        return $post
      },
      $wpPosts
    );
  }
}

Gebruik maken van injectie van dependencies

De injectie van dependencies, oftewel stukken code waar het programma van afhankelijk is, is een ontwerpmethode waarmee je alle onderdelen van de toepassing kan samenvoegen. Met dependency injectie kan de toepassing services gebruiken via de bijbehorende contracten, en de implementaties van die contracten worden dan “geïnjecteerd” in de toepassing via de configuratie.

Door alleen de configuratie te wijzigen, kunnen we dan eenvoudig wisselen tussen contractproviders. Er zijn diverse libraries voor dependency injection die we kunnen gebruiken. We raden je aan om één te kiezen die de PHP Standard Recommendations (PSR, oftewel PHP standaard aanbevelingen) volgt, zodat we de library eenvoudiger kunnen vervangen, mocht dat nodig zijn. Voor de dependency injectie moet de library voldoen aan PSR-11, die de specificatie bepaalt voor een “container interface”. De volgende libraries, onder andere, voldoen aan PSR-11:

Services gebruiken via de service container

De library voor dependency injectie zal een “service container” beschikbaar maken, die een contract verwerkt naar de bijbehorende class voor implementatie. De toepassing moet de service container gebruiken om toegang te krijgen tot alle functionaliteit. Ter illustratie, zo zouden we normaliter direct WordPress functies gebruiken:

$posts = get_posts();

… maar met de servicecontainer, moeten we eerst de service vinden die voldoet aan PostAPIInterface en de functie via die interface uitvoeren:

use Owner\MyApp\Contracts\PostAPIInterface;

// Obtain the service container, as specified by the library we use
$serviceContainer = ContainerBuilderFactory::getInstance();

// The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI
$postAPI = $serviceContainer->get(PostAPIInterface::class);

// Now we can invoke the WordPress functionality
$posts = $postAPI->get_posts();

Symfony’s DependencyInjection gebruiken

Symfony’s DependencyInjection component is momenteel de meest gebruikte library voor dependency injectie. Je kan de servicecontainer hierbij configureren via PHP, YAML of XML. Om bijvoorbeeld te definiëren dat het contract PostAPIInterface wordt geregeld via de class PostAPI kan je in YAML zo configureren:

services:
  Owner\MyApp\Contracts\PostAPIInterface:
    class: \Owner\MyAppForWP\ContractImplementations\PostAPI

Symfony’s DependencyInjection biedt ook de mogelijkheid dat instances van de ene service automatisch geïnjecteerd worden (ook wel “autowired” genoemd) in een andere service die dit nodig heeft. Daarnaast is het makkelijk om een class te definiëren die een implementatie is van de eigen service: Kijk bijvoorbeeld eens naar onderstaande YAML configuratie:

services:
  _defaults:
    public: true
    autowire: true

  GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface:
    class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry'

  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface:
    class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization'
    
  GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\:
    resource: '../src/Security/UserAuthorizationSchemes/*'

Deze configuratie definieert het volgende:

Laten we eens kijken hoe dit “autowiring” werkt. De class UserAuthorization is afhankelijk van een service met het contract UserAuthorizationSchemeRegistryInterface:

class UserAuthorization implements UserAuthorizationInterface
{
  public function __construct(
      protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
  ) {
  }

  // ...
}

Dankzij autowire: true, zal de component DependencyInjection component automatisch zorgen dat de service UserAuthorization de vereiste dependency ontvangt, wat in dit geval een instance is van UserAuthorizationSchemeRegistry.

Wanneer je abstractie moet gebruiken

Abstractie van code kan veel tijd en moeite kosten, dus we moeten er alleen aan beginnen als het moeite waard is. Onderstaande voorbeelden zijn situaties waarin abstractie de investering waard is. Je kan dit dan doen door de stukjes code uit dit artikel te gebruiken, of via de WordPress abstractieplugins die we verderop aanbevelen.

Gaining Access to Tooling

Zoals eerder genoemd is het uitvoeren van PHP-Scoper op WordPress erg moeilijk. Door de WordPress code te ontkoppelen via specifieke packages, wordt het mogelijk om een WordPress plugin direct te scopen.

Verminderen van tijd en kosten van tooling

Het uitvoeren van een PHPUnit testpakket duurt langer wanneer WordPress ook geïnitialiseerd en uitgevoerd moet worden. Wanneer je deze extra tijd kan besparen, kan dat ook minder kosten betekenen voor het uitvoeren van de tests. Zo betaal je bij GitHub Actions bijvoorbeeld voor GitHub-hosted runners op basis van de hoeveelheid gebruikte tijd.

Minder ingrijpende refactoring nodig

Een bestaand project kan ingrijpende en vergaande refactoring nodig hebben om vereiste architectuur te kunnen implementeren (dependency injectie, opsplitsen naar packages, etc.), wat ergens veeleisend kan worden. Door codeabstractie toe te passen vanaf het begin van een project, kan een project beter beheersbaar blijven.

Code voor verschillende platforms maken

Door 90% van de code in een CMS-onafhankelijk package te zetten, maken we een versie van onze toepassing die op een andere CMS of framework kan werken na een aanpassing van slechts 10% van de code.

Migreren naar een ander platform

Als we een project moeten migreren van Drupal naar WordPress, van WordPress naar Laravel, of een andere combinatie, hoeft ook slechts 10% van de code herschreven te worden: een enorme besparing.

Best practices

Bij het ontwerpen van de contracten voor codeabstractie zijn er een aantal verbeteringen die we meteen op de codebase kunnen doorvoeren.

PSR-12 volgen

Bij het definiëren van de interface voor de toegang tot WordPress methoden, zouden we ons moeten houden aan PSR-12. Deze recente specificatie is bedoeld om de cognitieve inspanning te verminderen bij het scannen van code van verschillende auteurs. Het volgen van PSR-12 betekent een nieuwe naam geven aan WordPress functies.

WordPress gebruikt namen voor functies op basis van het format snake_case, terwijl PSR-12 camelCase aanraadt (dus een hoofdletter bij een nieuw woord, in plaats van underscores). get_posts zou dus bijvoorbeeld getPosts worden:

interface PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[];
}

…en:

class PostAPI implements PostAPIInterface
{
  public function getPosts(array $args = null): PostInterface[]|int[]
  {
    // This var will contain WP_Post[] or int[]
    $wpPosts = \get_posts($args);

    // Rest of the code
    // ...
  }
}

Methoden splitsen

Methoden in de interface hoeven geen kopie te zijn van de versies uit WordPress. We kunnen ze transformeren wanneer dat zin heeft. De WordPress functie get_user_by($field, $value) weet bijvoorbeeld hoe de gebruiker uit de database opgehaald kan worden via de parameter $field, die gebruik kan maken van de waarden "id""ID""slug""email" of "login". Maar hier zitten enkele nadelen aan:

We kunnen deze situatie verbeteren door de functie op te splitsen:

namespace Owner\MyApp\Contracts;

interface UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface;
  public function getUserByEmail(string $email): ?UserInterface;
  public function getUserBySlug(string $slug): ?UserInterface;
  public function getUserByLogin(string $login): ?UserInterface;
}

Dit contract wordt dan zo verwerkt bij WordPress (aangenomen dat we UserWrapper en UserInterface, al gemaakt hebben, zoals hierboven):

namespace Owner\MyAppForWP\ContractImplementations;

use Owner\MyApp\Contracts\UserAPIInterface;

class UserAPI implements UserAPIInterface
{
  public function getUserById(int $id): ?UserInterface
  {
    return $this->getUserByProp('id', $id);
  }

  public function getUserByEmail(string $email): ?UserInterface
  {
    return $this->getUserByProp('email', $email);
  }

  public function getUserBySlug(string $slug): ?UserInterface
  {
    return $this->getUserByProp('slug', $slug);
  }

  public function getUserByLogin(string $login): ?UserInterface
  {
    return $this->getUserByProp('login', $login);
  }

  private function getUserByProp(string $prop, int|string $value): ?UserInterface
  {
    if ($user = \get_user_by($prop, $value)) {
      return new UserWrapper($user);
    }
    return null;
  }
}

Verwijderen van details van de implementatie uit de handtekening van de functie

Functies in WordPress kunnen informatie bieden over hoe ze geïmplementeerd worden in hun eigen ‘handtekening’, oftewel de signature. Deze informatie kan verwijderd worden om de functie vanuit een abstract principe te kunnen gebruiken. Zo kan bijvoorbeeld het ophalen van de achternaam van een gebruiker in WordPress gedaan worden via get_the_author_meta, waarbij het expliciet is dat de achternaam van de gebruiker opgeslagen is als een “meta” waarde (in de tabel wp_usermeta):

$userLastname = get_the_author_meta("user_lastname", $user_id);

Maar je hoeft deze informatie niet over te brengen naar het contract. Interfaces zijn alleen maar geïnteresseerd in het “wat”, niet in het “hoe”. Daarom kan het contract ook een methode get_UserLastname hebben, zonder verdere informatie over de implementatie:

Alle Kinsta hostingpakketten bevatten 24/7 support van onze doorgewinterde WordPress developers en engineers. Chat met hetzelfde team dat ook onze Fortune 500 klanten ondersteun. Bekijk onze pakketten!

interface UserAPIInterface
{
  public function getUserLastname(UserWrapper $userWrapper): string;
  ...
}

Voeg striktere types toe

Sommige WordPress functies kunnen op verschillende manieren parameters ontvangen, wat onduidelijkheid veroorzaakt. Zo kan bijvoorbeeld de functie add_query_arg een enkele combinatie van key en value accepteren:

$url = add_query_arg('id', 5, $url);

…of een hele array van key => value:

$url = add_query_arg(['id' => 5], $url);

Onze interface kan een veel vollediger doel definiëren door de functies in verschillende aparte delen op te splitsen, die allemaal een eigen unieke combinatie van inputs accepteren:

public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $keyValues, string $url);

Technische achterstanden verwijderen

De WordPress functie get_posts stuurt niet alleen “posts” terug, maar ook “pages” van elke entiteit van het type “custom posts”, maar die entiteiten zijn niet uitwisselbaar. Zowel artikelen als pagina’s zijn custom artikelen, maar een pagina is geen artikel. Daarom kan het uitvoeren van get_posts pagina’s terugsturen. Dit gedrag is conceptueel gezien vreemd.

Om dat te corrigeren, zou get_posts eigenlijk get_customposts genoemd moeten worden, maar dat is nooit gebeurd in de WordPress core. Dit is een probleem dat je vaker ziet bij lang bestaande software en wordt “technische schuld” genoemd, namelijk code met problemen, die nooit opgelost worden omdat dat grote veranderingen zou betekenen.

Maar bij het maken van onze contracten hebben we de mogelijkheid om dergelijke achterstanden te corrigeren. In dit geval maken we een nieuwe interface ModelAPIInterface die met entiteiten van verschillende typen kan werken, en maken we diverse methoden, die allemaal met een specifiek type kunnen werken:

interface ModelAPIInterface
{
  public function getPosts(array $args): array;
  public function getPages(array $args): array;
  public function getCustomPosts(array $args): array;
}

Op deze manier verdwijnt deze discrepantie, en krijg je deze resultaten:

Voordelen van abstractie van code

De voornaamste voordelen van de abstractie van de code van een toepassing zijn:

Problemen met abstractie van code

Er zijn ook nadelen aan de abstractie van de code van een toepassing:

Mogelijkheden voor abstractie met WordPress plugins

Although it’s generally wisest to extract your code to a local environment before working on it, some WordPress plugins can help you toward your abstraction goals. These are our top picks.

1. WPide

De populaire WPide plugin van WebFactory breidt de functionaliteit van de standaard WordPress code-editor aanzienlijk uit. Het dient als een WordPress abstractieplugin omdat je de code op de plek van implementatie kan bekijken, waardoor je beter ziet wat er nodig is.

De WPide plugin
De WPide plugin

WPide heeft ook een functie voor zoeken en vervangen, waarmee je snel verouderde code kan opzoeken en meteen kan vervangen met een verbeterde versie.

Daarnaast biedt WPide nog allerlei andere features, zoals:

2. Ultimate DB Manager

De Ultimate WP DB Manager plugin van WPHobby biedt een snelle manier om je databases te downloaden voor extractie en refactoring.

De Ultimate DB Manager plugin
De Ultimate DB Manager plugin

Natuurlijk zijn dit soort plugins niet echt nodig voor gebruikers van Kinsta, aangezien je bij Kinsta directe toegang tot je database krijgt, voor alle klanten. Maar als je momenteel niet genoeg toegang tot je database krijgt van je hostingprovider, kan Ultimate DB Manager een handige WordPress plugin voor abstractie zijn.

3. Je eigen custom WordPress abstractieplugin

Uiteindelijk is de beste keus voor abstractie altijd het maken van je eigen plugin. Dat lijkt misschien een groot project, maar als je maar beperkte directe toegang tot de bestanden van je WordPress core hebt, kan dit een handige oplossing zijn.

Er zijn ook andere voordelen:

Je kan leren hoe je een WordPress abstractieplugin kan maken via het WordPress’ Plugin Developer Handbook.

Lees hoe je handmatig je code kan abstraheren en WordPress abstractieplugins kan gebruiken in deze uitgebreide uitleg 🚀⬇️Click to Tweet

Samenvatting

Moeten we abstractie gebruiken voor de code in onze toepassingen? Zoals met alles, is er niet één perfect antwoord, aangezien het altijd verschilt per project. Projecten die veel tijd kwijt zijn aan analyses met PHPUnit of PHPStan zullen het meeste voordeel hebben, maar het zal niet altijd de aanzienlijke investering waard zijn.

Je hebt nu alles gelezen wat je moet weten om te beginnen met de abstractie van WordPress code.

Ben je van plan om deze strategie ook toe te passen in je eigen project? Zo ja, ben je van plan om een WordPress abstractieplugin te gebruiken? Laat het ons weten in de reacties hieronder!


Bespaar tijd en kosten en maximaliseer siteprestaties met:

  • Directe hulp van WordPress-hostingexperts, 24/7.
  • Cloudflare Enterprise integration.
  • Globaal bereik met 29 datacenters verspreid over de wereld.
  • Optimalisatie met onze ingebouwde Application Performance Monitoring.

Dat alles en nog veel meer, in één pakket zonder langlopende contracten, met migraties en een 30 dagen geld-terug-garantie. Bekijk onze pakketten of neem contact op met sales om het pakket te vinden dat bij je past.