<?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>PHP Archives | DOTPROGS</title>
	<atom:link href="https://www.dotprogs.com/etiquette/php/feed/" rel="self" type="application/rss+xml" />
	<link></link>
	<description>DOTPROGS - Conception de sites web</description>
	<lastBuildDate>Tue, 19 Mar 2024 19:23:07 +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>PHP 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>Tester le chargement (disponibilité) d&#8217;une video en iframe avec AJAX</title>
		<link>https://www.dotprogs.com/tester-le-chargement-d-une-video-en-iframe-avec-ajax/</link>
		
		<dc:creator><![CDATA[DOTPROGS]]></dc:creator>
		<pubDate>Tue, 05 Feb 2019 21:17:18 +0000</pubDate>
				<category><![CDATA[Développement web]]></category>
		<category><![CDATA[Dotweb]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[AJAX]]></category>
		<category><![CDATA[C.O.R.S]]></category>
		<category><![CDATA[Cross Origin Resource Sharing]]></category>
		<category><![CDATA[JSON]]></category>
		<category><![CDATA[XMLHttpRequest]]></category>
		<guid isPermaLink="false">https://www.dotprogs.com/?p=909</guid>

					<description><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/iframe-loading-cors-request.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Iframe loading with C.O.R.S AJAX request" decoding="async" loading="lazy" /></p>
<p>J'ai été confronté à un problème que je n'avais pas envisagé en voulant lister en iframe un certain nombre de vidéos Youtube, Vimeo et Dailymotion (toutes disponibles en lecture) sur une page web.<br />
Ces vidéos sont gérées avec leur API JavaScript respectives pour interagir entre elles (autrement-dit stopper une video en lecture si une autre démarre), car quoi de plus embêtant que d'avoir deux videos lues en même temps, super cacophonie !<br />
Mon objectif initial était de gérer au mieux l'expérience utilisateur avec un loader pour chacune des videos en attendant que le chargement sur la page soit terminé.<br />
<strong><u>Note :</u> la notion de chargement consiste ici à s'assurer que la vidéo est un contenu disponible (sans erreur 404 par exemple).</strong><br />
Cela semblait assez nécessaire étant donné que plusieurs vidéos d'origine différente pouvaient être chargées au même moment.<br />
Au passage, je comprends mieux pourquoi Youtube ou d'autres services affichent une vidéo à la fois, car cela est plus judicieux et pragmatique !<br />
Seulement voilà, je n'avais pas accès au contenu des iframes du fait de la fameuse restriction de sécurité dans un contexte de "<a href="https://developer.mozilla.org/fr/docs/Web/HTTP/CORS" title="C.O.R.S" target="_blank" rel="noopener noreferrer nofollow">Cross Origin Resource Sharing</a>" (contenu provenant d'un domaine différent).<br />
je n'avais donc pas la possibilité d'accéder à un contenu externe (vidéo) en iframe et surtout tester son chargement directement en JavaScript !<br />
D'autant plus que la gestion d'évènement en JavaScript sur une iframe est plutôt limitée ...</p>
<p>Après un certain nombre de recherche et en me documentant sur de possibles solutions, j'ai opté pour une approche similaire à ce qui est présenté ici en vidéo :<br />
Cross domain proxy et requête AJAX : <a href="https://www.youtube.com/watch?v=o8puzjzpjqo" title="Cross domain proxy et requête AJAX" target="_blank" rel="noopener noreferrer nofollow">https://www.youtube.com/watch?v=o8puzjzpjqo</a></p>
<p>L'idée pour moi est d'effectuer une requête AJAX en passant en paramètre l'URL de la vidéo afin de vérifier en PHP que la vidéo est une ressource exploitable.<br />
Le simple retour attendu sera une chaîne JSON contenant l'équivalent d'un booléan 0 ("chargement" impossible) et 1 ("chargement" exploitable côté client en JavaScript) pour basiquement faire apparaître soit un message d'erreur, soit faire disparaître le loader et rendre accessible l'iframe. Cela ressemble plus à une astuce mais permet de s'assurer que la vidéo va être disponible pour la lecture par l'utilisateur.</p>
<p>Seules les portions de code principales sont présentées ici, je pense qu'elles suffisent à voir globalement le principe.<br />
Tous les commentaires sont en anglais car c'est une habitude personnelle !<br />
Le code PHP provient de développements présents dans un projet Symfony 4, le principe requête - réponse reste cependant classique.</p>
<p>- Voici la partie requête AJAX réalisée côté client :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">// Ajax request ES6 function
return new Promise((resolve, reject) =&gt; {
        let xhr = new XMLHttpRequest();
        xhr.open(obj.method || "GET", obj.url, obj.async || false );
        if (obj.overrideMimeType) {
            xhr.overrideMimeType(obj.overrideMimeType);
        }
        if (obj.responseType) {
            xhr.responseType = obj.responseType;
        }
        if (obj.withCredentials) {
            xhr.withCredentials = obj.withCredentials;
        }
        if (obj.headers) {
            Object.keys(obj.headers).forEach(key =&gt; {
                xhr.setRequestHeader(key, obj.headers[key]);
            });
        }
        // Custom functions for loader
        if (obj.onProgressFunction) {
            // Custom loader
            xhr.onprogress = () =&gt; {
                obj.onProgressFunction(xhr);
            };
        }
        if (obj.onLoadStartFunction) {
            xhr.onloadstart = () =&gt; {
                obj.onLoadStartFunction(xhr);
            };
        }
        if (obj.onLoadEndFunction) {
            xhr.onloadend = () =&gt; {
                obj.onLoadEndFunction(xhr);
            };
        }
        xhr.onerror = () =&gt; reject(xhr);
        xhr.onload = xhr.onreadystatechange = () =&gt; {
            if (xhr.readyState === XMLHttpRequest.DONE ) {
                if (xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300) {
                    resolve(xhr.response);
                } else {
                    reject(xhr);
                }
            }
        };
        // Send request
        xhr.send(obj.body !== undefined ? obj.body : null);
    });
};</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="php">import request from './all/ajax-request';

export default function() {

    // Other scripts before here ...
    
    // Check video loading with C.O.R.S
    function checkLoadingCORSRequest(method, url, resolvedCallback, errorCallback, args, timeOut) {
        // XMLHttpRequest object
        const obj = {
            method: method,
            url: url.replace(/(\?.+)/gi, ''),
            async: true,
            withCredentials: false,
            responseType: 'json'
        };
        // Use promise with callbacks
        request(obj).then((response) =&gt; {
            // no need to parse with JSON.parse(response).status: response is already an object
            if (response.status === 1) {
                resolvedCallback.apply(null, args);
                // Dispatch checked video success event to manage asynchronous execution and enable API
                let customEvent = new Event('checkedVideoSuccess');
                args[0].dispatchEvent(customEvent);
            } else {
                errorCallback.apply(null, args);
            }
            // Cancel timeOut
            clearTimeout(timeOut);
        }).catch(() =&gt; {
            errorCallback.apply(null, args);
            // Cancel timeOut
            clearTimeout(timeOut);
        });
    }

    // Manage iframe after loading success: media argument is an iframe element
    function afterMediaLoaded(media) {
        // Make loader disappear and render iframe correctly for instance
        // ...
    }

    // Manage loading failure: media argument is an iframe element    
    function whenMediaError(media) {
        // Display an error message instead of iframe element for instance
        // ...
    }

    // Convert NodeLists to arrays (example with multiple Youtube iframes in slider)
    let singleSliderElement = document.getElementById('...');
    let youtubeIframes = Array.from(singleSliderElement.querySelectorAll('...')),
    let ytTimeOut = [];
    for (let i = 0; i &lt; youtubeIframes.length; i ++) { // Call iframe loading rendering behavior with Youtube example in loop: ytTimeOut[i] = setTimeout(() =&gt; {
            // Dynamic URL to call action PHP class script (can obviously be static)
            let proxyURL = singleSliderElement.getAttribute('data-video-proxy') + youtubeIframes[i].getAttribute('src');
            checkLoadingCORSRequest('GET', proxyURL, afterMediaLoaded, whenMediaError, [youtubeIframes[i]], ytTimeOut[i]);
        }, 10);
    }

    // Other scripts after here ...
}</pre>
<p>- L'action (controller) appelée par la requête AJAX qui envoie la réponse JSON grâce à un responder (pattern ADR), une fois la vidéo contrôlée par un service :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
 
declare(strict_types = 1);
 
namespace App\Action;
 
use App\Responder\Json\JsonResponder;
use App\Service\Medias\VideoURLProxyChecker;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
 
/**
 * Class AjaxVideoURLCheckAction.
 *
 * Verify if video URL can be loaded using ajax.
 */
class AjaxVideoURLCheckAction
{
    use LoggerAwareTrait;
 
    /**
     * @var VideoURLProxyChecker
     */
    private $videoChecker;
 
    /**
     * AjaxTrickListAction constructor.
     *
     * @param VideoURLProxyChecker $videoChecker
     *
     * @param LoggerInterface $logger
     *
     * @return void
     */
    public function __construct(VideoURLProxyChecker $videoChecker, LoggerInterface $logger)
    {
       $this-&gt;videoChecker = $videoChecker;
       $this-&gt;setLogger($logger);
    }
 
    /**
     * Check if single trick video URL can be loaded from ajax request.
     *
     * @Route("/{_locale}/load-video/url/{url&lt;(.+)&gt;?}", name="load_video_url_check")
     *
     * @param JsonResponder $responder
     * @param Request       $request
     *
     * @return Response
     *
     * @see https://symfony.com/doc/current/routing/slash_in_parameter.html
     */
    public function __invoke(JsonResponder $responder, Request $request): JsonResponse
    {
        // Check video URL value
        $url = $this-&gt;videoChecker-&gt;filterURLAttribute($request);
        if (\is_null($url)) {
            $this-&gt;logger-&gt;error(
                "[trace app videos] AjaxVideoURLCheckAction/__invoke =&gt; " .
                "Technical error due to video url set to null: check loading process for both client and server side!"
            );
        }
        // Check if URL is formatted as expected (validation) and accessible
        $data = $this-&gt;videoChecker-&gt;verify($url);
        return $responder($data);
    }
}

</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
 
declare(strict_types = 1);
 
namespace App\Responder\Json;
 
use Symfony\Component\HttpFoundation\JsonResponse;
 
/**
 * Class JsonResponder.
 *
 * Manage a simple JSON response with status from ajax request.
 */
final class JsonResponder
{
    /**
     * Invokable Responder with Magic method.
     *
     * @param array $data
     *
     * @return JsonResponse
     */
    public function __invoke(array $data): JsonResponse
    {
        // Encode data with JSON string with serializer
        return new JsonResponse($data);
    }
}</pre>
<p>- Le service, classe PHP simple qui effectue les contrôles sur la ressource vidéo :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">&lt;?php

declare(strict_types = 1);

namespace App\Service\Medias;

use Symfony\Component\HttpFoundation\Request;

/*
 * Class VideoURLProxyChecker.
 *
 * Check if a video URL can be correctly loaded.
 * .
 */
class VideoURLProxyChecker
{
    // CAUTION: these iframe URL patterns should certainly be improved and are very important for a quite "secure" use!
    // Even more, they can evolve, so it is preferable to use providers APIs!
    const ALLOWED_URL_PATTERNS = [
        '/^https?:\/\/www\.youtube\.com\/embed\/[a-zA-Z0-9_-]+$/', // [\w-]+
        '/^https?:\/\/player\.vimeo\.com\/video\/[0-9]+$/',
        '/^https?:\/\/www\.dailymotion\.com\/embed\/video\/[a-zA-Z0-9]+$/'
    ];

    /**
     * Filter provided URL.
     *
     * @param Request $request
     * @param bool    $isDecoded
     *
     * @return null|string
     */
    public function filterURLAttribute(Request $request, bool $isDecoded = false): ?string
    {
        // Get URL to check
        $url = null;
        if (!\is_null($request-&gt;attributes-&gt;get('url'))) {
            $url = $request-&gt;attributes-&gt;get('url');
        }
        return $isDecoded ? $url : urldecode($url);
    }

    /**
     * Check if URL format is allowed.
     *
     * @param string|null $url
     *
     * @return bool
     */
    public function isAllowed(?string $url): bool
    {
        if (\is_null($url)) {
            return false;
        }
        $patterns = self::ALLOWED_URL_PATTERNS;
        // Use of "array_filter" would be more appropriate here!
        for ($i = 0; $i &lt; count($patterns); $i ++) {
            if (preg_match( $patterns[$i], urldecode($url))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Request URL to check if a content can be loaded. Choice is made to use cURL here.
     *
     * CAUTION: do not use this method alone because of potential "SSRF" attacks! At least use isAllowed() before...
     * @link https://www.vaadata.com/blog/understanding-web-vulnerability-server-side-request-forgery-1/
     *
     * @param string|null $url
     *
     * @return bool
     */
    public function isContent(?string $url): bool
    {
        if (\is_null($url)) {
            return false;
        }
        // Youtube particular case to check availability correctly
        // otherwise HTTP code is always 200!
        if (preg_match( '/youtube/', $url)) {
            $url = $this-&gt;prepareAccessToYoutubeVideoContent($url);
        }
        // Use cURL
        $handle = curl_init(urldecode($url));
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
        // Avoid content loading by getting the headers only
        curl_setopt($handle, CURLOPT_NOBODY, 1);
        // Request with cURL
        curl_exec($handle);
        $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
        // Check resource availability with HTTP code 200
        $isContentFound = 200 === $httpCode ? true : false;
        curl_close($handle);
        return $isContentFound;
    }

    /**
     * Check particular youtube video availability.
     *
     * @link Particular case for youtube video:
     * https://stackoverflow.com/questions/29166402/verify-if-video-exist-with-youtube-api-v3
     *
     * @param $url
     *
     * @return string the correct url to use to check availability
     */
    private function prepareAccessToYoutubeVideoContent($url): string
    {
        // Extract video id and use correct URL
        preg_match( '/embed\/(.+)/', urldecode($url), $matches);
        $videoID = $matches[1];
        $url ='https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=' . $videoID;
        return $url;
    }

    /**
     * Return a status code to be converted later in JSON string.
     *
     * Value 1 means URL can be loaded and value 0 means error context must be used!
     *
     * @param string|null $url
     *
     * @return array
     *
     * @see https://symfony.com/doc/current/controller.html#returning-json-response
     */
    public function verify(?string $url): array
    {
        // Prepare array to be converted in JSON string with Symfony JsonResponse object (no need to use "json_encode" here)
        if (\is_null($url)) {
            return ['status' =&gt; 0];
        }
        $url = urldecode($url);
        return $this-&gt;isAllowed($url) &amp;&amp; $this-&gt;isContent($url) ? ['status' =&gt; 1] : ['status' =&gt; 0];
    }

}</pre>
<p>Des erreurs (de copié-collé) peuvent s'être glissées dans le code bien que celui-ci soit issu d'un projet opérationnel.<br />
Je vous invite à me les signaler en me contactant si c'est le cas.<br />
Je suis également preneur si vous avez des conseils pour améliorer certains scripts, ou si vous estimez que certains points sont inexactes.</p>
<p>L'idée de départ, bien que perfectible, reste simple et intéressante pour vérifier l'accès à des ressources externes.</p>
<p>- Promesse et XMLHttpRequest : la fonction JavaScript "request(object)" est en grande partie inspirée de ce script : <a href="http://ccoenraets.github.io/es6-tutorial-data/promisify" title="XMLHttpRequest et promesse pour de l'AJAX" target="_blank" rel="noopener noreferrer nofollow">http://ccoenraets.github.io/es6-tutorial-data/promisify</a><br />
J'apprécie cette manière de faire notamment pour la gestion d'évènements vraiment souple.<br />
il n'y a pas que "axios" et "fetch api" dans la vie !<br />
- Voici un lien vers <a href="https://symfony.com/4" title="Symfony 4" target="_blank" rel="noopener noreferrer nofollow">Symfony 4</a> qui est utilisé en partie dans ce post.</p>
<p>Cet article <a href="https://www.dotprogs.com/tester-le-chargement-d-une-video-en-iframe-avec-ajax/">Tester le chargement (disponibilité) d&rsquo;une video en iframe avec AJAX</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/iframe-loading-cors-request.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="Iframe loading with C.O.R.S AJAX request" decoding="async" loading="lazy" /></p>J'ai été confronté à un problème que je n'avais pas envisagé en voulant lister en iframe un certain nombre de vidéos Youtube, Vimeo et Dailymotion (toutes disponibles en lecture) sur une page web.
Ces vidéos sont gérées avec leur API JavaScript respectives pour interagir entre elles (autrement-dit stopper une video en lecture si une autre démarre), car quoi de plus embêtant que d'avoir deux videos lues en même temps, super cacophonie !
Mon objectif initial était de gérer au mieux l'expérience utilisateur avec un loader pour chacune des videos en attendant que le chargement sur la page soit terminé.
<strong><u>Note :</u> la notion de chargement consiste ici à s'assurer que la vidéo est un contenu disponible (sans erreur 404 par exemple).</strong>
Cela semblait assez nécessaire étant donné que plusieurs vidéos d'origine différente pouvaient être chargées au même moment.
Au passage, je comprends mieux pourquoi Youtube ou d'autres services affichent une vidéo à la fois, car cela est plus judicieux et pragmatique !
Seulement voilà, je n'avais pas accès au contenu des iframes du fait de la fameuse restriction de sécurité dans un contexte de "<a href="https://developer.mozilla.org/fr/docs/Web/HTTP/CORS" title="C.O.R.S" target="_blank" rel="noopener noreferrer nofollow">Cross Origin Resource Sharing</a>" (contenu provenant d'un domaine différent).
je n'avais donc pas la possibilité d'accéder à un contenu externe (vidéo) en iframe et surtout tester son chargement directement en JavaScript !
D'autant plus que la gestion d'évènement en JavaScript sur une iframe est plutôt limitée ...

Après un certain nombre de recherche et en me documentant sur de possibles solutions, j'ai opté pour une approche similaire à ce qui est présenté ici en vidéo :
Cross domain proxy et requête AJAX : <a href="https://www.youtube.com/watch?v=o8puzjzpjqo" title="Cross domain proxy et requête AJAX" target="_blank" rel="noopener noreferrer nofollow">https://www.youtube.com/watch?v=o8puzjzpjqo</a>

L'idée pour moi est d'effectuer une requête AJAX en passant en paramètre l'URL de la vidéo afin de vérifier en PHP que la vidéo est une ressource exploitable.
Le simple retour attendu sera une chaîne JSON contenant l'équivalent d'un booléan 0 ("chargement" impossible) et 1 ("chargement" exploitable côté client en JavaScript) pour basiquement faire apparaître soit un message d'erreur, soit faire disparaître le loader et rendre accessible l'iframe. Cela ressemble plus à une astuce mais permet de s'assurer que la vidéo va être disponible pour la lecture par l'utilisateur.

Seules les portions de code principales sont présentées ici, je pense qu'elles suffisent à voir globalement le principe.
Tous les commentaires sont en anglais car c'est une habitude personnelle !
Le code PHP provient de développements présents dans un projet Symfony 4, le principe requête - réponse reste cependant classique.

- Voici la partie requête AJAX réalisée côté client :
<pre class="EnlighterJSRAW" data-enlighter-language="php">// Ajax request ES6 function
return new Promise((resolve, reject) =&gt; {
        let xhr = new XMLHttpRequest();
        xhr.open(obj.method || "GET", obj.url, obj.async || false );
        if (obj.overrideMimeType) {
            xhr.overrideMimeType(obj.overrideMimeType);
        }
        if (obj.responseType) {
            xhr.responseType = obj.responseType;
        }
        if (obj.withCredentials) {
            xhr.withCredentials = obj.withCredentials;
        }
        if (obj.headers) {
            Object.keys(obj.headers).forEach(key =&gt; {
                xhr.setRequestHeader(key, obj.headers[key]);
            });
        }
        // Custom functions for loader
        if (obj.onProgressFunction) {
            // Custom loader
            xhr.onprogress = () =&gt; {
                obj.onProgressFunction(xhr);
            };
        }
        if (obj.onLoadStartFunction) {
            xhr.onloadstart = () =&gt; {
                obj.onLoadStartFunction(xhr);
            };
        }
        if (obj.onLoadEndFunction) {
            xhr.onloadend = () =&gt; {
                obj.onLoadEndFunction(xhr);
            };
        }
        xhr.onerror = () =&gt; reject(xhr);
        xhr.onload = xhr.onreadystatechange = () =&gt; {
            if (xhr.readyState === XMLHttpRequest.DONE ) {
                if (xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300) {
                    resolve(xhr.response);
                } else {
                    reject(xhr);
                }
            }
        };
        // Send request
        xhr.send(obj.body !== undefined ? obj.body : null);
    });
};</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="php">import request from './all/ajax-request';

export default function() {

    // Other scripts before here ...
    
    // Check video loading with C.O.R.S
    function checkLoadingCORSRequest(method, url, resolvedCallback, errorCallback, args, timeOut) {
        // XMLHttpRequest object
        const obj = {
            method: method,
            url: url.replace(/(\?.+)/gi, ''),
            async: true,
            withCredentials: false,
            responseType: 'json'
        };
        // Use promise with callbacks
        request(obj).then((response) =&gt; {
            // no need to parse with JSON.parse(response).status: response is already an object
            if (response.status === 1) {
                resolvedCallback.apply(null, args);
                // Dispatch checked video success event to manage asynchronous execution and enable API
                let customEvent = new Event('checkedVideoSuccess');
                args[0].dispatchEvent(customEvent);
            } else {
                errorCallback.apply(null, args);
            }
            // Cancel timeOut
            clearTimeout(timeOut);
        }).catch(() =&gt; {
            errorCallback.apply(null, args);
            // Cancel timeOut
            clearTimeout(timeOut);
        });
    }

    // Manage iframe after loading success: media argument is an iframe element
    function afterMediaLoaded(media) {
        // Make loader disappear and render iframe correctly for instance
        // ...
    }

    // Manage loading failure: media argument is an iframe element    
    function whenMediaError(media) {
        // Display an error message instead of iframe element for instance
        // ...
    }

    // Convert NodeLists to arrays (example with multiple Youtube iframes in slider)
    let singleSliderElement = document.getElementById('...');
    let youtubeIframes = Array.from(singleSliderElement.querySelectorAll('...')),
    let ytTimeOut = [];
    for (let i = 0; i &lt; youtubeIframes.length; i ++) { // Call iframe loading rendering behavior with Youtube example in loop: ytTimeOut[i] = setTimeout(() =&gt; {
            // Dynamic URL to call action PHP class script (can obviously be static)
            let proxyURL = singleSliderElement.getAttribute('data-video-proxy') + youtubeIframes[i].getAttribute('src');
            checkLoadingCORSRequest('GET', proxyURL, afterMediaLoaded, whenMediaError, [youtubeIframes[i]], ytTimeOut[i]);
        }, 10);
    }

    // Other scripts after here ...
}</pre>
- L'action (controller) appelée par la requête AJAX qui envoie la réponse JSON grâce à un responder (pattern ADR), une fois la vidéo contrôlée par un service :
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
 
declare(strict_types = 1);
 
namespace App\Action;
 
use App\Responder\Json\JsonResponder;
use App\Service\Medias\VideoURLProxyChecker;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
 
/**
 * Class AjaxVideoURLCheckAction.
 *
 * Verify if video URL can be loaded using ajax.
 */
class AjaxVideoURLCheckAction
{
    use LoggerAwareTrait;
 
    /**
     * @var VideoURLProxyChecker
     */
    private $videoChecker;
 
    /**
     * AjaxTrickListAction constructor.
     *
     * @param VideoURLProxyChecker $videoChecker
     *
     * @param LoggerInterface $logger
     *
     * @return void
     */
    public function __construct(VideoURLProxyChecker $videoChecker, LoggerInterface $logger)
    {
       $this-&gt;videoChecker = $videoChecker;
       $this-&gt;setLogger($logger);
    }
 
    /**
     * Check if single trick video URL can be loaded from ajax request.
     *
     * @Route("/{_locale}/load-video/url/{url&lt;(.+)&gt;?}", name="load_video_url_check")
     *
     * @param JsonResponder $responder
     * @param Request       $request
     *
     * @return Response
     *
     * @see https://symfony.com/doc/current/routing/slash_in_parameter.html
     */
    public function __invoke(JsonResponder $responder, Request $request): JsonResponse
    {
        // Check video URL value
        $url = $this-&gt;videoChecker-&gt;filterURLAttribute($request);
        if (\is_null($url)) {
            $this-&gt;logger-&gt;error(
                "[trace app videos] AjaxVideoURLCheckAction/__invoke =&gt; " .
                "Technical error due to video url set to null: check loading process for both client and server side!"
            );
        }
        // Check if URL is formatted as expected (validation) and accessible
        $data = $this-&gt;videoChecker-&gt;verify($url);
        return $responder($data);
    }
}

</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
 
declare(strict_types = 1);
 
namespace App\Responder\Json;
 
use Symfony\Component\HttpFoundation\JsonResponse;
 
/**
 * Class JsonResponder.
 *
 * Manage a simple JSON response with status from ajax request.
 */
final class JsonResponder
{
    /**
     * Invokable Responder with Magic method.
     *
     * @param array $data
     *
     * @return JsonResponse
     */
    public function __invoke(array $data): JsonResponse
    {
        // Encode data with JSON string with serializer
        return new JsonResponse($data);
    }
}</pre>
- Le service, classe PHP simple qui effectue les contrôles sur la ressource vidéo :
<pre class="EnlighterJSRAW" data-enlighter-language="generic">&lt;?php

declare(strict_types = 1);

namespace App\Service\Medias;

use Symfony\Component\HttpFoundation\Request;

/*
 * Class VideoURLProxyChecker.
 *
 * Check if a video URL can be correctly loaded.
 * .
 */
class VideoURLProxyChecker
{
    // CAUTION: these iframe URL patterns should certainly be improved and are very important for a quite "secure" use!
    // Even more, they can evolve, so it is preferable to use providers APIs!
    const ALLOWED_URL_PATTERNS = [
        '/^https?:\/\/www\.youtube\.com\/embed\/[a-zA-Z0-9_-]+$/', // [\w-]+
        '/^https?:\/\/player\.vimeo\.com\/video\/[0-9]+$/',
        '/^https?:\/\/www\.dailymotion\.com\/embed\/video\/[a-zA-Z0-9]+$/'
    ];

    /**
     * Filter provided URL.
     *
     * @param Request $request
     * @param bool    $isDecoded
     *
     * @return null|string
     */
    public function filterURLAttribute(Request $request, bool $isDecoded = false): ?string
    {
        // Get URL to check
        $url = null;
        if (!\is_null($request-&gt;attributes-&gt;get('url'))) {
            $url = $request-&gt;attributes-&gt;get('url');
        }
        return $isDecoded ? $url : urldecode($url);
    }

    /**
     * Check if URL format is allowed.
     *
     * @param string|null $url
     *
     * @return bool
     */
    public function isAllowed(?string $url): bool
    {
        if (\is_null($url)) {
            return false;
        }
        $patterns = self::ALLOWED_URL_PATTERNS;
        // Use of "array_filter" would be more appropriate here!
        for ($i = 0; $i &lt; count($patterns); $i ++) {
            if (preg_match( $patterns[$i], urldecode($url))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Request URL to check if a content can be loaded. Choice is made to use cURL here.
     *
     * CAUTION: do not use this method alone because of potential "SSRF" attacks! At least use isAllowed() before...
     * @link https://www.vaadata.com/blog/understanding-web-vulnerability-server-side-request-forgery-1/
     *
     * @param string|null $url
     *
     * @return bool
     */
    public function isContent(?string $url): bool
    {
        if (\is_null($url)) {
            return false;
        }
        // Youtube particular case to check availability correctly
        // otherwise HTTP code is always 200!
        if (preg_match( '/youtube/', $url)) {
            $url = $this-&gt;prepareAccessToYoutubeVideoContent($url);
        }
        // Use cURL
        $handle = curl_init(urldecode($url));
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
        // Avoid content loading by getting the headers only
        curl_setopt($handle, CURLOPT_NOBODY, 1);
        // Request with cURL
        curl_exec($handle);
        $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
        // Check resource availability with HTTP code 200
        $isContentFound = 200 === $httpCode ? true : false;
        curl_close($handle);
        return $isContentFound;
    }

    /**
     * Check particular youtube video availability.
     *
     * @link Particular case for youtube video:
     * https://stackoverflow.com/questions/29166402/verify-if-video-exist-with-youtube-api-v3
     *
     * @param $url
     *
     * @return string the correct url to use to check availability
     */
    private function prepareAccessToYoutubeVideoContent($url): string
    {
        // Extract video id and use correct URL
        preg_match( '/embed\/(.+)/', urldecode($url), $matches);
        $videoID = $matches[1];
        $url ='https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=' . $videoID;
        return $url;
    }

    /**
     * Return a status code to be converted later in JSON string.
     *
     * Value 1 means URL can be loaded and value 0 means error context must be used!
     *
     * @param string|null $url
     *
     * @return array
     *
     * @see https://symfony.com/doc/current/controller.html#returning-json-response
     */
    public function verify(?string $url): array
    {
        // Prepare array to be converted in JSON string with Symfony JsonResponse object (no need to use "json_encode" here)
        if (\is_null($url)) {
            return ['status' =&gt; 0];
        }
        $url = urldecode($url);
        return $this-&gt;isAllowed($url) &amp;&amp; $this-&gt;isContent($url) ? ['status' =&gt; 1] : ['status' =&gt; 0];
    }

}</pre>
Des erreurs (de copié-collé) peuvent s'être glissées dans le code bien que celui-ci soit issu d'un projet opérationnel.
Je vous invite à me les signaler en me contactant si c'est le cas.
Je suis également preneur si vous avez des conseils pour améliorer certains scripts, ou si vous estimez que certains points sont inexactes.

L'idée de départ, bien que perfectible, reste simple et intéressante pour vérifier l'accès à des ressources externes.

- Promesse et XMLHttpRequest : la fonction JavaScript "request(object)" est en grande partie inspirée de ce script : <a href="http://ccoenraets.github.io/es6-tutorial-data/promisify" title="XMLHttpRequest et promesse pour de l'AJAX" target="_blank" rel="noopener noreferrer nofollow">http://ccoenraets.github.io/es6-tutorial-data/promisify</a>
J'apprécie cette manière de faire notamment pour la gestion d'évènements vraiment souple.
il n'y a pas que "axios" et "fetch api" dans la vie !
- Voici un lien vers <a href="https://symfony.com/4" title="Symfony 4" target="_blank" rel="noopener noreferrer nofollow">Symfony 4</a> qui est utilisé en partie dans ce post.<p>Cet article <a href="https://www.dotprogs.com/tester-le-chargement-d-une-video-en-iframe-avec-ajax/">Tester le chargement (disponibilité) d&rsquo;une video en iframe avec AJAX</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>
		<item>
		<title>Kirki, la boîte à outils pour le « customizer » de WordPress</title>
		<link>https://www.dotprogs.com/kirki-wp-custom-controls/</link>
		
		<dc:creator><![CDATA[DOTPROGS]]></dc:creator>
		<pubDate>Sat, 15 Oct 2016 07:43:49 +0000</pubDate>
				<category><![CDATA[Développement web]]></category>
		<category><![CDATA[Dotweb]]></category>
		<category><![CDATA[custom controls]]></category>
		<category><![CDATA[custom panels]]></category>
		<category><![CDATA[custom sections]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[librairie Kirki]]></category>
		<category><![CDATA[personnalisation WordPress]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress theme customizer]]></category>
		<category><![CDATA[WP customizer]]></category>
		<guid isPermaLink="false">https://www.dotprogs.com/?p=510</guid>

					<description><![CDATA[<p><img width="1024" height="576" src="https://www.dotprogs.com/wp-content/uploaded-files/wp-customizer-kirki-tools.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="WP customizer API - Kirki Tools" decoding="async" loading="lazy" /></p>
<h2 class="dp-level2-title">DE NOUVELLES FONCTIONNALITÉS POUR LE CUSTOMIZER DE WORDPRESS</h2>
<h3 class="dp-level3-title">UNE EXPÉRIENCE UTILISATEUR AMÉLIORÉE ET PLUS AGRÉABLE</h3>
<p>Le "customizer" de WordPress est l'interface de personnalisation avancée proposée au sein de l'administration du CMS (système de gestion de contenu).</p>
<p>Avant le customizer, le principe de "pages d'options" peu uniformes en fonction des thèmes était la règle. Le "customizer" propose de ce fait d'unifier cette approche avec une API ("interface de programmation").</p>
<p>Le "customizer" a pour intérêt de permettre de visualiser sa page web en même temps qu'elle est modifiée.</p>
<p>On y accède depuis le menu "Apparence -&gt; Personnaliser", il est possible de piloter la forme et le fond du site web en fonction des champs de formulaires ("custom controls") définis et configurés par le thème activé.</p>
<p>Ses "contrôles personnalisés" sont organisés en "panels" (qui sont les grands ensembles de personnalisation), des "sections" divisent ces "panels" en sous-ensembles pour rendre plus lisibles les actions proposées par ces custom controls".</p>
<p>En fonction du thème choisi, la personnalisation peut être très avancée ou quasi inexistante si les développeurs de thèmes n'ont pas fait le choix du "customizer".</p>
<p>Le principe de base de l'API du customizer est de permettre la création de classes php qui héritent de classes natives aussi bien pour les panels, les sections que pour les "custom controls". Ainsi des contrôles personnalisés totalement nouveaux peuvent être créés sur la base d'un code uniforme proposé par cette API.</p>
<p>Un exemple de "custom control" non natif créé par un développeur est présenté ci-dessous, et propose spécifiquement les typographie de Google les plus populaires pour personnaliser les textes d'un site web :</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
 
if ( ! class_exists( 'WP_Customize_Control' ) )
    return NULL;
 
/**
 * A class to create a dropdown for all google fonts
 */
 class Google_Font_Dropdown_Custom_Control extends WP_Customize_Control
 {
    private $fonts = false;
 
    public function __construct($manager, $id, $args = array(), $options = array())
    {
        $this-&gt;fonts = $this-&gt;get_fonts();
        parent::__construct( $manager, $id, $args );
    }
 
    /**
     * Render the content of the category dropdown
     *
     * @return HTML
     */
    public function render_content()
    {
        if(!empty($this-&gt;fonts))
        {
            ?&gt;
                &lt;label&gt;
                    &lt;span class="customize-category-select-control"&gt;&lt;?php echo esc_html( $this-&gt;label ); ?&gt;&lt;/span&gt;
                    &lt;select &lt;?php $this-&gt;link(); ?&gt;&gt;
                        &lt;?php
                            foreach ( $this-&gt;fonts as $k =&gt; $v )
                            {
                                printf('&lt;option value="%s" %s&gt;%s&lt;/option&gt;', $k, selected($this-&gt;value(), $k, false), $v-&gt;family);
                            }
                        ?&gt;
                    &lt;/select&gt;
                &lt;/label&gt;
            &lt;?php
        }
    }
 
    /**
     * Get the google fonts from the API or in the cache
     *
     * @param  integer $amount
     *
     * @return String
     */
    public function get_fonts( $amount = 30 )
    {
        $fontFile = '/cache/google-web-fonts.txt';
 
        //Total time the file will be cached in seconds, set to a week
        $cachetime = 86400 * 7;
 
        if(file_exists($fontFile) &amp;&amp; $cachetime &lt; filemtime($fontFile))
        {
            $content = json_decode(file_get_contents($fontFile));
        } else {
 
            $googleApi = 'https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&amp;key={API_KEY}';
 
            $fontContent = wp_remote_get( $googleApi, array('sslverify'   =&gt; false) );
 
            $fp = fopen($fontFile, 'w');
            fwrite($fp, $fontContent['body']);
            fclose($fp);
 
            $content = json_decode($fontContent['body']);
        }
 
        if($amount == 'all')
        {
            return $content-&gt;items;
        } else {
            return array_slice($content-&gt;items, 0, $amount);
        }
    }
 }</pre>
<p>Plus d'exemples sur le site des développeurs sont accessibles par ce <!-- <a title="exemples de custom controls développés sur la base de l'API du customizer" href="https://paulund.co.uk/custom-wordpress-controls" target="_blank" rel="noopener">lien</a> --> <a title="exemples de custom controls développés sur la base de l'API du customizer" href="https://docs.themeum.com/kirki/getting-started/introduction/" target="_blank" rel="noopener">lien</a>.</p>
<p>Quelques développeurs web ou sociétés concevant des thèmes WordPress premium enrichissent, petit à petit&nbsp;par leur travail, la connaissance du "customizer" et contribuent à accroître les ressources sur le sujet.</p>
<p>Il existe peu de ressources de manière générale (même si un certain nombre de sites web anglophones s'y mettent)&nbsp;bien que le customizer existe depuis la version WordPress 3.4. A ce jour, on peut considérer qu'il a déjà bien évolué même s'il n'est pas un outil mature.</p>
<p>Il faut remarquer qu'il existe également une API JavaScript en parallèle mise à disposition par le "customizer" avec de <a title="tutoriels sur l'API JavaScript du customizer" href="https://code.tutsplus.com/tutorials/customizer-javascript-apis-getting-started--cms-26838" target="_blank" rel="noopener">bons tutoriels</a> ici ou là, ce qui le rend très efficace.</p>
<p>En addition de cette API JavaScript, les créateurs de sites web ont aussi la possibilité de réaliser des "custom controls"&nbsp;avec des templates <a title="Undescore JS" href="http://underscorejs.org/" target="_blank" rel="noopener">Underscore JS</a>.</p>
<p>On peut aussi noter que la documentation proposée à la communauté par WordPress est assez succincte à ce jour, les développeurs web ont donc la nécessité d'étudier le code en détail !</p>
<p>Quelques types de champs efficaces sont proposés nativement, <a title="API du customizer" href="https://developer.wordpress.org/themes/customize-api/" target="_blank" rel="noopener">la documentation de l'API</a> les présente brièvement, cependant ils limitent les possibilités de personnalisations avancées que l'on peut trouver ailleurs sur de nombreux formulaires servant à de la configuration.</p>
<p>Heureusement l'API du "customizer" est étudiée&nbsp;pour que les codeurs puissent s'exprimer et adapter celui-ci&nbsp;à leurs besoins et objectifs.</p>
<p>C'est à ce niveau qu'intervient <a title="librairie PHP Kirki" href="https://github.com/themeum/kirki" target="_blank" rel="noopener">Kirki</a>&nbsp;en proposant des composants d'interface variés visible sur <a title="composants de Kirki" href="https://github.com/themeum/kirki/tree/develop/docs/files" target="_blank" rel="noopener">cette page</a>, dit "user friendly" (facilitant l'expérience utilisateur), et très pratiques pour contrôler l'apparence et le contenu de son site web. Le "customizer" de WordPress, est de fait, véritablement&nbsp;amélioré et ses capacités sont accrues.</p>
<p>La librairie proposée par Kirki a pour but de rendre plus aisée l'utilisation des "custom controls" tout en respectant au maximum les bases de l'API.</p>
<p>Kirki est disponible sous forme de plugin et peut aussi être intégré directement à un thème pour les besoins des développeurs et intégrateurs web.</p>
<p>Avec Kirki, il est davantage question de l'API PHP du "customizer", adaptée par ce dernier pour accélérer le déploiement de l'interface.<br />
Bien sûr, étant donné l'apparence des contrôles de la librairie, du JavaScript et des CSS3 rendent le rendu très soigné !</p>
<p>Le <a title="plugin WordPress Kirki" href="https://fr.wordpress.org/plugins/kirki/" target="_blank" rel="noopener">plugin</a> WordPress Kirki est disponible sur wordpress.org.</p>
<p>Des&nbsp;ressources plus générales sur le "customizer" sont présentes <a title="ressources sur le customizer" href="https://make.wordpress.org/themes/2015/05/07/customizer-tutorials-and-documentation/" target="_blank" rel="noopener">ici</a>.</p>
<p>Cet article <a href="https://www.dotprogs.com/kirki-wp-custom-controls/">Kirki, la boîte à outils pour le « customizer » de WordPress</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/wp-customizer-kirki-tools.png" class="attachment-md_post_thumb_large size-md_post_thumb_large wp-post-image" alt="WP customizer API - Kirki Tools" decoding="async" loading="lazy" /></p><h2 class="dp-level2-title">DE NOUVELLES FONCTIONNALITÉS POUR LE CUSTOMIZER DE WORDPRESS</h2>
<h3 class="dp-level3-title">UNE EXPÉRIENCE UTILISATEUR AMÉLIORÉE ET PLUS AGRÉABLE</h3>
Le "customizer" de WordPress est l'interface de personnalisation avancée proposée au sein de l'administration du CMS (système de gestion de contenu).

Avant le customizer, le principe de "pages d'options" peu uniformes en fonction des thèmes était la règle. Le "customizer" propose de ce fait d'unifier cette approche avec une API ("interface de programmation").

Le "customizer" a pour intérêt de permettre de visualiser sa page web en même temps qu'elle est modifiée.

On y accède depuis le menu "Apparence -&gt; Personnaliser", il est possible de piloter la forme et le fond du site web en fonction des champs de formulaires ("custom controls") définis et configurés par le thème activé.

Ses "contrôles personnalisés" sont organisés en "panels" (qui sont les grands ensembles de personnalisation), des "sections" divisent ces "panels" en sous-ensembles pour rendre plus lisibles les actions proposées par ces custom controls".

En fonction du thème choisi, la personnalisation peut être très avancée ou quasi inexistante si les développeurs de thèmes n'ont pas fait le choix du "customizer".

Le principe de base de l'API du customizer est de permettre la création de classes php qui héritent de classes natives aussi bien pour les panels, les sections que pour les "custom controls". Ainsi des contrôles personnalisés totalement nouveaux peuvent être créés sur la base d'un code uniforme proposé par cette API.

Un exemple de "custom control" non natif créé par un développeur est présenté ci-dessous, et propose spécifiquement les typographie de Google les plus populaires pour personnaliser les textes d'un site web :
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
 
if ( ! class_exists( 'WP_Customize_Control' ) )
    return NULL;
 
/**
 * A class to create a dropdown for all google fonts
 */
 class Google_Font_Dropdown_Custom_Control extends WP_Customize_Control
 {
    private $fonts = false;
 
    public function __construct($manager, $id, $args = array(), $options = array())
    {
        $this-&gt;fonts = $this-&gt;get_fonts();
        parent::__construct( $manager, $id, $args );
    }
 
    /**
     * Render the content of the category dropdown
     *
     * @return HTML
     */
    public function render_content()
    {
        if(!empty($this-&gt;fonts))
        {
            ?&gt;
                &lt;label&gt;
                    &lt;span class="customize-category-select-control"&gt;&lt;?php echo esc_html( $this-&gt;label ); ?&gt;&lt;/span&gt;
                    &lt;select &lt;?php $this-&gt;link(); ?&gt;&gt;
                        &lt;?php
                            foreach ( $this-&gt;fonts as $k =&gt; $v )
                            {
                                printf('&lt;option value="%s" %s&gt;%s&lt;/option&gt;', $k, selected($this-&gt;value(), $k, false), $v-&gt;family);
                            }
                        ?&gt;
                    &lt;/select&gt;
                &lt;/label&gt;
            &lt;?php
        }
    }
 
    /**
     * Get the google fonts from the API or in the cache
     *
     * @param  integer $amount
     *
     * @return String
     */
    public function get_fonts( $amount = 30 )
    {
        $fontFile = '/cache/google-web-fonts.txt';
 
        //Total time the file will be cached in seconds, set to a week
        $cachetime = 86400 * 7;
 
        if(file_exists($fontFile) &amp;&amp; $cachetime &lt; filemtime($fontFile))
        {
            $content = json_decode(file_get_contents($fontFile));
        } else {
 
            $googleApi = 'https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&amp;key={API_KEY}';
 
            $fontContent = wp_remote_get( $googleApi, array('sslverify'   =&gt; false) );
 
            $fp = fopen($fontFile, 'w');
            fwrite($fp, $fontContent['body']);
            fclose($fp);
 
            $content = json_decode($fontContent['body']);
        }
 
        if($amount == 'all')
        {
            return $content-&gt;items;
        } else {
            return array_slice($content-&gt;items, 0, $amount);
        }
    }
 }</pre>
Plus d'exemples sur le site des développeurs sont accessibles par ce <!-- <a title="exemples de custom controls développés sur la base de l'API du customizer" href="https://paulund.co.uk/custom-wordpress-controls" target="_blank" rel="noopener">lien</a> --> <a title="exemples de custom controls développés sur la base de l'API du customizer" href="https://docs.themeum.com/kirki/getting-started/introduction/" target="_blank" rel="noopener">lien</a>.

Quelques développeurs web ou sociétés concevant des thèmes WordPress premium enrichissent, petit à petit&nbsp;par leur travail, la connaissance du "customizer" et contribuent à accroître les ressources sur le sujet.

Il existe peu de ressources de manière générale (même si un certain nombre de sites web anglophones s'y mettent)&nbsp;bien que le customizer existe depuis la version WordPress 3.4. A ce jour, on peut considérer qu'il a déjà bien évolué même s'il n'est pas un outil mature.

Il faut remarquer qu'il existe également une API JavaScript en parallèle mise à disposition par le "customizer" avec de <a title="tutoriels sur l'API JavaScript du customizer" href="https://code.tutsplus.com/tutorials/customizer-javascript-apis-getting-started--cms-26838" target="_blank" rel="noopener">bons tutoriels</a> ici ou là, ce qui le rend très efficace.

En addition de cette API JavaScript, les créateurs de sites web ont aussi la possibilité de réaliser des "custom controls"&nbsp;avec des templates <a title="Undescore JS" href="http://underscorejs.org/" target="_blank" rel="noopener">Underscore JS</a>.

On peut aussi noter que la documentation proposée à la communauté par WordPress est assez succincte à ce jour, les développeurs web ont donc la nécessité d'étudier le code en détail !

Quelques types de champs efficaces sont proposés nativement, <a title="API du customizer" href="https://developer.wordpress.org/themes/customize-api/" target="_blank" rel="noopener">la documentation de l'API</a> les présente brièvement, cependant ils limitent les possibilités de personnalisations avancées que l'on peut trouver ailleurs sur de nombreux formulaires servant à de la configuration.

Heureusement l'API du "customizer" est étudiée&nbsp;pour que les codeurs puissent s'exprimer et adapter celui-ci&nbsp;à leurs besoins et objectifs.

C'est à ce niveau qu'intervient <a title="librairie PHP Kirki" href="https://github.com/themeum/kirki" target="_blank" rel="noopener">Kirki</a>&nbsp;en proposant des composants d'interface variés visible sur <a title="composants de Kirki" href="https://github.com/themeum/kirki/tree/develop/docs/files" target="_blank" rel="noopener">cette page</a>, dit "user friendly" (facilitant l'expérience utilisateur), et très pratiques pour contrôler l'apparence et le contenu de son site web. Le "customizer" de WordPress, est de fait, véritablement&nbsp;amélioré et ses capacités sont accrues.

La librairie proposée par Kirki a pour but de rendre plus aisée l'utilisation des "custom controls" tout en respectant au maximum les bases de l'API.

Kirki est disponible sous forme de plugin et peut aussi être intégré directement à un thème pour les besoins des développeurs et intégrateurs web.

Avec Kirki, il est davantage question de l'API PHP du "customizer", adaptée par ce dernier pour accélérer le déploiement de l'interface.
Bien sûr, étant donné l'apparence des contrôles de la librairie, du JavaScript et des CSS3 rendent le rendu très soigné !

Le <a title="plugin WordPress Kirki" href="https://fr.wordpress.org/plugins/kirki/" target="_blank" rel="noopener">plugin</a> WordPress Kirki est disponible sur wordpress.org.

Des&nbsp;ressources plus générales sur le "customizer" sont présentes <a title="ressources sur le customizer" href="https://make.wordpress.org/themes/2015/05/07/customizer-tutorials-and-documentation/" target="_blank" rel="noopener">ici</a>.<p>Cet article <a href="https://www.dotprogs.com/kirki-wp-custom-controls/">Kirki, la boîte à outils pour le « customizer » de WordPress</a> est apparu en premier sur <a href="https://www.dotprogs.com">DOTPROGS</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
