WordPress est un CMS ancien, mais aussi le plus utilisé. Grâce à son historique de prise en charge de versions PHP obsolètes et de code hérité, il manque encore d’implémentation de pratiques de codage modernes – l’abstraction de WordPress en est un exemple.

Par exemple, il serait tellement mieux de diviser le code du cœur de WordPress en paquets gérés par Composer. Ou peut-être, pour charger automatiquement les classes WordPress à partir des chemins de fichiers.

Cet article vous apprendra à abstraire le code de WordPress manuellement et à utiliser les capacités d’abstraction des extensions WordPress.

Problèmes d’intégration de WordPress et des outils PHP

En raison de son ancienne architecture, nous rencontrons parfois des problèmes lors de l’intégration de WordPress avec des outils pour les bases de code PHP, tels que l’analyseur statique PHPStan, la bibliothèque de tests unitaires PHPUnit et la bibliothèque de détection des espaces de noms PHP-Scoper. Par exemple, considèrez les cas suivants :

Le code WordPress au sein de nos projets ne représentera qu’une fraction du total; le projet contiendra également du code commercial agnostique du CMS sous-jacent. Pourtant, rien qu’en ayant un peu de code WordPress, le projet risque de ne pas s’intégrer correctement à l’outillage.

Pour cette raison, il pourrait être judicieux de diviser le projet en paquets, certains contenant du code WordPress et d’autres ne contenant que du code commercial utilisant du PHP « vanilla » et aucun code WordPress. De cette façon, ces derniers paquets ne seront pas affectés par les problèmes décrits ci-dessus mais pourront être parfaitement intégrés à l’outillage.

Qu’est-ce que l’abstraction de code ?

L’abstraction de code supprime les dépendances fixes du code, produisant des paquets qui interagissent les uns avec les autres via des contrats. Ces paquets peuvent ensuite être ajoutés à différentes applications avec des piles différentes, ce qui maximise leur utilisabilité. Le résultat de l’abstraction de code est une base de code proprement découplée basée sur les piliers suivants :

  1. Coder contre les interfaces, pas contre les implémentations.
  2. Créez des paquets et distribuez-les via Composer.
  3. Collez toutes les parties ensemble via l’injection de dépendances.

Coder contre les interfaces, pas contre les implémentations

Le codage contre les interfaces est la pratique consistant à utiliser des contrats pour faire interagir des morceaux de code entre eux. Un contrat est simplement une interface PHP (ou tout autre langage différent) qui définit les fonctions disponibles et leurs signatures, c’est-à-dire les entrées qu’elles reçoivent et leur sortie.

Une interface déclare l’intention de la fonctionnalité sans expliquer comment la fonctionnalité sera mise en œuvre. En accédant aux fonctionnalités par le biais d’interfaces, notre application peut s’appuyer sur des morceaux de code autonomes qui accomplissent un objectif spécifique sans savoir, ou sans se soucier, de la façon dont ils le font. Ainsi, l’application n’a pas besoin d’être adaptée pour passer à un autre morceau de code qui accomplit le même objectif – par exemple, d’un fournisseur différent.

Exemple de contrats

Le code suivant utilise le contrat de Symfony CacheInterface et le contrat de la recommandation standard PHP (PHP Standard Recommendation ou PSR) CacheItemInterface pour implémenter la fonctionnalité de mise en cache :

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

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

$cache implémente CacheInterface, qui définit la méthode get pour récupérer un objet dans le cache. En accédant à cette fonctionnalité via le contrat, l’application peut ignorer où se trouve le cache. Que ce soit dans la mémoire, sur le disque, dans la base de données, sur le réseau ou ailleurs. Elle doit tout de même exécuter la fonction. CacheItemInterface définit la méthode expiresAfter pour déclarer combien de temps l’élément doit être conservé dans le cache. L’application peut invoquer cette méthode sans se soucier de l’objet mis en cache; elle ne se soucie que de la durée pendant laquelle il doit être mis en cache.

Coder contre les interfaces dans WordPress

Comme nous abstrayons le code de WordPress, le résultat sera que l’application ne fera pas directement référence au code de WordPress, mais toujours via une interface. Par exemple, la fonction WordPress get_posts a cette signature :

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

Au lieu d’invoquer cette méthode directement, nous pouvons y accéder via le contrat Owner\MyApp\\Contracts\PostsAPIInterface:

namespace Owner\MyApp\Contracts;

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

Note que la fonction WordPress get_posts peut renvoyer des objets de la classe WP_Post, qui est spécifique à WordPress. Lorsque nous abstrayons le code, nous devons supprimer ce type de dépendance fixe. La méthode get_posts du contrat renvoie des objets du type PostInterface, ce qui vous permet de faire référence à la classe WP_Post sans être explicite à ce sujet. La classe PostInterface devra fournir un accès à toutes les méthodes et tous les attributs de 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;
  // ...
}

L’exécution de cette stratégie peut changer notre compréhension de la place de WordPress dans notre pile. Au lieu de considérer WordPress comme l’application elle-même (sur laquelle nous installons des thèmes et des extensions), nous pouvons le considérer simplement comme une autre dépendance au sein de l’application, remplaçable comme tout autre composant. (Même si nous ne remplacerons pas WordPress dans la pratique, il est remplaçable d’un point de vue conceptuel)

Créer et distribuer des paquets

Composer est un gestionnaire de paquets pour PHP. Il permet aux applications PHP de récupérer des paquets (c’est-à-dire des morceaux de code) dans un dépôt et de les installer en tant que dépendances. Pour découpler l’application de WordPress, nous devons distribuer son code dans des paquets de deux types différents : ceux qui contiennent le code WordPress et les autres qui contiennent la logique commerciale (c’est-à-dire aucun code WordPress).

Enfin, nous ajoutons tous les paquets en tant que dépendances dans l’application, et nous les installons via Composer. Comme l’outillage sera appliqué aux paquets de code commercial, ceux-ci doivent contenir la majeure partie du code de l’application; plus le pourcentage est élevé, mieux c’est. Leur faire gérer environ 90 % du code global est un bon objectif.

Extraire le code de WordPress dans les paquets

En suivant l’exemple précédent, les contrats PostAPIInterface et PostInterface seront ajoutés au paquet contenant le code commercial, et un autre paquet comprendra l’implémentation WordPress de ces contrats. Pour satisfaire PostInterface, nous créons une classe PostWrapper qui récupérera tous les attributs d’un objet WP_Post:

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;
  }

  // ...
}

Lors de l’implémentation de PostAPI, puisque la méthode get_posts renvoie PostInterface[], nous devons convertir les objets de WP_Post en PostWrapper:

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
    );
  }
}

Utiliser l’injection de dépendances

L’injection de dépendances est un modèle de conception qui vous permet de coller ensemble toutes les parties de l’application d’une manière peu contraignante. Avec l’injection de dépendances, l’application accède aux services par le biais de leurs contrats, et les mises en œuvre des contrats sont « injectées » dans l’application par le biais de la configuration.

En modifiant simplement la configuration, on peut facilement passer d’un fournisseur de contrat à un autre. Il existe plusieurs bibliothèques d’injection de dépendances parmi lesquelles nous pouvons choisir. Nous vous conseillons d’en choisir une qui adhère aux recommandations standard de PHP (souvent appelées « PSR »), afin que nous puissions facilement remplacer la bibliothèque par une autre si le besoin s’en fait sentir. Concernant l’injection de dépendances, la bibliothèque doit satisfaire la PSR-11, qui fournit la spécification d’une « interface de conteneur » Les bibliothèques suivantes, entre autres, sont conformes à PSR-11 :

Accéder aux services via le conteneur de services

La bibliothèque d’injection de dépendances mettra à disposition un « conteneur de services », qui résout un contrat en sa classe d’implémentation correspondante. L’application doit s’appuyer sur le conteneur de services pour accéder à toutes les fonctionnalités. Par exemple, alors que nous invoquerions généralement directement les fonctions de WordPress :

$posts = get_posts();

…avec le conteneur de services, nous devons d’abord obtenir le service qui satisfait PostAPIInterface et exécuter la fonctionnalité par son intermédiaire :

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();

Utiliser le composant DependencyInjection de Symfony

Le composant DependencyInjection de Symfony est actuellement la bibliothèque d’injection de dépendances la plus populaire. Il vous permet de configurer le conteneur de services via un code PHP, YAML ou XML. Par exemple, pour définir que le contrat PostAPIInterface est satisfait via la classe PostAPI, il faut le configurer dans YAML comme ceci :

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

DependencyInjection de Symfony permet également aux instances d’un service d’être automatiquement injectées (ou « autowired ») dans tout autre service qui en dépend. En outre, il est facile de définir qu’une classe est une implémentation de son propre service. Par exemple, considérez la configuration YAML suivante:

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/*'

Cette configuration définit ce qui suit :

  • Le contrat UserAuthorizationSchemeRegistryInterface est satisfait via la classe UserAuthorizationSchemeRegistry
  • Le contrat UserAuthorizationInterface est satisfait via la classe UserAuthorization
  • Toutes les classes du répertoire UserAuthorizationSchemes/ sont une implémentation d’elles-mêmes
  • Les services doivent être automatiquement injectés les uns dans les autres(autowire : true)

Voyons comment fonctionne l’injection automatique. La classe UserAuthorization dépend du service avec le contrat UserAuthorizationSchemeRegistryInterface:

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

  // ...
}

Grâce à autowire : true, le composant DependencyInjection fera automatiquement en sorte que le service UserAuthorization reçoive sa dépendance nécessaire, qui est une instance de UserAuthorizationSchemeRegistry.

Quand faire l’abstraction

L’abstraction de code peut consommer beaucoup de temps et d’efforts, c’est pourquoi nous ne devrions l’entreprendre que lorsque ses avantages l’emportent sur ses coûts. Voici des suggestions pour savoir quand l’abstraction du code peut en valoir la peine. Vous pouvez le faire en utilisant les extraits de code de cet article ou les extensions WordPress d’abstraction suggérées ci-dessous.

Accéder à l’outillage

Comme mentionné précédemment, il est difficile d’exécuter PHP-Scoper sur WordPress. En découplant le code de WordPress en paquets distincts, il devient possible d’évaluer directement une extension WordPress.

Réduire le temps et le coût de l’outillage

L’exécution d’une suite de tests PHPUnit prend plus de temps lorsqu’elle doit initialiser et exécuter WordPress que lorsqu’elle ne le fait pas. Moins de temps peut aussi se traduire par moins d’argent dépensé pour exécuter les tests – par exemple, GitHub Actions facture les exécuteurs hébergés par GitHub en fonction du temps passé à les utiliser.

Un remaniement lourd n’est pas nécessaire

Un projet existant peut nécessiter un remaniement lourd pour introduire l’architecture requise (injection de dépendances, division du code en paquets, etc.), ce qui le rend difficile à retirer. Abstraire le code lors de la création d’un projet à partir de zéro le rend beaucoup plus facile à gérer.

Produire du code pour plusieurs plateformes

En extrayant 90 % du code dans un paquet agnostique aux CMS, nous pouvons produire une version de la bibliothèque qui fonctionne pour un CMS ou un framework différent en ne remplaçant que 10 % de la base de code globale.

Migrer vers une plateforme différente

Si nous devons migrer un projet de Drupal à WordPress, de WordPress à Laravel, ou toute autre combinaison, seuls 10% du code doivent être réécrits – une économie considérable.

Meilleures pratiques

Tout en concevant les contrats pour abstraire notre code, il existe plusieurs améliorations que nous pouvons appliquer à la base de code.

Adhèrer au PSR-12

Lorsque nous définissons l’interface pour accéder aux méthodes de WordPress, nous devons adhérer à PSR-12. Cette spécification récente vise à réduire les frictions cognitives lors de l’analyse du code de différents auteurs. Adhérer à PSR-12 implique de renommer les fonctions de WordPress.

WordPress nomme les fonctions en utilisant snake_case, alors que PSR-12 utilise camelCase. Ainsi, la fonction get_posts deviendra getPosts:

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

…et :

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
    // ...
  }
}

Diviser les méthodes

Les méthodes de l’interface n’ont pas besoin d’être une réplique de celles de WordPress. Nous pouvons les transformer lorsque cela a du sens. Par exemple, la fonction WordPress get_user_by($field, $value) sait comment récupérer l’utilisateur dans la base de données via le paramètre $field, qui accepte les valeurs « id », « ID », « slug », « email » ou « login ». Cette conception présente quelques problèmes :

  • Elle n’échouera pas au moment de la compilation si nous passons une mauvaise chaîne de caractères
  • Le paramètre $value doit accepter tous les types différents pour toutes les options, même si en passant « ID » il s’attend à un int, en passant « email » il ne peut recevoir qu’une string

Nous pouvons améliorer cette situation en divisant la fonction en plusieurs :

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;
}

Le contrat est résolu comme ceci pour WordPress (en supposant que nous avons créé UserWrapper et UserInterface, comme expliqué précédemment) :

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;
  }
}

Supprimer les détails d’implémentation de la signature de la fonction

Les fonctions de WordPress peuvent fournir des informations sur la façon dont elles sont implémentées dans leur propre signature. Ces informations peuvent être supprimées lors de l’évaluation de la fonction d’un point de vue abstrait. Par exemple, l’obtention du nom de l’utilisateur dans WordPress se fait en appelant get_the_author_meta, en rendant explicite le fait que le nom d’un utilisateur est stocké comme une valeur « méta » (sur la table wp_usermeta) :

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

Vous n’avez pas besoin de transmettre ces informations au contrat. Les interfaces ne s’intéressent qu’au quoi, pas au comment. Par conséquent, le contrat peut plutôt avoir une méthode getUserLastname, qui ne fournit aucune information sur la façon dont elle est implémentée :

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

Ajoutez des types plus stricts

Certaines fonctions WordPress peuvent recevoir des paramètres de différentes manières, ce qui entraîne une ambiguïté. Par exemple, la fonction add_query_arg peut soit recevoir une seule clé et une seule valeur :

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

… ou un tableau de clé => valeur:

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

Notre interface peut définir une intention plus compréhensible en divisant ces fonctions en plusieurs fonctions distinctes, chacune d’entre elles acceptant une combinaison unique d’entrées :

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

Effacer la dette technique

La fonction WordPress get_posts renvoie non seulement les « articles » mais aussi les « pages » ou toute entité de type « « publication personnalisée », et ces entités ne sont pas interchangeables. Les articles et les pages sont tous deux des publications personnalisées, mais une page n’est ni un article ni une page. Par conséquent, l’exécution de get_posts peut renvoyer des pages. Ce comportement est une divergence conceptuelle.

Pour le rendre correct, get_posts devrait plutôt s’appeler get_customposts, mais il n’a jamais été renommé dans le cœur de WordPress. C’est un problème courant avec la plupart des logiciels à longue durée de vie et c’est ce qu’on appelle la w dette technique » – un code qui a des problèmes, mais qui n’est jamais corrigé parce qu’il introduit des changements cassants.

Cependant, lorsque nous créons nos contrats, nous avons la possibilité d’éviter ce type de dette technique. Dans ce cas, nous pouvons créer une nouvelle interface ModelAPIInterface qui peut traiter des entités de différents types, et nous créons plusieurs méthodes, chacune pour traiter un type différent :

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

De cette façon, la divergence ne se produira plus, et vous verrez ces résultats :

  • getPosts renvoie uniquement les articles
  • getPages renvoie uniquement les pages
  • getCustomPosts renvoie les articles et les pages

Avantages de l’abstraction du code

Les principaux avantages de l’abstraction du code d’une application sont les suivants :

  • L’outillage fonctionnant sur des paquets contenant uniquement du code commercial est plus facile à configurer et prendra moins de temps (et moins d’argent) pour fonctionner.
  • Nous pouvons utiliser des outils qui ne fonctionnent pas avec WordPress, comme le scoping d’une extension avec PHP-Scoper.
  • Les paquets que nous produisons peuvent être autonomes pour être utilisés facilement dans d’autres applications.
  • La migration d’une application vers d’autres plateformes devient plus facile.
  • Nous pouvons changer notre état d’esprit en passant de la pensée WordPress à la pensée en termes de logique commerciale.
  • Les contrats décrivent l’intention de l’application, ce qui la rend plus compréhensible.
  • L’application s’organise par paquets, créant une application allégée contenant le strict minimum et l’améliorant progressivement selon les besoins.
  • Nous pouvons éliminer la dette technique.

Problèmes liés à l’abstraction du code

Les inconvénients de l’abstraction du code d’une application sont les suivants :

  • Cela implique une quantité considérable de travail au départ.
  • Le code devient plus verbeux; ajoute des couches de code supplémentaires pour obtenir le même résultat.
  • Vous pouvez peux finir par produire des dizaines de paquets qui doivent ensuite être gérés et maintenus.
  • Vous aurez peut-être besoin d’un mono-repo pour gérer tous les paquets ensemble.
  • L’injection de dépendances pourrait être excessive pour les applications simples (rendements décroissants).
  • L’abstraction du code ne sera jamais complètement accomplie car il y a généralement une préférence générale implicite dans l’architecture du CMS.

Options d’extensions WordPress d’abstraction

Bien qu’il soit généralement plus sage d’extraire votre code dans un environnement local avant de travailler dessus, certaines extensions WordPress peuvent vous aider à atteindre vos objectifs d’abstraction. Voici nos meilleurs choix.

1. WPide

Produit par WebFactory Ltd, la célèbre extension WPide étend considérablement les fonctionnalités de l’éditeur de code par défaut de WordPress. Il sert d’extension WordPress d’abstraction en vous permettant de voir votre code in situ pour mieux visualiser ce qui nécessite de l’attention.

WPide abstract wordpress plugin
L’extension WPide.

WPide dispose également d’une fonction de recherche et de remplacement pour localiser rapidement un code périmé ou expiré et le remplacer par un rendu remanié.

En plus de cela, WPide propose des tas de fonctionnalités supplémentaires, notamment :

  • Mise en évidence de la syntaxe et des blocs
  • Sauvegardes automatiques
  • Création de fichiers et de répertoires
  • Navigateur d’arborescence de fichiers complet
  • Accès à l’API du système de fichiers de WordPress

2. Ultimate DB Manager

L’extension Ultimate WP DB Manager de WPHobby vous donne un moyen rapide de télécharger vos bases de données dans leur intégralité pour les extraire et les remanier.

Screenshot of the Ultimate DB Manager plugin's logo with the words:
L’extension Ultimate DB Manager.

Bien sûr, les extensions de ce type ne sont pas nécessaires pour les utilisateurs de Kinsta, car Kinsta offre à tous ses clients un accès direct aux bases de données. Cependant, si vous n’avez pas un accès suffisant aux bases de données par le biais de votre hébergeur, Ultimate DB Manager pourrait être utile en tant qu’extension WordPress d’abstraction.

3. Votre propre extension WordPress d’abstraction personnalisée

En fin de compte, le meilleur choix pour l’abstraction sera toujours de créer votre extension. Cela peut sembler être une grande entreprise, mais si vous avez une capacité limitée à gérer directement tes fichiers du cœur de WordPress, cela offre une solution de contournement favorable à l’abstraction.

Faire cela présente des avantages évidents :

  • Abrégez vos fonctions à partir des fichiers de votre thème
  • Préservez votre code malgré les changements de thème et les mises à jour de la base de données

Vous pouvez apprendre à créer votre extension WordPress d’abstraction grâce au Manuel WordPress du développeur d’extensions.

Résumé

Devrions-nous abstraire le code de nos applications ? Comme pour tout, il n’y a pas de « bonne réponse » prédéfinie, car cela dépend de chaque projet. Les projets nécessitant une quantité énorme de temps pour être analysés avec PHPUnit ou PHPStan peuvent en bénéficier le plus, mais l’effort nécessaire pour y parvenir n’en vaut pas toujours la peine.

Vous avez appris tout ce que vous devez savoir pour commencer à abstraire le code de WordPress.

Vous prévoyez de mettre en œuvre cette stratégie dans votre projet ? Si oui, utiliserez-vous une extension WordPress d’abstraction ? Faites-nous signe dans la section des commentaires !

Leonardo Losoviz

Leo écrit sur les tendances innovantes en matière de développement web, principalement en ce qui concerne PHP, WordPress et GraphQL. Vous pouvez le trouver sur leoloso.com et X.com/losoviz.