Ajouter des URL dans WordPress – url rewriting

Il peut être nécessaire d’avoir des routes dont le contenu n’est pas directement connu du CMS. C’est le cas notamment si on se connecte à une API pour aller chercher des contenus à l’extérieur de WordPress. Cours de la bourse, offres d’emplois, liens de téléchargements contrôlés … Pour cela, on utilise l’URL rewriting.

Pour chaque requête reçue, WordPress va chercher à quel contenu correspond l’URL demandée, puis génère la réponse. Et s’il n’y ne trouve pas de contenu associé, il envoie une page 404.

Heureusement, il est facile de référencer des URL supplémentaires dans le CMS. La méthode n’est vraiment pas compliqué, mais comme je ne m’en souviens jamais, voici un petit mémo.

Ajouter une règle de réécriture pour que WordPress reconnaisse l’URL

add_rewrite_rule( string $regex, string|array $query, string $after = 'bottom' );

La documentation est ici : add_rewrite_rule

Ce code est à ajouter via le hook init.

Ajouter ses propres variables de requêtes (query_vars)

C’est ce que j’oublie tout le temps 😀

add_filter( 'query_vars', 'custom_query_vars' );
function custom_query_vars( $query_vars ) {
    $query_vars[] = 'customVar';
    return $query_vars;
}

Ce code est aussi à ajouter via le hook init.

Si cette partie est oubliée, vous ne retrouverez jamais vos variables dans les query vars.

Réagir à la route

A partir de là, WordPress connait votre URL, mais ne sais pas quoi en faire. Vous avez maintenant quantité de solutions pour « accrocher » l’exécution de votre code à cette route. Voici les deux qui me semblent les plus propres :

Générer la page grâce à un template

La meilleure méthode est probablement de renvoyer un template particulier pour notre route. Il suffit d’indiquer le fichier php correspondant avec le hook template_include.

add_action( 'template_include', function( $template ) {
    if ( get_query_var( 'customVar' ) == false || get_query_var( 'customVar' ) == '' ) {
        return $template;
    }
 
    return get_template_directory() . '/template-name.php';
} );

La bonne nouvelle est qu’aucun contenu n’a encore été envoyé lorsque le template est appelé, ce qui nous permet de modifier / ajouter les headers de la réponses. Très pratique si la route doit retourner un document qui n’est pas HTML (JSON, PDF, image, … )

Agir lors du choix du template

J’ai vu cette solution dans un plugin officiel. La première semble toutefois préférable si j’en crois la documentation du hook à utiliser : template_redirect.

add_action( 'template_redirect', 'custom_function', -100 );

A vous de choisir 😉

Err 500 CircularReferenceException : Petite astuce pour régler les problèmes de référence circulaire dans la sérialisation Symfony

Ce n’est pas documenté, mais il est possible de définir une classe pour intercepter les alertes de références circulaires lors des sérialisations. Je le partage ici pour ne pas l’oublier.

Source : https://stackoverflow.com/questions/54645363/use-the-circular-reference-handler-key-of-the-context-instead-symfony-4-2

CircularReferenceException. A circular reference has been detectedwhen serializing the object
CircularReferenceException

Configuration à ajouter :

# config/packages/serializer.yaml

framework:
    serializer:
        circular_reference_handler: 'App\Serializer\MyCircularReferenceHandler'

Classe php de gestion de l’exception :

<?php

namespace App\Serializer;


class MyCircularReferenceHandler {
    public function __invoke($object) {
        return [ 'id' => $object->getId() ];
    }
}

Testé et approuvé sur Symfony 5.4.9 😉

Créer vos tâches cron dans WordPress

Lancer les tâches cron de WordPress

Dans cet article nous allons voir comment lancer ses propres taches via le cron de WordPress. Mais avant, il peut être utile d’affiner la manière dont le CMS les lance.

Il existe de nombreux articles sur le net au sujet du cron de WordPress. Ils expliquent tous très bien comment paramétrer correctement WordPress. Le sujet de l’article étant la création de tâches cron et non leurs lancements, vous aurez ici simplement la version courte. Si vous souhaitez approfondir le sujet, je vous conseille notamment Le Cron WordPress : Problèmes Et Solutions de @100son_net qui est très bien.

Par défaut, WordPress cherche à chaque visite si une tâche de cron doit être réalisée. Si c’est le cas il la lance. Nous allons stopper ça pour ne lancer les tâches seulement sur appel spécifique du cron.

Pourquoi désactiver le système automatique ?

L’intérêt est multiple :

  • Dans le cas où vous avez peu (très peu) de visites, les tâches sont tout de même exécutées à temps.
  • Si au contraire vous avez un grand nombre de visiteur, la charge du serveur se trouve allégée puisqu’il ne se concentre plus que sur les requêtes utilisateurs.
  • Dans certains cas, cela permet d’éviter les erreurs de publication des articles planifiés (panification manquée)

Le paramétrage WordPress

Pour une install WP classique

define('DISABLE_WP_CRON', true);

A placer dans le wp-config.php, cette commande désactive l’exécution automatique du cron de WordPress.

Pour une install Bedrock

DISABLE_WP_CRON=true

A mettre dans le .env. La surcouche Bedrock créera la constante DISABLE_WP_CRON pour WordPress.

Le paramétrage serveur

On peut appeler le cron WP de deux façons : via une requête http(s) ou en exécutant un script php. Dans les 2 cas il suffit d’ajouter une tâche cron sur le serveur.

Via une requête http(s)

*/10 * * * * user wget https://example.org/wp-cron.php > /dev/null

Vous pouvez bien sûr utiliser curl à la place de wget.

Via l’exécution php

*/10 * * * * user php /path/to/wp-cron.php > /dev/null

L’exécution des scripts en utilisant directement php sans passer par apache ou Nginx permet souvent de s’affranchir des timeouts et autres restrictions exécutions pour les tâches un peu exigeantes.

Les deux exemples lancent les tâches toutes les 10 minutes avec l’utilisateur système user. Il peut être intéressant de lancer les tâches avec l’utilisateur apache ou Nginx (www-data) pour éviter les conflit de droits d’écriture de fichiers.

Ajouter une tâche au cron de WordPress

L’ajout d’une tâche passe par 2 étapes. La première est la mise en place d’un hook exécuté pérodiquement. La seconde et l’ancrage de notre script sur ce hook.

Création du hook

L’enregistrement de notre hook doit être fait une seule fois. Le CMS s’en souviendra. Le plus simple est d’utiliser les hooks d’activation et de désactivation des plugins.

register_activation_hook(__FILE__, 'registerMyPlugin');

ou, si vous utilisez une classe :

register_activation_hook(__FILE__, array($this, 'register'));

On va ensuite utiliser les fonctions wp_next_scheduled et wp_schedule_event respectivement pour vérifier la présence de notre cron et son activation.

if (!wp_next_scheduled('my_cron_task')) {
            wp_schedule_event( time(), 'hourly', 'my_cron_task' );
        }

Il y a 3 fréquences de lancement proposées par défaut dans le CMS :

  • hourly
  • daily
  • twicedaily

Mais vous pouvez paramétrer les vôtres pour plus de finesse.

Enfin, Il faut penser à supprimer cette exécution quand elle n’est plus désirée. Ici à la désactivation du plugin :

register_deactivation_hook(__FILE__, 'unregisterMyPlugin'));

function unregisterMyPlugin() {
        // de-register cron task
        if (wp_next_scheduled('mp_cron_import')) {
            $timeStamp = wp_next_scheduled('my_cron_task');
            wp_unschedule_event( $timeStamp, 'my_cron_task');
        }
    }

Utilisation du hook

Là c’est super simple (si vous connaissez les hook WordPress). Il suffit « d’accrocher » votre fonction au hook.

add_action('my_cron_task', 'myCronTask' );

ou, si vous utilisez une classe :

add_action('my_cron_task', array($this, 'myCronTask'));

Plugin de démo

Et donc, ce que vous attendez depuis le début de l’article : le code complet de création de tâches WordPress.

Il est présenté sous forme de plugin pour avoir un script fonctionnel. Il suffit de copier ou de forker tout ça et c’est parti. Bon dev !

Une petite mise en garde qui peut vous faire gagner du temps

Attention à WP_Query !

Attention, pour une raison que je n’explique pas, WP_Query fonctionne bizarrement. En particulier, les requêtes utilisant les posts metas renvoient des valeurs incohérentes. Mais seulement lors de l’appel via la tache cron. La solution pour régler le problème : Utilisez get_posts() à la place.

Bon dev !

Utiliser Doctrine dans le framework php Slim 4

La documentation de Slim est globalement bien faite, mais pour la version 4 il manque encore des parties. Comme je suis novice sur ces technologies j’ai mis un moment avant de comprendre comment transposer l’intégration de Doctrine ORM de Slim 3 (bien documenté) – vers Slim 4 (peu documenté).

Cet article est donc surtout un mémo pour le moi du futur. Cependant, s’il peut vous être utilise mais que mes explications ne sont pas assez claires, les commentaires sont ouverts.

Ajouter Doctrine à l’application

composer require doctrine/orm:^2

Paramétrer l’accès à la base de donnée

Le paramétrage de connexion à la base s’ajoute dans les settings de l’app.

En dur dans code

Ce n’est pas un bonne idée mais c’est simple à comprendre.

<?php

// settings.php

define('APP_ROOT', __DIR__);

return [
    'settings' => [
        'displayErrorDetails' => true,
        'determineRouteBeforeAppMiddleware' => false,

        'doctrine' => [
            // if true, metadata caching is forcefully disabled
            'dev_mode' => true,

            // path where the compiled metadata info will be cached
            // make sure the path exists and it is writable
            'cache_dir' => APP_ROOT . '/var/doctrine',

            // you should add any other path containing annotated entity classes
            'metadata_dirs' => [APP_ROOT . '/src/Domain'],

            'connection' => [
                'driver' => 'pdo_mysql',
                'host' => 'localhost',
                'port' => 3306,
                'dbname' => 'mydb',
                'user' => 'user',
                'password' => 'secret',
                'charset' => 'utf8'
            ]
        ]
    ]
];

En utilisant un .env

Pour ne pas avoir dans le code d’informations liés à l’environnement d’execution, une technique classique est d’utiliser un ficher .env qui n’est pas versionné. Il contiendra les données de paramètrage propre à l’environnement d’execution. Dans notre cas, les accès à la base de données.

Pour me simplifier la vie, j’utilise la librairie Dotenv de Vance Luacs.

composer require vlucas/phpdotenv

Si vous ne connaissez pas, reportez vous à la documentation, la librairie est très simple à utiliser.

Définir le service EntityManager

Dans la documentation de Slim V3, la definition de l’EntityManager est faite dans un ficher bootstrap.php placé à la racine du projet. Pour Slim V4, je préfère définir le service directement dans le containerBuilder. Je place le code dans /app/dependencies.php

<?php
declare(strict_types=1);

use DI\ContainerBuilder;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor\UidProcessor;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Cache\FilesystemCache;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Tools\Setup;

return function (ContainerBuilder $containerBuilder) {
    $containerBuilder->addDefinitions([
        LoggerInterface::class => function (ContainerInterface $c) {
            $settings = $c->get('settings');

            $loggerSettings = $settings['logger'];
            $logger = new Logger($loggerSettings['name']);

            $processor = new UidProcessor();
            $logger->pushProcessor($processor);

            $handler = new StreamHandler($loggerSettings['path'], $loggerSettings['level']);
            $logger->pushHandler($handler);

            return $logger;
        },

        EntityManager::class => function (ContainerInterface $container): EntityManager {
            $settings = $container->get('settings');
            
            $config = Setup::createAnnotationMetadataConfiguration(
                $settings['doctrine']['metadata_dirs'],
                $settings['doctrine']['dev_mode']
            );
        
            $config->setMetadataDriverImpl(
                new AnnotationDriver(
                    new AnnotationReader,
                    $settings['doctrine']['metadata_dirs']
                )
            );
        
            $config->setMetadataCacheImpl(
                new FilesystemCache(
                    $settings['doctrine']['cache_dir']
                )
            );
        
            return EntityManager::create(
                $settings['doctrine']['connection'],
                $config
            );
        }
    ]);
};

La console Doctrine

Il suffit de crér un ficher cli-config.php à la racine de l’app avec le code suivant :

<?php

// cli-config.php

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner;

use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\Tools\Console\Command\SchemaTool;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand;
use DI\ContainerBuilder;

require __DIR__ . '/vendor/autoload.php';

// Instantiate PHP-DI ContainerBuilder
$containerBuilder = new ContainerBuilder();

if (false) { // Should be set to true in production
	$containerBuilder->enableCompilation(__DIR__ . '/var/cache');
}

// Set up settings
$settings = require __DIR__ . '/app/settings.php';
$settings($containerBuilder);

// Set up dependencies
$dependencies = require __DIR__ . '/app/dependencies.php';
$dependencies($containerBuilder);

// Build PHP-DI Container instance
$container = $containerBuilder->build();

return ConsoleRunner::createHelperSet($container->get(EntityManager::class));

Le moment crucial !

A partir de là, Doctrine est oppérationnel. Vous pouvez vérifier que tout va bien en utilisant la commande :

php vendor/bin/doctrine

Si vous avez la liste des commandes Docrine, tout va bien. Si vous avez une erreur, commencez par vérifier les déclaration des namespaces. Il en manque peut-être.

Vos entities

Avec la configuration de cet exemple, les classes Entity que vous aller écrire sont à placer dans /src/Domain. Ou dans un sous dossier.

Voici un exemple de classe pour démarrer rapidement dans oublier de namespace :

<?php

namespace App\Domain\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Option
 * @ORM\Entity @ORM\Table(name="options")
 */
class Option
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(name="key", type="string")
     */
    protected $key;

    /**
     * @ORM\Column(name="value", type="string")
     */
    protected $value;

    public function getKey() {
        return $this->key;
    }
    public function setKey( $key ) {
        $this->key = $key;
    }

    public function getValue() {
        return $this->value;
    }
    public function setValue( $value ) {
        $this->value = $value;
    }
}

Utiliser le EntityManager dans votre code

Créer une Entity

Les classes des repository sont à placer dans le dossier spécifié dans la configuration. Dans cet exemple, /src/Domain

namespace App\Domain\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Exemple
 * @ORM\Entity @ORM\Table(name="exemple")
 */
class Exemple
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     * @ORM\Column(name="name", type="string")
     */
    protected $name;
    

    public function getName() {
        return $this->name;
    }
    public function SetName( $value ) {
        $this->name = $value;
    }

}

Utiliser le EntityManager

L’objet EntityManager de Doctrine peut être passé directement au constructeur la classe Slim. Pour celà il suffit de définir un paramètre correctement typé.

use Doctrine\ORM\EntityManager;

public function __construct(ContainerInterface $container, LoggerInterface $logger, EntityManager $em = null)
{
        $this->logger = $logger;
        $this->container = $container;
        $this->em = $em;
}

Dans cette exemple le constructeur reçoit le container Slim, le logger et l’EntityManager.