Manipuler les dates en PHP

En PHP l’objet DateTime (ou DateTimeImmutable) permet de manipuler les dates aisément. Quelques exemples commentés ci dessous :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//récupérer la date (et heure) du jour
$now = new DateTime();
//travailler avec une date donnée (4 octobre 2016)
$date = new DateTime('2016-10-04');
//modifier l'heure de cette date (18h20)
$date->setTime(18,20,00);
//afficher la date et l'heure au format voulu
echo $date->format('d/m/Y H:i:s');//04/10/2016 18:20:00
echo '<br>';
//ajouter 3 jours
$date->modify('+3 day');
//on récupére le dernier jour du mois de $date
$end = new DateTime($date->format('Y-m-t'));
//on récupére la différence entre ces 2 dates
$interval = $date->diff($end);
//on affiche le nombre de jour entre notre date et la fin du mois
echo $interval->format('%a jours');//23 jours

Pour plus de précisions voir la documentation PHP.

jQuery : Astuces génériques et bibliothèques utiles

1 : Ouvrir ou fermer n’importe quel élément, ciblé par sa classe ou identifiant. Demo

js

1
2
3
    $(document).on("click", ".toggle-target", function () {
        $($(this).data("target")).toggle();
    });

html

1
2
<div class="toggle-target" data-target=".paragraphe1">paragraphe 1</div>
<p class="paragraphe1">bla bla bla bla bla bla ....<p>

2 : Afficher une boite de confirmation affichant un texte paramétré. Demo

js

1
2
3
4
5
6
    $(document).on("click", ".delete-btn", function () {
        if (confirm($(this).data("delete-confirm"))) {
            return true;
        }
        return false;
    });

html

1
2
    <a href="#suppr1" class="btn btn-danger delete-btn" data-delete-confirm="Message Numéro 1">Suppression 1</a>
    <a href="#suppr2" class="btn btn-danger delete-btn" data-delete-confirm="Autre message">Suppression 2</a>

Diverses bibliothèques utiles :

  • bootstrap js popup, onglet, infobulle, slider…
  • bootstrap-datePicker calendrier sélectionneur de date.
  • select2 transforme un élément select en boite de recherche parmi les options disponibles
  • datatable permet de paginer, trier, rechercher..etc dans les colonnes d’un tableau html

Snippet : js/jQuery recupérer un tableau « key -> value » à partir d’un select html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//tableau clé valeur à récupérer à partir d'un select html
var listOptions = [];
$("#id_du_select").find('option').each(function () {
    listOptions[$(this).val()] = $(this).text();
});

function initRegime(options) {
    //valeur selon laquelle les options du select doivent êtres modifiées.
    var val = $("#id_input_de_reference").val();
    if (val == '9000') {
        for (var key in options) {
            //clés à afficher pour cette valeurs.
            if (key == "SS" || key == "SA" || key == "LX") {
                $("#app_quotation_regime").append('<option value="' + key + '">' + options[key] + '</option>');
            }
        }
    } else {
        //par defaut on affiche tous les élements.
        for (var key in options) {
            $("#id_du_select").append('<option value="' + key + '">' + options[key] + '</option>');
        }
    }
}
//appel de la fonction
initRegime(regimeOptions);

Image qui ne s’affiche pas sous firefox (masqué par une class css bizarre)

Voici une explication à un problème qui pourras vous surprendre lors d’un développement.
Si vous avez des images ne s’affichant pas sous firefox, et qu’en inspectant l’élément vous trouvez une classe incongrue s’y étant ajouté du type « class=’yrbrjeoidciaiotneclu' »

Avec une règle correspondante :

1
2
3
.yrbrjeoidciaiotneclu {
    -moz-binding: url("chrome://global/content/bindings/general.xml#foobarbazdummy") !important;
}

Le problème vient en fait d’adblock plus qui prend votre image pour une publicité et décide de la masquer.
Dans mon cas cela se produisait sur une image dont le nom terminait par une taille. du type « nom_de_limage_650x350.jpg » ça semble être le cas sur pas mal de bannière publicitaire d’où le blocage.

On peux donc facilement contourner le problème en changeant le nom de l’image. Et si l’on veux quand même avoir la taille, (pour une génération dynamique par exemple) on peut contourné le blocage en remplacement « 650×350 » par « 650t350 » par exemple.

Symfony : Boucler sur les champs d’un formulaire avec twig

Sous symfony, il est très simple d’afficher un formulaire entier avec twig. Il suffit d’une ligne {{form(form)}}
Il y’a aussi différentes possibilités pour personnaliser l’affichage (afficher les champs un par un, surcharger les template de formulaires, créer des champs personnalisés…etc

Cependant si on veut adapter seulement un champs particulier de notre formulaire, pour y afficher par exemple une explication au dessus ou quelque chose de plus complexe.
La solution la plus rapide est la plus simple est de boucler sur les champs du formulaire et de placer une condition comme ci-dessous :

1
2
3
4
5
6
7
8
9
10
11
{{ form_start(form, {'attr': {'class': 'form-class'}}) }}
    {% for key, field in form.children %}
        {# condition pour afficher le champ particulier #}
       {% if key=='mon_champ_particulier' %}
            <div class="ma_classe">Mon explication, widget ou tout autre éléments spécifique pour ce champ.</div>
            {{form_row(field)}}
        {% else %}
            {{form_row(field)}}
        {% endif %}
    {% endfor %}    
{{ form_end(form) }}

Comme indiqué au dessus, cette solution est à appliqué pour les cas particuliers, pour des éléments semblables qui se répété mieux vaut mettre en place une solution générique avec un type de champ personnalisé par exemple.

Exemple de requête « complète » avec Doctrine (dans un repository symfony)

Un exemple commenté d’une recherche dans une entité doctrine comprenant pas mal de cas avec le QueryBuilder.
Il s’agit ici d’une méthode situé dans le repository d’une entité doctrine sous Symfony.
De quoi s’en inspirer pour faire son petit moteur de recherche avec multiple critères et pagination.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
    /**
     * recherche les résultats correspondants aux critères passés en paramètres
     * @param type $data - tableau des paramètres au format ('nomDeLaPropriete'=>'valeur')
     * @param type $limit - le nombre de résultats voulus. false si on veut tous les résultats.
     * @param int $page - le numéro de la page de résultat attendus (en fonction du nombre de résultats $limit)
     * @param type $count - à true retourne seulement le nombre total de résultats pour les critères $data passés
     * @return type
     */

    public function findByParameters($data, $limit = 20, $page = 1, $count = false) {
        $query = $this->createQueryBuilder('a');
        //si on veut le total
        if ($count) {
            $query->select('count(a.id)');
        } else {
            //select et jointures facultative
            $query->select(array('a','p', 'c'))->leftJoin('a.profession', 'p')->leftJoin('a.city', 'c');
        }
        //recherche la valeur exacte pour les propriétés listés dans le tableaux ci-dessous
        $search = array('id', 'status');
        foreach ($search as $fieldName) {
            if (isset($data[$fieldName])) {
                $query->andwhere('a.' . $fieldName . ' = :' . $fieldName . '')->setParameter($fieldName, $data[$fieldName]);
            }
        }
        //recherche la valeur contenant (%like%) pour les propriétés listés dans le tableaux ci-dessous
        $searchLike = array('title', 'subtitle', 'description');
        foreach ($searchLike as $fieldName) {
            if (isset($data[$fieldName])) {
                $query->andwhere('a.' . $fieldName . ' LIKE :' . $fieldName . '')->setParameter($fieldName, '%' . $data[$fieldName] . '%');
            }
        }
        //recherche les propriétés listés ayant la valeur exacte ou null (pratique pour les relations non obligatoire)
        $searchOrNull = array('profession', 'city');
        foreach ($searchOrNull as $fieldName) {
            if (isset($data[$fieldName])) {
                $query->andwhere('a.' . $fieldName . ' = :' . $fieldName . ' OR a.' . $fieldName . ' IS NULL')->setParameter($fieldName, $data[$fieldName]);
            }
        }
        //exemple de recherche multiples. Exemple l'entité doit appartenir à une ou plusieurs de ces "category"
        //les valeurs des propriétés listés doivent être un tableaux des valeurs possibles
        $searchMultiple = array('category');
        foreach ($searchMultiple as $fieldName) {
            if (isset($data[$fieldName])) {
                $req = '';
                $i = 0;
                foreach ($data[$fieldName] as $item) {
                    if ($i > 0) {
                        $req.=' OR ';
                    }
                    $req.='a.' . $fieldName . ' = :item' . $i . ' ';
                    $query->setParameter('item' . $i, $item);
                    $i++;
                }
                $query->andWhere($req);
            }
        }
        //on peut imagine d'autre exemple recherche supérieur ou inférieur à la valeur, recherche sur des dates...etc
        //
        //si on veut seulement le total retourne simplement le nombre de résultats
        if ($count) {
            return $query->getQuery()->getSingleScalarResult();
        } else {
            //ajout d'un classement (facultatif)
            $query->addOrderBy('a.dateCreate', 'DESC');
            if (!$limit) {
                //rien
            } else {
                //si on veut un numéro de page particulier sélection des résultats en fonction de ce ce numéro de page et du nombre $limit de résultats voulus.
                if ($page < 1) {
                    $page = 1;
                }
                $query->setFirstResult(($page - 1) * $limit)->setMaxResults($limit);
            }
            //retourne la liste des résultats
            return $query->getQuery()->getResult();
        }
    }

Symfony, Générer des PDF avec EnseparHtml2pdfBundle

Pour générer des PDF en utilisant HTML2PDF (permet de générer des PDF à partir de code HTML) avec symfony on peut utiliser le EnseparHtml2pdfBundle

Ajout du bundle dans le fichier composer.json

1
2
3
4
"require": {
    ...
    "ensepar/html2pdf-bundle" : "dev-master"
},

On active dans app/AppKernel.php

1
2
3
4
$bundles = array(
    ...
    new Ensepar\Html2pdfBundle\EnseparHtml2pdfBundle(),
);

On update le framework avec composer

1
php ../composer.phar update

On peux utiliser html2pdf dans un controler pour générer un pdf

1
2
3
4
5
6
7
8
9
10
//on stocke la vue à convertir en PDF, en n'oubliant pas les paramètres twig si la vue comporte des données dynamiques
$html = $this->renderView('AppAppliBundle:Pdf:page-pdf.html.twig', array('data'=>$data));
//on appelle le service html2pdf
$html2pdf = $this->get('html2pdf_factory')->create();
//real : utilise la taille réelle
$html2pdf->pdf->SetDisplayMode('real');
//writeHTML va tout simplement prendre la vue stocker dans la variable $html pour la convertir en format PDF
$html2pdf->writeHTML($html);
//Output envoit le document PDF au navigateur internet
return new Response($html2pdf->Output('nom-du-pdf.pdf'), 200, array('Content-Type' => 'application/pdf'));

Migrer des utilisateurs et leurs mot de passe (md5) vers Symfony

Dans le cadre d’une migration d’un site sur le framework symfony. On doit parfois migrer des utilisateur existants vers le nouveau système d’authentification de symfony.
Dans un cas « basique » ou le mot de passe des membre est simplement codé en md5 c’est en fait très simple (depuis symfony 2.5)

Il faut configurer 2 « encodeurs », le classique, et celui pour les anciens membres.
Dans le fichier /app/config/security.yml

1
2
3
4
5
6
7
8
9
10
security:
    encoders:
        #votre encoder classique
        App\MemberBundle\Entity\Member: sha512
        #encoder pour les membres pré-existants
        v1_encoder:
            #md5 simple dans notre cas
            algorithm: md5
            encode_as_base64: false
            iterations: 0

Il faut ensuite implémenter « encoderAwareInterface » dans notre classe membre et la fonction getEncoderName() qui retourne le nom de l’encoder à utiliser. Pour le définir un simple champs enregistré en base de donnée sur le type de membre que l’on à convient (membre migré ou pas).
Voici ce que ça donne par exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;

class Member implements AdvancedUserInterface, \Serializable, EncoderAwareInterface {

    //implémenter une classe user normale, ajouter un champs isV1 pour définir la version du membre et la méthode ci dessous.

    /**
     * encoder à utiliser
     * @return string
     */

    public function getEncoderName() {
        if ($this->getIsV1()) {
            return 'v1_encoder';
        }
        return null; // use the default encoder
    }
}

sources : http://symfony.com/doc/current/cookbook/security/named_encoders.html et
http://stackoverflow.com/questions/18739101/using-md5-in-symfony2-security-yml-for-legacy-users

Symfony & FOSUserBundle : « Integrity constraint violation: 1062 Duplicate entry »

Si vous utilisez le FOSUserBundle sous Symfony 2.5 et que vous avez modifé votre formulaire d’inscription en redéfinissant le « RegistrationFormType ». En essayant de vous inscrire 2 fois avec la même adresse email, à la place d’une jolie erreur vous allez avoir droit à un gros plantage avec l’erreur « … Integrity constraint violation: 1062 Duplicate entry … »

Arrêtez de vous arrachez les cheveux vous n’y êtes pour rien, cela vient d’un bug de la version 2.5 de Symfony. Pour le corriger il faut passer l’api validation du framework en version 2.4 comme ceci :

Dans le fichier /app/config/config.yml

1
2
framework:
    validation: { enabled: true, api: 2.4, enable_annotations: true }

Réponse trouvé dans cette discussion : https://github.com/FriendsOfSymfony/FOSUserBundle/issues/1516