Dans ma boĂźte, CodeBuds, nous utilisons la plateforme de chat Mattermost pour communiquer. C'est un systĂšme open source trĂšs sympathique que nous pouvons mettre en place sur nos serveurs. Ceci permet de ne pas avoir de limite de messages comme sur un compte Slack gratuit par exemple.

Nous nous sommes dit qu'il serait sympa d'avoir la possibilité de faire communiquer nos projets Symfony facilement sur la plateforme. C'est pour cela que nous avons créé un bundle Symfony qui permet de faire tout ça.

Ici nous allons voir en détails comment ce bundle a été créé et pourquoi certains choix ont étés faits.

Exigences

Ce bundle nécessite aussi :

  • php: ^7.4
  • symfony/http-client: 5.0.*

Nous avons choisis d'ĂȘtre compatible avec seulement la derniĂšre version de PHP, 7.4, pour utiliser ses nouvelles fonctionnalitĂ©s.

Nous aurions pu ne pas utiliser le symfony/http-client mais comme c'est de toute façon un bundle Symfony ceci ne rajoutera pas de surcharge. Il nous permettra aussi de faire des requĂȘtes http de façon plus simple et plus sĂ©curisĂ©e.

Comme nous utilisons la version 5.0.* du paquet Symfony ce bundle ne sera compatible que avec la derniĂšre version de symfony.

Comment l'utiliser

Pour que le bundle fonctionne, vous devez le télécharger avec la commande composer suivante: composer require codebuds / mattermost-publication-bundle

Si vous utilisez Symfony flex, cela ajoutera la ligne suivante dans votre fichier .env:

###> codebuds/mattermost-publication###
MATTERMOST_WEBHOOK_URL="http://{your-mattermost-site}/hooks/xxx-generatedkey-xxx"
###< codebuds/mattermost-publication###

Et créez un fichier config/packages/mattermost_publication.yaml :

mattermost_publication:
    webhook_url: '%env(MATTERMOST_WEBHOOK_URL)%'
    # You can also set the default channel, username and icon_url
    # for more information you can checkout the bundle documentation
    # https://github.com/codebuds33/mattermost-publication-bundle/blob/master/README.md

Si vous n'utilisez pas les recettes flex, vous devrez l'ajouter à la main. Si vous voulez vérifier rapidement comment l'utiliser, vous pouvez le trouver dans le [fichier README] (https://github.com/codebuds33/mattermost-publication-bundle/blob/master/README.md). Si vous voulez en savoir plus continuez à lire et nous nous rapprocherons de la source ;)

Code

Le code du bundle est publique sur sa page github

Configuration

Étudions le code. Pour commencer les bundles Symfony ont besoin d'un fichier de configuration services.xml dans le rĂ©pertoire Resources/config.

<?xml version="1.0" encoding="UTF-8" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="codebuds_mattermost_publication.mattermost_publication"
                 class="CodeBuds\MattermostPublicationBundle\MattermostPublication" public="true"/>
        <service id="CodeBuds\MattermostPublicationBundle\MattermostPublication"
                 alias="codebuds_mattermost_publication.mattermost_publication" public="true"/>
    </services>
</container>

Cela peux sembler étrange au début mais en regardant de prÚs nous pouvons nous apercevoir que les lignes <services> sont inverser l'un par rapport à l'autre. Ceci permettra à Symfony de autowire ce bundle quand il sera utilisé.

Nous allons avoir besoin des certaines variables configurables avec des valeurs par défaut. Pour ce faire il faut créer deux fichiers PHP dans la répertoire DependencyInjection.

# DependencyInjection/Configuration.php
<?php

namespace CodeBuds\MattermostPublicationBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder("mattermost_publication");
        $rootNode = $treeBuilder->getRootNode();

        $rootNode
            ->children()
                ->scalarNode("webhook_url")->defaultValue("http://{your-mattermost-site}/hooks/xxx-generatedkey-xxx")->end()
                ->scalarNode("username")->defaultNull()->end()
                ->scalarNode("icon_url")->defaultNull()->end()
                ->scalarNode("channel")->defaultNull()->end()
            ->end();

        return $treeBuilder;
    }
}

Afin de pouvoir publier sur Mattermost il faudra créer un webhook entrant dans celui-ci.

Sans cela il sera impossible d'envoyer un message. Il y a également trois variables optionnelles et permettront de modifier le bot qui publiera (username et icon_url) ou de publier dans un channel différent à l'intérieur de l'équipe Mattermost pour lequel le webhook entrant à été créé.

La chose suivante est de s'assurer que ces variables puissent ĂȘtre utiliser Ă  l'intĂ©rieur du bundle. Il est nĂ©cessaire de crĂ©er un DependencyInjection\Extension.

<?php

namespace CodeBuds\MattermostPublicationBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

class MattermostPublicationExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
        try {
            $loader->load('services.xml');
        } catch (\Exception $exception) {
            var_dump($exception);
        }

        $configuration = $this->getConfiguration($configs, $container);

        $config = $this->processConfiguration($configuration, $configs);

        $definition = $container->getDefinition("codebuds_mattermost_publication.mattermost_publication");
        $definition->setArgument(0, $config['webhook_url']);
        $definition->setArgument(1, $config['username']);
        $definition->setArgument(2, $config['icon_url']);
        $definition->setArgument(3, $config['channel']);
    }
}

Il va regarder le fichier XML de configuration et rajouter une "definition". Pour ce faire il cherchera la "definition" de codebuds_mattermost_publication.mattermost_publication, qui est le nom de notre classCodeBuds\MattermostPublicationBundle\MattermostPublication et y injecter les variables.

Cela semble ĂȘtre beaucoup d'Ă©tapes alors que rien qui ne pourra faire du boulot a Ă©tĂ© crĂ©Ă© pour l'instant. Nous verrons dans quelques secondes l'intĂ©rĂȘt de tout cela. Et maintenant nous pouvons regarder Ă  quoi ressemble un Message !

Message

L'idée de ce Bundle est d'envoyer un message, c'est donc pour celai qu'il y a une classe Message dans le répertoire Model.

# Model/message.php
<?php

namespace CodeBuds\MattermostPublicationBundle\Model;

class Message
{
    private ?string $webhookUrl;

    private ?string $text;

    private ?string $username;

    private ?string $iconUrl;

    private ?string $channel;

    public function __construct()
    {
        $this->webhookUrl = null;
        $this->text = null;
        $this->username = null;
        $this->iconUrl = null;
        $this->channel = null;
    }

    # all the getters and setters ...

    public function toArray()
    {
        $array = [
            'webhook_url' => $this->getText(),
            'text' => $this->getText(),
            'username' => $this->getUsername(),
            'icon_url' => $this->getIconUrl(),
            'channel' => $this->getChannel()
        ];

        return array_filter($array, fn($var) => $var !== null);
    }
}

Celui-ci prend les quatre variables dont nous venons de parler afin de mettre en place l'adresse ou nous allons envoyer le message. Cela nous permettra également de rajouter des décorations sur son enveloppe. Il y a une autre variable, text, qui bien sur est le contenu de ce que nous allons envoyer.

Il y a Ă©galement une fonction toArray qui va permettre de passer l'objet en tableau. Cette fonction met en place un filtre qui permet d’enlever toutes les variables qui n'auront pas de valeur. Voici donc notre message, voyons comment nous allons pouvoir l'envoyer !

Action

Pour ce fichier MattermostPublication.php qui est le Controller de notre bundle commençons par regarder son constructor et voir comment il prends en compte les variables.

private ?string $webhookUrl;

private ?string $username;

private ?string $iconUrl;

private ?string $channel;

public function __construct(?string $webhookUrl, ?string $username, ?string $iconUrl, ?string $channel)
{
    $this->webhookUrl = $webhookUrl;
    $this->username = $username;
    $this->iconUrl = $iconUrl;
    $this->channel = $channel;
}

Ici nous avons dit que quand la class est instanciĂ©e cela prendra 4 variables qui peuvent ĂȘtre un string ou null. Cela peut sembler bizarre pour la variable $webhookUrl vu que sans adresse (URL webhook) aucun message ne peut ĂȘtre envoyĂ©. Nous allons regarder ça de plus prĂšs par la suite. Maintenant que le facteur est lĂ  observons comment il fait son travail.

/**
* @param Message|string $message
* @return void
* @throws TransportExceptionInterface
*/
public function publish($message)
{
    $message instanceof Message
        ?: $message = (new Message())->setText($message);

    $message->setIconUrl($message->getIconUrl() ?? $this->iconUrl);
    $message->setUsername($message->getUsername() ?? $this->username);
    $message->setChannel($message->getChannel() ?? $this->channel);
    $message->setWebhookUrl($message->getWebhookUrl() ?? $this->webhookUrl);

    $this->publishRequest($message);
}

Cette fonction publish peut prendre seulement du texte ou un modÚle Message. Ceci est fait pour rendre l'envoie de message simple plus rapide sans avoir à mettre un timbre sur chaque. Si c'est le choix de la personne qui envoie le message nous allons l'emballer pour. Pour ce faire nous allons tout d'abord vérifier si ce qui nous est envoyé est un message correctement fait ou juste du texte grùce à ?:. Si c'est seulement du texte nous allons l'emballer.

Une fois le message emballé nous allons y ajouter les détails sur l'emballage. Pour ce faire nous vérifions si il y a eu des valeurs spécifique pour les éléments sur ce message, sinon si il y en a de façon général et sinon il n y aura pas de décoration en particulier. Pour éviter d'écrire trop de code à ce sujet nous utilisons l'opérateur ternaire ??. Une fois le message et son emballage bien fini nous allons le donner à la prochaine fonction publishRequest.

/**
* @param Message $message
* @return void
* @throws TransportExceptionInterface
* @throws Exception
*/
private function publishRequest(Message $message)
{
    if ($message->getWebhookUrl() === null) {
        $errors[] = "No webhook URL set for message";
    }

    if ($message->getText() === null) {
        $errors[] = "No text set for message";
    }

    if (isset($errors)) {
        $message = array_reduce($errors, function ($carry, $error) {
            $carry .= "Error: {$error} ";
            return $carry;
        });
        throw new Exception($message);
    }

    $request = HttpClient::create()
        ->request(
        'POST',
        $message->getWebhookUrl(),
        [
            'headers' =>
            [
                'Content-Type' => 'application/json',
            ],
            'json' => $message->toArray()
        ]
    );

    if ($request->getStatusCode() !== 200) {
        throw new Exception("Publication failed, verify the channel and the settings for the webhook");
    };
}

En premier nous allons vĂ©rifier que le message Ă  bien un endroit oĂč aller et du contenu. Si ce n'est pas le cas nous allons rajouter des erreurs. Ensuite si il y a des erreurs nous allons nous assurer que le message ne sera pas envoyĂ© et envoyer des Exceptions Ă  l'envoyeur pour l'informer pourquoi le message n'a pas Ă©tĂ© envoyer.

Si le message est correct et que nous avons toutes les informations dont nous avons besoin afin de pouvoir l'envoyer nous allons utiliser le Symfony HttpClient afin de faire ceci. C'est Ă  ce moment lĂ  que la fonction toArray du modĂšle de Message sera utile pour ĂȘtre certain de ne pas envoyer du contenu en trop qui n'a aucune utilitĂ©. Si tout va bien Mattermost nous enverra un accusĂ© de rĂ©ception indiquant 200. Si cela n'est pas le cas nous allons Ă  nouveau envoyer une Exception Ă  l'envoyeur pour lui indiquer que le message n'a pas Ă©tĂ© reçu. Cela peux arriver si l'adresse (ici donc l'URL du webhook) Ă©tait mauvais ou si par exemple l'envoyeur Ă  essayĂ© de l'adresser Ă  quelqu'un de spĂ©cifique dans le bĂątiment (la team Mattemost avec un channel prĂ©cis) mais que cela est soit interdit soit la personne n'y a jamais Ă©tĂ©.

Voilà pour le fonctionnement de ce bundle et pourquoi nous l'avons réalisé. Pour rendre la configuration un peu plus rapide et plus facile, nous avons également ajouté une recette Symfony Flex, voyons comment cela est fait (spoiler il n'y a pas grand chose à voir;))

Recettes Flex

Les recettes Symfony Flex gĂšrent beaucoup de choses qui devaient ĂȘtre faites manuellement lors de l'ajout d'un package de composition Ă  votre projet. Il peut crĂ©er de nouveaux fichiers (comme les fichiers de configuration de package) ou ajouter Ă  ceux existants (comme le fichier .env par dĂ©faut) par exemple. Il existe de nombreuses recettes officielles que vous pouvez consulter sur le public [GitHub the contains them] (https://github.com/symfony/recipes) et pour vos propres bundles, vous pouvez cloner le [recette contrib repository] (https: //github.com/symfony/recipes-contrib). Nous n'entrerons pas trop dans les dĂ©tails sur les recettes de cet article, mais jetez un Ɠil Ă  ce que nous avons ajoutĂ© pour cet ensemble.

Ajout de votre répertoire

Lorsque vous clonez le référentiel recettes-contrib, vous verrez de nombreux répertoires, à la racine nous ajouterons tout d'abord un répertoire avec notre nom d'utilisateur [Packagist] (https://packagist.org/packages/codebuds/) codebuds. alors nous avons besoin du nom de notre bundle mattermost-publication-bundle et de la version du bundle dont la recette est pour0.3.

codebuds/
 ├── mattermost-publication-bundle/
 │   ├── 0.3/

C'est là que nous mettrons tous les fichiers (et répertoires) dont nous avons besoin. Tout d'abord un fichier manifest.json contenant les informations sur notre bundle.

{
    "bundles": {
        "CodeBuds\\MattermostPublicationBundle\\MattermostPublicationBundle": ["all"]
    },
    "copy-from-recipe": {
        "config/": "%CONFIG_DIR%/"
    },
    "env": {
        "MATTERMOST_WEBHOOK_URL": "http://{your-mattermost-site}/hooks/xxx-generatedkey-xxx"
    }
}

La partie bundles est juste des informations sur le bundle auquel cette recette est liĂ©e. Viens ensuite copier-de-recette qui indique Ă  la recette Ă  partir de quel rĂ©pertoire Ă  partir de laquelle vous voulez copier des fichiers dans le projet Symfony dans lequel votre bundle est installĂ© et oĂč les copier. Enfin, la partie env vous permet d'ajouter des informations par dĂ©faut dans le fichier global .env du projet.

Ajout du fichier de configuration par défaut

Nous pouvons donc voir ici que nous voulons copier les fichiers de config / vers les projets % CONFIG_DIR% /. Donc, pour générer automatiquement une configuration par défaut pour les packages, nous ajouterons le fichier config / packages / mattermost_publication.yaml à notre recette:

mattermost_publication:
    webhook_url: '%env(MATTERMOST_WEBHOOK_URL)%'
    # You can also set the default channel, username and icon_url
    # for more information you can checkout the bundle documentation
    # https://github.com/codebuds33/mattermost-publication-bundle/blob/master/README.md

Et c'est tout ! Vous pouvez maintenant faire une pull request à partir de votre dépÎt forké et si elle est acceptée, la prochaine personne qui téléchargera votre bundle dans son projet aura ces fichiers par défaut automatiquement ajoutés !