<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Symfony Archives | DOTPROGS</title>
	<atom:link href="https://www.dotprogs.com/etiquette/symfony/feed/" rel="self" type="application/rss+xml" />
	<link></link>
	<description>DOTPROGS - Conception de sites web</description>
	<lastBuildDate>Wed, 08 Jun 2022 08:57:30 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.dotprogs.com/wp-content/uploaded-files/cropped-dotprogs-favicon-32x32.png</url>
	<title>Symfony Archives | DOTPROGS</title>
	<link></link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>www.abeille-infos.com, les premières fondations du site sous Drupal !</title>
		<link>https://www.dotprogs.com/un-futur-site-web-pour-les-abeilles/</link>
		
		<dc:creator><![CDATA[DOTPROGS]]></dc:creator>
		<pubDate>Sun, 12 Sep 2021 20:04:00 +0000</pubDate>
				<category><![CDATA[Dotweb]]></category>
		<category><![CDATA[Projet personnel]]></category>
		<category><![CDATA[Webdesign]]></category>
		<category><![CDATA[abeille]]></category>
		<category><![CDATA[actualité]]></category>
		<category><![CDATA[apiculture]]></category>
		<category><![CDATA[apprendre]]></category>
		<category><![CDATA[comprendre]]></category>
		<category><![CDATA[Drupal 9+]]></category>
		<category><![CDATA[environnement]]></category>
		<category><![CDATA[informations.]]></category>
		<category><![CDATA[menaces]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[planète]]></category>
		<category><![CDATA[Symfony]]></category>
		<guid isPermaLink="false">https://www.dotprogs.com/?p=1</guid>

					<description><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/abeille-infos.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Nouveau site web abeille-infos.com" decoding="async" fetchpriority="high" /></p>
<h2 class="dp-level2-title">L'abeille comme révélateur des problématiques environnementales</h2>
<h3 class="dp-level3-title">Des sujets très vastes portés par ce merveilleux insecte.</h3>
<p>En attendant la création du prochain site web www.abeille-infos.com en cours de réalisation, sans doute pour un certain nombre de semaines, j'ai défini également son identité avec un logo évocateur en ce qui concerne son objectif.<br />
Bien que ce logo ne soit pas encore définitif, notamment au niveau des couleurs choisies, je considère qu'il me convient déjà en l'état, car il illustre bien la volonté d'informer et traiter l'actualité autour de l'abeille, comme témoin des évolutions.</p>
<p>Il sera aussi question d'aborder les causes et conséquences de l'activité de l'homme sur la planète avec l'abeille comme sentinelle involontaire et aux avant-postes.</p>
<p>D'autres thématiques seront abordées comme bien sûr l'apiculture responsable et davantage respectueuse de l'insecte, les menaces qui pèse sur sa disparition et les effets constatés, quelques bonnes adresses pour trouver du miel de qualité directement auprès du producteur, des annuaires spécifiques ...<br />
Mais aussi des articles proposeront de s'initier à l'apiculture en tant que débutant, de manière simple et sans prétention, pour contribuer à son niveau, au maintien de l'existence des abeilles. La mise en avant d'actions associatives ou autres en faveur de l'abeille feront l'objet de toute mon attention.</p>
<p>Bref, des mauvaises nouvelles en guise de prise de conscience, mais aussi surtout des approches positives mises en exergue qui veulent donner un avenir à l'abeille et à la planète !</p>
<p>Le futur site web est actuellement développé sous Drupal 9+, ceci afin de joindre ma formation technique et mes convictions.</p>
<p>Cet article <a href="https://www.dotprogs.com/un-futur-site-web-pour-les-abeilles/">www.abeille-infos.com, les premières fondations du site sous Drupal !</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/abeille-infos.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Nouveau site web abeille-infos.com" decoding="async" /></p><h2 class="dp-level2-title">L'abeille comme révélateur des problématiques environnementales</h2>
<h3 class="dp-level3-title">Des sujets très vastes portés par ce merveilleux insecte.</h3>
En attendant la création du prochain site web www.abeille-infos.com en cours de réalisation, sans doute pour un certain nombre de semaines, j'ai défini également son identité avec un logo évocateur en ce qui concerne son objectif.
Bien que ce logo ne soit pas encore définitif, notamment au niveau des couleurs choisies, je considère qu'il me convient déjà en l'état, car il illustre bien la volonté d'informer et traiter l'actualité autour de l'abeille, comme témoin des évolutions.

Il sera aussi question d'aborder les causes et conséquences de l'activité de l'homme sur la planète avec l'abeille comme sentinelle involontaire et aux avant-postes.

D'autres thématiques seront abordées comme bien sûr l'apiculture responsable et davantage respectueuse de l'insecte, les menaces qui pèse sur sa disparition et les effets constatés, quelques bonnes adresses pour trouver du miel de qualité directement auprès du producteur, des annuaires spécifiques ...
Mais aussi des articles proposeront de s'initier à l'apiculture en tant que débutant, de manière simple et sans prétention, pour contribuer à son niveau, au maintien de l'existence des abeilles. La mise en avant d'actions associatives ou autres en faveur de l'abeille feront l'objet de toute mon attention.

Bref, des mauvaises nouvelles en guise de prise de conscience, mais aussi surtout des approches positives mises en exergue qui veulent donner un avenir à l'abeille et à la planète !

Le futur site web est actuellement développé sous Drupal 9+, ceci afin de joindre ma formation technique et mes convictions.<p>Cet article <a href="https://www.dotprogs.com/un-futur-site-web-pour-les-abeilles/">www.abeille-infos.com, les premières fondations du site sous Drupal !</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Exemple d&#8217;utilisation d&#8217;un « token CSRF » sans formulaire avec Symfony</title>
		<link>https://www.dotprogs.com/exemple-utilisation-token-csrf-sans-formulaire-avec-symfony/</link>
		
		<dc:creator><![CDATA[DOTPROGS]]></dc:creator>
		<pubDate>Thu, 20 Feb 2020 15:50:20 +0000</pubDate>
				<category><![CDATA[Développement web]]></category>
		<category><![CDATA[Dotweb]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[CSRF]]></category>
		<category><![CDATA[sécurité]]></category>
		<category><![CDATA[session]]></category>
		<category><![CDATA[Symfony]]></category>
		<category><![CDATA[token]]></category>
		<guid isPermaLink="false">https://www.dotprogs.com/?p=991</guid>

					<description><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/bout-de-code0120.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Bout de code - Token CSRF sans formulaire avec Symfony" decoding="async" /></p>
<p>L'idée, ici, est de présenter l'usage d'un "token" (jeton) pour se prémunir d'une attaque <a href="https://fr.wikipedia.org/wiki/Cross-site_request_forgery" title="Présentation Cross Site Request Forgery" target="_blank" rel="noopener noreferrer nofollow">CSRF</a> (Cross Site Request Forgery) en utilisant le framework Symfony 4+ (doit pouvoir fonctionner en Sf 3 également à peu de chose près) mais sans s'appuyer sur un formulaire.</p>
<p>En effet les tokens CSRF sont proposés pour la sécurité d'un formulaire Symfony, donc l'objectif de ma démarche avec ce "bout de code" est de mettre en place la même sécurité, en utilisant un simple lien ou un bouton HTML par exemple avec une requête "DELETE" en AJAX pour appeler l'action dédiée "<strong>DeleteMemberAction</strong>" ci-dessous.<br />
<strong><u>Note :</u> utiliser une requête en "GET" pour une suppression de ressource est une mauvaise pratique d'un point de vue</strong> <a href="https://developer.mozilla.org/fr/docs/Web/HTTP/Resources_and_specifications" title="Protocole HTTP" target="_blank" rel="noopener noreferrer nofollow">HTTP</a><strong>.</strong></p>
<p>Cet exemple tient compte du fait que l'utilisateur à l'origine de l'action est <strong>normalement authentifié</strong> sur un espace sécurisé, ce qui fait le danger par principe d'une attaque CSRF.<br />
Un compte "membre" doit être supprimé, ce que l'on peut considérer comme une opération sensible !</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php

declare(strict_types = 1);

namespace App\Action;

use App\Responder\DeleteMemberResponder; 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

/**
 * Class DeleteMemberAction.
 *
 * Delete a member account.
 */
class DeleteMemberAction
{
    /**
     *  Manage member deletion.
     *
     * @Route({
     *     "en": "/user/{id}/delete/{token}"
     * }, name="delete_member", methods={"DELETE"})
     *
     * @param CsrfTokenManagerInterface $csrfTokenManager
     * @param DeleteMemberResponder     $responder
     * @param Request                   $request
     * 
     * @return Response
     *
     * @throws InvalidCsrfTokenException
     */ 
    public function __invoke(CsrfTokenManagerInterface $csrfTokenManager, DeleteMemberResponder $responder, Request $request): Response
    {
        // "delete_member" must be a unique token id for session storage inside application
        $token = new CsrfToken('delete_member', $request-&gt;attributes-&gt;get('token'));

        // Action is stopped since token is not allowed!
        if (!$csrfTokenManager-&gt;isTokenValid($token)) {
            throw new InvalidCsrfTokenException('CSRF Token is not valid.');
        }

        // Handle request and proceed to parameters validation and member deletion
        // ...
        // Return expected response (e.g. JSON, HTML...)
    }
}</pre>
<p><strong><u>Note :</u> cette action est auto-configurée en tant que "controller" au sein du framework pour être appelée correctement via la route précisée.</strong><br />
Ce "controller" va en outre enclencher la vérification de la validité du token par l'intermédiaire d'un service "<strong>CsrfTokenManager</strong>" implémentant une interface. <a href="https://github.com/symfony/security-csrf/blob/master/CsrfTokenManager.php" title="Symfony - class CsrfTokenManager" target="_blank" rel="noopener noreferrer nofollow">Vous trouverez ici</a> le détail de cette classe concrète.</p>
<p>Cet objet "CsrfTokenManager" vérifie la validité d'un autre objet "CsrfToken" associant un identifiant et une valeur de token.</p>
<p><strong>Mais à ce propos comment le token est généré ?</strong><br />
Généralement, il est créé dans un template via une fonction Twig avec "<strong>csrf_token('delete_member')</strong>" qui permet d'avoir le retour de la valeur du token CSRF en passant l'identifiant correspondant attendu.<br />
Voici ce que cela peut donner sur un simple lien HTML côté Twig (l'allié fidèle de Symfony) :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">{# "id" parameter can also be a uuid value! #}
&lt;a href="{{ path('delete_member', {'id': 450, 'token': csrf_token('delete_member')}) }}" title="Validate deletion"&gt;Delete account&lt;/a&gt;</pre>
<p>Bien entendu, il est préférable par exemple de demander une confirmation avant suppression, afin d'être plus user friendly !</p>
<p>Le token CSRF peut être généré aussi simplement dans un service, toujours grâce à l'instance "CsrfTokenManager" et sa méthode "getToken('delete_member')" qui retourne un objet "<strong>CsrfToken</strong>" et va générer/stocker le token s'il n'existe pas déjà.<br />
L'objet "CsrfToken" a quant à lui une méthode "getValue()" afin de récupérer en définitive la valeur du token.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">{# Strange getter which can also generate a token! #}
$token = $csrfTokenManager-&gt;getToken('delete_member')-&gt;getValue();</pre>
<p>Ce manager fait appel à d'autres instances pour générer le token et le stocker en session notamment via les classes concrètes "<strong>UriSafeTokenGenerator</strong>" et "<strong>NativeSessionTokenStorage</strong>" ou des implémentations similaires.</p>
<p>J'espère que ces quelques lignes pourront vous rendre service, sans mauvais jeu de mot !</p>
<p>Cet article <a href="https://www.dotprogs.com/exemple-utilisation-token-csrf-sans-formulaire-avec-symfony/">Exemple d&rsquo;utilisation d&rsquo;un « token CSRF » sans formulaire avec Symfony</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/bout-de-code0120.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Bout de code - Token CSRF sans formulaire avec Symfony" decoding="async" loading="lazy" /></p>L'idée, ici, est de présenter l'usage d'un "token" (jeton) pour se prémunir d'une attaque <a href="https://fr.wikipedia.org/wiki/Cross-site_request_forgery" title="Présentation Cross Site Request Forgery" target="_blank" rel="noopener noreferrer nofollow">CSRF</a> (Cross Site Request Forgery) en utilisant le framework Symfony 4+ (doit pouvoir fonctionner en Sf 3 également à peu de chose près) mais sans s'appuyer sur un formulaire.

En effet les tokens CSRF sont proposés pour la sécurité d'un formulaire Symfony, donc l'objectif de ma démarche avec ce "bout de code" est de mettre en place la même sécurité, en utilisant un simple lien ou un bouton HTML par exemple avec une requête "DELETE" en AJAX pour appeler l'action dédiée "<strong>DeleteMemberAction</strong>" ci-dessous.
<strong><u>Note :</u> utiliser une requête en "GET" pour une suppression de ressource est une mauvaise pratique d'un point de vue</strong> <a href="https://developer.mozilla.org/fr/docs/Web/HTTP/Resources_and_specifications" title="Protocole HTTP" target="_blank" rel="noopener noreferrer nofollow">HTTP</a><strong>.</strong>

Cet exemple tient compte du fait que l'utilisateur à l'origine de l'action est <strong>normalement authentifié</strong> sur un espace sécurisé, ce qui fait le danger par principe d'une attaque CSRF.
Un compte "membre" doit être supprimé, ce que l'on peut considérer comme une opération sensible !
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php

declare(strict_types = 1);

namespace App\Action;

use App\Responder\DeleteMemberResponder; 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

/**
 * Class DeleteMemberAction.
 *
 * Delete a member account.
 */
class DeleteMemberAction
{
    /**
     *  Manage member deletion.
     *
     * @Route({
     *     "en": "/user/{id}/delete/{token}"
     * }, name="delete_member", methods={"DELETE"})
     *
     * @param CsrfTokenManagerInterface $csrfTokenManager
     * @param DeleteMemberResponder     $responder
     * @param Request                   $request
     * 
     * @return Response
     *
     * @throws InvalidCsrfTokenException
     */ 
    public function __invoke(CsrfTokenManagerInterface $csrfTokenManager, DeleteMemberResponder $responder, Request $request): Response
    {
        // "delete_member" must be a unique token id for session storage inside application
        $token = new CsrfToken('delete_member', $request-&gt;attributes-&gt;get('token'));

        // Action is stopped since token is not allowed!
        if (!$csrfTokenManager-&gt;isTokenValid($token)) {
            throw new InvalidCsrfTokenException('CSRF Token is not valid.');
        }

        // Handle request and proceed to parameters validation and member deletion
        // ...
        // Return expected response (e.g. JSON, HTML...)
    }
}</pre>
<strong><u>Note :</u> cette action est auto-configurée en tant que "controller" au sein du framework pour être appelée correctement via la route précisée.</strong>
Ce "controller" va en outre enclencher la vérification de la validité du token par l'intermédiaire d'un service "<strong>CsrfTokenManager</strong>" implémentant une interface. <a href="https://github.com/symfony/security-csrf/blob/master/CsrfTokenManager.php" title="Symfony - class CsrfTokenManager" target="_blank" rel="noopener noreferrer nofollow">Vous trouverez ici</a> le détail de cette classe concrète.

Cet objet "CsrfTokenManager" vérifie la validité d'un autre objet "CsrfToken" associant un identifiant et une valeur de token.

<strong>Mais à ce propos comment le token est généré ?</strong>
Généralement, il est créé dans un template via une fonction Twig avec "<strong>csrf_token('delete_member')</strong>" qui permet d'avoir le retour de la valeur du token CSRF en passant l'identifiant correspondant attendu.
Voici ce que cela peut donner sur un simple lien HTML côté Twig (l'allié fidèle de Symfony) :
<pre class="EnlighterJSRAW" data-enlighter-language="php">{# "id" parameter can also be a uuid value! #}
&lt;a href="{{ path('delete_member', {'id': 450, 'token': csrf_token('delete_member')}) }}" title="Validate deletion"&gt;Delete account&lt;/a&gt;</pre>
Bien entendu, il est préférable par exemple de demander une confirmation avant suppression, afin d'être plus user friendly !

Le token CSRF peut être généré aussi simplement dans un service, toujours grâce à l'instance "CsrfTokenManager" et sa méthode "getToken('delete_member')" qui retourne un objet "<strong>CsrfToken</strong>" et va générer/stocker le token s'il n'existe pas déjà.
L'objet "CsrfToken" a quant à lui une méthode "getValue()" afin de récupérer en définitive la valeur du token.
<pre class="EnlighterJSRAW" data-enlighter-language="generic">{# Strange getter which can also generate a token! #}
$token = $csrfTokenManager-&gt;getToken('delete_member')-&gt;getValue();</pre>
Ce manager fait appel à d'autres instances pour générer le token et le stocker en session notamment via les classes concrètes "<strong>UriSafeTokenGenerator</strong>" et "<strong>NativeSessionTokenStorage</strong>" ou des implémentations similaires.

J'espère que ces quelques lignes pourront vous rendre service, sans mauvais jeu de mot !<p>Cet article <a href="https://www.dotprogs.com/exemple-utilisation-token-csrf-sans-formulaire-avec-symfony/">Exemple d&rsquo;utilisation d&rsquo;un « token CSRF » sans formulaire avec Symfony</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Exemple d&#8217;utilisation d&#8217;une fonction « macro » Twig récursive</title>
		<link>https://www.dotprogs.com/exemple-utilisation-fonction-macro-twig-recursive/</link>
		
		<dc:creator><![CDATA[DOTPROGS]]></dc:creator>
		<pubDate>Tue, 04 Jun 2019 18:50:43 +0000</pubDate>
				<category><![CDATA[Développement web]]></category>
		<category><![CDATA[Dotweb]]></category>
		<category><![CDATA[Intégration web]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[function macro]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[récursivité]]></category>
		<category><![CDATA[Symfony]]></category>
		<category><![CDATA[template]]></category>
		<category><![CDATA[Twig]]></category>
		<guid isPermaLink="false">https://www.dotprogs.com/?p=965</guid>

					<description><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/bout-de-code0119.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Bout de code - Fonction macro Twig récursive" decoding="async" loading="lazy" /></p>
<p>Partons du principe que l'on souhaite lister les erreurs de validation lors de l'utilisation d'un formulaire avec Symfony 4.<br />
Ça ne sert pas à grand chose mais cet exemple permet de récupérer "l'équivalent" simplifié des données qui remontent dans le profiler de Symfony pour les violations de contraintes sur les champs de ce formulaire.</p>
<p>Pour faire du "debug" rapide, j'utilise simplement un <strong>dump()</strong> des erreurs directement côté Twig pour m'assurer que les contraintes s'appliquent bien en testant visuellement le formulaire.</p>
<p>J'ai donc déclaré une fonction <strong>"macro"</strong> utilisée de façon récursive et exécutée pour boucler sur les objets <strong>FormErrorsIterator</strong> de chaque champ enfant (et enfant(s) d'enfant ...).</p>
<p><strong><u>Note:</u> "_self" désigne le template courant et permet une utilisation immédiate.</strong></p>
<p>Pour Symfony, tout type de champ de formulaire est également un <a href="https://symfony.com/doc/current/forms.html" title="Formulaire Symfony" rel="noopener" target="_blank">formulaire</a>, il existe donc la notion de formulaire parent et enfant.</p>
<p>La récursivité pour cet exemple provient simplement du fait que j'ai décidé d'afficher l'ensemble des violations de contraintes présentes au sein du <strong>root form</strong> ici appelé "<strong>myRootForm</strong>" (variable déclarée pour le formulaire global) :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">{% macro form_errors(forms) %}
  {% for form in forms %}
    {% for formError in form.vars.errors %}
      {{ dump(formError.cause.propertyPath, formError.cause.constraint, formError.cause.message) }}
    {% endfor %}
    {% if form.children is not empty %}
      {# Use macro recursively #}
      {{ _self.form_errors(form.children) }}
    {% endif %}
  {% endfor %}
{% endmacro %}

{# Use macro immediately after declaration in the same template #}
{{ _self.form_errors(myRootForm.children) }}</pre>
<p>Ce "dump" affiche le propertyPath (le champ concerné), le type de contrainte objet, et le message d'erreur personnalisé ou proposé par défaut.<br />
Les contraintes personnalisées (<a href="https://symfony.com/doc/current/validation/custom_constraint.html" title="Custom constraints Symfony" rel="noopener" target="_blank">custom constraints</a>) proposées par Symfony, éventuellement mises en place sur un projet, remontent également dans les boucles.</p>
<p>Pour utiliser votre macro où vous le souhaitez, il suffit de la déclarer dans un template dédié aux déclarations des fonctions macros et ensuite de faire un import de ce template dans un autre template pour pouvoir l'utiliser :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">{# Import macro in another template #}
{% from 'macros.html.twig' import form_errors %}

{# Use macro #}
{{ form_errors(myRootForm.children) }}

{# ------------------------------------------- #}

{# Or import macro like this #} 
{% import "macros.html.twig" as macros %}

{# Use macro in the same way #}
{{ macros.form_errors(myRootForm.children) }}</pre>
<p>Vous pouvez consulter la <a href="https://twig.symfony.com/doc/3.x/tags/macro.html" title="Documentation macro Twig" rel="noopener" target="_blank">documentation</a> concernant les macros Twig.</p>
<p>Voilà, c'est déjà fini !</p>
<p>Cet article <a href="https://www.dotprogs.com/exemple-utilisation-fonction-macro-twig-recursive/">Exemple d&rsquo;utilisation d&rsquo;une fonction « macro » Twig récursive</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/bout-de-code0119.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Bout de code - Fonction macro Twig récursive" decoding="async" loading="lazy" /></p>Partons du principe que l'on souhaite lister les erreurs de validation lors de l'utilisation d'un formulaire avec Symfony 4.
Ça ne sert pas à grand chose mais cet exemple permet de récupérer "l'équivalent" simplifié des données qui remontent dans le profiler de Symfony pour les violations de contraintes sur les champs de ce formulaire.

Pour faire du "debug" rapide, j'utilise simplement un <strong>dump()</strong> des erreurs directement côté Twig pour m'assurer que les contraintes s'appliquent bien en testant visuellement le formulaire.

J'ai donc déclaré une fonction <strong>"macro"</strong> utilisée de façon récursive et exécutée pour boucler sur les objets <strong>FormErrorsIterator</strong> de chaque champ enfant (et enfant(s) d'enfant ...).

<strong><u>Note:</u> "_self" désigne le template courant et permet une utilisation immédiate.</strong>

Pour Symfony, tout type de champ de formulaire est également un <a href="https://symfony.com/doc/current/forms.html" title="Formulaire Symfony" rel="noopener" target="_blank">formulaire</a>, il existe donc la notion de formulaire parent et enfant.

La récursivité pour cet exemple provient simplement du fait que j'ai décidé d'afficher l'ensemble des violations de contraintes présentes au sein du <strong>root form</strong> ici appelé "<strong>myRootForm</strong>" (variable déclarée pour le formulaire global) :
<pre class="EnlighterJSRAW" data-enlighter-language="php">{% macro form_errors(forms) %}
  {% for form in forms %}
    {% for formError in form.vars.errors %}
      {{ dump(formError.cause.propertyPath, formError.cause.constraint, formError.cause.message) }}
    {% endfor %}
    {% if form.children is not empty %}
      {# Use macro recursively #}
      {{ _self.form_errors(form.children) }}
    {% endif %}
  {% endfor %}
{% endmacro %}

{# Use macro immediately after declaration in the same template #}
{{ _self.form_errors(myRootForm.children) }}</pre>
Ce "dump" affiche le propertyPath (le champ concerné), le type de contrainte objet, et le message d'erreur personnalisé ou proposé par défaut.
Les contraintes personnalisées (<a href="https://symfony.com/doc/current/validation/custom_constraint.html" title="Custom constraints Symfony" rel="noopener" target="_blank">custom constraints</a>) proposées par Symfony, éventuellement mises en place sur un projet, remontent également dans les boucles.

Pour utiliser votre macro où vous le souhaitez, il suffit de la déclarer dans un template dédié aux déclarations des fonctions macros et ensuite de faire un import de ce template dans un autre template pour pouvoir l'utiliser :
<pre class="EnlighterJSRAW" data-enlighter-language="php">{# Import macro in another template #}
{% from 'macros.html.twig' import form_errors %}

{# Use macro #}
{{ form_errors(myRootForm.children) }}

{# ------------------------------------------- #}

{# Or import macro like this #} 
{% import "macros.html.twig" as macros %}

{# Use macro in the same way #}
{{ macros.form_errors(myRootForm.children) }}</pre>
Vous pouvez consulter la <a href="https://twig.symfony.com/doc/3.x/tags/macro.html" title="Documentation macro Twig" rel="noopener" target="_blank">documentation</a> concernant les macros Twig.

Voilà, c'est déjà fini !<p>Cet article <a href="https://www.dotprogs.com/exemple-utilisation-fonction-macro-twig-recursive/">Exemple d&rsquo;utilisation d&rsquo;une fonction « macro » Twig récursive</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Mettre en place une pagination personnalisée avec Twig</title>
		<link>https://www.dotprogs.com/mettre-en-place-une-pagination-personnalisee-avec-twig/</link>
		
		<dc:creator><![CDATA[DOTPROGS]]></dc:creator>
		<pubDate>Sun, 18 Nov 2018 18:02:48 +0000</pubDate>
				<category><![CDATA[Développement web]]></category>
		<category><![CDATA[Dotweb]]></category>
		<category><![CDATA[Intégration web]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Symfony]]></category>
		<category><![CDATA[Templating]]></category>
		<category><![CDATA[Twig]]></category>
		<guid isPermaLink="false">https://www.dotprogs.com/?p=856</guid>

					<description><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/twig-php-pagination-1.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Pagination personnalisée avec Twig" decoding="async" loading="lazy" /></p>
<p>Dans le cadre de la création d'un blog, je me suis documenté sur quelques approches basiques pour réaliser une pagination en PHP - MySQL afin de lister des articles.<br />
une fois le principe de base compris, j'ai décidé par la suite de "personnaliser" un tantinet cette pagination et l'afficher avec <a href="https://twig.symfony.com" target="_blank" rel="noopener">Twig</a> dans un autre projet utilisant le framework <a href="https://symfony.com" target="_blank" rel="noopener">Symfony</a>.</p>
<p>- J'utilise par exemple un "<strong>entity service layer</strong>" qui est une classe PHP qui sert ici de passerelle entre un Repository Doctrine et une Action/Controller.<br />
Je prends en compte également un affichage descendant ou ascendant "$order" qui est bien sûr facultatif car on peut faire plus simple !<br />
L'objectif est de faire une requête SQL en utilisant les paramètres OFFSET ET LIMIT - ici grâce au Repository avec <strong>findByLimitOffsetWithOrder(...)</strong> dans la méthode <strong>getFilteredList(...)</strong> - pour obtenir les posts (articles) propres à la page courante "currentPage" définie dans <strong>getPaginationParameters(...)</strong>.<br />
Les principales méthodes personnelles nécessaires de cet "entity service layer" que j'utilisent sont présentées ci-dessous :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php 
 
declare(strict_types = 1); 
 
namespace App\Service; 
 
// Used classes are not declared to simplify code demo! 
// use ... 
// use ... 
 
class PostServiceLayer 
{ 
    // Traits, properties and constructor with dependency injection and other previous methods 
    // ...
 
    /** 
    * Count all posts without filter. 
    * 
    * @return int 
    * 
    * @throws \Doctrine\ORM\NonUniqueResultException 
    * @throws \UnexpectedValueException 
    */ 
    public function countAll() : int 
    { 
        $result = $this-&gt;repository-&gt;countAll();
        if (\is_null($result)) {
            throw new \UnexpectedValueException('Post total count error: list can not be generated!');
        }
        return $result;
    }
 
    /**
     * Get filtered post list depending on parameters.
     *
     * @param int|null $offset
     * @param int      $limit
     * @param string   $order
     *
     * @return array
     *
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function getFilteredList(
        int $offset = null,
        int $limit = Post::POST_NUMBER_PER_LOADING,
        string $order = Post::POST_LOADING_MODE
    ) : array {
        // Init value to define starting rank
        $init = ('DESC' === $order) ? $this-&gt;countAll() : -1;
        // Offset starts at 0 (i.e. the 15th Post rank has a value of 14)
        $start = $offset;
        $end = $offset + $limit;
        return $this-&gt;repository-&gt;findByLimitOffsetWithOrder($order, $init, $start, $end);
    }
 
    /**
     * Get default parameters to show a post list.
     *
     * (e.g. sort direction, post number for "load more", ...)
     *
     * @return array
     */
    public function getListDefaultParameters() : array
    {
        return [
            'loadingMode'      =&gt; Post::POST_LOADING_MODE,
            'numberPerLoading' =&gt; Post::POST_NUMBER_PER_LOADING,
            'numberPerPage'    =&gt; Post::POST_NUMBER_PER_PAGE
        ];
    }
 
    /**
     * Get pagination parameters to manage page links.
     *
     * This is used on complete post list accessible on "posts" page.
     *
     * @param int $pageIndex
     *
     * @return array|null
     *
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function getPaginationParameters(int $pageIndex) : ?array
    {
        $countAll = $this-&gt;countAll();
        $listDefaultParameters = $this-&gt;getListDefaultParameters();
        $postNumberPerPage = $listDefaultParameters['numberPerPage'];
        $pageCount = $countAll % $postNumberPerPage == 0
            ? $countAll / $postNumberPerPage
            : (int) floor($countAll / $postNumberPerPage) + 1;
        $loadingMode = $listDefaultParameters['loadingMode'];
        if ($pageIndex &lt;= 0 || $pageIndex &gt; $pageCount) {
            return null;
        }
        if ('DESC' === $loadingMode) {
            $offset = $countAll - $pageIndex * $postNumberPerPage &lt; 0
                ? 0 : $countAll - $pageIndex * $postNumberPerPage;
            $limit = $offset === 0
                ? $countAll % $postNumberPerPage : $postNumberPerPage;
 
        } else {
            $offset = $pageIndex === 1
                ? 0 : ($pageIndex - 1) * $postNumberPerPage;
            $limit = $offset + $postNumberPerPage &gt; $countAll - 1
                ? $countAll % $postNumberPerPage : $postNumberPerPage;
        }
        return [
            'currentPage'   =&gt; $pageIndex,
            'currentOffset' =&gt; $offset,
            'currentLimit'  =&gt; $limit,
            'pageCount'     =&gt; $pageCount,
            'loadingMode'   =&gt; $loadingMode,
            'postCount'     =&gt; $countAll
        ];
    }
 
    // Other following methods
    // ...
 
}</pre>
<p>- Voici la partie Twig à intégrer dans un template (les commentaires sont en anglais, c'est une habitude personnelle !) :<br />
Les deux variables importantes à transmettre à la vue depuis une "<strong>Action</strong>" ou un "<strong>Controller</strong>" (non détaillé ici) sont le nombre de page total "<strong>pageCount</strong>" et la page courante "<strong>currentPage</strong>" pour sa mise en exergue et structurer les comportements autour d'elle.<br />
Dans une deuxième temps, il s'agit d'initialiser des variables en fonction des conditions induites par la personnalisation que l'on souhaite obtenir.<br />
Je décide par exemple d'afficher les 2 pages précédentes et suivantes (libre à vous d'en afficher plus!) autour de la page courante : si cela n'est pas possible, j'évalue la possibilité d'afficher 1 page, ou aucune, avant ou après la page courante, ce qui explique les conditions initialisées au préalable.<br />
Je substitue les autres numéros de page par "<strong>...</strong>" pour les matérialiser, à l'exception de la première et la dernière pour conserver les bornes extrêmes, et ainsi gérer un nombre conséquent de page à afficher le cas échéant.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;!-- Generate pagination block if there is at least more than 1 page! --&gt;
    {% if pageCount &gt; 1 %}
        &lt;!-- Pagination --&gt;
        {# Page quantity to show around current page is 2 or a calculated minimum value #}
        {% set defaultPageQuantityAround = 2 %}
        {# Mininum value #}
        {% set minimumPageQuantityAround = min(currentPage - 1, pageCount - currentPage) %}
        {# Condition to show the right page numbers before current page: default or minimum value #}
        {% set conditionBefore = currentPage != 1 and minimumPageQuantityAround &lt;= currentPage - 1 %}
        {# Condition to show the right page numbers after current page: default or minimum value #}
        {% set conditionAfter = currentPage != pageCount and minimumPageQuantityAround &lt;= pageCount - currentPage %}
        {# Define page numbers before, other pages will be replaced by "..." #}
        {% set PageQuantityAroundBefore = conditionBefore ? defaultPageQuantityAround : minimumPageQuantityAround %}
        {# Define page numbers after, other pages will be replaced by "..." #}
        {% set PageQuantityAroundAfter = conditionAfter ? defaultPageQuantityAround : minimumPageQuantityAround %}
        &lt;div class="uk-flex uk-flex-center"&gt;
            &lt;ul class="uk-pagination uk-text-bold uk-text-uppercase"&gt;
                {# Previous link #}
                {% if currentPage - 1 != 0 %}
                &lt;li&gt;&lt;a class="st-color-yellow" href="{{ path('posts', { 'page': currentPage - 1 }) }}" title="Previous"&gt;&lt;span class="uk-margin-small-right" uk-pagination-previous&gt;&lt;/span&gt; Previous&lt;/a&gt;&lt;/li&gt;
                {% endif %}
                {% for i in 1..pageCount %}
                {# Current page to show #}
                {% if currentPage == i %}
                &lt;li class="st-color-red"&gt;{{ i }}&lt;/li&gt;
                {# Show "..." before current page depending on page numbers to show before #}
                {% elseif (i &lt; currentPage and 1 != i) and (i == currentPage - PageQuantityAroundBefore - 1) %}
                &lt;li class="uk-disabled"&gt;...&lt;/li&gt;
                {# Show "..." after current page depending on page numbers to show after #}
                {% elseif (i &gt; currentPage and pageCount != i) and (i == currentPage + PageQuantityAroundAfter + 1) %}
                &lt;li class="uk-disabled"&gt;...&lt;/li&gt;
                {# Hide pages under current page and before "..." excepted page 1 #}
                {% elseif (1 != i) and (i &lt; currentPage - PageQuantityAroundBefore - 1) %}
                &lt;li class="uk-hidden"&gt;&lt;a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {# Hide pages over current page and after "..." excepted page with number "pageCount" (last) #}
                {% elseif (pageCount != i) and (i &gt; currentPage + PageQuantityAroundAfter + 1) %}
                &lt;li class="uk-hidden"&gt;&lt;a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {# Apply particular style for lowest link corresponding to fisrt page 1, and Highest link corresponding to page total count #}
                {% elseif i == 1 or i == pageCount %}
                &lt;li&gt;&lt;a class="st-color-blue" href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {# Normal links which are not concerned by conditions above #}
                {% else %}
                &lt;li&gt;&lt;a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {% endif %}
                {% endfor %}
                {# Next link #}
                {% if currentPage + 1 &lt;= pageCount %}
                &lt;li class="uk-margin-auto-left"&gt;&lt;a class="st-color-yellow" href="{{ path('posts', { 'page': currentPage + 1 }) }}" title="Next"&gt;Next &lt;span class="uk-margin-small-left" uk-pagination-next&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
                {% endif %}
            &lt;/ul&gt;
        &lt;/div&gt;
    {% endif %}</pre>
<p>Et voici le résultat visuel de la pagination :</p>
<p><img src="http://www.dotprogs.com/wp-content/uploaded-files/exemples-pagination-twig.jpg" alt="Exemples de pagination personnalisée Twig" width="500" height="250" class="size-full wp-image-872" /></p>
<p>Les classes CSS utilisées ici sont personnalisées (pour les couleurs) et issues du framework <a href="https://getuikit.com/" target="_blank" rel="noopener">UIkit</a> pour le comportement (lien désactivé, lien caché ...), donc rien de significatif pour ce qui est de l'aspect mise en forme.</p>
<p>Un tel système de pagination peut très facilement être adapté pour de l'<strong>AJAX</strong> et afficher les posts de la page courante (en renvoyant du contenu HTML avec un block Twig par exemple) en asynchrone afin d'être davantage "user friendly" !</p>
<p>Ces extraits de code sont issus d'un projet opérationnel, ceci-dit des "petites coquilles" peuvent s'être glissées entre les lignes.<br />
N'hésitez pas à me contacter pour signaler des erreurs éventuelles ou pour partager des améliorations, car on peut souvent mieux faire ou faire plus pragmatique !</p>
<p>Cet article <a href="https://www.dotprogs.com/mettre-en-place-une-pagination-personnalisee-avec-twig/">Mettre en place une pagination personnalisée avec Twig</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/twig-php-pagination-1.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Pagination personnalisée avec Twig" decoding="async" loading="lazy" /></p>Dans le cadre de la création d'un blog, je me suis documenté sur quelques approches basiques pour réaliser une pagination en PHP - MySQL afin de lister des articles.
une fois le principe de base compris, j'ai décidé par la suite de "personnaliser" un tantinet cette pagination et l'afficher avec <a href="https://twig.symfony.com" target="_blank" rel="noopener">Twig</a> dans un autre projet utilisant le framework <a href="https://symfony.com" target="_blank" rel="noopener">Symfony</a>.

- J'utilise par exemple un "<strong>entity service layer</strong>" qui est une classe PHP qui sert ici de passerelle entre un Repository Doctrine et une Action/Controller.
Je prends en compte également un affichage descendant ou ascendant "$order" qui est bien sûr facultatif car on peut faire plus simple !
L'objectif est de faire une requête SQL en utilisant les paramètres OFFSET ET LIMIT - ici grâce au Repository avec <strong>findByLimitOffsetWithOrder(...)</strong> dans la méthode <strong>getFilteredList(...)</strong> - pour obtenir les posts (articles) propres à la page courante "currentPage" définie dans <strong>getPaginationParameters(...)</strong>.
Les principales méthodes personnelles nécessaires de cet "entity service layer" que j'utilisent sont présentées ci-dessous :
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php 
 
declare(strict_types = 1); 
 
namespace App\Service; 
 
// Used classes are not declared to simplify code demo! 
// use ... 
// use ... 
 
class PostServiceLayer 
{ 
    // Traits, properties and constructor with dependency injection and other previous methods 
    // ...
 
    /** 
    * Count all posts without filter. 
    * 
    * @return int 
    * 
    * @throws \Doctrine\ORM\NonUniqueResultException 
    * @throws \UnexpectedValueException 
    */ 
    public function countAll() : int 
    { 
        $result = $this-&gt;repository-&gt;countAll();
        if (\is_null($result)) {
            throw new \UnexpectedValueException('Post total count error: list can not be generated!');
        }
        return $result;
    }
 
    /**
     * Get filtered post list depending on parameters.
     *
     * @param int|null $offset
     * @param int      $limit
     * @param string   $order
     *
     * @return array
     *
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function getFilteredList(
        int $offset = null,
        int $limit = Post::POST_NUMBER_PER_LOADING,
        string $order = Post::POST_LOADING_MODE
    ) : array {
        // Init value to define starting rank
        $init = ('DESC' === $order) ? $this-&gt;countAll() : -1;
        // Offset starts at 0 (i.e. the 15th Post rank has a value of 14)
        $start = $offset;
        $end = $offset + $limit;
        return $this-&gt;repository-&gt;findByLimitOffsetWithOrder($order, $init, $start, $end);
    }
 
    /**
     * Get default parameters to show a post list.
     *
     * (e.g. sort direction, post number for "load more", ...)
     *
     * @return array
     */
    public function getListDefaultParameters() : array
    {
        return [
            'loadingMode'      =&gt; Post::POST_LOADING_MODE,
            'numberPerLoading' =&gt; Post::POST_NUMBER_PER_LOADING,
            'numberPerPage'    =&gt; Post::POST_NUMBER_PER_PAGE
        ];
    }
 
    /**
     * Get pagination parameters to manage page links.
     *
     * This is used on complete post list accessible on "posts" page.
     *
     * @param int $pageIndex
     *
     * @return array|null
     *
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function getPaginationParameters(int $pageIndex) : ?array
    {
        $countAll = $this-&gt;countAll();
        $listDefaultParameters = $this-&gt;getListDefaultParameters();
        $postNumberPerPage = $listDefaultParameters['numberPerPage'];
        $pageCount = $countAll % $postNumberPerPage == 0
            ? $countAll / $postNumberPerPage
            : (int) floor($countAll / $postNumberPerPage) + 1;
        $loadingMode = $listDefaultParameters['loadingMode'];
        if ($pageIndex &lt;= 0 || $pageIndex &gt; $pageCount) {
            return null;
        }
        if ('DESC' === $loadingMode) {
            $offset = $countAll - $pageIndex * $postNumberPerPage &lt; 0
                ? 0 : $countAll - $pageIndex * $postNumberPerPage;
            $limit = $offset === 0
                ? $countAll % $postNumberPerPage : $postNumberPerPage;
 
        } else {
            $offset = $pageIndex === 1
                ? 0 : ($pageIndex - 1) * $postNumberPerPage;
            $limit = $offset + $postNumberPerPage &gt; $countAll - 1
                ? $countAll % $postNumberPerPage : $postNumberPerPage;
        }
        return [
            'currentPage'   =&gt; $pageIndex,
            'currentOffset' =&gt; $offset,
            'currentLimit'  =&gt; $limit,
            'pageCount'     =&gt; $pageCount,
            'loadingMode'   =&gt; $loadingMode,
            'postCount'     =&gt; $countAll
        ];
    }
 
    // Other following methods
    // ...
 
}</pre>
- Voici la partie Twig à intégrer dans un template (les commentaires sont en anglais, c'est une habitude personnelle !) :
Les deux variables importantes à transmettre à la vue depuis une "<strong>Action</strong>" ou un "<strong>Controller</strong>" (non détaillé ici) sont le nombre de page total "<strong>pageCount</strong>" et la page courante "<strong>currentPage</strong>" pour sa mise en exergue et structurer les comportements autour d'elle.
Dans une deuxième temps, il s'agit d'initialiser des variables en fonction des conditions induites par la personnalisation que l'on souhaite obtenir.
Je décide par exemple d'afficher les 2 pages précédentes et suivantes (libre à vous d'en afficher plus!) autour de la page courante : si cela n'est pas possible, j'évalue la possibilité d'afficher 1 page, ou aucune, avant ou après la page courante, ce qui explique les conditions initialisées au préalable.
Je substitue les autres numéros de page par "<strong>...</strong>" pour les matérialiser, à l'exception de la première et la dernière pour conserver les bornes extrêmes, et ainsi gérer un nombre conséquent de page à afficher le cas échéant.
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;!-- Generate pagination block if there is at least more than 1 page! --&gt;
    {% if pageCount &gt; 1 %}
        &lt;!-- Pagination --&gt;
        {# Page quantity to show around current page is 2 or a calculated minimum value #}
        {% set defaultPageQuantityAround = 2 %}
        {# Mininum value #}
        {% set minimumPageQuantityAround = min(currentPage - 1, pageCount - currentPage) %}
        {# Condition to show the right page numbers before current page: default or minimum value #}
        {% set conditionBefore = currentPage != 1 and minimumPageQuantityAround &lt;= currentPage - 1 %}
        {# Condition to show the right page numbers after current page: default or minimum value #}
        {% set conditionAfter = currentPage != pageCount and minimumPageQuantityAround &lt;= pageCount - currentPage %}
        {# Define page numbers before, other pages will be replaced by "..." #}
        {% set PageQuantityAroundBefore = conditionBefore ? defaultPageQuantityAround : minimumPageQuantityAround %}
        {# Define page numbers after, other pages will be replaced by "..." #}
        {% set PageQuantityAroundAfter = conditionAfter ? defaultPageQuantityAround : minimumPageQuantityAround %}
        &lt;div class="uk-flex uk-flex-center"&gt;
            &lt;ul class="uk-pagination uk-text-bold uk-text-uppercase"&gt;
                {# Previous link #}
                {% if currentPage - 1 != 0 %}
                &lt;li&gt;&lt;a class="st-color-yellow" href="{{ path('posts', { 'page': currentPage - 1 }) }}" title="Previous"&gt;&lt;span class="uk-margin-small-right" uk-pagination-previous&gt;&lt;/span&gt; Previous&lt;/a&gt;&lt;/li&gt;
                {% endif %}
                {% for i in 1..pageCount %}
                {# Current page to show #}
                {% if currentPage == i %}
                &lt;li class="st-color-red"&gt;{{ i }}&lt;/li&gt;
                {# Show "..." before current page depending on page numbers to show before #}
                {% elseif (i &lt; currentPage and 1 != i) and (i == currentPage - PageQuantityAroundBefore - 1) %}
                &lt;li class="uk-disabled"&gt;...&lt;/li&gt;
                {# Show "..." after current page depending on page numbers to show after #}
                {% elseif (i &gt; currentPage and pageCount != i) and (i == currentPage + PageQuantityAroundAfter + 1) %}
                &lt;li class="uk-disabled"&gt;...&lt;/li&gt;
                {# Hide pages under current page and before "..." excepted page 1 #}
                {% elseif (1 != i) and (i &lt; currentPage - PageQuantityAroundBefore - 1) %}
                &lt;li class="uk-hidden"&gt;&lt;a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {# Hide pages over current page and after "..." excepted page with number "pageCount" (last) #}
                {% elseif (pageCount != i) and (i &gt; currentPage + PageQuantityAroundAfter + 1) %}
                &lt;li class="uk-hidden"&gt;&lt;a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {# Apply particular style for lowest link corresponding to fisrt page 1, and Highest link corresponding to page total count #}
                {% elseif i == 1 or i == pageCount %}
                &lt;li&gt;&lt;a class="st-color-blue" href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {# Normal links which are not concerned by conditions above #}
                {% else %}
                &lt;li&gt;&lt;a href="{{ path('posts', { 'page': i }) }}" title="Page {{ i }}"&gt;{{ i }}&lt;/a&gt;&lt;/li&gt;
                {% endif %}
                {% endfor %}
                {# Next link #}
                {% if currentPage + 1 &lt;= pageCount %}
                &lt;li class="uk-margin-auto-left"&gt;&lt;a class="st-color-yellow" href="{{ path('posts', { 'page': currentPage + 1 }) }}" title="Next"&gt;Next &lt;span class="uk-margin-small-left" uk-pagination-next&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
                {% endif %}
            &lt;/ul&gt;
        &lt;/div&gt;
    {% endif %}</pre>
Et voici le résultat visuel de la pagination :

<img src="http://www.dotprogs.com/wp-content/uploaded-files/exemples-pagination-twig.jpg" alt="Exemples de pagination personnalisée Twig" width="500" height="250" class="size-full wp-image-872" />

Les classes CSS utilisées ici sont personnalisées (pour les couleurs) et issues du framework <a href="https://getuikit.com/" target="_blank" rel="noopener">UIkit</a> pour le comportement (lien désactivé, lien caché ...), donc rien de significatif pour ce qui est de l'aspect mise en forme.

Un tel système de pagination peut très facilement être adapté pour de l'<strong>AJAX</strong> et afficher les posts de la page courante (en renvoyant du contenu HTML avec un block Twig par exemple) en asynchrone afin d'être davantage "user friendly" !

Ces extraits de code sont issus d'un projet opérationnel, ceci-dit des "petites coquilles" peuvent s'être glissées entre les lignes.
N'hésitez pas à me contacter pour signaler des erreurs éventuelles ou pour partager des améliorations, car on peut souvent mieux faire ou faire plus pragmatique !<p>Cet article <a href="https://www.dotprogs.com/mettre-en-place-une-pagination-personnalisee-avec-twig/">Mettre en place une pagination personnalisée avec Twig</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
