Archives de catégorie : Développement

Symfony 4 : Les base d’une gestion des utilisateurs (inscription, connexion, droits d’accès)

Quand on démarre le développement d’une site sous Symfony, l’une des première chose à faire concerne la gestion des utilisateurs. En effet dans la plupart des cas notre application devra permettre à des utilisateurs de s’inscrire puis de se connecter au site afin d’avoir accès à des sections particulières (espace membre, espace administrateur). Je reprends dans cet article les différents éléments à mettre en place pour avoir une base de travail fonctionnelle.

On va rendre possible une inscription (email / mot de passe), puis une connexion (idem). Une fois connecté on aura accès a un espace membre.

Nous partirons d’une installation « website » de symfony 4 lancé avec la commande

composer create-project symfony/website-skeleton my-project

La première chose à faire sera de créer la base de données, et de la configurer dans le fichier « /.env », par exemple :

DATABASE_URL=mysql://root:@localhost/ma_base

Entrons dans le vif du sujet, avec la création de l’entité « User », dans « /src/Entity/User.php ». Elle contiendra les champs email, password, isActive, roles et étendra la classe « AdvancedUserInterface » de symfony. Ce qui nous donne avec les getters/setters et les méthodes obligatoires à implémenter, le code suivant :

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Table(name="user")
 * @UniqueEntity(fields="email")
 * @ORM\Entity()
 */

class User implements AdvancedUserInterface, \Serializable {

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */

    private $id;

    /**
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\NotBlank()
     * @Assert\Email()
     */

    private $email;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=250)
     */

    private $plainPassword;

    /**
     * The below length depends on the "algorithm" you use for encoding
     * the password, but this works well with bcrypt.
     *
     * @ORM\Column(type="string", length=64)
     */

    private $password;

    /**
     * @ORM\Column(name="is_active", type="boolean")
     */

    private $isActive;

    /**
     * @ORM\Column(name="roles", type="array")
     */

    private $roles = array();

    public function __construct() {
        $this->isActive = true;
        // may not be needed, see section on salt below
        // $this->salt = md5(uniqid('', true));
    }

    public function getUsername() {
        return $this->email;
    }

    public function getSalt() {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return null;
    }

    public function getPassword() {
        return $this->password;
    }

    function setPassword($password) {
        $this->password = $password;
    }

    public function getRoles() {
        if (empty($this->roles)) {
            return ['ROLE_USER'];
        }
        return $this->roles;
    }

    function addRole($role) {
        $this->roles[] = $role;
    }

    public function eraseCredentials() {
       
    }

    /** @see \Serializable::serialize() */
    public function serialize() {
        return serialize(array(
            $this->id,
            $this->email,
            $this->password,
            $this->isActive,
                // see section on salt below
                // $this->salt,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized) {
        list (
                $this->id,
                $this->email,
                $this->password,
                $this->isActive,
                // see section on salt below
                // $this->salt
                ) = unserialize($serialized);
    }

    public function isAccountNonExpired() {
        return true;
    }

    public function isAccountNonLocked() {
        return true;
    }

    public function isCredentialsNonExpired() {
        return true;
    }

    public function isEnabled() {
        return $this->isActive;
    }

    function getId() {
        return $this->id;
    }

    function getEmail() {
        return $this->email;
    }

    function getPlainPassword() {
        return $this->plainPassword;
    }

    function getIsActive() {
        return $this->isActive;
    }

    function setId($id) {
        $this->id = $id;
    }

    function setEmail($email) {
        $this->email = $email;
    }

    function setPlainPassword($plainPassword) {
        $this->plainPassword = $plainPassword;
    }

    function setIsActive($isActive) {
        $this->isActive = $isActive;
    }

}

Une fois ce fichier enregistré, on peux mettre à jour notre base de données afin d’y créer la table « User ». Pour cela on lance les 2 commandes suivantes :

php bin/console doctrine:migration:diff
php bin/console doctrine:migration:migrate

Il est temps de passer à la configuration. Rendons nous dans le fichier « /config/packages/security.yaml » et remplissons le de la façon suivante :

security:
   # encoder
    encoders
:
        App\Entity\User
:
            algorithm
: bcrypt
   
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers
:
        our_db_provider
:
            entity
:
                class
: App\Entity\User
                property
: email
       
    firewalls
:
        main
:
            pattern
:   ^/
            http_basic
: ~
            provider
: our_db_provider
            anonymous
: ~
            form_login
:
                login_path
: login
                check_path
: login
            logout
:
                path
:  /logout
                target
: /
           
    role_hierarchy
:
        ROLE_ADMIN
:      ROLE_USER
        #ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control
:
        - { path
: ^/admin, roles: ROLE_ADMIN }
        - { path
: ^/member, roles: ROLE_USER }

Comme on peux le comprendre on a mis en place la configuration pour l’encodage du mot de passe, on indique que nos utilisateurs passe par l’entité « App\Entity\User » et que le login est le champ « email ». On configure aussi les liens /login et /logout à utiliser, les différents roles possible (ROLE_USER, ROLE_ADMIN) et bien sur les droits d’accès en fonctions des rôles. Ici toutes les routes commençant par « /member » ne seront accessible que pour les membres connectés, et celle commençant par « /admin » pour les membres connecté ayant le rôle « ROLE_ADMIN ».

Pour faire fonctionner la déconnexion « logout », il faut simplement créer la route. On se rend donc dans le fichier « /config/routes.yaml » pour y mettre ce code :

logout:
    path
: /logout

Il nous reste à créer la page contenant le formulaire d’inscription, celle contenant le formulaire de connexion et tout sera fonctionnel.

Pour l’inscription, on va d’abord créer le formulaire dans un fichier à part qui sera « /src/Form/UserType.php ». J’ai ajoutés des classes css de bootstrap pour le bouton submit, car on verra plus bas que je propose d’utiliser bootstrap 4 pour l’affichage, mais il s’agit bien sur uniquement d’un exemple.

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class UserType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
                ->add('email', EmailType::class)
                ->add('plainPassword', RepeatedType::class, array(
                    'type' => PasswordType::class,
                    'first_options' => array('label' => 'Mot de passe'),
                    'second_options' => array('label' => 'Confirmation du mot de passe'),
                ))
                ->add('submit', SubmitType::class, ['label'=>'Envoyer', 'attr'=>['class'=>'btn-primary btn-block']])
        ;
    }
}

Nous allons utiliser ce formulaire dans le controller « /src/Controller/RegistrationController.php ». La page permettant l’inscription sera accessible sur l’url « /register », ici on enregistre et on valide directement le compte, on peut aussi attribuer le rôle utilisateur dans ce controlleur. C’est une base qui pourras être amélioré (envoie d’un email de confirmation…etc)

namespace App\Controller;

use App\Form\UserType;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class RegistrationController extends Controller {

    /**
     * @Route("/register")
     */

    public function registerAction(Request $request, UserPasswordEncoderInterface $passwordEncoder) {
        // 1) build the form
        $user = new User();
        $form = $this->createForm(UserType::class, $user);
        // 2) handle the submit (will only happen on POST)
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            // 3) Encode the password (you could also do this via Doctrine listener)
            $password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
            $user->setPassword($password);
            //on active par défaut
            $user->setIsActive(true);
            //$user->addRole("ROLE_ADMIN");
            // 4) save the User!
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($user);
            $entityManager->flush();
            // ... do any other work - like sending them an email, etc
            // maybe set a "flash" success message for the user
            $this->addFlash('success', 'Votre compte à bien été enregistré.');
            //return $this->redirectToRoute('login');
        }
        return $this->render('registration/register.html.twig', ['form' => $form->createView(), 'mainNavRegistration' => true, 'title' => 'Inscription']);
    }

}

Dans la foulée, on enchaine sur le controller « /src/Controller/SecurityController.php » avec la page « /login » qui permettra la connexion, cette fois on créer directement le formulaire dans le controller, cela permet de voir une autre manière de faire que pour l’enregistrement.

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends Controller {

    /**
     * @Route("/login", name="login")
     */

    public function login(Request $request, AuthenticationUtils $authenticationUtils) {
        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();
        //
        $form = $this->get('form.factory')
                ->createNamedBuilder(null)
                ->add('_username', null, ['label' => 'Email'])
                ->add('_password', \Symfony\Component\Form\Extension\Core\Type\PasswordType::class, ['label' => 'Mot de passe'])
                ->add('ok', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, ['label' => 'Ok', 'attr' => ['class' => 'btn-primary btn-block']])
                ->getForm();
        return $this->render('security/login.html.twig', [
                    'mainNavLogin' => true, 'title' => 'Connexion',
                    //
                    'form' => $form->createView(),
                    'last_username' => $lastUsername,
                    'error' => $error,
        ]);
    }

}

Tout ce qui concerne la partie « technique » et maintenant en place et fonctionnel. Pour aller jusqu’au bout on va aussi voir la mise en place des templates pour pouvoir tester le tout (avec un joli affichage à l’aide de bootstrap 4). Pour avoir accès à nos route « /register » et « /login » de nos controllers il nous faut créer les 2 templates appelés par ceux-ci. Les 2 sont très simples et assez proches.

« /templates/registration/register.html.twig »

{% extends 'base.html.twig' %}

{% block body %}
    {{form(form)}}
{% endblock %}

« /templates/security/login.html.twig »

{% extends 'base.html.twig' %}

{% block body %}

    {% if error %}
        <div class="alert alert-danger">
            <ul class="list-unstyled mb-0">
                <li><span class="initialism form-error-icon badge badge-danger">Error</span>
                    <span class="form-error-message">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
                </li>
            </ul>
        </div>
    {% endif %}
    {{form(form)}}
{% endblock %}

Ces 2 templates dépendent de « base.html.twig », c’est à dire le fichier « /templates/base.html.twig », ci dessous je vous en met un exemple. comme vu précédemment il utilise bootstrap 4. Pour aller jusqu’au bout j’ai inclus ici un menu vers différente page de l’application : les page d’inscription/connnexion ou de déconnexion si on est connecté. Ainsi qu’une page d’accueil, et un espace membre et/ou espace admin en fonction du rôle de l’utilisateur connecté. Pour que tout fonctionne il faudra donc créer les controllers pour ces 3 pages ainsi que leur vues, on voit ça juste après, à la fin de l’article.

<!DOCTYPE html>
<html lang="{{ app.request.locale }}">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
        <title>
            {% block title %}
                {% if title is defined %}{{title}}{% else %}Title{% endif %} | Nom du site
            {% endblock %}
        </title>
        {% block stylesheets %}
            {# bootstrap #}
            <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
            {# custom #}
            <link rel="stylesheet" href="{{ asset('assets/css/styles.css') }}">
        {% endblock %}
        <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
    </head>
    <body>
    <body id="{% block body_id %}{% endblock %}">

        {% block header %}
            <header id="header" class="bck1 border-bottom">
                <div class="container">
                    <h1>{% if title is defined %}{{title}}{% else %}Header{% endif %}</h1>
                </div>
            </header>
        {% endblock %}

        {% block nav %}
            <nav id="main-nav" class="container navbar navbar-expand-lg navbar-light bg-light border rounded mb-3">
                <ul class="navbar-nav">
                    <li class="nav-item {% if mainNavHome is defined %}active{% endif %}">
                        <a class="nav-link" href="{{path('app_homepage_index')}}">Accueil</a>
                    </li>
                    {% if is_granted('ROLE_USER') %}
                        <li class="nav-item {% if mainNavMember is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('app_member_index')}}">Espace membre</a>
                        </li>
                    {% endif %}
                    {% if is_granted('ROLE_ADMIN') %}
                        <li class="nav-item {% if mainNavAdmin is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('app_admin_homepage_index')}}">Espace admin</a>
                        </li>
                    {% endif %}
                </ul>
                <ul class="navbar-nav ml-auto">
                    {% if is_granted('ROLE_USER') %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{path('logout')}}">Déconnexion</a>
                        </li>
                    {% else %}
                        <li class="nav-item {% if mainNavLogin is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('login')}}">Connexion</a>
                        </li>
                        <li class="nav-item {% if mainNavRegistration is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('app_registration_register')}}">Inscription</a>
                        </li>
                    {% endif %}
                </ul>
            </nav>
        {% endblock %}

        {% block flash %}
            {% for flash_message in app.session.flashbag.get('success') %}
                <div class="container alert alert-success" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
            {% for flash_message in app.session.flashbag.get('info') %}
                <div class="container alert alert-info" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
            {% for flash_message in app.session.flashbag.get('warning') %}
                <div class="container alert alert-warning" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
            {% for flash_message in app.session.flashbag.get('danger') %}
                <div class="container alert alert-danger" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
        {% endblock %}

        <div class="container bck1 padded">
            {% block body %}
            {% endblock %}
        </div>

        {% block footer %}
            <footer id="footer" class="bck1 border-top">
                <div class="container">
                    <h4>Footer</h4>
                </div>
            </footer>
        {% endblock %}

        {% block javascripts %}{% endblock %}
    </body>
</html>

Le layout présenté ci-dessus inclus en plus de bootstrap un fichier css custom permettant d’ajouter nos propres classe de présentation. Voici les quelques lignes ajouté pour ma part dans « /public/assets/css/styles.css »

/*base*/
body{background: #ebeff2;}
.bck1{background: #fff;}

/*generique*/
.padded{padding: 15px;}

/*header*/
header#header {margin-bottom: 20px;padding: 10px;}
header#header h1 {text-align: center;color: #5cb85c;}

/*footer*/
footer#footer{margin-top: 20px;padding: 10px;}

Et pour que nos formulaires s’affichent en suivant les conventions de bootstrap 4 on remercie symfony et sa communauté, une seule ligne de configuration suffit ! Dans « /config/packages/twig.yaml » :

twig:
    form_themes
: ['bootstrap_4_layout.html.twig']

Pour la page d’accueil le simple controller « /src/Controller/HomepageController.php » suffira :

namespace App\Controller;

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

class HomepageController extends Controller {

    /**
     * @Route("/")
     */

    public function index() {
        return $this->render('homepage/index.html.twig', ['mainNavHome'=>true, 'title'=>'Accueil']);
    }

}

Rien de spécial pour la vue il suffit de créer le fichier correspandant « /templates/homepage/index.html.twig » de la même façon qu’on l’a fait pour « inscription » et « login », avec le contenu que l’on souhaite.

Le dernier points à voir, c’est la création des « espace membre » et « espace administrateur » limité selon les rôles de l’utilisateur. Il suffit pour cela, en accord avec la configuration faite dans « /config/packages/security.yaml » de faire commencer les routes par « /member » ou « /admin » selon ce que l’on souhaite. Voici pour l’espace utilisateur (fichier « /src/Controller/MemberController.php ») :

namespace App\Controller;

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

/** @Route("/member") */
class MemberController extends Controller {

    /**
     * @Route("/")
     */

    public function index() {
        return $this->render('member/index.html.twig', ['mainNavMember'=>true, 'title'=>'Espace Membre']);
    }

}

Exactement pareil pour l’admin, sauf que dans un soucis d’organisation je place le fichier dans un dossier admin, ce qui donne « src/Controller/Admin/HomepageController.php : « 

namespace App\Controller\Admin;

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

/** @Route("/admin") */
class HomepageController extends Controller {

    /**
     * @Route("/")
     */

    public function index() {
        return $this->render('admin/homepage/index.html.twig', ['mainNavAdmin' => true, 'title' => 'Espace Admin']);
    }

}

Je ne rajoute pas le code des « views » car rien de particulier et donc même processus que pour la page d’accueil.

C’est terminé pour ce long article à réutiliser à souhait lors du démarrage d’un projet symfony 4

WordPress : Fonctions de gestions des utilisateurs

Ci-dessous un petit mémo de quelques fonctions permettant la gestion des utilisateurs sous WordPress

//Savoir si le visiteur est connecté en tant qu'utilisateur :
is_user_logged_in();

//Récupération de l'identifiant de l'utilisateur courant :
get_current_user_id();

//Récupération de l'objet user courant et affichage de son nom
$user = wp_get_current_user();
echo $user->display_name;

//Afficher l'avatar d'un utilisateur (l'utilisateur courant dans ce cas) avec une taille de 60px
echo get_avatar(get_current_user_id(), 60);

//Récupérer la valeur meta unique "nom_de_le_meta" pour l'utilisateur courant :
get_user_meta(get_current_user_id(), 'nom_de_le_meta', true);

//Enregistrer la valeur "mavaleur" pour la meta unique "nom_de_le_meta" de l'utilisateur courant :
add_user_meta(get_current_user_id(), 'nom_de_le_meta', "mavaleur", true);

WordPress : Utilisation de l’ajax dans WordPress

Voici un post d’exemple sur l’utilisation de l’ajax dans WordPress. Nous allons voir à travers la création d’un mini plugin, comment lancer une requête ajax utilisant le système conçu pour ça dans wordpress.

Le plugin sera utilisable en plaçant le shortcode

[submit-input-ajax]

dans une page. La démonstration est ici

On avait déjà vu comment créer un plugin permettant l’utilisation d’un shortcode. Pour ajouter de l’ajax il faudra ajouter certains éléments « wp_ajax_ ». Le code fonctionnel du plugin est présenté ci-dessous :

/*
  Plugin Name: Plugin action ajax
  Description: Demonstration d'un plugin effectuant une action ajax
 */


class PluginActionAjax {

    public function __construct() {
        //shortcode permettant d'afficher le formulaire avec le champs input et le bouton de soumission
        add_shortcode('submit-input-ajax', array($this, 'shortcode_submit_input_ajax'));
        //chargement des scripts à inclures (css, js...)
        add_action('wp_enqueue_scripts', array($this, 'action_ajax_scripts'));
        //appel ajax de notre fonction "process_action_ajax" si l'utilisateur est loggé "wp_ajax_" ou si il ne l'est pas "wp_ajax_nopriv_"
        add_action('wp_ajax_process_action', array($this, 'process_action_ajax'));
        add_action('wp_ajax_nopriv_process_action', array($this, 'process_action_ajax'));
    }

    /**
     * génère le formulaire lorsqu'on utilise le plugin [submit-input-ajax]
     */

    public function shortcode_submit_input_ajax($atts) {
        $html = '<form class="submit-input-ajax-container">';
        $html .= '<div><label for="submit-input-ajax">Veuillez entrez une information et valider en cliquant sur le bouton</label></div>';
        $html .= '<input type="text" id="submit-input-ajax" />';
        $html .= '<button type="submit" id="btn-submit-input-ajax" >Valider</button>';
        $html .= '</form>';
        return $html;
    }

    /**
     * traite l'appel ajax
     */

    public function process_action_ajax() {
        //verifie l'existence non vide de l'input
        if (isset($_POST['input']) && $_POST['input'] != '') {
            //n'importe quel traitement
            $message = 'Voici votre message en sha1 : ';
            $message .= sha1($_POST['input']);
            //retour success
            wp_send_json_success(['message' => $message]);
        }
        //retour erreur
        wp_send_json_error(['message' => 'Impossible !']);
    }

    /**
     * Appel des fichiers js/css nécessaires au fonctionnement
     */

    public function action_ajax_scripts() {
        global $post;
        //si notre shortcode est présent dans le post courant.
        if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'submit-input-ajax')) {
            wp_enqueue_style('paa-style', plugin_dir_url(__FILE__) . '/styles.css');
            //ajout du script lancant la requete ajax
            wp_enqueue_script('paa-ajax', plugin_dir_url(__FILE__) . '/script.js', array('jquery'));
            //on informe le script précédent de l'adresse "adminAjax" de wordpress
            wp_localize_script('paa-ajax', 'adminAjax', admin_url('admin-ajax.php'));
        }
    }

}
new PluginActionAjax();

Il ne reste plus qu’a créer le fichier script.js qui lancera l’appel de « process_action » lors de la soumission du formulaire généré par le shortcode. Il contient le code suivant :

jQuery(document).ready(function () {
    /*lors de la soumission du formulaire, on vérifie que le champ est replie et on appelle notre action ajax*/
    jQuery(document).on("submit", ".submit-input-ajax-container", function (e) {
        e.preventDefault();
        var input = jQuery("#submit-input-ajax").val();
        if (input == "") {
            alert("Vous devez completer le champ ! ");
        } else {
            jQuery.ajax({
                //apel de l'url ajax wordpress
                url: adminAjax,
                method: 'POST',
                data: {
                    //nom de notre action déclaré dans le plugin "wp_ajax_..."
                    action: 'process_action',
                    input: input
                },
                success: function (data) {
                    alert(data.data.message);
                },
                error: function (data) {
                    //erreur
                }
            });
        }
    });
});

Pour des explication détaillées, vous pouvez vous rendre sur cette page

Jquery : Trier des éléments en fonction d’attributs data-*

Nous allons voir aujourd’hui comment classer des éléments d’une page en fonctions de certaines données (nom, position) grâce à quelques lignes de javascript.
La démonstration du résultat que l’on va obtenir est visible ici

On commence par créer notre html de manière classique, comme d’habitude j’importe bootstrap et jquery pour la forme et la simplicité.
En plus de ça on affiche dans notre page une série d’élément ayant la classe « card » ainsi que des attributs « data-position », « data-note » et « data-name » pour stocker les valeurs sur lesquelles on va effectuer le classement. On affiche auparavant un « select » qui nous permettra de choisir l’ordre que l’on souhaite afficher.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <!-- CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
        <link rel="stylesheet" href="./css/style.css">
        <!-- JS -->
        <script src="./js/jquery-3.3.1.min.js"></script>
        <script src="./js/custom.js"></script>
    </head>
    <body>
        <div class="container main-container">
            <div class="form-group">
                <label for="select-order">Classer par</label>
                <select class="form-control" id="select-order">
                    <option value="position">Position</option>
                    <option value="note">Note</option>
                    <option value="name">Nom</option>
                </select>
            </div>
            <div class="card-container row">
                <div class="item col-md-4" data-position="1" data-note="8" data-name="Paul">
                    <div class="card mb-3">
                        <img class="card-img-top" src="http://lorempixel.com/400/200/abstract" alt="illustration">
                        <div class="card-body">
                            <h3 class="card-title">Paul</h3>
                            <div class="card-subtitle">Position : 1</div>
                            <div class="card-text">Note : 8</div>
                        </div>
                    </div>
                </div>
                <!-- d'autres éléments avec différentes valeurs -->
            </div>
        </div>
    </body>
</html>

On va maintenant s’occuper du javascript qui nous permettra de réordonner les éléments, il s’agit de détecter le choix sélectionné et de lancer le tri (numérique, numérique inversé ou alphabétique) sur nos attributs data-*. Le code suivant est suffisant :

$(document).ready(function () {
    /* à la sélection du classement on met à jour */
    $(document).on("change", "#select-order", function () {
        updateOrder();
    });
    /* au chargement de la page on lance le classement */
    updateOrder();
});

function updateOrder() {
    var order = $("#select-order").val();
    if (order == 'position' || order == 'note' || order == 'name') {
        var divList = $(".card");
        if (order == 'position') { //tri numerique croissant
            divList.sort(function (a, b) {
                return  $(a).data(order) - $(b).data(order);
            });
        } else if (order == 'note') {//tri numerique decroissant
            divList.sort(function (a, b) {
                return $(b).data(order) - $(a).data(order);
            });
        } else if (order == 'name') { //tri alphabetique
            divList.sort(function (a, b) {
                var compA = $(a).data(order).toUpperCase();
                var compB = $(b).data(order).toUpperCase();
                return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
            });
        }
        $(".card-container").html(divList);
    }
}

Symfony : Afficher des dates en français dans Twig

Lorsque travaille sous Symfony, avec des templates twig, on peut afficher des dates (généralement stocké coté PHP sous forme de DateTime()) grâce au filtre « date » de la façon suivante :

{{maDate|date('d/m/Y')}}//affiche la date au format 22/01/2018

Mais si l’on veux un affichage textuel en français du type « lundi 22 janvier 2018 » il nous faut utiliser une extension twig.
Pour installer cette extensions (ainsi que d’autres) dans votre projet, il faut lancer la commande composer suivante :

composer require twig/extensions

Nous devons ensuite configurer l’extension « intl » qui nous intéresse, dans le fichier de configurations des services :

services:
    twig.extension.intl
:
        class
: Twig_Extensions_Extension_Intl
        tags
:
            - { name
: twig.extension }

Le filtre « localizeddate » est maintenant utilisable et permet d’afficher la date au format que l’on souhaite, et dans la langue que l’on souhaite (en utilisant par défaut la langue configuré dans notre projet symfony) En voici 2 exemples.

{{entity.dateEvent|localizeddate('none', 'none', null, null, 'EEEE')}}//lundi
{{entity.dateEvent|localizeddate('none', 'none', null, null, 'MMMM Y')}}//Janvier 2018

Pour plus d’information veuillez vous référez au documentations suivantes :
http://twig-extensions.readthedocs.io/en/latest/intl.html#localizeddate
http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax

jQuery : Accéder à des champs de formulaire ayant un nom dans un tableau

Avec jQuery on peux récupérer la valeur d’un champ en le sélectionnant grâce à son nom (attribut name).
Par exemple pour connaitre la valeur sélectionné des radio boutons suivants :

<label for="radio1"><input name="choix" value="1" type="radio" id="radio1"> Choix 1</label>
<label for="radio2"><input name="choix" value="2" type="radio" id="radio2"> Choix 2</label>
<label for="radio3"><input name="choix" value="3" type="radio" id="radio3"> Choix 3</label>

Il suffit de faire :

$("input[name='choix']:checked").val();

Par contre si le nom est composé, comme dans l’exemple ci-dessous :

<label for="radio1"><input name="mon_formulaire[choix]" value="1" type="radio" id="radio1"> Choix 1</label>
<label for="radio2"><input name="mon_formulaire[choix]" value="2" type="radio" id="radio2"> Choix 2</label>
<label for="radio3"><input name="mon_formulaire[choix]" value="3" type="radio" id="radio3"> Choix 3</label>

Il faut utiliser les carractères d’échappement « \\ » pour que ça fonctionne. Comme ci-dessous.

$("input[name='mon_formulaire\\[choix\\]']:checked").val()

Doctrine : Recherche dans une table contenant des latitudes et longitudes celle situés à moins de « XX » km

Il y’a quelques mois je présentais une fonction PHP permettant de calculer la distance entre 2 coordonnées GPS (latitude et longitude). Coordonnées qui peuvent par exemple être récupérer via l’API Google Map

Nous allons voir dans cette article, comment récupérer toutes les enregistrements d’une table qui sont situé à moins de « 50km » d’une position précise. Il nous faut donc les coordonées de la position pour laquelle on fait la recherche, ainsi qu’une table avec les colonnes « lat » et « lng »

La formule permettant de faire le calcul (en km) directement en MySql et la suivante :

//la formule utilisant nos coordonées "$lat" et "$lng" ainsi que les colonne "lat" et "lng" de la table "table"
$sqlDistance = '(6378 * acos(cos(radians(' . $lat . ')) * cos(radians(table.lat)) * cos(radians(table.lng) - radians(' . $lng . ')) + sin(radians(' . $lat . ')) * sin(radians(table.lat))))';

Formule utilisable pour rechercher les enregistrements inférieur à 50km de notre position, via le requête suivante :

$requete = 'SELECT * FROM table WHERE '.$sqlDistance.' < 50 ';

Si l’on travaille sous Symfony et avec Doctrine, en ajoutant notre condition dans un « querybuilder » comme ci-dessous ça ne va pas fonctionner.

//Imaginons que nous sommes dans une requête classique crée avec le querybuilder $qb
//On veut rajouter la condition de la distance (en utilisant une $distance variable dans ce cas).
$qb->andWhere("" . $sqlDistance . " < :distance")->setParameter('distance', $distance);

Doctrine ne connait pas les différentes fonctions de calculs « acos », « cos », « radian » et « sin » utilisées dans la formule. Nous allons les importer grâce à la bibliothèque DoctrineExtensions. Pour cela, il suffit de faire un petit coup de composer sur notre projet :

composer require beberlei/DoctrineExtensions

Et ensuite d’indiquer les fonctions dont on à besoin dans notre configuration de doctrine de la manière suivante :

doctrine:
    orm
:
        dql
:
            numeric_functions
:
                acos
: DoctrineExtensions\Query\Mysql\Acos
                cos
: DoctrineExtensions\Query\Mysql\Cos
                radians
: DoctrineExtensions\Query\Mysql\Radians
                sin
: DoctrineExtensions\Query\Mysql\Sin

Tout doit à présente fonctionner. Vous pouvez de cette manière importer une flopée de fonctions, la configuration complète étant disponible Sur cette page

Javascript : Mettre en place un lecteur audio avec « AmplitudeJS »

Aujourd’hui nous allons découvrir AmplitudeJS qui permet de mettre en place sur vos page un lecteur audio flexible et complètement personnalisable. Comme on peux le voir sur leurs exemples avec un peu de travail on peux avoir des résultats très sympas.

Avant d’en arriver la, je vous propose un exemple bien plus simple. Le résultat est visible ici et il suffit de regarder le code source de la page pour comprendre le fonctionnement.

Le visuel obtenu sera le suivant, mais graphiquement tout est possible, il n’y à pas de contraintes à ce niveau.

Quelques explications supplémentaires ci-dessous.

Pour commencer on va inclure la « library » « amplitude.js », ainsi que « bootstrap », « font-awesome » et un fichier « styles.css » pour la mise en page.

    <!-- CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" >
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
    <link rel="stylesheet" href="./css/style.css">
    <!-- JS -->
    <script src="./js/amplitude.min.js"></script>

Dans le corps de la page mettons en place notre player en utilisant les éléments « amplitude », les boutons « bootstrap » et les icones « font-awesome » :

    <div class="glob-player">
        <div class="glob-btn">
            <span class="amplitude-prev btn btn-primary"><i class="fa fa-step-backward" aria-hidden="true"></i></span>
            <span class="btn amplitude-play-pause btn-primary" amplitude-main-play-pause="true"><i class="fa fa-play" aria-hidden="true"></i> <i class="fa fa-pause" aria-hidden="true"></i></span>
            <span class="my-next-btn amplitude-next btn btn-primary"><i class="fa fa-step-forward" aria-hidden="true"></i></span>
        </div>
        <div>
            <div class="meta-name" amplitude-song-info="name" amplitude-main-song-info="true"></div>
            <progress class="amplitude-song-played-progress" amplitude-main-song-played-progress="true" id="song-played-progress"></progress>
        </div>
    </div>

On ajoute juste quelques ligne pour le style dans notre fichier « styles.css »

/* player */
.glob-player{padding: 10px;border-radius: 5px;background: #000;}
.glob-player .glob-btn{text-align: center;}
.glob-player .meta-name{color:#fff;text-align: center;padding:10px;}
/*progres-bar*/
.amplitude-song-played-progress {
    -webkit-appearance: none;-moz-appearance: none;appearance: none;
    background-color: #ccc;width: 100%;height: 5px;display: block;border: none;
}
progress.amplitude-song-played-progress[value]::-webkit-progress-bar {
    background-color: #0069d9;
}
progress.amplitude-song-played-progress[value]::-moz-progress-bar {
    background-color: #0069d9;
}
progress.amplitude-song-played-progress[value]::-webkit-progress-value {
    background-color: #0069d9;
}

Il nous suffit maintenant d’uploader nos MP3 dans le dossier que l’on souhaite et d’indiquer à « amplitude » de les prendre en charge.

<script>
    Amplitude.init({
        "songs": [
            {
                "name": "Le bal masqué",
                "artist": "Opium du peuple",
                "album": "La révolte des opiumettes",
                "url": "./song/le-bal-masque.mp3",
            },
            {
                "name": "Poupée de cire, poupée de son",
                "artist": "Opium du peuple",
                "album": "La révolte des opiumettes",
                "url": "./song/poupee-de-cire.mp3",
            }
        ],
        callbacks: {
            //pour démarrer la lecture à cuaque fois que l'on passe au morceau suivant ou préc
            song_change: function () {
                Amplitude.play();
            }
        }
    });
</script>

jQuery : Sélection des départements français sur une carte

Dans cet article nous allons voir comment transformer un select multiple contenant la liste des département français en carte permettant de sélectionner/désélectionner les départements en cliquant dessus.

Nous allons passer d’un select multiple classique comme celui-ci :

à un sélecteur visuel comme ci-dessous :

Une page de démonstration est visible ici.

Pour cela nous utiliseront jQuery, le plugin jVectorMap et sa carte pour les départements français disponible ici

On attaque par la mise en place du code html, il s’agit d’un select multiple classique, on va juste prendre soin de lui donner un identifiant « map-selector » et de l’englober dans un « div » parent. Pour l’utiliser avec la carte, nous allons donner pour chaque département la valeur « FR-CODE_DU_DEPARTEMENT », exemple « FR-34 » pour l’Hérault.

<div>
    <select id="map-selector" name="departements" multiple="multiple">
        <option value="FR-01">Ain</option>
        <option value="FR-02">Aisne</option>
        <option value="FR-03">Allier</option>
        <!-- etc... -->
    </select>
</div>

On inclus dans la page les script nécessaires (jquey, jvectormap, la carte, un fichier « map-selector.js » ou l’on mettra notre code ainsi qu’une feuille de styles css.)

<link rel="stylesheet" href="jquery-jvectormap-2.0.3.css">
<link rel="stylesheet" href="style.css">
<script src="jquery-3.2.1.min.js"></script>
<script src="jquery-jvectormap-2.0.3.min.js"></script>
<script src="jquery-jvectormap-fr-merc.js"></script>
<script src="map-selector.js"></script>

Tout est prêt ! Il nous reste plus qu’à coder notre fichier « map-selector.js » pour mettre en place le système. Le code de quelques lignes commenté ci-dessous :

$(document).ready(function () {
    //on masque le select classique
    $("#map-selector").css("display", "none");
    //on ajoute un div #container-map-selector qui contiendra la carte
    $("#map-selector").parent().append("<div id='container-map-selector'></div>");
    //on initie la carte sur cet élément
    var map = new jvm.Map({
        container: $("#container-map-selector"),
        map: 'fr_merc',
        regionsSelectable: true,
       //à chaque clic sur un département
        onRegionSelected: function () {
            //on vide le select
            $("#map-selector").val("");
            //et on sélectionne chaque options correspondant au département sélectionné sur la carte
            $.each(map.getSelectedRegions(), function (index, region) {
                $("#map-selector option[value=" + region + "]").prop("selected", true);
            });
        }
    });
    //au départ si des options du select sont présélectionnés, on les sélectionnes sur la carte
    $("#map-selector option:selected").each(function () {
        map.setSelectedRegions($(this).val());
    });
});

Symfony : Utilisation du « Simple Cache »

Depuis sa version 3.1, Symfony dispose d’un Cache Component permettant comme son nom l’indique de gérer un système de cache assez facilement. Depuis la version 3.3 du framework, une version simplifié est disponible.

C’est un jeu d’enfant de mettre une information en cache, la récupérer et la supprimer

use Symfony\Component\Cache\Simple\FilesystemCache;
#
$cache = new FilesystemCache();
$cache->set('mon.information', 'Je place ce texte en cache'); //mise en cache
$cache->get('mon.information'); //récupération
$cache->delete('mon.information'); //suppression

Cela peut être utilisé par exemple pour stocker des paramètres de configuration.
Afin de permettre à l’utilisateur de notre application de modifier ces paramètres depuis un back-office on les stocke en base de données. Cependant pour ne pas avoir a faire une requête à chaque fois que l’on doit utiliser un des paramètre, on les met en cache.

Voici un cas concret d’utilisation :
– Mon utilisateur peut depuis le back-office créer des « Organization » (entité), il leur donne un nom et peux cocher 4 différents bloc d’options possible pour cette « organization ».
– J’ai plus tard dans mon application besoin de connaitre la liste des « Organization » ayant telle option.
Pour des question d’optimisation (moins de requêtes) mais aussi pour des questions pratiques je vais utiliser le système de cache.
Ci dessous le service « CacheManager » que je vais utiliser :

namespace AppBundle\Service;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Cache\Simple\FilesystemCache;
use AppBundle\Entity\Organization;

class CacheManager {

    private $em;

    public function __construct(EntityManagerInterface $em) {
        $this->em = $em;
    }

    /**
     * Met à jour les informations en cache.
     * Stocke un tableau contenant les identifiants des "organization" ouvertes aux différentes options
     */

    public function updateOrganizationOptions() {
        $cache = new FilesystemCache();
        $entities = $this->em->getRepository(Organization::class)->findAll();
        $option1 = [];
        $option2 = [];
        $option3 = [];
        $option4 = [];
        foreach ($entities as $entity) {
            if ($entity->getOptions1()) {
                $option1[] = $entity->getId();
            }
            if ($entity->getOptions2()) {
                $option2[] = $entity->getId();
            }
            if ($entity->getOptions3()) {
                $option3[] = $entity->getId();
            }
            if ($entity->getOptions4()) {
                $option4[] = $entity->getId();
            }
        }
        $cache->set('organization.ids.options1', $option1);
        $cache->set('organization.ids.options2', $option2);
        $cache->set('organization.ids.options3', $option3);
        $cache->set('organization.ids.options4', $option4);
    }

    public static function getOrganizationOptions($num = 1) {
        $cache = new FilesystemCache();
        $options = $cache->get('organization.ids.options' . $num);
        return $options;
    }

}

J’utilise ce service pour mettre à jour le cache des options à la création ou la modification d’une « organization ».
Dans les controllers correspondants c’est fait de la façon suivante :

//traitement classique (si le formulaire posté par l’utilisateur est valide, j'enregistre les données)
//+
$CM = $this->get('app.cache.manager');//appel du service
$CM->updateOrganizationOptions();//mise à jour du cache

N’oubliez pas que si les données peuvent être modifiées depuis plusieurs endroits, vous pouvez utiliser les Event listeners de doctrine pour automatiser le traitement et être sur d’avoir toujours les informations à jour.

Je peux maintenant récupérer ma liste d’organisations ouverte à telle option n’importe ou grâce à ma méthode statique :

CacheManager::getOrganizationOptions(1)

Dans la réalité il s’agissait pour moi de proposer les différents champs correspondants au options dans un formulaire. Formulaire dans lequel on choisissait une organisation dans une liste déroulante, en fonction du choix certaines options apparaissaient donc. Idem pour le contrôle lors de la soumission du formulaire afin de valider l’entité sans passer dans l’organization liée.

Javascript : Faire parler le navigateur

les navigateurs récents possèdent une fonctionnalité JavaScript permettant de leur demander de lire un texte.

Il s’agit de la « Web Speech API », et il suffit des quelques lignes suivantes pour demander à votre navigateur de déclamer un texte dans la langue de votre souhait

//création de l'objet message avec le texte de votre choix
var message = new SpeechSynthesisUtterance("Mon message texte");
//on paramètre la langue
message.lang = 'fr-FR';
//on lance la lecture du message
window.speechSynthesis.speak(message);

Je ne résiste pas à vous proposer une page de démonstration pour faire mumuse !

Symfony : Obtenir un enregistrement aléatoire avec doctrine

Doctrine ne permet pas par défaut d’obtenir un enregistrement aléatoire, ce qui est pourtant en principe très simple avec mysql. Par contre il est possible de faire une requête native et de demander a doctrine de « mapper » les résultats. Il devient alors facile de récupérer notre enregistrement aléatoire sous le format habituel (entité doctrine). Voici ci-dessous la méthode d’un repository permettant de la faire.

class ExempleRepository extends \Doctrine\ORM\EntityRepository {

    public function findRandom() {
        $rsm = new \Doctrine\ORM\Query\ResultSetMappingBuilder($this->getEntityManager());
        $rsm->addRootEntityFromClassMetadata(\AppBundle\Entity\Exemple::class, 'exemple');
        $sql = "SELECT * FROM exemple ORDER BY RAND() LIMIT 1";
        return $this->getEntityManager()->createNativeQuery($sql, $rsm)->getOneOrNullResult();
    }

}

jQuery : rendre l’option d’un select permanente avec le plugin « select2 »

Le plugin jQuery « Select2 » permet d’améliorer les champs de type « select » (liste déroulante). Une de ces principales fonctionnalité et d’intégrer un champs de recherche à la liste. Très pratique pour retrouver et sélectionner un éléments dans une longue liste.

Nous allons voir ci-dessous comment mettre en place une astuce afin qu’une option de la liste reste toujours disponible. Afin qu’un résultat de secours soit toujours sélectionnable quand on recherche un élément inexistant.

Notre exemple consiste à sélectionner un instrument dans la liste, avec la possibilité toujours visible de sélectionner « Autre instrument » si celui que l’on recherche n’est pas disponible. On en profitera pour afficher des champs supplémentaires quand cette option est sélectionné afin de pouvoir récolter les informations concernant l’instrument réellement recherché.

L’exemple est disponible ici

Comme d’habitude on commence par inclure jQuery, le plugin select2 et un fichier qui contiendra notre script dans le head de la page :

<!-- jQuery -->
<script src="./js/jquery-3.2.1.min.js"></script>
<!-- select2 -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.4/css/select2.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.4/js/select2.min.js"></script>
<!--custom -->
<script src="./js/main.js"></script>

On met en place notre html de manière classique, avec une option de valeur « autre » dans la liste des option de notre champs « select »

<div class="form-group">
    <label for="instrument">Instrument</label>
    <select id="instrument" class="form-control" name="instrument">
        <option value="1">accordéon</option>
        <option value="2">bandonéon</option>
        <option value="3">banjo</option>
        <option value="4">batterie</option>
        <!-- Toutes les autres options -->
        <!-- Notre option de valeur "autre" -->
        <option value="autre">Autre instrument</option>
    </select>
</div>
<!-- les autre champs à afficher/masquer selon le choix dans la liste précédente -->
<div class="form-group">
    <label for="otherInstrumentCategory">Catégorie de l'instrument</label>
    <select class="form-control" id="otherInstrumentCategory" name="otherInstrumentCategory" >
        <option value="1">Instrument à corde</option>
        <option value="2">Percussion</option>
        <option value="4">Instrument à vent</option>
        <option value="5">MAO</option>
        <option value="6">Autre</option>
    </select>
</div>
<div class="form-group">
    <label for="otherInstrument">Nom de l'
instrument</label>
    <input type="text" id="otherInstrument" name="otherInstrument" class="form-control">
</div>

Et pour finir notre javascript qui va permettre de toujours laisser en place l’option « autre instrument » malgré une recherche, ainsi que d’afficher ou masquer les champs supplémentaires. Rien de bien complexe, suivez les commentaires dans le code pour comprendre.

$(document).ready(function () {
    /*au changement du champs instrument on met à jour l'affichage des champs*/
    $(document).on("change", "#instrument", function () {
        updateFields();
    });
    /*initialisation du champs instrument, pour l'utilisation du plugin select2 avec un filtre de recherche particulier*/
    $('select#instrument').select2({matcher: select2matchCustomInstrument});
    /*initialisation de 'laffichage des champs*/
    updateFields();
});

/*On va utiliser le système de recherche classique du plugin select2*/
var defaultMatcher = $.fn.select2.defaults.defaults.matcher;
function select2matchCustomInstrument(params, data) {
    /* Si l'option est "Autre" on l'affiche toujours */
    if (params.term && data.id == "autre") {
        return data;
    }
    /* Sinon utilisation de la recherche classique */
    return defaultMatcher(params, data);
}

/*la fonction de mise à jour de l'affichage des champs*/
function updateFields() {
    /* Si la valeur du champ instrument est "autre" on affiche les champs supplémentaire, sinon on les masque */
    if ($("#instrument").val() == "autre") {
        $("#otherInstrumentCategory").parents(".form-group").css("display", "block");
        $("#otherInstrument").parents(".form-group").css("display", "block");
    } else {
        $("#otherInstrumentCategory").parents(".form-group").css("display", "none");
        $("#otherInstrument").parents(".form-group").css("display", "none");
    }
}

Ce plugin peux être adapté de façon diverse, comme par exemple pour générer un font-picker comme vu dans un article précédent.

Symfony : Automatiser des actions avec les « Event listeners » de doctrine

Le framework Symfony couplé à l’ORM doctrine permet de créer des « Event Listeners », c’est à dire connaitre dés qu’il y’a un événement (création/modification/suppression) sur une entité. C’est un outil très intéressant pour automatiser des actions quand certaines propriétés d’une entité sont mis à jour.

Prenons un exemple concret : Dans les semaines précédentes, nous avons vu comment récupérer les coordonnées d’une adresse grâce à l’API google map. Afin de limiter les appels à l’API google map (payants si un certain quota et dépassé) nous allons enregistrer ces coordonnées en base de données pour pouvoir les réutiliser directement. Il nous faut cependant les enregistrer/mettre à jour à chaque fois qu’une adresse est crée ou modifiée. C’est ici que nous faisons intervenir les Event Listener.

Commençons par configurer nos listeners dans les services (fichiers services.yml du dossier /app/config/) de symfony.

services:
   #...
    #LISTENER DOCTRINE#
    app.listener.doctrineevent
:
        class
: AppBundle\EventListener\DoctrineEvent
        tags
:
            - { name
: doctrine.event_listener, event: prePersist, lazy: true }
            - { name
: doctrine.event_listener, event: preUpdate, lazy: true }

La configuration indique que nous allons écouter les événements « PrePersist » et « PreUpdate ». Ce qui permettra d’effectuer notre action lors de la création d’une adresse et lors de sa modification. Créons la classe qui va gérer tout ça. Comme indiqué il s’agit du fichier : AppBundle\EventListener\DoctrineEvent

Pour appréhender la suite, consultez les commentaires dans le code

namespace AppBundle\EventListener;

use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
//les adresses de nos utilisateur sont stockés dans une entité "Contact"
use AppBundle\Entity\Contact;

class DoctrineEvent implements EventSubscriber {

    public function getSubscribedEvents() {
        return array('prePersist', 'preUpdate');//les événements écoutés
    }

    public function prePersist(LifecycleEventArgs $args) {
        $entity = $args->getEntity();
        //Si c'est bien une entité Contact qui va être "persisté"
        if ($entity instanceof Contact) {
            $entity->updateGmapData();//on met à jour les coordonnées via l'appel à google map
        }
    }

    public function preUpdate(LifecycleEventArgs $args) {
        $entity = $args->getEntity();
        $changeset = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($entity);
        //Si c'est bien une entité Contact qui va être modifié
        if ($entity instanceof Contact) {
            //Si il y'a eu une mise a jour sur les propriétés en relation avec l'adresse (ici "address", "city" et "postalCode")
            if (array_key_exists("address", $changeset) || array_key_exists("city", $changeset) || array_key_exists("postalCode", $changeset)) {
                $entity->updateGmapData();//on met à jour les coordonnées via l'appel à google map
            }
        }
    }

}

De cette manière nos coordonnées seront toujours à jour. Il manque pour que l’exemple soit complet la fonction « updateGmapData() » de notre entité Contact. Cela ne concerne pas le sujet des Event Listener, mais je vous en met un exemple qui utilise bien sur la fonction « geocodeAddress() » détaillé ici

    public function updateGmapData() {
        $data = \AppBundle\Utils\GmapApi::geocodeAddress($this->getAddress() . ' ' . $this->getZipcode() . ' ' . $this->getCity());
        $this->setGmapLat($data['lat']);
        $this->setGmapLng($data['lng']);
        $this->setGmapAddress($data['address']);
        $this->setGmapPostalCode($data['postal_code']);
        $this->setGmapCity($data['city']);
        $this->setGmapDepartment($data['department']);
        $this->setGmapRegion($data['region']);
        $this->setGmapCountry($data['country']);
    }