Archives de l’auteur : Numa

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']);
    }

jQuery : soumettre un formulaire contenant des fichiers grâce à FormData

Aujourd’hui il est possible d’uploader des fichiers en ajax grâce à XMLHttpRequest Niveau 2 et l’interface FormData de javascript. Pour l’utiliser avec la méthode ajax de jQuery il suffit de lui passer comme données l’objet FormData et de configurer les options correctement comme ci-dessous.

    //au clic du bouton submit des formulaires ayant la classe "form-ajax"
    $(document).on("submit", ".form-ajax", function (e) {
        e.preventDefault();//on bloque l'envoie classique
        var formData = new FormData($(this)[0]);//récupération des données du formulaire dans l'objet FormData
        //appel ajax jQuery
        $.ajax({
            url: '/ma/route/ajax',
            method: "POST",
            data: formData,
            processData: false,//ne pas oublier cette option
            contentType: false,//ne pas oublier cette option
            error: function (msg, string) {
                //gestion des erreurs
            },
            success: function (response) {
                //gestion de la réponse
            }
        });
    });

De cette manière vous pouvez traiter coté serveur le formulaire de la même manière que si il n’était pas en ajax.

Pour un exemple de traitement d’upload d’image coté serveur en php vous pouvez vous référer a cet article

Symfony : Afficher toutes les erreurs d’un formulaire dans une liste

Mise à jour le 03/04/2020 pour fonctionner avec Symfony 5

Par défaut l’affichage des erreurs sur les formulaires dans symfony se fait de la manière suivante :

  • Les erreurs globales (qui ne concerne pas un champ particulier) sont affichés en haut.
  • Les erreurs concernant chaque champs, sont affiché au niveau du champ.

Dans certains cas, par exemple sur des longs formulaires, il peut être intéressant de retrouver la liste de toutes les erreurs au mêmes endroits. Voici ci-dessous un code permettant de le faire (pour un formulaire « form »)

{% if not form.vars.valid %}
    <ul class="alert alert-danger">
        {# on affiche en premier les erreurs globales (cas classiques) #}
        {% for error in form.vars.errors %}
            <li>{{error.message}}</li>
        {% endfor %}
        {# ainsi que les erreurs concernant chaque champs #}
        {% for child in form.children %}
            {# si le champ à des erreur #}
            {% if child.vars.errors is defined %}
                {% for error in child.vars.errors %}
                    <li>"{{child.vars.label}}" : {{error.message}} </li> {# on ajoute le label du champ devant l'erreur pour plus de précisions. #}
                {% endfor %}
            {% endif %}
        {% endfor %}
    </ul>
{% endif %}

PHP & Google Map API : Récupérer les détails d’une adresse (latitude, longitude, département, région…)

La semaine dernière nous avons vu une fonction permettant de calculer la distance entre deux coordonnées GPS, mais comment récupérer ces coordonnées (et d’autres informations) à partir d’une adresse ? Nous allons voir comment faire avec l’API google Map, il vous faudra tout d’abord récupérer une clé pour les appel à l’API (par ici).

Ensuite vous pouvez utiliser la classe et la fonction ci-dessous afin de récupérer les informations (adresse complète, latitude, longitude, vile, code postal, département, région, pays) correspondant à l’adresse passé au format texte.

class GmapApi {

    private static $apikey = 'VOTRE_API_KEY';

    public static function geocodeAddress($address) {
        //valeurs vide par défaut
        $data = array('address' => '', 'lat' => '', 'lng' => '', 'city' => '', 'department' => '', 'region' => '', 'country' => '', 'postal_code' => '');
        //on formate l'adresse
        $address = str_replace(" ", "+", $address);
        //on fait l'appel à l'API google map pour géocoder cette adresse
        $json = file_get_contents("https://maps.google.com/maps/api/geocode/json?key=" . self::$apikey . "&address=$address&sensor=false&region=fr");
        $json = json_decode($json);
        //on enregistre les résultats recherchés
        if ($json->status == 'OK' && count($json->results) > 0) {
            $res = $json->results[0];
            //adresse complète et latitude/longitude
            $data['address'] = $res->formatted_address;
            $data['lat'] = $res->geometry->location->lat;
            $data['lng'] = $res->geometry->location->lng;
            foreach ($res->address_components as $component) {
                //ville
                if ($component->types[0] == 'locality') {
                    $data['city'] = $component->long_name;
                }
                //départment
                if ($component->types[0] == 'administrative_area_level_2') {
                    $data['department'] = $component->long_name;
                }
                //région
                if ($component->types[0] == 'administrative_area_level_1') {
                    $data['region'] = $component->long_name;
                }
                //pays
                if ($component->types[0] == 'country') {
                    $data['country'] = $component->long_name;
                }
                //code postal
                if ($component->types[0] == 'postal_code') {
                    $data['postal_code'] = $component->long_name;
                }
            }
        }
        return $data;
    }

}

Par exemple en appelant la fonction de cette manière :

$data = GmapApi::geocodeAddress('151 avenue du pont trinquat 34070 Montpellier');
//on affiche les différente infos
echo '<ul>';
foreach ($data as $key=>$value){
    echo '<li>'.$key.' : '.$value.'</li>';
}
echo '</ul>';
/* va afficher
address : 151 Avenue du Pont-Trinquat, 34000 Montpellier, France
lat : 43.6008177
lng : 3.8873392
city : Montpellier
department : Hérault
region : Occitanie
country : France
postal_code : 34000
*/

On peux maintenant utiliser cette fonction pour trouver la distance entre 2 adresses, grâce à notre fonction de l’article précédent. Par exemple :

$data1 = GmapApi::geocodeAddress('151 avenue du pont trinquat 34000 montpellier');
$data2 = GmapApi::geocodeAddress('Avenue des cévennes 30360 vézénobre');
echo round(Misc::distance($data1['lat'], $data1['lng'], $data2['lat'], $data2['lng'])).' Km';
//Affiche : 54 Km

PHP : Calcul de la distance entre 2 coordonnées GPS (latitude, longitude)

La fonction toute prête afin de pouvoir la retrouver facilement, pas plus d’explications, c’est des maths, ça pique la tête !

class Misc {
    /**
     * Retourne la distance en metre ou kilometre (si $unit = 'k') entre deux latitude et longitude fournit
     */

    public static function distance($lat1, $lng1, $lat2, $lng2, $unit = 'k') {
        $earth_radius = 6378137;   // Terre = sphère de 6378km de rayon
        $rlo1 = deg2rad($lng1);
        $rla1 = deg2rad($lat1);
        $rlo2 = deg2rad($lng2);
        $rla2 = deg2rad($lat2);
        $dlo = ($rlo2 - $rlo1) / 2;
        $dla = ($rla2 - $rla1) / 2;
        $a = (sin($dla) * sin($dla)) + cos($rla1) * cos($rla2) * (sin($dlo) * sin($dlo));
        $d = 2 * atan2(sqrt($a), sqrt(1 - $a));
        //
        $meter = ($earth_radius * $d);
        if ($unit == 'k') {
            return $meter / 1000;
        }
        return $meter;
    }
}

Exemple d’utilisation :

echo round(Misc::distance(48.86417880,2.34250440,43.6008177,3.8873392), 3);//affiche 597.833 (pour 597,833 km entre les deux coordonnées fournit)

Il s’agit bien entendu de distance à vol d’oiseau.

L’article suivant explique comment obtenir les coordonnées latitude et longitude correspondant à une adresse grâce à l’API Google Map.

Rendez vous ici pour faire le calcul directement en MySql et dans Doctrine

source : http://www.phpsources.org/scripts459-PHP.htm

jQuery : Créer un « font picker » avec le plugin Select2

Créer une liste déroulante (select) proposant un choix parmi plusieurs polices de caractères, tout en permettant de prévisualiser le style d’écriture de la police dans cette liste n’est pas la chose la plus évidente à mettre en place.
Ci-dessous une image présentant le résultat recherché. Et ici une page de démonstration

font-picker

Pour réaliser cela nous allons utiliser le plugin jQuery select2 qui permet de transformer les listes déroulantes. On peux grâce à lui avoir un champs de recherche dans la liste -ce n’est pas la fonctionnalité qui nous intéresse- on peux aussi donner du styles aux options, c’est ce que l’on va utiliser.

On commence donc par importer jQuery et ce plugin sur notre page, ainsi que les polices que l’on va proposer, j’utilise ici l’outil google fonts pour cela.

<!-- google fonts -->
<link href="https://fonts.googleapis.com/css?family=Alfa+Slab+One|Amatic+SC|Anton|Cinzel|Cutive+Mono|Frijole|Great+Vibes|Indie+Flower|Josefin+Sans|Just+Another+Hand|Lobster|Monoton|Pacifico|Permanent+Marker|Playfair+Display|Raleway|Righteous|Roboto+Condensed|Roboto+Slab|Saira+Semi+Condensed|Sedgwick+Ave+Display" rel="stylesheet">
<!-- jquery + select2 -->
<script src="./js/jquery-3.2.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" rel="stylesheet" />
<!-- le script nous permettant de mettre en place le font picker -->
<script src="./js/font-picker.js"></script>

Dans la page on utiliser une balise select classique à laquelle on attribue un identifiant particulier (ici « select-font »)

<select id="select-font" class="form-control">
 <option value="Arial">Arial</option>
 <option value="Times New Roman">Times New Roman</option>
 <option value="Alfa Slab One">Alfa Slab One</option>
 <option value="Amatic SC">Amatic SC</option>
 <option value="Anton">Anton</option>
 ....
</select>

Et voici ci-dessous les quelques lignes de javascript permettant de faire fonctionner le système.

$(document).ready(function () {
    //on applique le plugin "select2" sur notre liste deroulante en lui indiquant d'utiliser un template particulier
    $("#select-font").select2({templateResult: formatFont}).on('change', function (e) {
        updateSelectFontStyle($(this));
    });
    //on initialise la liste au chargement de la page en fonction de la police sélectionné.
    updateSelectFontStyle($("#select-font"));
});

/**
 * Permet d'appliquer la bonne police sur l'option du select2
 */

function formatFont(opt) {
    if (!opt.id) {
        return opt.text;
    }
    var $state = $('<span style="font-family:\'' + opt.element.value + '\';"> ' + opt.text + '</span>');
    return $state;
}

/**
 * Donne la font family sélectionné à l'élément passé en paramètre
 */

function updateSelectFontStyle($el) {
    var font = $el.val();
    var id = '#select2-' + $el.attr("id") + '-container';
    $(id).css("font-family", "'" + font + "'");
}

Terminé !

PHP : Remplacer du texte dans un fichier

Après avoir vu comment créer un fichier et écrire dedans en php, en continuant l’exemple d’un installateur de site, on peux aussi avoir besoin de modifier le texte d’un fichier, par exemple pour configurer des url dans un fichier « .htaccess ». Le code ci-dessous permet de le faire facilement :

class Misc {
    /**
     * Va remplacer toutes les chaines $find par $replace dans le fichier $file
     */

    public static function replaceInfile($file, $find, $replace) {
        if ($find != $replace) {
            //recupere la totalité du fichier
            $str = file_get_contents($file);
            if ($str === false) {
                return false;
            } else {
                //effectue le remplacement dans le texte
                $str = str_replace($find, $replace, $str);
                //remplace dans le fichier
                if (file_put_contents($file, $str) === false) {
                    return false;
                }
            }
        }
        return true;
    }
}

L’utilisation se faisant de cette manière :

if (!Misc::replaceInfile('../.htaccess', 'ancienne chaine', 'nouvelle chaine')) {
   //gestion erreur
}

Comme dans plusieurs articles précédents, pour ce code, j’ai imaginé cela sous la forme d’une fonction statique placé dans une classe Misc qui contiendrait diverses fonctions utilitaires

WordPress : Passer des variables à get_template_part()

Dans wordpress l’appel à un template via get_template_part() ne transmet pas les variables définis au dessus de l’appel.
Ci-dessous une solution de contournement afin que toutes les variables préalablement définis à l’appel du template restent accessibles.

include(locate_template('listing-loop.php'));//'listing-loop' à remplacer par votre template bien sur.

WordPress : WP_Query avec un tableau de « post__in » vide retourne des résultats !

La fonction WP_Query de wordpress qui permet de récupérer des posts en fonction d’une liste de critères contient une incohérence au niveau de l’argument « post__in ».
Cet argument permet de passer un tableau de posts (liste d’identifiants) afin de limiter la requête à ces posts. Pourtant si on lui passe un tableau vide, la recherche se fera sur tous les posts et on aura donc des résultats, alors qu’on s’attend à ne rien avoir.
Cela peut être gênant si notre tableau d’identifiants et généré dynamiquement et que l’on ne sait pas à l’avance si il va être vide.
Il faut donc penser à cette subtilité lors du développement afin de retrouver un comportement cohérent.
Une des solution est de toujours donner un identifiant inexistant (0) au tableau.

//!! va retourner des résultats !!
$listingIDs = array();
$args = array('post_type' => $type, 'post__in' => $listingIDs);
$my_query = new WP_Query($args);
//Ne retournera rien//
$listingIDs = array(0);
$args = array('post_type' => $type, 'post__in' => $listingIDs);
$my_query = new WP_Query($args);

Sortie de Bootstrap 4 (beta)

Après plusieurs mois (années) de développement en version alpha, le framework CSS Bootstrap qui c’est imposé comme référence pour le développement responsive sort en version 4 (beta). La page d’accueil de l’outil ne dirige plus vers la version précédente et toute la documentation pour la nouvelle version est disponible. Il est temps de s’y mettre.

WordPress : Réécriture d’un paramètre dans une URL

Exemple de code wordpress permettant de transformer une url avec un paramètre du type « ?param=slug-param » en une url réécrite plus « propre »

Grace à ce code on va transformer
« https://numa-bord.com/custom-slug/slug-post/?param=slug-autre-post »
en
« https://numa-bord.com/custom-slug/slug-post/slug-autre-post »

add_action('init', 'yoursite_init');

function yoursite_init() {
    global $wp, $wp_rewrite;
    $wp->add_query_var('custom_param');
    $wp_rewrite->add_rule('custom-slug/([^/]+)/([^/]+)', 'index.php?custom_param=$matches[2]&post_type=custom-type&name=$matches[1]', 'top');

    // activer seulement la première fois
    //$wp_rewrite->flush_rules(false);
}

Dans le template, on peux récupérer notre paramètre de la façon suivante :

$param = get_query_var('custom_param');

C’est une technique qui permet de faire pas mal de chose, l’exemple ci dessus pourrait permettre de comparer deux « post ».
Plus concrètement sur un site ayant des véhicules en « custom post type », permettre de comparer les caractéristiques de deux d’entre eux avec une url de ce type :
https://numa-bord.com/comparateur/vehicule1/vehicule2