<?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>Twig Archives | DOTPROGS</title>
	<atom:link href="https://www.dotprogs.com/etiquette/twig/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>Twig Archives | DOTPROGS</title>
	<link></link>
	<width>32</width>
	<height>32</height>
</image> 
	<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" fetchpriority="high" /></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" /></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" /></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>
