Développons un Chat avec Symfony, Mercure et Vue.js !

Suite au Symfonylive 2019 et à la présentation de Mercure par l’excellent Kevin Dunglas (@dunglas) (ça se voit que je suis fan ??) j’avais toujours dans un coin de ma tête de développer un chat avec ces technos, alors c’est parti !

Pour ce tuto il faut savoir que Symfony n’est pas indispensable, j’aurai très bien pu m’en passer, mais si vous voulez y ajouter une connexion utilisateur, ou stocker les messages de chat ce sera mieux !
Dans ce chat n’importe qui pourra poster n’importe quoi ! Mais la discussion sera effacé après rechargement de la page !

Comme d’habitude, vous pourrez retrouver les sources sur mon github :

https://github.com/gponty/sf4chatMercure

Et une démo ici du tuto terminé (avec un peu plus de joli) :

http://chat.dev-web.io/

Il faut savoir que le dev d’un tel chat est hyper simple, là où ça c’est grandement plus compliqué c’est pour la mise en prod et plus particulièrement de mercure (avec docker), c’est pour cette raison que entre autre la démo n’est pas en HTTPS.

On va commencer par installer tout ce qu’il nous faut, c’est à dire :

  • Symfony
  • Vue.js
  • Mercure (en utilisant une image docker)

Installation de Symfony

Pour Symfony rien de plus simple, mais je vous redonne la procédure :

wget https://get.symfony.com/cli/installer -O - | bash
/your_home_directory/.symfony/bin/symfony new sf4chatMercure

Installation de Vue.js

Pour vue.js c’est un peu plus compliqué mais rien de transcendant non plus :

composer require symfony/webpack-encore-bundle
yarn install

Vous ajoutez cette ligne dans le fichier webpack.config.js :

.enableVueLoader()

Puis on installe les bibliothèques vue.js :

yarn add vue vue-loader@^15.0.11 vue-template-compiler --dev

Installation de Mercure

On va d’abord installer le Hub de Mercure, c’est lui qui va se charger de dispatcher les messages du chat aux navigateurs (ou toutes autres plateformes). Il y a plusieurs méthodes pour cela, moi je passe par une image docker :

docker run \
    -e JWT_KEY='!ChangeMe!' -e DEMO=1 -e ALLOW_ANONYMOUS=1 -e CORS_ALLOWED_ORIGINS=* -e PUBLISH_ALLOWED_ORIGINS='http://chatblog.localhost' \
    -p 3000:80 \
    dunglas/mercure

Pour installer le composant Symfony :

 composer require mercure

Vous allez devoir générer un token JWT grâce à la clé que vous avez mis dans votre container (JWT_KEY), vous pouvez vous aider de ce site : https://jwt.io/

Il faut ensuite modifier les 2 lignes suivantes dans votre fichier .env.local :

MERCURE_PUBLISH_URL=http://chat.localhost:3000/.well-known/mercure
MERCURE_JWT_TOKEN=VotreJWTtoken

Une fois cette configuration faites on va entrer dans le vif du sujet et créer le controller qui va publier les messages, comme on est feignant on va utiliser le maker de symfony :

composer req --dev maker
composer require doctrine/annotations
php bin/console make:controller PublisherController

Pour ce controller on a juste besoin du message à envoyer (bien entendu dans une version plus poussée il faudrait par exemple aussi un Id user, dans la demo j’envoie un id qui me sert juste pour une question de design)

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Publisher;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Routing\Annotation\Route;

class PublisherController extends AbstractController
{
    /**
     * @Route("/publish", name="publish", methods={"POST"})
     * @param Request $request
     * @param Publisher $publisher
     * @return Response
     */
    public function publish(Request $request, Publisher $publisher): Response
    {
        $data = json_decode($request->getContent());

        $update = new Update(
            'http://chat.localhost',
            json_encode(
                [
                    'message' => $data->message
                ]
            )
        );

        // The Publisher service is an invokable object
        $publisher($update);

        return new Response('published!');
    }
}

Pour voir si cela fonctionne vous pouvez faire le test avec une commande Curl :

curl --request POST \
  --url 'http://chatblog.localhost/publish?=' \
  --header 'content-type: application/json' \
  --data '{
	"message" : "Mercure rocks !"
}'

Si tout va bien vous devriez voir le message « Published » ainsi qu’une ligne dans les logs de mercure.

C’est bien beau ça mais l’idéal serait maintenant d’afficher ces messages ! Et évidemment sans avoir à rafraîchir la page !

Et ça là où intervient l’excellent vue-js !

Il nous faut d’abord créer un controller qui va afficher notre page :

composer req symfony/twig-pack
php bin/console make:controller ChatController
<?php
//src/Controller/ChatController.php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class ChatController extends AbstractController
{
    /**
     * @Route("/", name="chat")
     */
    public function index()
    {
        return $this->render('chat.html.twig');
    }
}

Et le template qui va avec (/templates/chat.html) :

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Chattons !{% endblock %}</title>
        {{ encore_entry_link_tags('app') }}
    </head>
    <body>
    <div id="app">
        <chat></chat>
    </div>

        {{ encore_entry_script_tags('app') }}
    </body>
</html>

les 2 lignes « encore_entry…. » vont ajoutés automatiquement les fichiers css et javascripts générés par webpack grâche à la commande yarn watch
La partie id= « app » et <chat> sera notre composant vue.js
On va d’ailleurs créer 2 composants : l’affichage des messages, et le message en lui même.
Afin de simplifier le code je ne vais pas faire de « jolie » (donc pas de bootstrap :), j’ai quand même installé la lib JS moment qui va permettre de formater l’heure d’arrivée du message :

yarn add moment
yarn watch

On va créer le composant message, qui va être très très simple, on passe juste le texte du message en props :

<template>
    <div>
        <p>
            {{ message }}
        </p>
        <time>{{ datetimePost }}</time>
    </div>
</template>
<script>
    import moment from 'moment';

    export default {
        name: "messagechat",
        props: [
            'message'
        ],
        data() {
            return {
                datetimePost: moment().format('H:mm:ss')
            }
        }
    }
</script>

<style scoped>

</style>

Puis le composant qui va afficher les messages (le plus « compliqué »):

<template>
    <div>
        <div>
            <span v-for="m in messages">
                <message-chat :message="m.message"></message-chat>
            </span>
        </div>
        <div>
            <form @submit="submitMessage">
                <input type="text" v-model='messageEnCours' class="form-control" placeholder="Say something">
                <button type="submit">Send</button>
            </form>
        </div>
    </div>
</template>

<script>
    import MessageChat from "./MessageChat";

    export default {
        name: "chat",
        components: {MessageChat},
        data() {
            return {
                messages: [
                    {message: 'Salut, ça va ?'},
                    {message: 'ça biche et toi ?'}
                ],
                messageEnCours: ''
            }
        },
        mounted: function () {
            const url = new URL('http://localhost:3000/.well-known/mercure');
            url.searchParams.append('topic', 'http://chat.localhost');
            const eventSource = new EventSource(url);

            eventSource.onmessage = e => {
                console.log('Nouveau message');
                const data = JSON.parse(e.data);
                this.messages.push({message: data.message});
                this.messageEnCours = '';
            }
        }
        ,
        methods: {
            submitMessage: function (e) {

                const postData = JSON.stringify({message: this.messageEnCours});
                const param = {
                    method: "POST",
                    body: postData
                };

                fetch('/publish', param)
                    .then(data => {
                        console.log('Ils sont partiiiiiss !!!!');
                    })
                    .catch(error => console.log(error));

                e.preventDefault();
            }

        }
    }
</script>

<style scoped>

</style>

Ici le bloc important est l’écoute du hub (ce qui se trouve dans la partie mounted) , à chaque fois qu’un message va être transmis au HUB il sera dispatché sur les differents clients.

Vous pouvez maintenant faire un test en lançant 2 navigateurs et magie ils recevront tous les 2 les messages.

That’s all !

Symfony 4 – Doctrine Event Subscriber

« Symfony, peux-tu mettre automatiquement une date d’ajout à chaque création d’une entité ? », voici un des exemples pour lesquels vous pourriez avoir besoin des subscribers.


(Il peut y avoir d’autres méthodes qui pourront faire la même chose, comme les listeners mais pour ce cours nous resterons sur les subscribers, par contre il faut savoir que les listeners sont chargés uniquement quand l’event est déclenché contrairement aux subscribers qui eux sont chargés chaque fois que l’application s’exécute).

J’avais prévu pas mal de blabla mais finalement je me dis que de travailler sur un exemple va être beaucoup mieux, et pour ça nous allons développer… un distributeur de friandises !

Le MCD sera hyper simple :

Un produit aura un nom et la quantité restante dans le distributeur. Une activité sera soit un achat (on réalimente le distributeur) soit une vente (on vend une friandise)

A chaque nouvelle activité il faut remettre à jour la quantité restante du produit, vous pouvez le faire à la main avec quelque chose comme :

$activite->getProduit()->setQteRestante($activite->getProduit->getQteRestante() - $activite->getQuantite());

Et ça c’est dans le cas d’une vente, dans le cas d’achat il faudra faire une addition. A la rigueur si vous ne devez le faire qu’à un seul endroit ça peut encore passer, mais si vous avez plusieurs endroits où c’est mis à jour la maintenance du code peut devenir compliqué.
Vous pourriez par exemple à avoir à créer des activités (donc achat ou vente) via l’import d’un fichier excel, via une tâche cron qui tournerai toutes les nuits, etc…

C’est là ou les subscribers vont nous être utiles : la mise à jour de la quantité restante (ainsi que la dateActivite) ne sera écrit qu’à un seul endroit et sera exécuté automatiquement.

Je ne vous mettrai ici que le code intéressant, mais vous pourrez retrouver l’intégralité du code sur mon github :

https://github.com/gponty/distributeur

Et une démo du résultat final ici :

https://distributeur.dev-web.io/

On va commencer par dire à Symfony que notre subscriber existe (ou en tout cas va exister!), dans le fichier config/services.yaml :

App\EventListener\ActiviteSubscriber:
    tags:
        - { name: doctrine.event_subscriber, connection: default }

Là vraiment rien de compliqué mais indispensable.

Avant de passer à l’écriture du subscriber on va regarder à quel moment il peut se déclencher, je ne vais pas tous les énumérer, vous pourrez les retrouver en intégralité sur le site de doctrine. En ce qui concerne notre projet nous en utiliserons 2 :

- postPersist : Le subscriber va se déclencher après la persistance d'une entité, ce qui va nous intéresser pour la mise à jour des quantités restantes

- prePersist :Le subscriber va se declencher avant la persistance d'une entité ce qui sera intéressant pour la mise à jour de la date d'activité.

La magie de Symfony c’est qu’on va pouvoir regrouper ces 2 events dans le même code :

namespace App\EventListener;

use App\Entity\Activite;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;

class ActiviteSubscriber implements EventSubscriber
{
    public function getSubscribedEvents()
    {
        return [
            Events::postPersist,
            Events::prePersist,
        ];
    }

    public function postPersist(LifecycleEventArgs $args)
    {
        $this->updateQuantite($args);
    }

    public function prePersist(LifecycleEventArgs $args)
    {
        $this->updateDateActivite($args);
    }

    public function updateQuantite(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();
        $entityManager = $args->getObjectManager();

        if ($entity instanceof Activite) {
            if($entity->getTypeActivite()==='A') {
                $entity->getProduit()->addQteRestante($entity->getQuantite());
            }

            if($entity->getTypeActivite()==='V'){
                $entity->getProduit()->subQteRestante($entity->getQuantite());
            }

            $entityManager->flush();

        }
    }

    public function updateDateActivite(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();

        if ($entity instanceof Activite) {

            $entity->setDateAtivite(new \DateTime());

        }
    }
}

On pourrait très bien n’avoir qu’une seule fonction qui regroupe addQteRestante/subQteRestante mais je trouvais que pour l’exemple c’était mieux comme ça.

Vous pouvez maintenant tester et ajouter des « activite », vous devrez voir que la quantité restante et la date sur activite se met bien à jour. Et vous pouvez ajouter des activités de de n’importe quelle façon sans ajouter de code (mais attention, seulement à partir du moment où ça passe par doctrine, un INSERT pur dans la base ne mettra pas à jour la donnée)

Je pense qu’il n’y a pas beaucoup plus à ajouter, le code est suffisamment parlant, mais si vous avez des questions n’hésitez pas à commenter cet article.

Switch user

Une des fonctionnalités de Symfony que j’utilise dans tous mes projets est la possibilité d’être connecté à la place d’un autre utilisateur (ou de « impersonate a User » selon le terme de Symfony).

Très utile quand vous avez le client au bout du fil qui vous dit que ça ne fonctionne pas alors que chez vous ça fonctionne (un petit « switch user » plus tard, vous vous rendrez compte qu’il y a bien un bug spécifique à son profil)

On va repartir du source sur github « userdemo2018″ que nous avons écrit pour l’article http://blog.dev-web.io/2018/10/30/symfony-4-gestion-utilisateurs-sans-fosuserbundle-v2018-chapitre-1/

Pour ça il n’y a pas besoin de package particulier, il faut juste modifier la config pour activer cette fonctionnalité, ça se passe dans le fichier security.yaml, il faut ajouter un switch_user à true :

firewalls:
    main:
        switch_user: true

Il faut maintenant affecter le rôle ROLE_ALLOWED_TO_SWITCH aux utilisateurs qui auront la possibilités de switcher sur un autre user, pour ça on va tout simplement modifier nos fixtures :

$user->setRoles(['ROLE_ALLOWED_TO_SWITCH']);

Et ne pas oublier de les relancer après la modif :

gponty@e721fa3a3f95:/var/www/userDemo2018$ php bin/console doctrine:fixtures:load

 Careful, database "userdemo" will be purged. Do you want to continue? (yes/no) [no]:
 > yes

   > purging database
   > loading App\DataFixtures\UserFixtures
gponty@e721fa3a3f95:/var/www/userDemo2018$ 

On peut tout de suite tester en se rendant à n’importe quelle adresse de votre site et ajouter à la fin de l’URL : ?_switch_user=userdemo5@example.com

Pour être sûr que ça a fonctionné il suffit de jeter un oeil à la debug toolbar, vous devriez voir l’adresse de l’utiliser switché s’afficher, et pour sortir du mode « switch » et revenir à votre profile, il faut cliquer sur « Exit Impersonation »

On va aller un peu plus loin afin de faire quelque chose d’un peu plus sexy et faire un tableau des utilisateurs avec un lien pour switcher et un lien pour sortir du « switch », tout va se passer dans la vue twig :

    {% if is_granted('ROLE_PREVIOUS_ADMIN') %}
        <a href="{{ path('homepage', {'_switch_user': '_exit'}) }}">Exit User</a>
    {% endif %}
    <table>
        {% for user in users %}
            <tr>
                <td>{{ user.email }}</td>
                <td>{{ user.nomComplet }}</td>
                <td><a href="{{ path('homepage', {'_switch_user': user.email}) }}">Switch!</a></td>
            </tr>
        {% endfor %}
    </table>

Plusieurs remarques :
L’utilisateur à l’origine du switch a un nouveau rôle : ROLE_PREVIOUS_ADMIN, ce qui nous permet de tester si on est en train de switch ou pas
Pour sortir d’un « switch » il suffit d’ajouter _switch_user=_exit à la fin de l’URL
Vous ne pouvez pas switcher sur un autre utilisateur si vous avec déjà un switche en cours, il faut d’abord en sortir.

A vous de jouer ! Pour ma part je fais mon baluchon pour Paris et la Symfony Live qui va se dérouler jeudi et vendredi !

Test de la distro Linux Elementary

Ca faisait pas mal de temps que je suivais Elementary, je l’avais testé à plusieurs reprises mais elle n’était pas encore assez mature pour que j’en fasse ma distro de travail.

Un peu influencé par le fait qu’elle soit dans le top 5 de distrowatch et avec la sortie de la version 5.0 (Juno) j’ai décidé de me lancer et de l’utiliser à plein temps.

Et après quelques semaines d’utilisation je dois dire que je suis assez déçu, il y a pas mal de détail assez énervant, dont voici quelques exemples :

  • Il y a très peu d’applications fournies avec, et même pas un pauvre éditeur de texte
  • Un bug qui fait que les fenêtre de certaines applications sont « doublés » (bug connu)
  • Il faut bidouiller pour désactiver l’économiseur d’écran (en passant par le menu paramètres ça ne fonctionne pas : bug connu)
  • Et surtout, le copier coller entre le terminal et une appli externe (genre navigateur) ne fonctionne pas !

Pas mal de petits détails qui au quotidien rends la distro très agaçante, je vais donc me tourner vers la MX Linux, ou sinon je reviendrai à l’incontournable Linux Mint.

Utiliser Symfony dans docker

Dans ce tutorial nous allons voir comment développer un site Symfony à l’aide d’une stack docker.

On part du principe qu’il n’y a rien d’installé sur votre poste de travail, à part docker et docker-compose (et que vous êtes sous linux!) Vous trouverez le tutorial d’installation de docker sur le site officiel (par exemple si vous êtes sous Debian : https://docs.docker.com/install/linux/docker-ce/debian/)

Continuer la lecture de « Utiliser Symfony dans docker »

Utiliser docker pour le développement web

Dans ma vie de développeur j’ai connu 2 révolutions qui ont changé ma façon de développer :

  • En 2000, Le développement sous Linux
  • En 2012, la découverte de Symfony

Avec docker je vais ajouter une 3eme ligne à cette liste.

Voici quelques avantages d’utiliser docker (pour l’utilisation que j’en ai) :

  • Tester votre site dans plusieurs version de php, apache, mysql, …. et tout ça en ne changeant que le port du http
  • Plus besoin d’installer php/apache/mysql en local sur votre poste, vous avez toujours un poste propre
  • Des tonnes d’images disponibles sur le net (par exemple voir plus bas, maildev)
  • Vous êtes sûrs que votre stack est dans la même version que celle de vos collègues et/ou la prod

J’ai mis à dispo ma stack sur mon github :

https://github.com/gponty/docker-mysql-apache-php

Il est fait pour mes besoins personnels donc très spécifiques, c’est à dire :

  • Utilisation des dernières versions de PHP 7.1 et 7.2
  • Utilisation d’apache
  • Utilisation de mysql
  • Utilisation de maildev qui permet de rediriger tous les envois smtp vers une boite locale (un must-have)
  • Installation des drivers PHP pour sql server
  • Installation de wkhtml2pdf
  • Installation de phploy (déploiement du code sur un serveur)
  • Installation de composer
  • Installation de Xdebug

Toutes ces commandes se trouvent dans le fichier Dockerfile qui se trouve dans les répertoires PHP (pour une raison x j’avais besoin de 2 fichiers, mais je pense que je pourrai en regrouper une grosse partie dans 1 seul fichier et ainsi éviter la duplication de code) Même si ça peut paraître compliqué, pris ligne à ligne c’est assez simple à comprendre.

Si vous n’avez pas encore passé le cap, je vous recommande donc chaudement d’essayer docker, ça demande un petit investissement afin de bien comprendre comment cela fonctionne, mais au final ça vous fera gagner du temps très précieux.

Symfony 4 : Gestion utilisateurs sans FOSUserBundle v2018 : Chapitre 3

Cet article est écrit en 3 parties :

Dans ce chapitre nous allons voir comment s’enregistrer et récupérer son mot de passe lors d’un oubli.

Continuer la lecture de « Symfony 4 : Gestion utilisateurs sans FOSUserBundle v2018 : Chapitre 3 »

Symfony 4 : Gestion utilisateurs sans FOSUserBundle v2018 : Chapitre 2

Cet article est écrit en 3 parties :

Au chapitre précédent on a mis en place notre base de données et l’avons alimenté de quelques users, maintenant il va bien falloir qu’ils se connectent !

Continuer la lecture de « Symfony 4 : Gestion utilisateurs sans FOSUserBundle v2018 : Chapitre 2 »

Symfony 4 : Gestion utilisateurs sans FOSUserBundle v2018 : Chapitre 1

Cet article est écrit en 3 parties : J’avais écrit l’année dernière une série d’articles sur comment gérer les utilisateurs sans FOSUserBundle, il me restait encore à publier « comment modifier son mot de passe et comment faire un « J’ai oublié mon mot de passe ». Vu que les choses ont pas mal bougé en 1 an j’ai décidé de repartir de 0 (mais en reprenant les grandes lignes de l’ancien article). Mon environnement :
  • Linux Mint 19
  • Php 7.2.10
  • MariadB 10.2.18
  • Symfony 4.1.6
Continuer la lecture de « Symfony 4 : Gestion utilisateurs sans FOSUserBundle v2018 : Chapitre 1 »

Appli de gestion de mot de passe en Symfony et Vue.js (Partie 3)

Dernier chapitre de notre développement, où nous allons ajouter, modifier et supprimer des mots de passe.

Cet article est écrit en 3 parties :

Vous pouvez retrouver le projet complet ici : https://github.com/gponty/gespassvuejs

Continuer la lecture de « Appli de gestion de mot de passe en Symfony et Vue.js (Partie 3) »