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.

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 »

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) »

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

Suite de notre développement !

Dans le chapitre précédent on a pu mettre en place tout notre environnement ainsi que notre première page.

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 2) »

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

Le tutorial du jour va être de développer une application qui servira à stocker vos nombreux mots de passe. (site internet, numéro CB, code PIN, etc…)
Elle portera le doux nom de Gespass.
Cette application servira uniquement d’exercice, je ne vous conseille pas de l’utiliser en production sans en améliorer la sécurité (par exemple au moins ajouter une authentification .htaccess)

Mon environnement de dev actuel :

  • Mint 19 xfce
  • php 7.2.5
  • mariadb 10.1.29
  • symfony 4.1.1
  • server web de symfony
  • vue.js 2.5.16

Cet article sera é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 1) »

[Symfony 4] Création de fixtures aléatoires – Faker

Rien de plus ennuyant que de créer des jeux de données afin de tester votre application, d’autant qu’il existe des scripts pour ça, comme Faker qui fait très bien le boulot.
Ces données s’appellent plus communément des « fixtures ».

On va commencer par créer un nouveau projet Symfony 4 :

composer create-project symfony/skeleton sf4-faker

Et y ajouter quelques librairies qui nous serons utiles :

composer req orm
composer req --dev make doctrine/doctrine-fixtures-bundle

Puis tout de suite ajouter le composant qui nous intéressent : Faker (https://github.com/fzaninotto/Faker) :

composer req --dev fzaninotto/faker

Ensuite on va créer une nouvelle entité appelée « Personne » :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\PersonneRepository")
 */
class Personne
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private $nom;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $adresse;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private $ville;

    /**
     * @ORM\Column(type="string", length=15)
     */
    private $codePostal;

    /**
     * @ORM\Column(type="text")
     */
    private $description;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private $email;

    /**
     * Get the value of id
     */ 
    public function getId()
    {
        return $this->id;
    }

    /**
     * Get the value of nom
     */ 
    public function getNom()
    {
        return $this->nom;
    }

    /**
     * Set the value of nom
     *
     * @return  self
     */ 
    public function setNom($nom)
    {
        $this->nom = $nom;

        return $this;
    }

    /**
     * Get the value of adresse
     */ 
    public function getAdresse()
    {
        return $this->adresse;
    }

    /**
     * Set the value of adresse
     *
     * @return  self
     */ 
    public function setAdresse($adresse)
    {
        $this->adresse = $adresse;

        return $this;
    }

    /**
     * Get the value of ville
     */ 
    public function getVille()
    {
        return $this->ville;
    }

    /**
     * Set the value of ville
     *
     * @return  self
     */ 
    public function setVille($ville)
    {
        $this->ville = $ville;

        return $this;
    }

    /**
     * Get the value of codePostal
     */ 
    public function getCodePostal()
    {
        return $this->codePostal;
    }

    /**
     * Set the value of codePostal
     *
     * @return  self
     */ 
    public function setCodePostal($codePostal)
    {
        $this->codePostal = $codePostal;

        return $this;
    }

    /**
     * Get the value of description
     */ 
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Set the value of description
     *
     * @return  self
     */ 
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get the value of email
     */ 
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Set the value of email
     *
     * @return  self
     */ 
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }
}

lors de l’installation du composant fixtures ce dernier a installé un nouveau répertoire : DataFixtures, c’est dans ce dernier que nous allons générer nos fixtures.

Vous trouverez ici tous les types de données que Faker peut générer : https://github.com/fzaninotto/Faker#formatters

Voilà notre fichier FakerFixtures :

<?php
// src/DataFixtures/FakerFixtures.php
namespace App\DataFixtures;

use App\Entity\Personne;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Faker;

class FakerFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {

        // On configure dans quelles langues nous voulons nos données
        $faker = Faker\Factory::create('fr_FR');

        // on créé 10 personnes
        for ($i = 0; $i < 10; $i++) {
            $personne = new Personne();
            $personne->setNom($faker->name);
            $personne->setAdresse($faker->streetAddress);
            $personne->setVille($faker->city);
            $personne->setCodePostal($faker->postcode);
            $personne->setDescription($faker->text);
            $personne->setEmail($faker->email);
            $manager->persist($personne);
        }

        $manager->flush();
    }
}

Un petit :

php bin/console doctrine:fixtures:load

Et voilà notre base remplie de 10 personnes :

Vous pouvez retrouver les sources ici :

https://github.com/gponty/sf4-faker