Catégorie : développement Internet

Symfony 2 : mon site de démonstration est sorti : livrepizzas

Mon site de démonstration pour l’utilisation du framework Symfony 2 est sorti.

http://www.livrepizzas.fr

Ce n’est qu’un site de démonstration, il est loin d’être parfait !

  • Bien sûr il est optimisé pour google et pour les mobiles et tablettes.
  • Bien sûr il est compatible w3c.
  • Bien sûr il est moderne et utilise du HTML5.
  • Bien sûr il est en responsive design.
  • Bien sûr c’est facile à faire. 🙂

Si vous avez des commentaires ou suggestions à faire, n’hésitez pas !

Php, AJAX et problèmes de cache sur iOS : comment les résoudre

J’ai expliqué qu’il fallait mettre, avant de faire le tout dernier ordre de sortie echo json_encode($resultat_final); les headers JSON.

header('Cache-Control: no-cache, must-revalidate');
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Content-type: application/json');

On m’a demandé à quoi servaient les entête de cache. J’ai expliqué que ça servait à s’assurer que les interrogations AJAX se feraient toujours (notez le futur), c’est à dire à demander au navigateur de ne rien garder en cache.

Comme toujours, ma mémoire me fait défaut mais mon expérience reste, et je savais qu’il faut éviter tout risque de problème cache.
J’ai retrouvé une explication concrète du problème : certaines versions du navigateur Safari sur iOS6 ne réinterrogent pas les appels AJAX.

Il suffit donc de faire deux choses :

  • Soit modifier la configuration Apache (c’est pour ça que j’ai consacré quelques heures sur les hôtes virtuels et leur configuration) pour qu’il y ait ces entêtes par défaut :
    Header set Cache-Control "no-cache"
  • Soit faire ma solution directement en Php (mais ça implique de le faire dans tous les retours AJAX – ce qui ne gêne en rien pour les petits projets)
  • Soit préciser en JavaScript de modifier le header :
    $.ajaxSetup({
        type: 'POST',
        headers: { "cache-control": "no-cache" }
    });

Toutes les solutions sont prises via le site de questions/réponses de reférence : en Anglais ici.

jQuery Mobile : rafraîchir une liste créée dynamiquement

Si vous créez dynamiquement une liste en jQuery Mobile, et que la liste n’apparaît pas correctement, il faut demander à jQuery Mobile de la rafraîchir. La solution ? « Refresh ». Voici le code, avec en gras la ligne qu’il vous faut ajouter :

$(document).ready(function() {
    $.ajax("../php/monajax.php")
    .done(function(data) {
        /* Vider la liste avant de la remplir : */
        $("#list").empty();
        /* Remplir la liste : */
        for (var i in data) {
            $('#list').append(
                $('<li />').append(
                    $('<a />')
                        .attr('title', tab[i].description)
                        .html(tab[i].contenu)
                )
            );
        }
        $("#list").listview("refresh");
    });
});

jQuery et submit : comment éviter que le POST ne soit envoyé « classiquement »

Si jamais vous essayez de faire ce que je préconisais (et qui fonctionne dans la plupart des cas) à savoir :

$('#mondiv').submit( function (event) {
    ...
    /* blabla plein de code */
    ...
    return false;
});

Alors ça fonctionne : lorsqu’on détourne la soumission du formulaire, le « return false; » est censé simuler une erreur et stopper la propagation de l’événement.
Seulement il arrive des cas où ce n’est pas suffisant, et où le formulaire est tout de même soumis « à l’ancienne », c’est à dire qu’il efface la page courante et attend le retour du « post » classique. Pour éviter ce genre de désagrément, il faut utiliser event.preventDefault(); :

$('#mondiv').submit( function (event) {
    event.preventDefault();
    ...
    /* blabla plein de code */
    ...
    return false;
});

Symfony 2 et Doctrine et repository : faire un leftJoin avec createQueryBuilder

Comment utiliser createQueryBuilder() ?

La documentation n’est pas très claire sur le sujet. Enfin, disons que si vous êtes comme moi, il va vous manquer des exemples pour mieux comprendre. Je vais essayer de vous faire gagner du temps.

Voilà le problème : j’ai crée un repository pour mes partenaires, que j’ai appelé PartenaireRepository.php.

Dans la plupart des exemples, ils utilisent createQueryBuilder('p'), qui semble pratique (puisqu’il référence immédiatement la table et en fait un alias (dans mon exemple c’est p)

J’ai donc voulu utiliser createQueryBuilder() mais j’ai eu besoin deux jointures d’affilée : les partenaires avaient une ou plusieurs adresses, et ces adresses étaient réliées à des villes. La solution est en fait simple, à partir du moment où on a compris le principe :

    class PartenaireRepository extends EntityRepository
    {
        /**
         * Récupération de tous les partenaires donnés pour un
         * code postal donné.
         */
        public function findAllActiveByCp($cp)
        {
            return $this->createQueryBuilder('p')
                ->leftJoin('p.adresses', 'a')
                ->leftJoin('a.ville', 'v')
                ->where('v.cp=:cp')
                ->setParameter('cp', $cp);
        ... blabla
        }
    }

Ici, le leftJoin('p.adresses', 'a') signifie : dans la classe Partenaire que j’ai déclarée dans le fichier Entity\Partenaire.php, il y a la propriété adresses et tu vas faire une jointure dessus, et cette jointure, tu vas l’aliaser "a". On aura donc, à partir de cette jointure, une référence à une table adresse qu’on pourra utiliser via le "a".

Il est possible de refaire une jointure avec cet alias !

La preuve : la jointure juste après : ->leftJoin('a.ville', 'v') qui signifie, exactement sur le même principe : dans le fichier Entity\Adresse.php, il y a la propriété "ville" et tu vas faire une jointure dessus, et cette jointure, tu vas l’aliaser "v".

Enfin, je termine sur le "where" classique :

->where('v.cp=:cp').

D’après ce que j’ai compris, on ne peut faire des jointures que sur des propriétés qui sont elles même déclarées en tant que jointures. Donc, sur mon fichier « entité » Partenaire, ma jointure est déclarée ainsi :

/**
 * @ORM\ManyToMany(targetEntity="Adresse")
 * @ORM\JoinTable(name="partenaire_adresse",
 *      joinColumns={@ORM\JoinColumn(name="id_partenaire", referencedColumnName="id")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="id_adresse", referencedColumnName="id")}
 *      )
 */
private $adresses;

Et de la même façon, sur mon fichier « entité » Ville, ma jointure est déclarée ainsi :

/**
 * @var string
 *
 * @ORM\ManyToOne(targetEntity="Ville")
 * @ORM\JoinColumn(name="id_ville", referencedColumnName="id")
 */
private $ville;

J’espère vous avoir fait gagner du temps, parce que pour moi, la syntaxe n’était pas évidente à trouver.

Symfony 2 et Doctrine: comment ajouter les fonctions spécifiques MySQL

Imaginons que vous fassiez un Repository dans lequel vous essayez d’éxecuter une fonction ainsi :

    <?php
    public function findNearest(...) {
        return $this->createQueryBuilder('p')
            ->select(array(
                'p.id',
                'COS(5) as distance'
            ))
            ->having('distance>:dmin')
            ->addOrderBy('distance', 'DESC')
            ->setParameters(array(
                'dmin' => $distance_min,
            ));
    }

Alors vous verrez que Doctrine ne connait pas la fonction cosinus COS().
Vous aurez l’erreur :

[Syntax Error] line 0, col 27: Error: Expected known function, got 'COS'

Doctrine ne connait pas la fonction cosinus COS(), mais aussi plein d’autres fonctions.

Pour les avoir disponibles, il faut aller les chercher sur github, ici.

Ici, vous pourrez récupérer toutes ces fonctions : DateAdd(), Sha1(), NullIf(), Field(), Month(), Sin(), MatchAgainst(), Acos(), Day(), CountIf(), GroupConcat(), Radians(), TimestampDiff(), ConcatWs(), Cot(), IfElse(), FindInSet(), IfNull(), Cos(), Asin(), Md5(), Year(), Sha2(), Week(), CharLength(), DateDiff(), StrToDate(), Atan(), Tan(), Atan2() et Degrees()

Ensuite, comme je ne voulais pas tous les fichiers, mais que les extensions, je les ai toutes mises dans mon répertoire dédié : src/HQF/Bundle/PizzasBundle/DQL/MySQL/.
Puis j’ai modifié à la main tous les fichiers en les déclarant dans le namespace adéquat (namespace HQF\Bundle\PizzasBundle\DQL\MySQL;).

Une fois cela fait, j’ai ajouté les déclarations de ces fonctions dans le fichier de config général app/config/config.yml. La petite astuce très importante et qui m’a fait perdre un temps fou (et c’est pour ça que je fait cet article en fait), qui n’est pas très claire dans la documentation : le nom qu’on déclare doit correspondre au texte qu’on trouve dans la fonction. Donc, dans le fichier, on a [nom de la fonction]:[classe] :

J’essayais de déclarer mafonction_acos: HQF\Bundle\PizzasBundle\DQL\MySQL\Acos et ça ne fonctionnait pas. Il fallait donner le nom de la fonction : acos: HQF\Bundle\PizzasBundle\DQL\MySQL\Acos

doctrine:
    ...blabla...
    orm:
        default_entity_manager: default
        entity_managers:
            default:
                connection: default
                ...blabla...
                dql:
                    numeric_functions:
                        acos: HQF\Bundle\PizzasBundle\DQL\MySQL\Acos
                        asin: HQF\Bundle\PizzasBundle\DQL\MySQL\Asin
                        atan2: HQF\Bundle\PizzasBundle\DQL\MySQL\Atan2
                        atan: HQF\Bundle\PizzasBundle\DQL\MySQL\Atan
                        charlength: HQF\Bundle\PizzasBundle\DQL\MySQL\CharLength
                        concatws: HQF\Bundle\PizzasBundle\DQL\MySQL\ConcatWs
                        cos: HQF\Bundle\PizzasBundle\DQL\MySQL\Cos
                        cot: HQF\Bundle\PizzasBundle\DQL\MySQL\Cot
                        countif: HQF\Bundle\PizzasBundle\DQL\MySQL\CountIf
                        dateadd: HQF\Bundle\PizzasBundle\DQL\MySQL\DateAdd
                        datediff: HQF\Bundle\PizzasBundle\DQL\MySQL\DateDiff
                        day: HQF\Bundle\PizzasBundle\DQL\MySQL\Day
                        degrees: HQF\Bundle\PizzasBundle\DQL\MySQL\Degrees
                        field: HQF\Bundle\PizzasBundle\DQL\MySQL\Field
                        findinset: HQF\Bundle\PizzasBundle\DQL\MySQL\FindInSet
                        groupconcat: HQF\Bundle\PizzasBundle\DQL\MySQL\GroupConcat
                        ifelse: HQF\Bundle\PizzasBundle\DQL\MySQL\IfElse
                        ifnull: HQF\Bundle\PizzasBundle\DQL\MySQL\IfNull
                        matchagainst: HQF\Bundle\PizzasBundle\DQL\MySQL\MatchAgainst
                        md5: HQF\Bundle\PizzasBundle\DQL\MySQL\Md5
                        month: HQF\Bundle\PizzasBundle\DQL\MySQL\Month
                        nullif: HQF\Bundle\PizzasBundle\DQL\MySQL\NullIf
                        radians: HQF\Bundle\PizzasBundle\DQL\MySQL\Radians
                        sha1: HQF\Bundle\PizzasBundle\DQL\MySQL\Sha1
                        sha2: HQF\Bundle\PizzasBundle\DQL\MySQL\Sha2
                        sin: HQF\Bundle\PizzasBundle\DQL\MySQL\Sin
                        strtodate: HQF\Bundle\PizzasBundle\DQL\MySQL\StrToDate
                        tan: HQF\Bundle\PizzasBundle\DQL\MySQL\Tan
                        timestampdiff: HQF\Bundle\PizzasBundle\DQL\MySQL\TimestampDiff
                        week: HQF\Bundle\PizzasBundle\DQL\MySQL\Week
                        year: HQF\Bundle\PizzasBundle\DQL\MySQL\Year

Symfony 2: générer une url dynamique dans twig dans du code javascript

J’ai eu à faire face à un problème que vous allez très certainement rencontrer si vous faites Symfony2. Dans les fichiers de template, vous allez sûrement mettre du Javascript. Exemple :

<script type="text/javascript">
<!--
function verif_formulaire(){
    window.location = '/test/mon/url/';
}
-->
</script>

Maintenant, si on essaie de faire ça en Twig, c’est simple. Je ne m’attarderai que sur l’URL :

    window.location = '{{ path('my_path') }}';

Supposons que votre path nécessite un paramètre, par exemple, dans mon cas, le code postal :

    window.location = '{{ path('my_path', {'cp': "13480" }) }}';

Facile. Mais supposons que votre code en JavaScript veuille le générer dynamiquement :

/* récupération de la valeur quelque part : */
var monCP = document.getElementById('cp').value;
window.location = ="{{ path('hqf_pizzas_searchpage', {'cp': monCP }) }}";

Eh bien ça ne fonctionnera pas sur Symfony2. Ne cherchez pas c’est comme ça. Vous aurez une erreur. L’erreur, pour reprendre mon code, était :

Variable "monCP" does not exist in HQFPizzasBundle:Default:index.html.twig at line 11

Voici la solution que j’ai trouvée : je reprends ma configuration et il vous suffira de l’adapter à vos besoins. Dans mon fichier de routing src/HQF/Bundle/PizzasBundle/Resources/config/routing.yml, j’ai ce path qui nécessite le paramètre cp :

hqf_pizzas_searchpage:
    pattern:  /search/cp/{cp}
    defaults: { _controller: ... }

L’objectif est de ressortir le path avec un '%s' dedans, de manière à pouvoir avoir une URL qui ressemble à :

/search/cp/%s

Ainsi, il suffira juste après d’utiliser la fonction Twig ‘format‘ et d’y passer la variable JavaScript, par exemple monCP.
Ainsi cet ordre twig:

{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | format('monCP') }}

génèrera ceci :

/search/cp/monCP

L’objectif final est de sortir du vrai code JavaScript qui ressemble à :

window.location="/search/cp/"+monCP+"";

Donc si veut y arriver, le mélange code Twig + JavaScript, avec les guillemets, devrait être :

window.location = "{{ path('hqf_pizzas_searchpage', {'cp': "%s" })  | format('"+monCP+"') }}"

Seulement, problème : il escape tout ! Le code généré sera ainsi :

window.location ="/pizzas/search/cp/%25s";

Solution : pour que twig n’échappe pas le texte, il faut créer son propre filtre Twig !

Voici les étapes à suivre :

J’ai crée mon fichier src/HQF/Bundle/PizzasBundle/Twig/UrlDecodeExtension.php dans lequel j’ai mis ce code :

<?php

namespace HQF\Bundle\PizzasBundle\Twig;

class UrlDecodeExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return array(
            'url_decode' => new \Twig_Filter_Method($this, 'urlDecode'),
        );
    }

    public function urlDecode( $url )
    {
        return urldecode( $url );
    }

    public function getName()
    {
        return 'url_decode_extension';
    }
}

Ensuite, je l’ai enregistré dans les services.
C’est dans le fichier src/HQF/Bundle/PizzasBundle/Resources/config/services.yml les lignes :

services:
    cme.twig.url_decode_extension:
        class: HQF\Bundle\PizzasBundle\Twig\UrlDecodeExtension
        tags:
            - { name: twig.extension }

A partir de là le filtre url_decode fonctionne. Il suffit de faire le code qui suit :

/* récupération de la valeur quelque part */
var monCP = document.getElementById('cp').value;
window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode | format('"+monCP+"') | raw }}";

Afin de générer cela en JavaScript :

/* récupération de la valeur quelque part */
var monCP = document.getElementById('cp').value;
window.location ="/pizzas/search/cp/"+monCP+"";

Ce qui est du code JavaScript parfaitement exécutable.

Voici les explications pas à pas :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) }}";

Génère cela :

window.location ="/pizzas/search/cp/%25s";

C’est échappé, et il ne le faut pas ! Donc ajouter le filtre url_decode :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode }}";

Là le résultat sera celui attendu :

window.location ="/pizzas/search/cp/%s";

Ensuite on y ajoute la fonction format afin d’y ajouter la variable JavaScript :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode | format('"+monCP+"') }}";

Mais là encore le résultat sera échappé :

window.location ="/pizzas/search/cp/&quot;+monCP+&quot;";

Donc il faut lui dire de ressortir le résultat final au format raw :

window.location ="{{ path('hqf_pizzas_searchpage', {'cp': "%s" }) | url_decode | format('"+monCP+"') | raw }}";

Et le résultat de sortie sera (enfin !) parfait :

window.location ="/pizzas/search/cp/"+monCP+"";

Cet article est un mélange de l’explication de création des filtres Twig, ici, et de la question qui ressemble très fortement à la mienne sur stackoverflow, ici.

En espérant que cela serve à quelqu’un, un jour 😉

Drupal drush : comment ajouter un champ lié à un vocabulaire

La solution est simple mais pas forcément évidente.

Imaginez que vous ayez un vocabulaire « MonDico ».

Dans ce dictionnaire (ou « vocabulaire« , pour reprendre l’expression Drupal), vous avez ajouté plein de mots. Enfin, pas des mots, des « termes« , pour reprendre l’expression Drupal.

Imaginez que vous vouliez créer des champs pour ce « vocabulaire« .

Voici le code qui permet de faire cela :

<?php
$vocabulary = new stdClass();
$vocabulary->name = 'PAA';
$vocabulary->description = 'Paa - Description';
$vocabulary->machine_name = 'paa';
$vocabulary->help = 'Vocabulaire related to Paa';
$vocabulary->nodes = array();
$vocabulary->weight = 0;
taxonomy_vocabulary_save($vocabulary);

$vmn = $vocabulary->machine_name;
$vvid = $vocabulary->vid;
$term = new stdClass();
$term->vid = $vvid;
$term->name = 'MonTerme';
$term->description = 'Description of terme';
$term->format = 'text';
$term->parent = 0;
$term->vocabulary_machine_name = $vmn;
taxonomy_term_save($term);

$field = array(
    'field_name' => 'test_mon_type', 
    'type' => 'text', 
);
field_create_field($field);

// Create the instance on the bundle.
$instance = array(
    'field_name' => 'test_mon_type',
    'entity_type' => 'taxonomy_term',
    'label' => 'Mon nom de champ',
    'bundle' => 'taxonomy', 
    'required' => TRUE,
    'widget' => array(
        'type' => 'textfield',
    ), 
);
field_create_instance($instance);

Rien de plus simple (mais, comme pour toute chose concernant Drupal en général) : encore fallait-il le savoir !

Symfony: Could not open input file: composer.phar solution

Si jamais vous avez ce problème, une solution qui peut fonctionner est de lancer un shell, et d’aller dans votre répertoire source.
De là, lancez cet ordre :

curl -s http://getcomposer.org/installer | php

Et tout rentrera dans l’ordre !