Overview
Comment:Utiliser des signaux génériques pour les boucles de squelettes, plutôt que des hooks spécifiques. Si besoin on mettra ensuite en place un cache statique des associations pour ne pas taper dans SQLite à chaque fois qu'on veut savoir si un signal est associé à qq chose.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 10409a33535943e1adb526e3734a1fa3ed95f7fe
User & Date: bohwaz on 2015-01-23 03:49:02
Other Links: manifest | tags
Context
2015-01-23
04:28
Exclure les éléments des squelettes check-in: 7f3f931fc1 user: bohwaz tags: trunk
03:49
Utiliser des signaux génériques pour les boucles de squelettes, plutôt que des hooks spécifiques. Si besoin on mettra ensuite en place un cache statique des associations pour ne pas taper dans SQLite à chaque fois qu'on veut savoir si un signal est associé à qq chose. check-in: 10409a3353 user: bohwaz tags: trunk
2015-01-20
06:29
Adaptation à la révision de MiniSkel pour permettre les tags imbriqués. Vérification de sécurité à la compilation plutôt qu'à l'exécution. Ajout d'une balise #LANGUE_VISITEUR. check-in: eb6cca0e56 user: bohwaz tags: trunk
Changes

Modified doc/dev/plugins.skriv from [e269494bdf] to [b8ff24ef4e].

95
96
97
98
99
100
101

102
103
104
105
106
107
108
...
111
112
113
114
115
116
117




118
119
120
121
122
123
124
...
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
* getConfig(string $key) : récupère la valeur de la clé $key pour la configuration du plugin
* getInfos() : renvoie les informations enregistrées sur le plugin
* upgrade() : mise à jour du plugin
* needsUpgrade() : le plugin doit-il être mis à jour ?
* uninstall() : désinstaller le plugin
* id() : renvoie l'identifiant du plugin
* path() : renvoie le chemin vers l'archive du plugin


=== Base de données ===

Tous les plugins ont un accès illimité à la base de données principale de Garradin. Cependant il est interdit d'ajouter, modifier ou supprimer des données directement dans les tables de cette BDD afin de ne pas compromettre l'intégrité des données. Pour modifier ces données il faut utiliser les méthodes de Garradin.

Chaque plugin peut créer une ou plusieurs tables dans cette BDD, elles devront par contre être supprimées à la désinstallation. Dans ce cas un plugin peut modifier directement ses tables.

................................................................................
Les plugins ne doivent pas créer, modifier ou supprimer de fichiers dans l'arborescence de Garradin.

Les seuls fichiers qu'un plugin devrait modifier sont :
* du cache : utiliser de préférence l'objet Static_Cache de Garradin, ou faire attention aux collisions de noms ;
* les squelettes dans le répertoire **www/squelettes/**

Pour enregistrer et récupérer des documents il faut utiliser les méthodes de stockage de fichiers fournies par Garradin.





=== Dans les templates ===

Garradin fournit la lib Template_Lite pour les pages privées (répertoire /admin/), où elle est déjà chargée par défaut. C'est une version allégée de Smarty 2.

Afficher un template contenu dans le plugin :
<pre>
................................................................................

=== Site public ===

Le site public de l'association n'utilise pas Template_Lite mais des squelettes, qui sont très similaires aux squelettes utilisés par SPIP.

Un plugin peut modifier les squelettes (dans ce cas il est intéressant de proposer une sauvegarde à l'utilisateur pour ne pas écraser les modifs qu'il a pu faire), via la méthode Squelette::editSource(string $file, string $content). Ne pas modifier les squelettes directement, mais seulement en utilisant cette méthode.

Un plugin peut également implémenter de nouveaux types de boucles. Dans ce cas l'installation du plugin doit appeler la méthode $plugin->registerSkelLoopName(string $loop_name). Ensuite quand cette boucle sera utilisée dans un squelette, le fichier skel_loop.php à la racine du plugin sera appelé. Les variables fournies sont :

* $loopName = le nom de la boucle (par exemple dans <BOUCLE_actu(ARTICLES)> c'est **actu**)
* $loopType = le type de boucle
* $loopCriterias = les critères de la boucle, sous forme de tableau, voir la doc de KD2\MiniSkel pour les détails
* $loopContent = le contenu de la boucle
* $preContent = le contenu (facultatif) pré-boucle qui ne sera renvoyé qu'en cas de succès
* $postContent = le contenu (facultatif) post-boucle qui ne sera renvoyé qu'en cas de succès
* $altContent = le contenu (facultatif) alternatif qui sera affiché si la boucle ne renvoie rien

La manière la plus simple d'implémenter un nouveau type de boucle est de remplir la variable $query dans skel_loop.php. Exemple :

<pre>
if ($loopType == 'stats_articles')
{
	$query = 'SELECT COUNT(*) AS nb_articles FROM wiki_pages;';







>







 







>
>
>
>







 







|

|
|
|
|
|
|
|







95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
...
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
...
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
* getConfig(string $key) : récupère la valeur de la clé $key pour la configuration du plugin
* getInfos() : renvoie les informations enregistrées sur le plugin
* upgrade() : mise à jour du plugin
* needsUpgrade() : le plugin doit-il être mis à jour ?
* uninstall() : désinstaller le plugin
* id() : renvoie l'identifiant du plugin
* path() : renvoie le chemin vers l'archive du plugin
* registerSignal(string $signal, string $callback) 

=== Base de données ===

Tous les plugins ont un accès illimité à la base de données principale de Garradin. Cependant il est interdit d'ajouter, modifier ou supprimer des données directement dans les tables de cette BDD afin de ne pas compromettre l'intégrité des données. Pour modifier ces données il faut utiliser les méthodes de Garradin.

Chaque plugin peut créer une ou plusieurs tables dans cette BDD, elles devront par contre être supprimées à la désinstallation. Dans ce cas un plugin peut modifier directement ses tables.

................................................................................
Les plugins ne doivent pas créer, modifier ou supprimer de fichiers dans l'arborescence de Garradin.

Les seuls fichiers qu'un plugin devrait modifier sont :
* du cache : utiliser de préférence l'objet Static_Cache de Garradin, ou faire attention aux collisions de noms ;
* les squelettes dans le répertoire **www/squelettes/**

Pour enregistrer et récupérer des documents il faut utiliser les méthodes de stockage de fichiers fournies par Garradin.

=== Signaux ===



=== Dans les templates ===

Garradin fournit la lib Template_Lite pour les pages privées (répertoire /admin/), où elle est déjà chargée par défaut. C'est une version allégée de Smarty 2.

Afficher un template contenu dans le plugin :
<pre>
................................................................................

=== Site public ===

Le site public de l'association n'utilise pas Template_Lite mais des squelettes, qui sont très similaires aux squelettes utilisés par SPIP.

Un plugin peut modifier les squelettes (dans ce cas il est intéressant de proposer une sauvegarde à l'utilisateur pour ne pas écraser les modifs qu'il a pu faire), via la méthode Squelette::editSource(string $file, string $content). Ne pas modifier les squelettes directement, mais seulement en utilisant cette méthode.

Un plugin peut également implémenter de nouveaux types de boucles. Dans ce cas l'installation du plugin doit enregistrer un signal du nom 'boucle.NOM_DE_LA_BOUCLE'. Par exemple : $plugin->registerSignal('boucle.meteo', 'Garradin\Plugin\Meteo::Boucle'). Ensuite quand cette boucle sera utilisée dans un squelette, le fichier signals.php à la racine du plugin sera inclus et la méthode indiquée sera appelée. Les paramètres sont fournis dans un tableau contenant :

* loopName = le nom de la boucle (par exemple dans <BOUCLE_actu(ARTICLES)> c'est **actu**)
* loopType = le type de boucle
* loopCriterias = les critères de la boucle, sous forme de tableau, voir la doc de KD2\MiniSkel pour les détails
* loopContent = le contenu de la boucle
* preContent = le contenu (facultatif) pré-boucle qui ne sera renvoyé qu'en cas de succès
* postContent = le contenu (facultatif) post-boucle qui ne sera renvoyé qu'en cas de succès
* altContent = le contenu (facultatif) alternatif qui sera affiché si la boucle ne renvoie rien

La manière la plus simple d'implémenter un nouveau type de boucle est de remplir la variable $query dans skel_loop.php. Exemple :

<pre>
if ($loopType == 'stats_articles')
{
	$query = 'SELECT COUNT(*) AS nb_articles FROM wiki_pages;';

Modified src/include/data/0.7.0.sql from [4da22f7ea1] to [8c45d58f37].

1

2

3
4

5
CREATE TABLE plugins_skel_boucles

(

    plugin TEXT NOT NULL REFERENCES plugins (id),
    nom TEXT NOT NULL

);
|
>

>

|
>

1
2
3
4
5
6
7
8
CREATE TABLE plugins_signaux
-- Association entre plugins et signaux (hooks)
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

Modified src/include/data/schema.sql from [1a0ccb3ec8] to [aeb1567568].

311
312
313
314
315
316
317
318

319

320
321

322
    auteur TEXT,
    url TEXT,
    version TEXT NOT NULL,
    menu INTEGER NOT NULL DEFAULT 0,
    config TEXT
);

CREATE TABLE plugins_skel_boucles

(

    plugin TEXT NOT NULL REFERENCES plugins (id),
    nom TEXT NOT NULL

);







|
>

>

|
>

311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
    auteur TEXT,
    url TEXT,
    version TEXT NOT NULL,
    menu INTEGER NOT NULL DEFAULT 0,
    config TEXT
);

CREATE TABLE plugins_signaux
-- Association entre plugins et signaux (hooks)
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

Modified src/include/lib/Garradin/Plugin.php from [842384f851] to [0764afa302].

21
22
23
24
25
26
27


28
29
30
31
32
33
34
...
213
214
215
216
217
218
219

220
221
222
223
224
225
226
...
252
253
254
255
256
257
258






259
260
261
262

263
264

265
266


267
268

269
270


271
272





273
274
275
276
277



278
279
280
281
282
283
284
...
559
560
561
562
563
564
565
566


























		'pdf' => 'application/pdf',
		'png' => 'image/png',
		'swf' => 'application/shockwave-flash',
		'xml' => 'text/xml',
		'svg' => 'image/svg+xml',
	];



	/**
	 * Construire un objet Plugin pour un plugin
	 * @param string $id Identifiant du plugin
	 * @throws UserException Si le plugin n'est pas installé (n'existe pas en DB)
	 */
	public function __construct($id)
	{
................................................................................
		{
			include $this->path() . '/uninstall.php';
		}
		
		unlink(PLUGINS_ROOT . '/' . $this->id . '.tar.gz');

		$db = DB::getInstance();

		return $db->simpleExec('DELETE FROM plugins WHERE id = ?;', $this->id);
	}

	/**
	 * Renvoie TRUE si le plugin a besoin d'être mis à jour
	 * (si la version notée dans la DB est différente de la version notée dans garradin_plugin.ini)
	 * @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon
................................................................................

		$db = DB::getInstance();
		return $db->simpleUpdate('plugins', 
			['version' => $infos['version']],
			'id = \''.$db->escapeString($this->id).'\'');
	}







	public function registerSkelLoopName($name)
	{
		$db = DB::getInstance();
		$registered = $db->simpleQuerySingle('SELECT plugin FROM plugins_skel_boucles WHERE nom = ?;', $name);


		if ($registered)

		{
			if ($registered != $this->id)


			{
				throw new \LogicException('La boucle ' . $name . ' est déjà associée au plugin "'.$registered.'"');

			}
			else


			{
				return true;





			}
		}

		return $db->simpleInsert('plugins_skel_boucles', 
			['nom' => $name, 'plugin' => $this->id]);



	}

	/**
	 * Liste des plugins installés (en DB)
	 * @return array Liste des plugins triés par nom
	 */
	static public function listInstalled()
................................................................................
	 * @param  string $id Identifiant du plugin
	 * @return mixed      Numéro de version du plugin ou FALSE
	 */
	static public function getInstalledVersion($id)
	{
		return DB::getInstance()->simpleQuerySingle('SELECT version FROM plugins WHERE id = ?;');
	}
}

































>
>







 







>







 







>
>
>
>
>
>
|

<
<
>

<
>

<
>
>
|
<
>
|
<
>
>
|
<
>
>
>
>
>



|
|
>
>
>







 







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
...
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
...
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269


270
271

272
273

274
275
276

277
278

279
280
281

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
...
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
		'pdf' => 'application/pdf',
		'png' => 'image/png',
		'swf' => 'application/shockwave-flash',
		'xml' => 'text/xml',
		'svg' => 'image/svg+xml',
	];

	static protected $signal_files = [];

	/**
	 * Construire un objet Plugin pour un plugin
	 * @param string $id Identifiant du plugin
	 * @throws UserException Si le plugin n'est pas installé (n'existe pas en DB)
	 */
	public function __construct($id)
	{
................................................................................
		{
			include $this->path() . '/uninstall.php';
		}
		
		unlink(PLUGINS_ROOT . '/' . $this->id . '.tar.gz');

		$db = DB::getInstance();
		$db->simpleExec('DELETE FROM plugins_signaux WHERE plugin = ?;', $this->id);
		return $db->simpleExec('DELETE FROM plugins WHERE id = ?;', $this->id);
	}

	/**
	 * Renvoie TRUE si le plugin a besoin d'être mis à jour
	 * (si la version notée dans la DB est différente de la version notée dans garradin_plugin.ini)
	 * @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon
................................................................................

		$db = DB::getInstance();
		return $db->simpleUpdate('plugins', 
			['version' => $infos['version']],
			'id = \''.$db->escapeString($this->id).'\'');
	}

	/**
	 * Associer un signal à un callback du plugin
	 * @param  string $signal   Nom du signal (par exemple boucle.agenda pour la boucle de type AGENDA)
	 * @param  mixed  $callback Callback, sous forme d'un nom de fonction ou de méthode statique
	 * @return boolean TRUE
	 */
	public function registerSignal($signal, $callback)
	{


		$callable_name = '';


		if (!is_callable($callback, true, $callable_name) || !is_string($callback))
		{

			throw new \LogicException('Le callback donné n\'est pas valide.');
		}


		$db = DB::getInstance();


		// Signaux exclusifs, qui ne peuvent être attribués qu'à un seul plugin
		if (strpos($signal, 'boucle.') === 0)
		{

			$registered = $db->simpleQuerySingle('SELECT plugin FROM plugins_signaux WHERE signal = ? AND plugin != ?;', false, $signal, $this->id);

			if ($registered)
			{
				throw new \LogicException('Le signal ' . $name . ' est exclusif et déjà associé au plugin "'.$registered.'"');
			}
		}

		$st = $db->prepare('INSERT OR REPLACE INTO plugins_signaux VALUES (:signal, :plugin, :callback);');
		$st->bindValue(':signal', $signal);
		$st->bindValue(':plugin', $this->id);
		$st->bindValue(':callback', $callable_name);
		return $st->execute();
	}

	/**
	 * Liste des plugins installés (en DB)
	 * @return array Liste des plugins triés par nom
	 */
	static public function listInstalled()
................................................................................
	 * @param  string $id Identifiant du plugin
	 * @return mixed      Numéro de version du plugin ou FALSE
	 */
	static public function getInstalledVersion($id)
	{
		return DB::getInstance()->simpleQuerySingle('SELECT version FROM plugins WHERE id = ?;');
	}

	/**
	 * Déclenche le signal donné auprès des plugins enregistrés
	 * @param  string $signal Nom du signal
	 * @param  array  $params Paramètres du callback (array ou null)
	 * @return NULL 		  NULL si aucun plugin n'a été appelé, true sinon
	 */
	static public function fireSignal($signal, $params = null, &$return = null)
	{
		$list = DB::getInstance()->simpleStatementFetch('SELECT * FROM plugins_signaux WHERE signal = ?;', SQLITE3_ASSOC, $signal);

		foreach ($list as $row)
		{
			if (!in_array($row['plugin'], self::$signal_files))
			{
				require_once 'phar://' . PLUGINS_ROOT . '/' . $row['plugin'] . '.tar.gz/signals.php';
			}

			$return = call_user_func_array($row['callback'], [&$params, &$return]);

			if ($return)
				return $return;
		}

		return !empty($list) ? true : null;
	}
}

Modified src/include/lib/Garradin/Squelette.php from [1cd05c6913] to [8d37cf8c3d].

211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
...
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
...
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
...
472
473
474
475
476
477
478
479
480
481

482





483
484





485
486


487


488
489
490

491

492
493
494
495
496
497
498
499
...
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
        $this->assign('url_racine', WWW_URL);
        $this->assign('url_site', WWW_URL);
        $this->assign('url_atom', WWW_URL . 'feed/atom/');
        $this->assign('url_elements', WWW_URL . 'squelettes/');
        $this->assign('url_admin', WWW_URL . 'admin/');

        $lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) 
            ? preg_replace('/^.*(\w{2}).*$/U', '$1', $_SERVER['HTTP_ACCEPT_LANGUAGE'])
            : '';
        
        $this->assign('langue_visiteur', $lang);
    }

    public function __construct()
    {
        $this->_registerDefaultModifiers();
        $this->_registerDefaultTags();
    }
................................................................................
        $out->append(1, 'endif;');

        return $out;
    }

    protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent)
    {
        $query = $loopStart = '';
        $query_args = [];
        $db = DB::getInstance();

        // Types de boucles natifs
        if ($loopType == 'articles' || $loopType == 'rubriques' || $loopType == 'pages')
        {
            $where = $order = '';
................................................................................
            if ($search_rank && !$search)
            {
                throw new \KD2\MiniSkelMarkupException("Le critère par points n'est possible que dans les boucles de recherche.");
            }

            if (trim($loopContent))
            {
                $loopStart .= '$row[\'url\'] = WWW_URL . $row[\'uri\']; ';
            }

            $query .= $where . ' ' . $order;

            if (!$limit || $limit > 100)
                $limit = 100;

................................................................................
                }
                
                $query .= ','.(int)$limit;
            }
        }
        else
        {
            // Type de boucles gérés par des plugins
            if ($plugin = $db->simpleQuerySingle('SELECT plugin FROM plugins_skel_boucles WHERE nom = ? LIMIT 1;', false, $loopType))
            {

                $plugin = new \Garradin\Plugin($plugin);






                if (!file_exists($plugin->path() . '/skel_loop.php'))





                {
                    throw new \KD2\MiniSkelMarkupException("Le type de boucle '".$loopType."' est géré par un plugin, mais celui-ci ne contient pas de fichier skel_loop.php.");


                }



                // Ici le plugin peut soit peupler $query et $loopStart lui-même, soit faire un return
                include $plugin->path() . '/skel_loop.php';

            }

            else
            {
                throw new \KD2\MiniSkelMarkupException("Le type de boucle '".$loopType."' est inconnu.");
            }

            if (empty($query))
            {
                $query = 'SELECT 0 LIMIT 0;';
................................................................................
        if ($preContent)
        {
            $out->append(2, $this->parse($preContent, $loopName, self::PRE_CONTENT));
        }

        $out->append(1, 'while ($row = $result_'.$hash.'->fetchArray(SQLITE3_ASSOC)): ');
        $out->append(1, '$this->_vars[\''.$hash.'\'][\'compteur_boucle\'] += 1; ');
        $out->append(1, $loopStart);
        $out->append(1, '$this->_vars[\''.$hash.'\'] = array_merge($this->_vars[\''.$hash.'\'], $row); ');

        $out->append(2, $this->parseVariables($loopContent));

        $out->append(1, 'endwhile;');

        // we put the post-content after the loop content







|


|







 







|







 







|







 







|
|
<
>
|
>
>
>
>
>

<
>
>
>
>
>
|
<
>
>
|
>
>
|
<
<
>

>
|







 







|







211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
...
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
...
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
...
472
473
474
475
476
477
478
479
480

481
482
483
484
485
486
487
488

489
490
491
492
493
494

495
496
497
498
499
500


501
502
503
504
505
506
507
508
509
510
511
...
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
        $this->assign('url_racine', WWW_URL);
        $this->assign('url_site', WWW_URL);
        $this->assign('url_atom', WWW_URL . 'feed/atom/');
        $this->assign('url_elements', WWW_URL . 'squelettes/');
        $this->assign('url_admin', WWW_URL . 'admin/');

        $lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) 
            ? preg_replace('/^.*(\w{2}).*$/Ui', '$1', $_SERVER['HTTP_ACCEPT_LANGUAGE'])
            : '';
        
        $this->assign('langue_visiteur', strtolower($lang));
    }

    public function __construct()
    {
        $this->_registerDefaultModifiers();
        $this->_registerDefaultTags();
    }
................................................................................
        $out->append(1, 'endif;');

        return $out;
    }

    protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent)
    {
        $query = $loop_start = '';
        $query_args = [];
        $db = DB::getInstance();

        // Types de boucles natifs
        if ($loopType == 'articles' || $loopType == 'rubriques' || $loopType == 'pages')
        {
            $where = $order = '';
................................................................................
            if ($search_rank && !$search)
            {
                throw new \KD2\MiniSkelMarkupException("Le critère par points n'est possible que dans les boucles de recherche.");
            }

            if (trim($loopContent))
            {
                $loop_start .= '$row[\'url\'] = WWW_URL . $row[\'uri\']; ';
            }

            $query .= $where . ' ' . $order;

            if (!$limit || $limit > 100)
                $limit = 100;

................................................................................
                }
                
                $query .= ','.(int)$limit;
            }
        }
        else
        {
            $params = [
                'loopName'  =>  $loopName,

                'loopType'  =>  $loopType,
                'loopCriterias' =>  $loopCriterias,
                'loopContent'   =>  $loopContent,
                'preContent'    =>  $preContent,
                'postContent'   =>  $postContent,
                'altContent'    =>  $altContent,
            ];


            $callback_return = [
                'query'         =>  &$query,
                'query_args'    =>  &$query_args,
                'loop_start'    =>  &$loop_start,
            ];


            // Appel du plugin lié à cette boucle, si ça existe
            $return = Plugin::fireSignal('boucle.' . $loopType, $params, $callback_return);

            // Si le retour est du texte on le traite comme tel
            if (is_string($return))
            {


                return $return;
            }
            // Sinon si ce n'est pas true
            elseif (!$return)
            {
                throw new \KD2\MiniSkelMarkupException("Le type de boucle '".$loopType."' est inconnu.");
            }

            if (empty($query))
            {
                $query = 'SELECT 0 LIMIT 0;';
................................................................................
        if ($preContent)
        {
            $out->append(2, $this->parse($preContent, $loopName, self::PRE_CONTENT));
        }

        $out->append(1, 'while ($row = $result_'.$hash.'->fetchArray(SQLITE3_ASSOC)): ');
        $out->append(1, '$this->_vars[\''.$hash.'\'][\'compteur_boucle\'] += 1; ');
        $out->append(1, $loop_start);
        $out->append(1, '$this->_vars[\''.$hash.'\'] = array_merge($this->_vars[\''.$hash.'\'], $row); ');

        $out->append(2, $this->parseVariables($loopContent));

        $out->append(1, 'endwhile;');

        // we put the post-content after the loop content