Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.47.0-wmf.20
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
Accueil
0
15169
772360
772358
2026-09-17T12:10:28Z
Lionel Scheepmans
20012
Amélioration navigation
772360
wikitext
text/x-wiki
<templatestyles src="Main Page/minerva.css" />
<div style="display: none;">
Version : {{CURRENTTIMESTAMP}}
({{CURRENTHOUR}}h.. UTC)
<!-- Page à purger tous les jours pour tous. -->
Cette page a besoin de __NOCACHE__
</div>
{{Centrer|<big><big><big>Bienvenue sur Wikilivres</big></big></big>}}
{{Centrer|{{Bloc|Une bibliothèque}} {{Bloc|de {{NUMBEROFBOOKS}} livres pédagogiques libres d'usage}} {{Bloc|et un site web de {{NUMBEROFARTICLES}} pages web}} {{Bloc|que chacun peut enrichir et améliorer.}}}}
{{Centrer|[[Wikilivres:Présentation|Découvrir Wikilivres ?]] • [[Aide:Accueil|Besoin d'aide ?]] • [[Wikilivres:Accueil de la communauté|Rejoindre la communauté ?]] • [[Wikilivres|En savoir plus ?]]}}
{{Centrer|'''Parcourir la bibliothèque'''}}
{{Centrer|<big>[[:Catégorie:Livres terminés|Livres terminés]] - [[:Catégorie:Minilivres|Mini Livres]] - [[Wikilivres:Tous les livres|Tous les livres]]</big>}}
{| class="livre-accueil-table2" style="width:100%; border-spacing:8px 8px;"
| class="livre-presentation livre-accueil2" style="width:33%; padding:10px; vertical-align:top;" |
<div id="mf-livre1" title="Exemple de livre en vitrine">{{:Accueil/Vitrine|date={{LOCALYEAR}}-{{LOCALMONTH}}-{{LOCALDAY}}}}</div>
| class="livre-presentation livre-accueil2" style="width:33%; padding:10px; vertical-align:top;" |
<div id="mf-livre2" title="Exemple de livre Wikijunior">{{:Accueil/Wikijunior|date={{LOCALYEAR}}-{{LOCALMONTH}}-{{LOCALDAY}}}}</div>
| class="livre-presentation livre-accueil2" style="width:33%; padding:10px; vertical-align:top;" |
<div id="mf-livre3" title="Exemple de recette de cuisine">{{:Accueil/Recette|date={{LOCALYEAR}}-{{LOCALMONTH}}-{{LOCALDAY}}}}</div>
|}
<div style="width:100%; margin:0px; padding:0px 5px 5px 5px;">
<div id="mf-index" title="Index des livres" class="only_mobile">
* [[:Catégorie:Classe 7 - Arts. Divertissements. Sports|Arts]]
* [[:Catégorie:Informatique|Informatique]]
* [[:Catégorie:Classe 8 - Langue. Linguistique. Littérature|Langues]]
* [[:Catégorie:Loisirs|Loisirs]]
* [[:Catégorie:Classe 5 - Mathématique. Sciences exactes et naturelles|Sciences]]
* [[:Catégorie:Sciences humaines|Sciences humaines]]
* [[:Catégorie:Technologie|Technologie]]
* '''[[Wikilivres:Tous les livres|Tous les livres]]'''
</div>
<div id="mf-newbooks" class="newbooks-{{PAGESINCATEGORY:Nouveaux livres}}" title="Nouveaux livres" style="text-align: center">
'''[[:Catégorie:Nouveaux livres|Nouveautés]] :''' <dynamicpagelist>
category=Nouveaux livres
namespace=Main
suppresserrors=true
order=descending
count=20
mode=inline
</dynamicpagelist>
</div>
<hr style="border: 1px solid #4faff380; margin: 10px; border-top: 0px;" />
</div>
<div class="projects only_nonmobile">{{Projets Wikimedia}}</div>
<div class="projects only_mobile">{{Projets Wikimedia|Mobile}}</div>
<div style="margin-top:12px;border:1px solid #4faff380;border-radius:10px;text-align:center;" class="plainlinks">
[[Wikilivres:Avertissements généraux|Wikilivres ne garantit pas le contenu mis en ligne]].
<div style="font-size:85%;">La [{{fullurl:wikimedia:Accueil|uselang=fr}} Wikimedia Foundation] étant un hébergeur, elle ne saurait être tenue responsable des erreurs éventuelles contenues sur ce site.<br />Chaque rédacteur est responsable de ses contributions.</div>
</div>
__NOTOC__
__NOEDITSECTION__
[[Catégorie:Wikilivres]]
28e7fhv76elnwdefeczr40u2cxrq02t
772364
772360
2026-09-17T14:21:05Z
Lionel Scheepmans
20012
Mise en page
772364
wikitext
text/x-wiki
<templatestyles src="Main Page/minerva.css" />
<div style="display: none;">
Version : {{CURRENTTIMESTAMP}}
({{CURRENTHOUR}}h.. UTC)
<!-- Page à purger tous les jours pour tous. -->
Cette page a besoin de __NOCACHE__
</div>
{{Centrer|<big><big><big>Bienvenue sur Wikilivres</big></big></big>}}
{{Centrer|{{Bloc|Une bibliothèque}} {{Bloc|de {{NUMBEROFBOOKS}} livres pédagogiques libres d'usage}} {{Bloc|et un site web de {{NUMBEROFARTICLES}} pages web}} {{Bloc|que chacun peut enrichir et améliorer.}}}}
{{Centrer|<big>[[Wikilivres:Présentation|Découvrir Wikilivres ?]] • [[Aide:Accueil|Besoin d'aide ?]] • [[Wikilivres:Accueil de la communauté|Rejoindre la communauté ?]] • [[Wikilivres|En savoir plus ?]]</big>}}
{{Centrer|'''Parcourir la bibliothèque'''}}
{{Centrer|<big>[[:Catégorie:Livres terminés|Livres terminés]] - [[:Catégorie:Minilivres|Mini Livres]] - [[Wikilivres:Tous les livres|Tous les livres]]</big>}}
{| class="livre-accueil-table2" style="width:100%; border-spacing:8px 8px;"
| class="livre-presentation livre-accueil2" style="width:33%; padding:10px; vertical-align:top;" |
<div id="mf-livre1" title="Exemple de livre en vitrine">{{:Accueil/Vitrine|date={{LOCALYEAR}}-{{LOCALMONTH}}-{{LOCALDAY}}}}</div>
| class="livre-presentation livre-accueil2" style="width:33%; padding:10px; vertical-align:top;" |
<div id="mf-livre2" title="Exemple de livre Wikijunior">{{:Accueil/Wikijunior|date={{LOCALYEAR}}-{{LOCALMONTH}}-{{LOCALDAY}}}}</div>
| class="livre-presentation livre-accueil2" style="width:33%; padding:10px; vertical-align:top;" |
<div id="mf-livre3" title="Exemple de recette de cuisine">{{:Accueil/Recette|date={{LOCALYEAR}}-{{LOCALMONTH}}-{{LOCALDAY}}}}</div>
|}
<div style="width:100%; margin:0px; padding:0px 5px 5px 5px;">
<div id="mf-index" title="Index des livres" class="only_mobile">
* [[:Catégorie:Classe 7 - Arts. Divertissements. Sports|Arts]]
* [[:Catégorie:Informatique|Informatique]]
* [[:Catégorie:Classe 8 - Langue. Linguistique. Littérature|Langues]]
* [[:Catégorie:Loisirs|Loisirs]]
* [[:Catégorie:Classe 5 - Mathématique. Sciences exactes et naturelles|Sciences]]
* [[:Catégorie:Sciences humaines|Sciences humaines]]
* [[:Catégorie:Technologie|Technologie]]
* '''[[Wikilivres:Tous les livres|Tous les livres]]'''
</div>
<div id="mf-newbooks" class="newbooks-{{PAGESINCATEGORY:Nouveaux livres}}" title="Nouveaux livres" style="text-align: center">
'''[[:Catégorie:Nouveaux livres|Nouveautés]] :''' <dynamicpagelist>
category=Nouveaux livres
namespace=Main
suppresserrors=true
order=descending
count=20
mode=inline
</dynamicpagelist>
</div>
<hr style="border: 1px solid #4faff380; margin: 10px; border-top: 0px;" />
</div>
<div class="projects only_nonmobile">{{Projets Wikimedia}}</div>
<div class="projects only_mobile">{{Projets Wikimedia|Mobile}}</div>
<div style="margin-top:12px;border:1px solid #4faff380;border-radius:10px;text-align:center;" class="plainlinks">
[[Wikilivres:Avertissements généraux|Wikilivres ne garantit pas le contenu mis en ligne]].
<div style="font-size:85%;">La [{{fullurl:wikimedia:Accueil|uselang=fr}} Wikimedia Foundation] étant un hébergeur, elle ne saurait être tenue responsable des erreurs éventuelles contenues sur ce site.<br />Chaque rédacteur est responsable de ses contributions.</div>
</div>
__NOTOC__
__NOEDITSECTION__
[[Catégorie:Wikilivres]]
5uzo6c957zn0ypebgdd3b7f6oxyp50d
Discussion:Accueil
1
56846
772361
772339
2026-09-17T12:15:49Z
Xhungab
23827
772361
wikitext
text/x-wiki
{{Avertissement|Cette page est réservée aux discussions au sujet de la page d'[[Accueil]]. Les discussions sur le projet Wikilivres en général ont lieux sur [[Wikilivres:Le Bistro|Le Bistro]]}}
{{RemarqueIndex|Pour la maintenance, voir [[Wikilivres:Maintenance/Page d'accueil|Maintenance de la page d'accueil]].}}
*[[Discuter:Accueil/Archive 1|Archive ==>mars 2009]]
*[[Discuter:Accueil/Archive 2|Archive ==>mai 2013]]
== Chiffres ==
Pourquoi ne publie-t-on pas comme sur les autres wikis une indication de l'importance du site, avec quelque chose comme :
; {{PAGESINCATEGORY:Livres par titre}} livres sur {{NUMBEROFARTICLES}} pages
? [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<font color="#FF6600">$</font>♠]]) 16 août 2013 à 22:21 (CEST)
:Je pense qu'il s'agit d'afficher des stats sur la page d'accueil, comme sur celle de Wikipedia tout en haut à droite ou dans le bistro du jour. Pourquoi pas? ''Liebe Grüße'', [[Utilisateur:Perditax|Perditax]] ([[Discussion utilisateur:Perditax|d]]) 19 septembre 2013 à 13:45 (CEST)
:: Pour ! [[Utilisateur:Jean-Jacques MILAN|Jean-Jacques MILAN]] ([[Discussion utilisateur:Jean-Jacques MILAN|discussion]]) 22 septembre 2013 à 13:51 (CEST)
== Auteur ???? ==
Et si on cherche livres d'un auteur specifique ?
???
:Les auteurs de Wikilivres qui le souhaitent peuvent publier leur bibliographie sur leur profil. Quant aux auteurs célèbres, ils sont sur [[s:|Wikisource]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<font color="#FF6600">$</font>♠]]) 18 juillet 2015 à 16:56 (CEST)
== Version mobile ==
Bonjour,
Je pense que ce serait une bonne idée de mettre le lien de la version mobile de Wikibooks pour que celui qui utilise un téléphone ou une tablette ne soit pas perdu et ne cherche pas n'importe où comment avoir un affichage correct.
Merci, [[Utilisateur:Aabbccddeeffabcdef|Aabbccddeeffabcdef]] ([[Discussion utilisateur:Aabbccddeeffabcdef|discussion]]) 19 décembre 2018 à 15:39 (CET).
:Les navigateurs des téléphones basculent automatiquement dessus par défaut. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 19 décembre 2018 à 23:54 (CET)
== Rénovation ==
Bonjour,
Selon le projet [[w:fr:Projet:Charte graphique|Charte graphique]] de Wikipédia, pourrait-t-on rénover la page d'accueil avec les modèles [[Modèle:Boîte colorée|Boîte colorée]] et [[Modèle:Barre colorée|Barre colorée]].
Merci & Cordialement, [[Utilisateur:Aabbccddeeffabcdef|Aabbccddeeffabcdef]] ([[Discussion utilisateur:Aabbccddeeffabcdef|discussion]]), le [[Utilisateur:Aabbccddeeffabcdef|Aabbccddeeffabcdef]] ([[Discussion utilisateur:Aabbccddeeffabcdef|discussion]]) 13 mars 2019 à 17:38 (CET).
:Les rendus des versions desktop et mobile me semblent corrects aujourd'hui, donc je ne souhaiterais pas toucher à quelque chose qui fonctionne. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 mars 2019 à 20:52 (CET)
== Faute d'orthographe sur [[Ict@innovation: Free your IT Business in Africa/2-1]] ==
Il y a une faute d'orthographe sur [[Ict@innovation: Free your IT Business in Africa/2-1]], mais dès que je veux modifier cette page, je suis redirigé vers la page d'accueil. J'ai corrigé d'innombrables pages sur wikipedia et les autres wiki cousins, mais là : impossible.
Je veux juste remplacer "été crée" par "été créé".
[[Utilisateur:Romanc19s|Romanc19s]] ([[Discussion utilisateur:Romanc19s|discussion]]) 4 mars 2021 à 19:03 (CET)
:Salut Romanc19s
:Effectivement, il y a un script qui bloque la création et la modification des pages ayant un nom bizarre.
:La page ne peut avoir été créée que par un bot (javascript ignoré) ou un administrateur.
:J'ai effectué la correction. Mais on peut remettre en cause la pertinence de la présence d'une telle page.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 4 mars 2021 à 19:49 (CET)
== Connexion ==
Pourquoi ne puis-je plus me connecter sous mon autre nom ? --[[Utilisateur:Elnon|Elnon]] ([[Discussion utilisateur:Elnon|discussion]]) 9 juillet 2026 à 21:59 (CEST)
== Un changement de la page d'Accueil ==
J'ai enclenché un changement de la page d'Accueil pour éviter aux lecteurs de se perdre dans les méandres de Wikibook.
Je pense que les liens "Livres terminés" - "Mini Livres" font bien leur travail. Dans les nouveautés j'ai proposé de laisser les nouveaux livres pendant 1 mois, mais de permettre aux auteurs actifs de laisser leurs livres pendant plusieurs mois, voire plusieurs années tant qu'ils n'estimeront pas leurs livres terminés.
Il y a toujours un lien de déperdition qui permet de se perdre dans les méandres de Wikilivre. Où est '''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil l'espace pour les lecteurs ?]''' [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 16 septembre 2026 à 15:05 (CEST)
== Usage du Wikicode ==
@[[Utilisateur:DavidL|DavidL]] et @[[Utilisateur:JackPotte|JackPotte]]. Je suis en train de refaire la mise en page de notre page d'accueil suite à la décision initiée par
[[Utilisateur:Xhungab|Xhungab]]. J'en profite aussi pour traiter les problèmes d'affichage par les smartphones. Le code de cette page utilise beaucoup de code CSS, voire HTML. C'est sans doute très commode pour les personnes comme vous qui connaissent bien ce langage, mais j'y vois un inconvénient.
Cela rend les choses plus difficiles à comprendre et à modifier pour des gens comme moi qui ne maîtrisent pas le CSS et l'HTML, contrairement au Wikicode.
Le Wikicode a été créé pour être plus facile d'usage et je me dis qu'il sera aussi toujours pris en charge par les futurs contributeurs qui ne sont pas informaticiens. De plus, tout ce qui est écrit en Wikicode sera toujours pris en charge dans le cadre des améliorations faites par le personnel de la fondation.
Je propose donc de réécrire le code de la page d'accueil en utilisant au maximum le Wikicode et aucun code HTML ou CSS si possible.
Qu'en pensez-vous ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 septembre 2026 à 15:06 (CEST)
== Problème des cadre et tableau en version mobile ==
Je découvre que l'usage de cadre et de tableau contrarie l'affichage en version mobile. Je tenterai de trouver une solution. N'hésitez pas à réagir. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 septembre 2026 à 15:59 (CEST)
== Je ne suis donc pas content tous seul. (2) ==
'''Suite à l'installation de la nouvelle page de la Vitrine, je ne suis pas content, j'ai pris 24h pour réfléchir.
'''
J'avais posé une demande de prise de décision concernant le sujet suivant.
'''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil Créer un espace lecteurs et un espace auteurs sur la page d'accueil]'''
'''Créer un espace lecteurs''' : Où est l'espace pour les lecteurs?
Découvrir la bibliothèque
Livres terminés - Mini Livres - Tous les livres
'''Dans une bibliothèque''', un livre est disponible ou indisponible. C'est chez l'auteur ou chez l'imprimeur qu'un livre est terminé.
'''Je suis un nouveau sur Wikilivre'''. Je vois, "Découvrir la bibliothèque", je vais directement dans "'''[[Wikilivres:Tous les livres|Tous les livres]]'''". Je me retrouve dans un labyrinthe qui me balade de répertoires en répertoires pour finir sur une ébauche abandonnée depuis 5, 10, 20 ans.
"Tous les livres" devraient s'appeler la réserve de Wikibook. C'est l'endroit fermé au public où sont conservés les livres et les documents qui ne sont pas en accès libre dans les salles de lecture. C'est un espace pour les curieux pour connaître les entrailles de Wikibook.
Je ne suis pas content. J'avais proposé à l'administrateur un changement en 2 minutes. Copier cette page dans la Vitrine, [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab La Nouvelle Vitrine]. Il a décidé de passer plusieurs heures à modifier la prise de décision. Je ne suis pas content. Je suppose que je suis le seul.
Donc : '''Je ne suis donc pas content tous seul.''' mais je me soigne. Un pan bagnat et une tourte aux blettes.
Merci de m'avoir lu [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 septembre 2026 à 10:51 (CEST)
q7kjr8dy9tbbh473fysl4mx23j0r4q2
Programmation PHP avec Symfony/Doctrine
0
71848
772389
772340
2026-09-18T08:47:26Z
JackPotte
5426
/* EntityManager */
772389
wikitext
text/x-wiki
<noinclude>{{PHP}}</noinclude>
== Installation ==
{{w|Doctrine (ORM)|Doctrine}} est l'ORM par défaut de Symfony. Il utilise {{w|PHP Data Objects|PDO}}. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la [[Patrons de conception/Chaîne de responsabilité|chaîne de responsabilité]].
Installation en SF4<ref>https://symfony.com/doc/current/doctrine.html</ref> :
<pre>
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
</pre>
Renseigner l'accès au SGBD dans le .env :
<pre>
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
</pre>
Ensuite la base de données doit être créée avec :
<pre>
php bin/console doctrine:database:create
</pre>Si la commande précédente échoue avec le message d'erreur suivant:
''Could not create database "database_name" for connection named default''
''An exception occurred in the driver: could not find driver''
Ce qui veut dire que vous devez installer le driver approprié.
'''Exemple:'''
sudo apt install php8.3-pgsql{{remarque|1=symfony/orm-pack équivaut aux paquets suivants, qui peuvent bien sûr être installés séparément à la place :
*doctrine/doctrine-bundle
*doctrine/doctrine-migrations-bundle
*doctrine/orm
*symfony/proxy-manager-bridge
}}
== Commandes Doctrine ==
Exemples de commandes :
<pre>
php bin/console dbal:run-sql "SELECT * FROM ma_table"
php bin/console dbal:run-sql "$(< mon_fichier.sql)"
# Ces deux commandes sont équivalentes des précédentes mais sur Doctrine < 3.
php bin/console doctrine:query:sql "SELECT * FROM ma_table"
php bin/console doctrine:query:sql "$(< mon_fichier.sql)"
php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:cache:clear-query
php bin/console doctrine:cache:clear-result
</pre>
== Entity ==
Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs {{wt|getter}}s et {{wt|setter}}s respectifs. Pour en générer une :
<pre>
php bin/console generate:doctrine:entity
</pre>
Cette association est définie par des attributs Doctrine. Pour les vérifier :
<pre>
php bin/console doctrine:schema:validate
</pre>
=== Exemple ===
Voici par exemple plusieurs types d'attributs :
<syntaxhighlight lang=php>
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
</syntaxhighlight>
{{(|Exemple avec annotation (avant PHP 8)}}
<syntaxhighlight lang=php>
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="spelling", type="string", length=255, nullable=false)
*/
private $spelling;
/**
* @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
*/
private $pronunciation;
/**
* @var Language
*
* @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
* @ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
*/
private $homophons;
</syntaxhighlight>
{{)}}
Et leurs modificateurs (getters et setters) :
<syntaxhighlight lang=php>
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
</syntaxhighlight>
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec <code>$word->setLanguage($language)</code>.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi un deuxième attribut <code>#[ORM\JoinColumn</code> (anciennement <code>@ORM\JoinColumn</code>) mentionnant la clé étrangère en base de données (et pas en PHP).
=== Bonnes pratiques ===
L'attribut <code>#[ORM\Table(name: 'word')]</code> était facultatif dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.
Avant PHP 8, les contraintes d'unicité (utiles entre autres pour les clés composites) étaient encapsulées dans l'annotation <code>Table</code>, mais ce n'est plus le cas avec les attributs :
<pre>
#[ORM\UniqueConstraint(name: 'spelling-pronunciation', columns: ['spelling', 'pronunciation'])]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
</pre>
{{)}}
{{attention|Dans les relations *toMany :
* il faut initialiser l'attribut dans le constructeur en <code>ArrayCollection()</code>.
* on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
* cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
}}
{{attention|Il faut ajouter le <code>#[ORM\JoinColumn(</code> dans les deux entités liées, car :
* dans aucune cela renvoie <code>Could not resolve type of column "id"</code>
* dans une seule cela provoque un problème N+1 (celle qui ne l'a pas appelle celle qui l'a pour chacun de ses enregistrements, même si celle qui l'a n'est pas utilisée ensuite).
}}
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec <code>length=0</code><ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string</ref>. Le type "text" par contre n'a pas de limite.
=== ArrayCollection ===
Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
* Dans une entité : <code>#[ORM\OrderBy(['sort_order' => 'ASC'])]</code> (anciennement <code>@ORM\OrderBy({"sort_order" = "ASC"})</code>).
* Sinon, instancier un critère :
<pre>
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
</pre>
=== GeneratedValue ===
L'annotation ''GeneratedValue'' peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
{{attention|
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID<ref>https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations</ref>. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur '' Integrity constraint violation'' puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
<syntaxhighlight lang=php>
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
</syntaxhighlight>
}}
=== Triggers ===
Les opérations en cascade sont définies sous deux formes d'attributs :
* <code>#[ORM\OneToMany(cascade: ['persist', 'remove'])]</code> : au niveau ORM.
* <code>#[ORM\JoinColumn(onDelete: 'CASCADE')]</code> : au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
=== Concepts avancés ===
Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware<ref>https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html</ref>.
{{attention|Les types des attributs peuvent être quelque peu différents du SGBD<ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix</ref>.}}
{{attention|Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").}}
L'autojointure est appelé ''self-referencing association mapping'' par Doctrine<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing</ref>).
=== Héritage ===
Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html</ref> :
<syntaxhighlight lang=php>
/** @MappedSuperclass */
class MyEntityParent
...
</syntaxhighlight>
=== Tables sans classe ===
Doctrine peut créer des tables de mapping sans entité, si on précise son nom dans les deux tables reliées :
<pre>
#[ORM\JoinTable(name: 'table_de_mapping')]
#[ORM\JoinColumn(name: 'table_source_id', referencedColumnName: 'table_source_id')]
#[ORM\InverseJoinColumn(name: 'table_cible_id', referencedColumnName: 'table_cible_id')]
#[ORM\ManyToMany(targetEntity: TableCible::class)]
</pre>
== EntityManager ==
L'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données, définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
* <code>persist()</code> : prépare un <code>INSERT</code> SQL (rattache une entité à un entity manager).
* <code>remove()</code> : prépare un <code>DELETE</code> SQL.
* <code>flush()</code> : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
* <code>merge()</code> : fusionne une entité absente de l'em dedans.
* <code>clear()</code> : détache tous les objets de l'em.
* <code>refresh()</code> : rafraichit l'entité PHP à partir de la base de données. C'est utile par exemple pour tenir compte des résultats d'un trigger ''after insert'' sur le SGBD. Exemple si le trigger ajoute une date de création après le persist, à écraser par <code>$createdDate</code> :
<syntaxhighlight lang=php>
$entity = new MyEntity();
$em->persist($entity);
$em->flush($entity);
// Trigger SGBD déclenché ici en parallèle
$em->refresh($entity);
$entity->setCreatedDate($createdDate);
$em->flush($entity);
</syntaxhighlight>
{{attention|Ne jamais utiliser <code>refresh()</code> dans un environnement avec des replicas de la base de données pour éviter les incohérences. Le plus performant est alors de reprendre l'objet de l'instance censé être à jour à la place.}}
== Repository ==
On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de <code>Doctrine\ORM\EntityRepository</code>. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés ''NomDeLEntitéRepository''.
{{remarque|D'un point de vue architectural, avant d'instancier une nouvelle entité, on utilise généralement le repository pour savoir si son enregistrement existe en base ou si on doit le créer. Dans ce deuxième cas, la bonne pratique en {{wt|DDD}} est d'utiliser une Factory pour faire le new de l'entité, mais aussi pour les new de son agrégat si elle est le nœud racine. Par exemple une <code>CarFactory</code> fera un <code>new Car()</code> mais aussi créera et lui associera ses composants : <code>new Motor()</code>...}}
{{remarque|Il est possible de préciser le nom du repository d'une entité dans cette dernière :
<pre>
#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
@ORM\Entity(repositoryClass="App\Repository\WordRepository")
</pre>
{{)}}
}}
=== SQL ===
==== Depuis Doctrine ====
Utile pour exploiter les fonctionnalités du SGBD utilisé, absentes de Doctrine.
Par exemple, pour appeler une procédure stockée :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
</pre>
Ou tronquer une table :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('TRUNCATE TABLE ma_table', $rsm)->getResult();
</pre>
{{remarque|Dans cet exemple, on peut aussi utiliser ceci :
<pre>
$connection = $this->_em->getConnection();
$databasePlatform = $connection->getDatabasePlatform();
$connection->executeStatement(
$databasePlatform->getTruncateTableSQL($this->_em->getClassMetadata(MonEntite::class)->getTableName(), true),
);
</pre>
}}
==== Sans Doctrine ====
Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :
<pre>
return $this->connection->fetchAll($sql);
</pre>
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut ''entity manager'' (''em'', ou ''_em'' pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
<syntaxhighlight lang=php>
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
</syntaxhighlight>
2. En injectant le service de connexion dans le constructeur (<code>'@database_connection'</code>) :
<pre>
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
</pre>
=== DQL ===
==== Méthodes magiques ====
Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
* <code>$repo->find($id)</code> : cherche par la clé primaire définie dans l'entité.
* <code>$repo->findAll()</code> : récupère tous les enregistrements (sans clause <code>WHERE</code>).
* <code>$repo->findById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id</code> dans la table associée au repo.
* <code>$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])</code> engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname</code>.
* <code>$repo->findOneById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id LIMIT 1</code>.
* <code>$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])</code> : engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1</code>.
{{attention|Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "<code>findById</code>" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser <code>findBy()</code>.
}}
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
<syntaxhighlight lang=php>
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
</syntaxhighlight>
==== createQuery ====
DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec <code>IDENTITY()</code> pour les jointures. Ex :
<pre>
public function findComplicatedStuff()
{
$em = $this->getEntityManager();
$query = $em->createQuery("
SELECT
u.last_name, u.first_name
FROM
App\Entity\Users u
INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users)
WHERE
i.status='waiting'
");
return $query->getResult();
}
</pre>
==== createQueryBuilder ====
L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel <code>createQueryBuilder()</code> :
<syntaxhighlight lang=php>
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
</syntaxhighlight>
Pour éviter le <code>SELECT *</code> dans cet exemple, on peut y ajouter la méthode <code>->select()</code>.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
===== Jointures =====
Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
<pre>
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
</pre>
Pour filtrer quand une jointure toMany contient des résultats, utiliser <code>EMPTY</code> :
<pre>
...
->andWhere('files IS NOT EMPTY')
</pre>
===== Résultats =====
Doctrine peut renvoyer avec :
* <code>getResult()</code> : un objet ArrayCollection (iterable, pour rechercher dedans : <code>->contains()</code>), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;
* <code>getArrayResult()</code> ou <code>getScalarResult()</code> : un tableau de tableaux (entité normalisée) ;
* <code>getSingleColumnResult()</code> : un tableau unidimensionnel.
On peut aussi utiliser <code>->indexBy('id')</code> pour définir que les clés du tableau de résultat soir les IDs de l'entité.
===== Cache =====
====== Configuration globale ======
Doctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
<syntaxhighlight lang=yaml>
framework:
cache:
default_redis_provider: '%env(REDIS_URL)%'
pools:
doctrine.metadata_cache_pool:
adapter: cache.system
doctrine.query_cache_pool:
adapter: cache.system
doctrine.result_cache_pool:
adapter: cache.app
</syntaxhighlight>
Puis dans doctrine.yaml, les utiliser :
<syntaxhighlight lang=yaml>
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.metadata_cache_pool
query_cache_driver:
type: pool
pool: doctrine.query_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
</syntaxhighlight>
À partir de là le cache des métadonnées est utilisé partout.
====== Configuration par entité ======
Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
* Dans l'entité, avec un attribut <code>#[ORM\Cache(usage: 'READ_ONLY', region: 'write_rare')]</code> (anciennement <code>@ORM\Cache(usage="READ_ONLY", region="write_rare")</code><ref>https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6</ref>), utilisant la configuration doctrine.yaml :
<pre>
doctrine:
orm:
second_level_cache:
enabled: true
regions:
write_rare:
lifetime: 864000
cache_driver: { type: service, id: cache.app }
</pre>
* Dans le repository :
<syntaxhighlight lang=php>
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
</syntaxhighlight>
Dans cet exemple, on n'utilise pas <code>cache.system</code> pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. <code>cache.app</code> pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
Pour invalider le cache d'une entité afin que les findAll() renvoient la liste à jour depuis la base de données modifiée sur Doctrine 3 :
Une seule instance de cette entité :
<pre>
$em->getCache()->evictEntity(myEntity::class, $entityId);
</pre>
Toutes les instances de l'entité :
<pre>
$em->getCache()->getRegion(myEntity::class)->clear();
</pre>
{{(|Doctrine 2}}
<pre>
$em->getCache()->evictEntityRegion(myEntity::class);
</pre>
{{)}}
==== Expressions ====
Pour ajouter une expression en DQL, utilise <code>$qb->expr()</code>. Ex<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class</ref> :
* <code>$qb->expr()->count('u.id')</code>
* <code>$qb->expr()->between('u.id', 2, 10)</code> (entre 2 et 10)
* <code>$qb->expr()->gte('u.id', 2)</code> (plus grand ou égal à 2)
* <code>$qb->expr()->like('u.name', '%son')</code>
* <code>$qb->expr()->lower('u.name')</code>
* <code>$qb->expr()->substring('u.name', 0, 1)</code>
==== Injection de dépendances ====
Les repository DQL deoivent ''ServiceEntityRepository'' :
<syntaxhighlight lang=php>
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
</syntaxhighlight>
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
* Étendre une classe qui étend ''ServiceEntityRepository''.
* Le redéfinir dans services.yaml.
* Utiliser un trait.
== Transactions ==
Pour garantir d'intégrité d'une transaction<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly</ref> :
<syntaxhighlight lang=php>
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
</syntaxhighlight>
Il existe aussi une syntaxe alternative :
<syntaxhighlight lang=php>
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
</syntaxhighlight>
== Évènements ==
Pour ajouter des triggers sur la mise à jour d'une table, il y a deux solutions :
* ajouter dans son entité l'attribut <code>#[ORM\HasLifecycleCallbacks]</code>, et à ses méthodes l'attribut de l'évènement concerné. Ex :
<pre>
#[ORM\PrePersist]
public function setCreatedAt(): self
{
$this->createdAt = new DateTime();
return $this;
}
</pre>
* ajouter des tags dans services.yaml. Ex :
<pre>
App\EventListener\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: PrePersist }
</pre>
Voici les [[Programmation PHP avec Symfony/Évènement|évènements]] utilisables ensuite (dans les listeners / subscribers) :
=== prePersist ===
Se produit avant la persistance d'une entité (paramètre : <code>PrePersistEventArgs $args</code>).
=== postPersist ===
Se produit après la persistance d'une entité (<code>PostPersistEventArgs $args</code>).
=== preUpdate ===
Se produit avant l'update d'une entité (<code>PreUpdateEventArgs $args</code>).
=== postUpdate ===
Se produit après l'update d'une entité (<code>PostUpdateEventArgs $args</code>).
=== preRemove ===
Se produit avant l'update d'une entité (<code>PreRemoveEventArgs $args</code>).
=== postRemove ===
Se produit après l'update d'une entité (<code>PostRemoveEventArgs $args</code>).
=== preFlush ===
Se produit avant la sauvegarde d'une entité (<code>PreFlushEventArgs $args</code>).
{{attention|
* Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
* Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
* Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler <code>$unitOfWork->computeChangeSet($classMetadata, $entity)</code><ref>https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener</ref>.
}}
{{remarque|On peut aussi appeler computeChangeSet() depuis ailleurs pour savoir si une entité va occasionner une requête SQL lors de son flush<ref>https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2</ref>.}} Ex :
<pre>
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
//...
}
</pre>
{{remarque|On peut aussi utiliser le paramètre <code>LifecycleEventArgs $args</code> dans ces fonctions.}}
{{attention|Parfois le <code>$object::class</code> peut renvoyer <code>Proxies\__CG__\App\Entity\MyEntity</code> au lieu de <code>App\Entity\MyEntity</code>, selon le cache utilisé.}}
=== postFlush ===
Se produit après la sauvegarde d'une entité (<code>PostFlushEventArgs $args</code>).
== Migrations ==
Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
<syntaxhighlight lang=bash>
composer require doctrine/doctrine-migrations-bundle
</syntaxhighlight>
=== Création ===
Ensuite, on peut créer un squelette de "migration" :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:generate
</syntaxhighlight>
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée <code>migration_versions</code> enregistre leur succession).
==== Exemple SQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->connection->fetchAll('SHOW DATABASES;');
$this->addSql(<<<SQL
CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
}
public function down(Schema $schema) : void
{
$this->addSql('DROP TABLE ma_table');
}
}
</syntaxhighlight>
==== Exemple DQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$table = $schema->createTable('ma_table');
$table->addColumn('ma_colonne', 'string');
}
public function down(Schema $schema) : void
{
$schema->dropTable('ma_table');
}
}
</syntaxhighlight>
==== Exemple PHP ====
Depuis Symfony 7.0, il faut implémenter <code>MigrationFactory</code> pour injecter des dépendances dans les migrations (et on ne peut plus injecter tout le conteneur)<ref>https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies</ref>.
{{Boîte déroulante début|titre=Avant Symfony 7.0, il fallait juste utiliser <code>ContainerAwareTrait</code>}}
Exemple :
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function up(Schema $schema) : void
{
$em = $this->container->get('doctrine.orm.entity_manager');
$monEntite = new MonEntite();
$em->persist($monEntite);
$em->flush();
}
}
</syntaxhighlight>
{{Boîte déroulante fin}}
{{attention|Cette technique est déconseillée car les entités peuvent évoluer indépendamment de la migration. Mais elle peut s'avérer utile pour stocker des données dépendantes de l'environnement.}}
{{attention|<code>$this->container->getParameter()</code> ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple <code>$_SERVER['SUBAPI_URI']</code> renvoie la variable d'environnement et <code>$this->containergetParameter('env(SUBAPI_URI)')</code> sa valeur par défaut (définie dans services.yaml).}}
=== Exécution ===
La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate
</syntaxhighlight>
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --up 20170321095644
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
</syntaxhighlight>
Pour le rollback :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --down 20170321095644
</syntaxhighlight>
Pour éviter que Doctrine pose des questions durant les migrations, ajouter <code>--no-interaction</code> (ou <code>-n</code>).
Pour voir le code SQL au lieu de l'exécuter : <code>--write-sql</code>.
==== Sur plusieurs bases de données ====
Pour exécuter sur plusieurs bases :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
</syntaxhighlight>
Avec des migrations.yaml de type :
<syntaxhighlight lang=bash>
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
</syntaxhighlight>
=== Synchronisation ===
==== Vers le code ====
===== Vers les entités =====
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
</pre>
{{attention|Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer [[Programmation_PHP_avec_Symfony/Migration_de_Symfony_6_à_7#Rector|Rector]] pour convertir les annotations. Ex :
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable
vendor/bin/rector process src/Entity/MyNewEntity.php
</pre>
}}
===== Vers les migrations =====
Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
==== Vers la base ====
À contrario, pour mettre à jour la BDD à partir des entités :
php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
php bin/console doctrine:schema:update --dump-sql
== Fixtures ==
Il existe plusieurs bibliothèques pour créer des {{wt|fixture}}s, dont une de Doctrine<ref>https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html</ref> :
<syntaxhighlight lang=bash>
composer require --dev orm-fixtures
</syntaxhighlight>
Pour charger les fixtures du code dans la base :
<syntaxhighlight lang=bash>
php bin/console doctrine:fixtures:load -n
</syntaxhighlight>
== Types de champ ==
La liste des types de champ Doctrine se trouve dans <code>Doctrine\DBAL\Types</code>. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre <code>JsonType</code> pour surcharger le type JSON par défaut afin de lui faire faire <code>json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)</code> automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne<ref>https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23</ref>.
== Réplication SQL ==
Anciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml.
Ex<ref>https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35</ref> :
<pre>
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
replicas:
replica1:
url: '%env(resolve:REPLICA_DATABASE_URL)%'
</pre>
== Critique ==
# Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
# Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le {{wt|lazy loading}} est contourné par au choix :
## Avant l'appel null, un <code>ObjetJoint->get()</code>.
## Dans l'entité, un <code>@ManyToOne(…, fetch="EAGER")</code>.
## Dans le repository, un <code>$this->queryBuilder->addSelect()</code>. NB : si cela ajoute un problème N+1, joindre aussi la deuxième entité qui le provoque.
# Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
# Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
# Attention aux <code>$this->queryBuilder->setMaxResults()</code> et <code>$this->queryBuilder->setFirstResult()</code> en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar du <code>LIMIT</code> SQL). La solution consiste à ajouter un paginateur<ref>https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939</ref>.
# L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
# Pas de hints.
# Bug des <code>UNION ALL</code> quand on joint deux entités non liées dans le repo.
{{todo|
* Ajouter la connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL (patrons à copier-coller ?)
}}
== Références ==
{{Références}}
7w3q2wk0f3zsf4642b44h2wy73l9ohh
772390
772389
2026-09-18T08:48:17Z
JackPotte
5426
/* EntityManager */
772390
wikitext
text/x-wiki
<noinclude>{{PHP}}</noinclude>
== Installation ==
{{w|Doctrine (ORM)|Doctrine}} est l'ORM par défaut de Symfony. Il utilise {{w|PHP Data Objects|PDO}}. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la [[Patrons de conception/Chaîne de responsabilité|chaîne de responsabilité]].
Installation en SF4<ref>https://symfony.com/doc/current/doctrine.html</ref> :
<pre>
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
</pre>
Renseigner l'accès au SGBD dans le .env :
<pre>
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
</pre>
Ensuite la base de données doit être créée avec :
<pre>
php bin/console doctrine:database:create
</pre>Si la commande précédente échoue avec le message d'erreur suivant:
''Could not create database "database_name" for connection named default''
''An exception occurred in the driver: could not find driver''
Ce qui veut dire que vous devez installer le driver approprié.
'''Exemple:'''
sudo apt install php8.3-pgsql{{remarque|1=symfony/orm-pack équivaut aux paquets suivants, qui peuvent bien sûr être installés séparément à la place :
*doctrine/doctrine-bundle
*doctrine/doctrine-migrations-bundle
*doctrine/orm
*symfony/proxy-manager-bridge
}}
== Commandes Doctrine ==
Exemples de commandes :
<pre>
php bin/console dbal:run-sql "SELECT * FROM ma_table"
php bin/console dbal:run-sql "$(< mon_fichier.sql)"
# Ces deux commandes sont équivalentes des précédentes mais sur Doctrine < 3.
php bin/console doctrine:query:sql "SELECT * FROM ma_table"
php bin/console doctrine:query:sql "$(< mon_fichier.sql)"
php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:cache:clear-query
php bin/console doctrine:cache:clear-result
</pre>
== Entity ==
Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs {{wt|getter}}s et {{wt|setter}}s respectifs. Pour en générer une :
<pre>
php bin/console generate:doctrine:entity
</pre>
Cette association est définie par des attributs Doctrine. Pour les vérifier :
<pre>
php bin/console doctrine:schema:validate
</pre>
=== Exemple ===
Voici par exemple plusieurs types d'attributs :
<syntaxhighlight lang=php>
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
</syntaxhighlight>
{{(|Exemple avec annotation (avant PHP 8)}}
<syntaxhighlight lang=php>
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="spelling", type="string", length=255, nullable=false)
*/
private $spelling;
/**
* @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
*/
private $pronunciation;
/**
* @var Language
*
* @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
* @ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
*/
private $homophons;
</syntaxhighlight>
{{)}}
Et leurs modificateurs (getters et setters) :
<syntaxhighlight lang=php>
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
</syntaxhighlight>
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec <code>$word->setLanguage($language)</code>.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi un deuxième attribut <code>#[ORM\JoinColumn</code> (anciennement <code>@ORM\JoinColumn</code>) mentionnant la clé étrangère en base de données (et pas en PHP).
=== Bonnes pratiques ===
L'attribut <code>#[ORM\Table(name: 'word')]</code> était facultatif dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.
Avant PHP 8, les contraintes d'unicité (utiles entre autres pour les clés composites) étaient encapsulées dans l'annotation <code>Table</code>, mais ce n'est plus le cas avec les attributs :
<pre>
#[ORM\UniqueConstraint(name: 'spelling-pronunciation', columns: ['spelling', 'pronunciation'])]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
</pre>
{{)}}
{{attention|Dans les relations *toMany :
* il faut initialiser l'attribut dans le constructeur en <code>ArrayCollection()</code>.
* on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
* cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
}}
{{attention|Il faut ajouter le <code>#[ORM\JoinColumn(</code> dans les deux entités liées, car :
* dans aucune cela renvoie <code>Could not resolve type of column "id"</code>
* dans une seule cela provoque un problème N+1 (celle qui ne l'a pas appelle celle qui l'a pour chacun de ses enregistrements, même si celle qui l'a n'est pas utilisée ensuite).
}}
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec <code>length=0</code><ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string</ref>. Le type "text" par contre n'a pas de limite.
=== ArrayCollection ===
Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
* Dans une entité : <code>#[ORM\OrderBy(['sort_order' => 'ASC'])]</code> (anciennement <code>@ORM\OrderBy({"sort_order" = "ASC"})</code>).
* Sinon, instancier un critère :
<pre>
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
</pre>
=== GeneratedValue ===
L'annotation ''GeneratedValue'' peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
{{attention|
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID<ref>https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations</ref>. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur '' Integrity constraint violation'' puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
<syntaxhighlight lang=php>
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
</syntaxhighlight>
}}
=== Triggers ===
Les opérations en cascade sont définies sous deux formes d'attributs :
* <code>#[ORM\OneToMany(cascade: ['persist', 'remove'])]</code> : au niveau ORM.
* <code>#[ORM\JoinColumn(onDelete: 'CASCADE')]</code> : au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
=== Concepts avancés ===
Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware<ref>https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html</ref>.
{{attention|Les types des attributs peuvent être quelque peu différents du SGBD<ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix</ref>.}}
{{attention|Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").}}
L'autojointure est appelé ''self-referencing association mapping'' par Doctrine<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing</ref>).
=== Héritage ===
Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html</ref> :
<syntaxhighlight lang=php>
/** @MappedSuperclass */
class MyEntityParent
...
</syntaxhighlight>
=== Tables sans classe ===
Doctrine peut créer des tables de mapping sans entité, si on précise son nom dans les deux tables reliées :
<pre>
#[ORM\JoinTable(name: 'table_de_mapping')]
#[ORM\JoinColumn(name: 'table_source_id', referencedColumnName: 'table_source_id')]
#[ORM\InverseJoinColumn(name: 'table_cible_id', referencedColumnName: 'table_cible_id')]
#[ORM\ManyToMany(targetEntity: TableCible::class)]
</pre>
== EntityManager ==
L'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données. Ils sont définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
* <code>persist()</code> : prépare un <code>INSERT</code> SQL (rattache une entité à un entity manager).
* <code>remove()</code> : prépare un <code>DELETE</code> SQL.
* <code>flush()</code> : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
* <code>merge()</code> : fusionne une entité absente de l'em dedans.
* <code>clear()</code> : détache tous les objets de l'em.
* <code>refresh()</code> : rafraichit l'entité PHP à partir de la base de données. C'est utile par exemple pour tenir compte des résultats d'un trigger ''after insert'' sur le SGBD. Exemple si le trigger ajoute une date de création après le persist, à écraser par <code>$createdDate</code> :
<syntaxhighlight lang=php>
$entity = new MyEntity();
$em->persist($entity);
$em->flush($entity);
// Trigger SGBD déclenché ici en parallèle
$em->refresh($entity);
$entity->setCreatedDate($createdDate);
$em->flush($entity);
</syntaxhighlight>
{{attention|Ne jamais utiliser <code>refresh()</code> dans un environnement avec des replicas de la base de données pour éviter les incohérences. Le plus performant est alors de reprendre l'objet de l'instance censé être à jour à la place.}}
== Repository ==
On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de <code>Doctrine\ORM\EntityRepository</code>. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés ''NomDeLEntitéRepository''.
{{remarque|D'un point de vue architectural, avant d'instancier une nouvelle entité, on utilise généralement le repository pour savoir si son enregistrement existe en base ou si on doit le créer. Dans ce deuxième cas, la bonne pratique en {{wt|DDD}} est d'utiliser une Factory pour faire le new de l'entité, mais aussi pour les new de son agrégat si elle est le nœud racine. Par exemple une <code>CarFactory</code> fera un <code>new Car()</code> mais aussi créera et lui associera ses composants : <code>new Motor()</code>...}}
{{remarque|Il est possible de préciser le nom du repository d'une entité dans cette dernière :
<pre>
#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
@ORM\Entity(repositoryClass="App\Repository\WordRepository")
</pre>
{{)}}
}}
=== SQL ===
==== Depuis Doctrine ====
Utile pour exploiter les fonctionnalités du SGBD utilisé, absentes de Doctrine.
Par exemple, pour appeler une procédure stockée :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
</pre>
Ou tronquer une table :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('TRUNCATE TABLE ma_table', $rsm)->getResult();
</pre>
{{remarque|Dans cet exemple, on peut aussi utiliser ceci :
<pre>
$connection = $this->_em->getConnection();
$databasePlatform = $connection->getDatabasePlatform();
$connection->executeStatement(
$databasePlatform->getTruncateTableSQL($this->_em->getClassMetadata(MonEntite::class)->getTableName(), true),
);
</pre>
}}
==== Sans Doctrine ====
Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :
<pre>
return $this->connection->fetchAll($sql);
</pre>
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut ''entity manager'' (''em'', ou ''_em'' pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
<syntaxhighlight lang=php>
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
</syntaxhighlight>
2. En injectant le service de connexion dans le constructeur (<code>'@database_connection'</code>) :
<pre>
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
</pre>
=== DQL ===
==== Méthodes magiques ====
Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
* <code>$repo->find($id)</code> : cherche par la clé primaire définie dans l'entité.
* <code>$repo->findAll()</code> : récupère tous les enregistrements (sans clause <code>WHERE</code>).
* <code>$repo->findById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id</code> dans la table associée au repo.
* <code>$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])</code> engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname</code>.
* <code>$repo->findOneById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id LIMIT 1</code>.
* <code>$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])</code> : engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1</code>.
{{attention|Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "<code>findById</code>" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser <code>findBy()</code>.
}}
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
<syntaxhighlight lang=php>
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
</syntaxhighlight>
==== createQuery ====
DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec <code>IDENTITY()</code> pour les jointures. Ex :
<pre>
public function findComplicatedStuff()
{
$em = $this->getEntityManager();
$query = $em->createQuery("
SELECT
u.last_name, u.first_name
FROM
App\Entity\Users u
INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users)
WHERE
i.status='waiting'
");
return $query->getResult();
}
</pre>
==== createQueryBuilder ====
L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel <code>createQueryBuilder()</code> :
<syntaxhighlight lang=php>
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
</syntaxhighlight>
Pour éviter le <code>SELECT *</code> dans cet exemple, on peut y ajouter la méthode <code>->select()</code>.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
===== Jointures =====
Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
<pre>
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
</pre>
Pour filtrer quand une jointure toMany contient des résultats, utiliser <code>EMPTY</code> :
<pre>
...
->andWhere('files IS NOT EMPTY')
</pre>
===== Résultats =====
Doctrine peut renvoyer avec :
* <code>getResult()</code> : un objet ArrayCollection (iterable, pour rechercher dedans : <code>->contains()</code>), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;
* <code>getArrayResult()</code> ou <code>getScalarResult()</code> : un tableau de tableaux (entité normalisée) ;
* <code>getSingleColumnResult()</code> : un tableau unidimensionnel.
On peut aussi utiliser <code>->indexBy('id')</code> pour définir que les clés du tableau de résultat soir les IDs de l'entité.
===== Cache =====
====== Configuration globale ======
Doctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
<syntaxhighlight lang=yaml>
framework:
cache:
default_redis_provider: '%env(REDIS_URL)%'
pools:
doctrine.metadata_cache_pool:
adapter: cache.system
doctrine.query_cache_pool:
adapter: cache.system
doctrine.result_cache_pool:
adapter: cache.app
</syntaxhighlight>
Puis dans doctrine.yaml, les utiliser :
<syntaxhighlight lang=yaml>
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.metadata_cache_pool
query_cache_driver:
type: pool
pool: doctrine.query_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
</syntaxhighlight>
À partir de là le cache des métadonnées est utilisé partout.
====== Configuration par entité ======
Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
* Dans l'entité, avec un attribut <code>#[ORM\Cache(usage: 'READ_ONLY', region: 'write_rare')]</code> (anciennement <code>@ORM\Cache(usage="READ_ONLY", region="write_rare")</code><ref>https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6</ref>), utilisant la configuration doctrine.yaml :
<pre>
doctrine:
orm:
second_level_cache:
enabled: true
regions:
write_rare:
lifetime: 864000
cache_driver: { type: service, id: cache.app }
</pre>
* Dans le repository :
<syntaxhighlight lang=php>
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
</syntaxhighlight>
Dans cet exemple, on n'utilise pas <code>cache.system</code> pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. <code>cache.app</code> pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
Pour invalider le cache d'une entité afin que les findAll() renvoient la liste à jour depuis la base de données modifiée sur Doctrine 3 :
Une seule instance de cette entité :
<pre>
$em->getCache()->evictEntity(myEntity::class, $entityId);
</pre>
Toutes les instances de l'entité :
<pre>
$em->getCache()->getRegion(myEntity::class)->clear();
</pre>
{{(|Doctrine 2}}
<pre>
$em->getCache()->evictEntityRegion(myEntity::class);
</pre>
{{)}}
==== Expressions ====
Pour ajouter une expression en DQL, utilise <code>$qb->expr()</code>. Ex<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class</ref> :
* <code>$qb->expr()->count('u.id')</code>
* <code>$qb->expr()->between('u.id', 2, 10)</code> (entre 2 et 10)
* <code>$qb->expr()->gte('u.id', 2)</code> (plus grand ou égal à 2)
* <code>$qb->expr()->like('u.name', '%son')</code>
* <code>$qb->expr()->lower('u.name')</code>
* <code>$qb->expr()->substring('u.name', 0, 1)</code>
==== Injection de dépendances ====
Les repository DQL deoivent ''ServiceEntityRepository'' :
<syntaxhighlight lang=php>
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
</syntaxhighlight>
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
* Étendre une classe qui étend ''ServiceEntityRepository''.
* Le redéfinir dans services.yaml.
* Utiliser un trait.
== Transactions ==
Pour garantir d'intégrité d'une transaction<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly</ref> :
<syntaxhighlight lang=php>
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
</syntaxhighlight>
Il existe aussi une syntaxe alternative :
<syntaxhighlight lang=php>
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
</syntaxhighlight>
== Évènements ==
Pour ajouter des triggers sur la mise à jour d'une table, il y a deux solutions :
* ajouter dans son entité l'attribut <code>#[ORM\HasLifecycleCallbacks]</code>, et à ses méthodes l'attribut de l'évènement concerné. Ex :
<pre>
#[ORM\PrePersist]
public function setCreatedAt(): self
{
$this->createdAt = new DateTime();
return $this;
}
</pre>
* ajouter des tags dans services.yaml. Ex :
<pre>
App\EventListener\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: PrePersist }
</pre>
Voici les [[Programmation PHP avec Symfony/Évènement|évènements]] utilisables ensuite (dans les listeners / subscribers) :
=== prePersist ===
Se produit avant la persistance d'une entité (paramètre : <code>PrePersistEventArgs $args</code>).
=== postPersist ===
Se produit après la persistance d'une entité (<code>PostPersistEventArgs $args</code>).
=== preUpdate ===
Se produit avant l'update d'une entité (<code>PreUpdateEventArgs $args</code>).
=== postUpdate ===
Se produit après l'update d'une entité (<code>PostUpdateEventArgs $args</code>).
=== preRemove ===
Se produit avant l'update d'une entité (<code>PreRemoveEventArgs $args</code>).
=== postRemove ===
Se produit après l'update d'une entité (<code>PostRemoveEventArgs $args</code>).
=== preFlush ===
Se produit avant la sauvegarde d'une entité (<code>PreFlushEventArgs $args</code>).
{{attention|
* Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
* Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
* Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler <code>$unitOfWork->computeChangeSet($classMetadata, $entity)</code><ref>https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener</ref>.
}}
{{remarque|On peut aussi appeler computeChangeSet() depuis ailleurs pour savoir si une entité va occasionner une requête SQL lors de son flush<ref>https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2</ref>.}} Ex :
<pre>
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
//...
}
</pre>
{{remarque|On peut aussi utiliser le paramètre <code>LifecycleEventArgs $args</code> dans ces fonctions.}}
{{attention|Parfois le <code>$object::class</code> peut renvoyer <code>Proxies\__CG__\App\Entity\MyEntity</code> au lieu de <code>App\Entity\MyEntity</code>, selon le cache utilisé.}}
=== postFlush ===
Se produit après la sauvegarde d'une entité (<code>PostFlushEventArgs $args</code>).
== Migrations ==
Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
<syntaxhighlight lang=bash>
composer require doctrine/doctrine-migrations-bundle
</syntaxhighlight>
=== Création ===
Ensuite, on peut créer un squelette de "migration" :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:generate
</syntaxhighlight>
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée <code>migration_versions</code> enregistre leur succession).
==== Exemple SQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->connection->fetchAll('SHOW DATABASES;');
$this->addSql(<<<SQL
CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
}
public function down(Schema $schema) : void
{
$this->addSql('DROP TABLE ma_table');
}
}
</syntaxhighlight>
==== Exemple DQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$table = $schema->createTable('ma_table');
$table->addColumn('ma_colonne', 'string');
}
public function down(Schema $schema) : void
{
$schema->dropTable('ma_table');
}
}
</syntaxhighlight>
==== Exemple PHP ====
Depuis Symfony 7.0, il faut implémenter <code>MigrationFactory</code> pour injecter des dépendances dans les migrations (et on ne peut plus injecter tout le conteneur)<ref>https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies</ref>.
{{Boîte déroulante début|titre=Avant Symfony 7.0, il fallait juste utiliser <code>ContainerAwareTrait</code>}}
Exemple :
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function up(Schema $schema) : void
{
$em = $this->container->get('doctrine.orm.entity_manager');
$monEntite = new MonEntite();
$em->persist($monEntite);
$em->flush();
}
}
</syntaxhighlight>
{{Boîte déroulante fin}}
{{attention|Cette technique est déconseillée car les entités peuvent évoluer indépendamment de la migration. Mais elle peut s'avérer utile pour stocker des données dépendantes de l'environnement.}}
{{attention|<code>$this->container->getParameter()</code> ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple <code>$_SERVER['SUBAPI_URI']</code> renvoie la variable d'environnement et <code>$this->containergetParameter('env(SUBAPI_URI)')</code> sa valeur par défaut (définie dans services.yaml).}}
=== Exécution ===
La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate
</syntaxhighlight>
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --up 20170321095644
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
</syntaxhighlight>
Pour le rollback :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --down 20170321095644
</syntaxhighlight>
Pour éviter que Doctrine pose des questions durant les migrations, ajouter <code>--no-interaction</code> (ou <code>-n</code>).
Pour voir le code SQL au lieu de l'exécuter : <code>--write-sql</code>.
==== Sur plusieurs bases de données ====
Pour exécuter sur plusieurs bases :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
</syntaxhighlight>
Avec des migrations.yaml de type :
<syntaxhighlight lang=bash>
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
</syntaxhighlight>
=== Synchronisation ===
==== Vers le code ====
===== Vers les entités =====
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
</pre>
{{attention|Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer [[Programmation_PHP_avec_Symfony/Migration_de_Symfony_6_à_7#Rector|Rector]] pour convertir les annotations. Ex :
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable
vendor/bin/rector process src/Entity/MyNewEntity.php
</pre>
}}
===== Vers les migrations =====
Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
==== Vers la base ====
À contrario, pour mettre à jour la BDD à partir des entités :
php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
php bin/console doctrine:schema:update --dump-sql
== Fixtures ==
Il existe plusieurs bibliothèques pour créer des {{wt|fixture}}s, dont une de Doctrine<ref>https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html</ref> :
<syntaxhighlight lang=bash>
composer require --dev orm-fixtures
</syntaxhighlight>
Pour charger les fixtures du code dans la base :
<syntaxhighlight lang=bash>
php bin/console doctrine:fixtures:load -n
</syntaxhighlight>
== Types de champ ==
La liste des types de champ Doctrine se trouve dans <code>Doctrine\DBAL\Types</code>. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre <code>JsonType</code> pour surcharger le type JSON par défaut afin de lui faire faire <code>json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)</code> automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne<ref>https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23</ref>.
== Réplication SQL ==
Anciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml.
Ex<ref>https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35</ref> :
<pre>
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
replicas:
replica1:
url: '%env(resolve:REPLICA_DATABASE_URL)%'
</pre>
== Critique ==
# Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
# Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le {{wt|lazy loading}} est contourné par au choix :
## Avant l'appel null, un <code>ObjetJoint->get()</code>.
## Dans l'entité, un <code>@ManyToOne(…, fetch="EAGER")</code>.
## Dans le repository, un <code>$this->queryBuilder->addSelect()</code>. NB : si cela ajoute un problème N+1, joindre aussi la deuxième entité qui le provoque.
# Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
# Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
# Attention aux <code>$this->queryBuilder->setMaxResults()</code> et <code>$this->queryBuilder->setFirstResult()</code> en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar du <code>LIMIT</code> SQL). La solution consiste à ajouter un paginateur<ref>https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939</ref>.
# L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
# Pas de hints.
# Bug des <code>UNION ALL</code> quand on joint deux entités non liées dans le repo.
{{todo|
* Ajouter la connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL (patrons à copier-coller ?)
}}
== Références ==
{{Références}}
kxcqxm24nfzk0pso3mdy8o16ra314ep
772391
772390
2026-09-18T08:50:38Z
JackPotte
5426
/* EntityManager */
772391
wikitext
text/x-wiki
<noinclude>{{PHP}}</noinclude>
== Installation ==
{{w|Doctrine (ORM)|Doctrine}} est l'ORM par défaut de Symfony. Il utilise {{w|PHP Data Objects|PDO}}. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la [[Patrons de conception/Chaîne de responsabilité|chaîne de responsabilité]].
Installation en SF4<ref>https://symfony.com/doc/current/doctrine.html</ref> :
<pre>
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
</pre>
Renseigner l'accès au SGBD dans le .env :
<pre>
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
</pre>
Ensuite la base de données doit être créée avec :
<pre>
php bin/console doctrine:database:create
</pre>Si la commande précédente échoue avec le message d'erreur suivant:
''Could not create database "database_name" for connection named default''
''An exception occurred in the driver: could not find driver''
Ce qui veut dire que vous devez installer le driver approprié.
'''Exemple:'''
sudo apt install php8.3-pgsql{{remarque|1=symfony/orm-pack équivaut aux paquets suivants, qui peuvent bien sûr être installés séparément à la place :
*doctrine/doctrine-bundle
*doctrine/doctrine-migrations-bundle
*doctrine/orm
*symfony/proxy-manager-bridge
}}
== Commandes Doctrine ==
Exemples de commandes :
<pre>
php bin/console dbal:run-sql "SELECT * FROM ma_table"
php bin/console dbal:run-sql "$(< mon_fichier.sql)"
# Ces deux commandes sont équivalentes des précédentes mais sur Doctrine < 3.
php bin/console doctrine:query:sql "SELECT * FROM ma_table"
php bin/console doctrine:query:sql "$(< mon_fichier.sql)"
php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:cache:clear-query
php bin/console doctrine:cache:clear-result
</pre>
== Entity ==
Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs {{wt|getter}}s et {{wt|setter}}s respectifs. Pour en générer une :
<pre>
php bin/console generate:doctrine:entity
</pre>
Cette association est définie par des attributs Doctrine. Pour les vérifier :
<pre>
php bin/console doctrine:schema:validate
</pre>
=== Exemple ===
Voici par exemple plusieurs types d'attributs :
<syntaxhighlight lang=php>
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
</syntaxhighlight>
{{(|Exemple avec annotation (avant PHP 8)}}
<syntaxhighlight lang=php>
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="spelling", type="string", length=255, nullable=false)
*/
private $spelling;
/**
* @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
*/
private $pronunciation;
/**
* @var Language
*
* @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
* @ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
*/
private $homophons;
</syntaxhighlight>
{{)}}
Et leurs modificateurs (getters et setters) :
<syntaxhighlight lang=php>
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
</syntaxhighlight>
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec <code>$word->setLanguage($language)</code>.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi un deuxième attribut <code>#[ORM\JoinColumn</code> (anciennement <code>@ORM\JoinColumn</code>) mentionnant la clé étrangère en base de données (et pas en PHP).
=== Bonnes pratiques ===
L'attribut <code>#[ORM\Table(name: 'word')]</code> était facultatif dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.
Avant PHP 8, les contraintes d'unicité (utiles entre autres pour les clés composites) étaient encapsulées dans l'annotation <code>Table</code>, mais ce n'est plus le cas avec les attributs :
<pre>
#[ORM\UniqueConstraint(name: 'spelling-pronunciation', columns: ['spelling', 'pronunciation'])]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
</pre>
{{)}}
{{attention|Dans les relations *toMany :
* il faut initialiser l'attribut dans le constructeur en <code>ArrayCollection()</code>.
* on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
* cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
}}
{{attention|Il faut ajouter le <code>#[ORM\JoinColumn(</code> dans les deux entités liées, car :
* dans aucune cela renvoie <code>Could not resolve type of column "id"</code>
* dans une seule cela provoque un problème N+1 (celle qui ne l'a pas appelle celle qui l'a pour chacun de ses enregistrements, même si celle qui l'a n'est pas utilisée ensuite).
}}
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec <code>length=0</code><ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string</ref>. Le type "text" par contre n'a pas de limite.
=== ArrayCollection ===
Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
* Dans une entité : <code>#[ORM\OrderBy(['sort_order' => 'ASC'])]</code> (anciennement <code>@ORM\OrderBy({"sort_order" = "ASC"})</code>).
* Sinon, instancier un critère :
<pre>
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
</pre>
=== GeneratedValue ===
L'annotation ''GeneratedValue'' peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
{{attention|
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID<ref>https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations</ref>. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur '' Integrity constraint violation'' puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
<syntaxhighlight lang=php>
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
</syntaxhighlight>
}}
=== Triggers ===
Les opérations en cascade sont définies sous deux formes d'attributs :
* <code>#[ORM\OneToMany(cascade: ['persist', 'remove'])]</code> : au niveau ORM.
* <code>#[ORM\JoinColumn(onDelete: 'CASCADE')]</code> : au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
=== Concepts avancés ===
Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware<ref>https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html</ref>.
{{attention|Les types des attributs peuvent être quelque peu différents du SGBD<ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix</ref>.}}
{{attention|Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").}}
L'autojointure est appelé ''self-referencing association mapping'' par Doctrine<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing</ref>).
=== Héritage ===
Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html</ref> :
<syntaxhighlight lang=php>
/** @MappedSuperclass */
class MyEntityParent
...
</syntaxhighlight>
=== Tables sans classe ===
Doctrine peut créer des tables de mapping sans entité, si on précise son nom dans les deux tables reliées :
<pre>
#[ORM\JoinTable(name: 'table_de_mapping')]
#[ORM\JoinColumn(name: 'table_source_id', referencedColumnName: 'table_source_id')]
#[ORM\InverseJoinColumn(name: 'table_cible_id', referencedColumnName: 'table_cible_id')]
#[ORM\ManyToMany(targetEntity: TableCible::class)]
</pre>
== EntityManager ==
L'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données. Ils sont définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
* <code>persist()</code> : prépare un <code>INSERT</code> SQL (rattache une entité à un entity manager).
* <code>remove()</code> : prépare un <code>DELETE</code> SQL.
* <code>flush()</code> : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
* <code>merge()</code> : fusionne une entité absente de l'em dedans.
* <code>detach()</code> : détache un objet de l'em, il n'est alors plus synchronisé automatiquement avec la base.
* <code>clear()</code> : détache tous les objets de l'em.
* <code>refresh()</code> : rafraichit l'entité PHP à partir de la base de données. C'est utile par exemple pour tenir compte des résultats d'un trigger ''after insert'' sur le SGBD. Exemple si le trigger ajoute une date de création après le persist, à écraser par <code>$createdDate</code> :
<syntaxhighlight lang=php>
$entity = new MyEntity();
$em->persist($entity);
$em->flush($entity);
// Trigger SGBD déclenché ici en parallèle
$em->refresh($entity);
$entity->setCreatedDate($createdDate);
$em->flush($entity);
</syntaxhighlight>
{{attention|Ne jamais utiliser <code>refresh()</code> dans un environnement avec des replicas de la base de données pour éviter les incohérences. Le plus performant est alors de reprendre l'objet de l'instance censé être à jour à la place.}}
== Repository ==
On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de <code>Doctrine\ORM\EntityRepository</code>. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés ''NomDeLEntitéRepository''.
{{remarque|D'un point de vue architectural, avant d'instancier une nouvelle entité, on utilise généralement le repository pour savoir si son enregistrement existe en base ou si on doit le créer. Dans ce deuxième cas, la bonne pratique en {{wt|DDD}} est d'utiliser une Factory pour faire le new de l'entité, mais aussi pour les new de son agrégat si elle est le nœud racine. Par exemple une <code>CarFactory</code> fera un <code>new Car()</code> mais aussi créera et lui associera ses composants : <code>new Motor()</code>...}}
{{remarque|Il est possible de préciser le nom du repository d'une entité dans cette dernière :
<pre>
#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
@ORM\Entity(repositoryClass="App\Repository\WordRepository")
</pre>
{{)}}
}}
=== SQL ===
==== Depuis Doctrine ====
Utile pour exploiter les fonctionnalités du SGBD utilisé, absentes de Doctrine.
Par exemple, pour appeler une procédure stockée :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
</pre>
Ou tronquer une table :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('TRUNCATE TABLE ma_table', $rsm)->getResult();
</pre>
{{remarque|Dans cet exemple, on peut aussi utiliser ceci :
<pre>
$connection = $this->_em->getConnection();
$databasePlatform = $connection->getDatabasePlatform();
$connection->executeStatement(
$databasePlatform->getTruncateTableSQL($this->_em->getClassMetadata(MonEntite::class)->getTableName(), true),
);
</pre>
}}
==== Sans Doctrine ====
Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :
<pre>
return $this->connection->fetchAll($sql);
</pre>
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut ''entity manager'' (''em'', ou ''_em'' pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
<syntaxhighlight lang=php>
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
</syntaxhighlight>
2. En injectant le service de connexion dans le constructeur (<code>'@database_connection'</code>) :
<pre>
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
</pre>
=== DQL ===
==== Méthodes magiques ====
Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
* <code>$repo->find($id)</code> : cherche par la clé primaire définie dans l'entité.
* <code>$repo->findAll()</code> : récupère tous les enregistrements (sans clause <code>WHERE</code>).
* <code>$repo->findById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id</code> dans la table associée au repo.
* <code>$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])</code> engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname</code>.
* <code>$repo->findOneById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id LIMIT 1</code>.
* <code>$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])</code> : engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1</code>.
{{attention|Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "<code>findById</code>" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser <code>findBy()</code>.
}}
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
<syntaxhighlight lang=php>
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
</syntaxhighlight>
==== createQuery ====
DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec <code>IDENTITY()</code> pour les jointures. Ex :
<pre>
public function findComplicatedStuff()
{
$em = $this->getEntityManager();
$query = $em->createQuery("
SELECT
u.last_name, u.first_name
FROM
App\Entity\Users u
INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users)
WHERE
i.status='waiting'
");
return $query->getResult();
}
</pre>
==== createQueryBuilder ====
L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel <code>createQueryBuilder()</code> :
<syntaxhighlight lang=php>
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
</syntaxhighlight>
Pour éviter le <code>SELECT *</code> dans cet exemple, on peut y ajouter la méthode <code>->select()</code>.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
===== Jointures =====
Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
<pre>
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
</pre>
Pour filtrer quand une jointure toMany contient des résultats, utiliser <code>EMPTY</code> :
<pre>
...
->andWhere('files IS NOT EMPTY')
</pre>
===== Résultats =====
Doctrine peut renvoyer avec :
* <code>getResult()</code> : un objet ArrayCollection (iterable, pour rechercher dedans : <code>->contains()</code>), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;
* <code>getArrayResult()</code> ou <code>getScalarResult()</code> : un tableau de tableaux (entité normalisée) ;
* <code>getSingleColumnResult()</code> : un tableau unidimensionnel.
On peut aussi utiliser <code>->indexBy('id')</code> pour définir que les clés du tableau de résultat soir les IDs de l'entité.
===== Cache =====
====== Configuration globale ======
Doctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
<syntaxhighlight lang=yaml>
framework:
cache:
default_redis_provider: '%env(REDIS_URL)%'
pools:
doctrine.metadata_cache_pool:
adapter: cache.system
doctrine.query_cache_pool:
adapter: cache.system
doctrine.result_cache_pool:
adapter: cache.app
</syntaxhighlight>
Puis dans doctrine.yaml, les utiliser :
<syntaxhighlight lang=yaml>
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.metadata_cache_pool
query_cache_driver:
type: pool
pool: doctrine.query_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
</syntaxhighlight>
À partir de là le cache des métadonnées est utilisé partout.
====== Configuration par entité ======
Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
* Dans l'entité, avec un attribut <code>#[ORM\Cache(usage: 'READ_ONLY', region: 'write_rare')]</code> (anciennement <code>@ORM\Cache(usage="READ_ONLY", region="write_rare")</code><ref>https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6</ref>), utilisant la configuration doctrine.yaml :
<pre>
doctrine:
orm:
second_level_cache:
enabled: true
regions:
write_rare:
lifetime: 864000
cache_driver: { type: service, id: cache.app }
</pre>
* Dans le repository :
<syntaxhighlight lang=php>
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
</syntaxhighlight>
Dans cet exemple, on n'utilise pas <code>cache.system</code> pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. <code>cache.app</code> pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
Pour invalider le cache d'une entité afin que les findAll() renvoient la liste à jour depuis la base de données modifiée sur Doctrine 3 :
Une seule instance de cette entité :
<pre>
$em->getCache()->evictEntity(myEntity::class, $entityId);
</pre>
Toutes les instances de l'entité :
<pre>
$em->getCache()->getRegion(myEntity::class)->clear();
</pre>
{{(|Doctrine 2}}
<pre>
$em->getCache()->evictEntityRegion(myEntity::class);
</pre>
{{)}}
==== Expressions ====
Pour ajouter une expression en DQL, utilise <code>$qb->expr()</code>. Ex<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class</ref> :
* <code>$qb->expr()->count('u.id')</code>
* <code>$qb->expr()->between('u.id', 2, 10)</code> (entre 2 et 10)
* <code>$qb->expr()->gte('u.id', 2)</code> (plus grand ou égal à 2)
* <code>$qb->expr()->like('u.name', '%son')</code>
* <code>$qb->expr()->lower('u.name')</code>
* <code>$qb->expr()->substring('u.name', 0, 1)</code>
==== Injection de dépendances ====
Les repository DQL deoivent ''ServiceEntityRepository'' :
<syntaxhighlight lang=php>
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
</syntaxhighlight>
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
* Étendre une classe qui étend ''ServiceEntityRepository''.
* Le redéfinir dans services.yaml.
* Utiliser un trait.
== Transactions ==
Pour garantir d'intégrité d'une transaction<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly</ref> :
<syntaxhighlight lang=php>
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
</syntaxhighlight>
Il existe aussi une syntaxe alternative :
<syntaxhighlight lang=php>
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
</syntaxhighlight>
== Évènements ==
Pour ajouter des triggers sur la mise à jour d'une table, il y a deux solutions :
* ajouter dans son entité l'attribut <code>#[ORM\HasLifecycleCallbacks]</code>, et à ses méthodes l'attribut de l'évènement concerné. Ex :
<pre>
#[ORM\PrePersist]
public function setCreatedAt(): self
{
$this->createdAt = new DateTime();
return $this;
}
</pre>
* ajouter des tags dans services.yaml. Ex :
<pre>
App\EventListener\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: PrePersist }
</pre>
Voici les [[Programmation PHP avec Symfony/Évènement|évènements]] utilisables ensuite (dans les listeners / subscribers) :
=== prePersist ===
Se produit avant la persistance d'une entité (paramètre : <code>PrePersistEventArgs $args</code>).
=== postPersist ===
Se produit après la persistance d'une entité (<code>PostPersistEventArgs $args</code>).
=== preUpdate ===
Se produit avant l'update d'une entité (<code>PreUpdateEventArgs $args</code>).
=== postUpdate ===
Se produit après l'update d'une entité (<code>PostUpdateEventArgs $args</code>).
=== preRemove ===
Se produit avant l'update d'une entité (<code>PreRemoveEventArgs $args</code>).
=== postRemove ===
Se produit après l'update d'une entité (<code>PostRemoveEventArgs $args</code>).
=== preFlush ===
Se produit avant la sauvegarde d'une entité (<code>PreFlushEventArgs $args</code>).
{{attention|
* Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
* Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
* Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler <code>$unitOfWork->computeChangeSet($classMetadata, $entity)</code><ref>https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener</ref>.
}}
{{remarque|On peut aussi appeler computeChangeSet() depuis ailleurs pour savoir si une entité va occasionner une requête SQL lors de son flush<ref>https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2</ref>.}} Ex :
<pre>
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
//...
}
</pre>
{{remarque|On peut aussi utiliser le paramètre <code>LifecycleEventArgs $args</code> dans ces fonctions.}}
{{attention|Parfois le <code>$object::class</code> peut renvoyer <code>Proxies\__CG__\App\Entity\MyEntity</code> au lieu de <code>App\Entity\MyEntity</code>, selon le cache utilisé.}}
=== postFlush ===
Se produit après la sauvegarde d'une entité (<code>PostFlushEventArgs $args</code>).
== Migrations ==
Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
<syntaxhighlight lang=bash>
composer require doctrine/doctrine-migrations-bundle
</syntaxhighlight>
=== Création ===
Ensuite, on peut créer un squelette de "migration" :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:generate
</syntaxhighlight>
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée <code>migration_versions</code> enregistre leur succession).
==== Exemple SQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->connection->fetchAll('SHOW DATABASES;');
$this->addSql(<<<SQL
CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
}
public function down(Schema $schema) : void
{
$this->addSql('DROP TABLE ma_table');
}
}
</syntaxhighlight>
==== Exemple DQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$table = $schema->createTable('ma_table');
$table->addColumn('ma_colonne', 'string');
}
public function down(Schema $schema) : void
{
$schema->dropTable('ma_table');
}
}
</syntaxhighlight>
==== Exemple PHP ====
Depuis Symfony 7.0, il faut implémenter <code>MigrationFactory</code> pour injecter des dépendances dans les migrations (et on ne peut plus injecter tout le conteneur)<ref>https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies</ref>.
{{Boîte déroulante début|titre=Avant Symfony 7.0, il fallait juste utiliser <code>ContainerAwareTrait</code>}}
Exemple :
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function up(Schema $schema) : void
{
$em = $this->container->get('doctrine.orm.entity_manager');
$monEntite = new MonEntite();
$em->persist($monEntite);
$em->flush();
}
}
</syntaxhighlight>
{{Boîte déroulante fin}}
{{attention|Cette technique est déconseillée car les entités peuvent évoluer indépendamment de la migration. Mais elle peut s'avérer utile pour stocker des données dépendantes de l'environnement.}}
{{attention|<code>$this->container->getParameter()</code> ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple <code>$_SERVER['SUBAPI_URI']</code> renvoie la variable d'environnement et <code>$this->containergetParameter('env(SUBAPI_URI)')</code> sa valeur par défaut (définie dans services.yaml).}}
=== Exécution ===
La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate
</syntaxhighlight>
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --up 20170321095644
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
</syntaxhighlight>
Pour le rollback :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --down 20170321095644
</syntaxhighlight>
Pour éviter que Doctrine pose des questions durant les migrations, ajouter <code>--no-interaction</code> (ou <code>-n</code>).
Pour voir le code SQL au lieu de l'exécuter : <code>--write-sql</code>.
==== Sur plusieurs bases de données ====
Pour exécuter sur plusieurs bases :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
</syntaxhighlight>
Avec des migrations.yaml de type :
<syntaxhighlight lang=bash>
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
</syntaxhighlight>
=== Synchronisation ===
==== Vers le code ====
===== Vers les entités =====
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
</pre>
{{attention|Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer [[Programmation_PHP_avec_Symfony/Migration_de_Symfony_6_à_7#Rector|Rector]] pour convertir les annotations. Ex :
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable
vendor/bin/rector process src/Entity/MyNewEntity.php
</pre>
}}
===== Vers les migrations =====
Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
==== Vers la base ====
À contrario, pour mettre à jour la BDD à partir des entités :
php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
php bin/console doctrine:schema:update --dump-sql
== Fixtures ==
Il existe plusieurs bibliothèques pour créer des {{wt|fixture}}s, dont une de Doctrine<ref>https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html</ref> :
<syntaxhighlight lang=bash>
composer require --dev orm-fixtures
</syntaxhighlight>
Pour charger les fixtures du code dans la base :
<syntaxhighlight lang=bash>
php bin/console doctrine:fixtures:load -n
</syntaxhighlight>
== Types de champ ==
La liste des types de champ Doctrine se trouve dans <code>Doctrine\DBAL\Types</code>. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre <code>JsonType</code> pour surcharger le type JSON par défaut afin de lui faire faire <code>json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)</code> automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne<ref>https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23</ref>.
== Réplication SQL ==
Anciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml.
Ex<ref>https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35</ref> :
<pre>
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
replicas:
replica1:
url: '%env(resolve:REPLICA_DATABASE_URL)%'
</pre>
== Critique ==
# Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
# Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le {{wt|lazy loading}} est contourné par au choix :
## Avant l'appel null, un <code>ObjetJoint->get()</code>.
## Dans l'entité, un <code>@ManyToOne(…, fetch="EAGER")</code>.
## Dans le repository, un <code>$this->queryBuilder->addSelect()</code>. NB : si cela ajoute un problème N+1, joindre aussi la deuxième entité qui le provoque.
# Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
# Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
# Attention aux <code>$this->queryBuilder->setMaxResults()</code> et <code>$this->queryBuilder->setFirstResult()</code> en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar du <code>LIMIT</code> SQL). La solution consiste à ajouter un paginateur<ref>https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939</ref>.
# L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
# Pas de hints.
# Bug des <code>UNION ALL</code> quand on joint deux entités non liées dans le repo.
{{todo|
* Ajouter la connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL (patrons à copier-coller ?)
}}
== Références ==
{{Références}}
28o8o1cq9p8qncsjczkthzag6n97tjn
772392
772391
2026-09-18T08:53:55Z
JackPotte
5426
/* EntityManager */
772392
wikitext
text/x-wiki
<noinclude>{{PHP}}</noinclude>
== Installation ==
{{w|Doctrine (ORM)|Doctrine}} est l'ORM par défaut de Symfony. Il utilise {{w|PHP Data Objects|PDO}}. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la [[Patrons de conception/Chaîne de responsabilité|chaîne de responsabilité]].
Installation en SF4<ref>https://symfony.com/doc/current/doctrine.html</ref> :
<pre>
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
</pre>
Renseigner l'accès au SGBD dans le .env :
<pre>
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
</pre>
Ensuite la base de données doit être créée avec :
<pre>
php bin/console doctrine:database:create
</pre>Si la commande précédente échoue avec le message d'erreur suivant:
''Could not create database "database_name" for connection named default''
''An exception occurred in the driver: could not find driver''
Ce qui veut dire que vous devez installer le driver approprié.
'''Exemple:'''
sudo apt install php8.3-pgsql{{remarque|1=symfony/orm-pack équivaut aux paquets suivants, qui peuvent bien sûr être installés séparément à la place :
*doctrine/doctrine-bundle
*doctrine/doctrine-migrations-bundle
*doctrine/orm
*symfony/proxy-manager-bridge
}}
== Commandes Doctrine ==
Exemples de commandes :
<pre>
php bin/console dbal:run-sql "SELECT * FROM ma_table"
php bin/console dbal:run-sql "$(< mon_fichier.sql)"
# Ces deux commandes sont équivalentes des précédentes mais sur Doctrine < 3.
php bin/console doctrine:query:sql "SELECT * FROM ma_table"
php bin/console doctrine:query:sql "$(< mon_fichier.sql)"
php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:cache:clear-query
php bin/console doctrine:cache:clear-result
</pre>
== Entity ==
Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs {{wt|getter}}s et {{wt|setter}}s respectifs. Pour en générer une :
<pre>
php bin/console generate:doctrine:entity
</pre>
Cette association est définie par des attributs Doctrine. Pour les vérifier :
<pre>
php bin/console doctrine:schema:validate
</pre>
=== Exemple ===
Voici par exemple plusieurs types d'attributs :
<syntaxhighlight lang=php>
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
</syntaxhighlight>
{{(|Exemple avec annotation (avant PHP 8)}}
<syntaxhighlight lang=php>
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="spelling", type="string", length=255, nullable=false)
*/
private $spelling;
/**
* @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
*/
private $pronunciation;
/**
* @var Language
*
* @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
* @ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
*/
private $homophons;
</syntaxhighlight>
{{)}}
Et leurs modificateurs (getters et setters) :
<syntaxhighlight lang=php>
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
</syntaxhighlight>
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec <code>$word->setLanguage($language)</code>.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi un deuxième attribut <code>#[ORM\JoinColumn</code> (anciennement <code>@ORM\JoinColumn</code>) mentionnant la clé étrangère en base de données (et pas en PHP).
=== Bonnes pratiques ===
L'attribut <code>#[ORM\Table(name: 'word')]</code> était facultatif dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.
Avant PHP 8, les contraintes d'unicité (utiles entre autres pour les clés composites) étaient encapsulées dans l'annotation <code>Table</code>, mais ce n'est plus le cas avec les attributs :
<pre>
#[ORM\UniqueConstraint(name: 'spelling-pronunciation', columns: ['spelling', 'pronunciation'])]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
</pre>
{{)}}
{{attention|Dans les relations *toMany :
* il faut initialiser l'attribut dans le constructeur en <code>ArrayCollection()</code>.
* on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
* cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
}}
{{attention|Il faut ajouter le <code>#[ORM\JoinColumn(</code> dans les deux entités liées, car :
* dans aucune cela renvoie <code>Could not resolve type of column "id"</code>
* dans une seule cela provoque un problème N+1 (celle qui ne l'a pas appelle celle qui l'a pour chacun de ses enregistrements, même si celle qui l'a n'est pas utilisée ensuite).
}}
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec <code>length=0</code><ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string</ref>. Le type "text" par contre n'a pas de limite.
=== ArrayCollection ===
Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
* Dans une entité : <code>#[ORM\OrderBy(['sort_order' => 'ASC'])]</code> (anciennement <code>@ORM\OrderBy({"sort_order" = "ASC"})</code>).
* Sinon, instancier un critère :
<pre>
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
</pre>
=== GeneratedValue ===
L'annotation ''GeneratedValue'' peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
{{attention|
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID<ref>https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations</ref>. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur '' Integrity constraint violation'' puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
<syntaxhighlight lang=php>
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
</syntaxhighlight>
}}
=== Triggers ===
Les opérations en cascade sont définies sous deux formes d'attributs :
* <code>#[ORM\OneToMany(cascade: ['persist', 'remove'])]</code> : au niveau ORM.
* <code>#[ORM\JoinColumn(onDelete: 'CASCADE')]</code> : au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
=== Concepts avancés ===
Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware<ref>https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html</ref>.
{{attention|Les types des attributs peuvent être quelque peu différents du SGBD<ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix</ref>.}}
{{attention|Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").}}
L'autojointure est appelé ''self-referencing association mapping'' par Doctrine<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing</ref>).
=== Héritage ===
Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html</ref> :
<syntaxhighlight lang=php>
/** @MappedSuperclass */
class MyEntityParent
...
</syntaxhighlight>
=== Tables sans classe ===
Doctrine peut créer des tables de mapping sans entité, si on précise son nom dans les deux tables reliées :
<pre>
#[ORM\JoinTable(name: 'table_de_mapping')]
#[ORM\JoinColumn(name: 'table_source_id', referencedColumnName: 'table_source_id')]
#[ORM\InverseJoinColumn(name: 'table_cible_id', referencedColumnName: 'table_cible_id')]
#[ORM\ManyToMany(targetEntity: TableCible::class)]
</pre>
== EntityManager ==
L'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données. Ils sont définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
* <code>persist()</code> : prépare un <code>INSERT</code> SQL (rattache une entité à un entity manager).
* <code>remove()</code> : prépare un <code>DELETE</code> SQL.
* <code>flush()</code> : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
* <code>merge()</code> : fusionne une entité absente de l'em dedans.
* <code>detach()</code> : détache un objet de l'em, il n'est alors plus synchronisé automatiquement avec la base. Ce qui peut servir à diminuer la consommation mémoire si on prévoit de ne plus le faire dans l'instance PHP.
* <code>clear()</code> : détache tous les objets de l'em.
* <code>refresh()</code> : rafraichit l'entité PHP à partir de la base de données. C'est utile par exemple pour tenir compte des résultats d'un trigger ''after insert'' sur le SGBD. Exemple si le trigger ajoute une date de création après le persist, à écraser par <code>$createdDate</code> :
<syntaxhighlight lang=php>
$entity = new MyEntity();
$em->persist($entity);
$em->flush($entity);
// Trigger SGBD déclenché ici en parallèle
$em->refresh($entity);
$entity->setCreatedDate($createdDate);
$em->flush($entity);
</syntaxhighlight>
{{attention|Ne jamais utiliser <code>refresh()</code> dans un environnement avec des replicas de la base de données pour éviter les incohérences. Le plus performant est alors de reprendre l'objet de l'instance censé être à jour à la place.}}
== Repository ==
On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de <code>Doctrine\ORM\EntityRepository</code>. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés ''NomDeLEntitéRepository''.
{{remarque|D'un point de vue architectural, avant d'instancier une nouvelle entité, on utilise généralement le repository pour savoir si son enregistrement existe en base ou si on doit le créer. Dans ce deuxième cas, la bonne pratique en {{wt|DDD}} est d'utiliser une Factory pour faire le new de l'entité, mais aussi pour les new de son agrégat si elle est le nœud racine. Par exemple une <code>CarFactory</code> fera un <code>new Car()</code> mais aussi créera et lui associera ses composants : <code>new Motor()</code>...}}
{{remarque|Il est possible de préciser le nom du repository d'une entité dans cette dernière :
<pre>
#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
@ORM\Entity(repositoryClass="App\Repository\WordRepository")
</pre>
{{)}}
}}
=== SQL ===
==== Depuis Doctrine ====
Utile pour exploiter les fonctionnalités du SGBD utilisé, absentes de Doctrine.
Par exemple, pour appeler une procédure stockée :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
</pre>
Ou tronquer une table :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('TRUNCATE TABLE ma_table', $rsm)->getResult();
</pre>
{{remarque|Dans cet exemple, on peut aussi utiliser ceci :
<pre>
$connection = $this->_em->getConnection();
$databasePlatform = $connection->getDatabasePlatform();
$connection->executeStatement(
$databasePlatform->getTruncateTableSQL($this->_em->getClassMetadata(MonEntite::class)->getTableName(), true),
);
</pre>
}}
==== Sans Doctrine ====
Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :
<pre>
return $this->connection->fetchAll($sql);
</pre>
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut ''entity manager'' (''em'', ou ''_em'' pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
<syntaxhighlight lang=php>
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
</syntaxhighlight>
2. En injectant le service de connexion dans le constructeur (<code>'@database_connection'</code>) :
<pre>
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
</pre>
=== DQL ===
==== Méthodes magiques ====
Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
* <code>$repo->find($id)</code> : cherche par la clé primaire définie dans l'entité.
* <code>$repo->findAll()</code> : récupère tous les enregistrements (sans clause <code>WHERE</code>).
* <code>$repo->findById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id</code> dans la table associée au repo.
* <code>$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])</code> engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname</code>.
* <code>$repo->findOneById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id LIMIT 1</code>.
* <code>$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])</code> : engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1</code>.
{{attention|Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "<code>findById</code>" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser <code>findBy()</code>.
}}
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
<syntaxhighlight lang=php>
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
</syntaxhighlight>
==== createQuery ====
DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec <code>IDENTITY()</code> pour les jointures. Ex :
<pre>
public function findComplicatedStuff()
{
$em = $this->getEntityManager();
$query = $em->createQuery("
SELECT
u.last_name, u.first_name
FROM
App\Entity\Users u
INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users)
WHERE
i.status='waiting'
");
return $query->getResult();
}
</pre>
==== createQueryBuilder ====
L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel <code>createQueryBuilder()</code> :
<syntaxhighlight lang=php>
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
</syntaxhighlight>
Pour éviter le <code>SELECT *</code> dans cet exemple, on peut y ajouter la méthode <code>->select()</code>.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
===== Jointures =====
Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
<pre>
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
</pre>
Pour filtrer quand une jointure toMany contient des résultats, utiliser <code>EMPTY</code> :
<pre>
...
->andWhere('files IS NOT EMPTY')
</pre>
===== Résultats =====
Doctrine peut renvoyer avec :
* <code>getResult()</code> : un objet ArrayCollection (iterable, pour rechercher dedans : <code>->contains()</code>), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;
* <code>getArrayResult()</code> ou <code>getScalarResult()</code> : un tableau de tableaux (entité normalisée) ;
* <code>getSingleColumnResult()</code> : un tableau unidimensionnel.
On peut aussi utiliser <code>->indexBy('id')</code> pour définir que les clés du tableau de résultat soir les IDs de l'entité.
===== Cache =====
====== Configuration globale ======
Doctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
<syntaxhighlight lang=yaml>
framework:
cache:
default_redis_provider: '%env(REDIS_URL)%'
pools:
doctrine.metadata_cache_pool:
adapter: cache.system
doctrine.query_cache_pool:
adapter: cache.system
doctrine.result_cache_pool:
adapter: cache.app
</syntaxhighlight>
Puis dans doctrine.yaml, les utiliser :
<syntaxhighlight lang=yaml>
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.metadata_cache_pool
query_cache_driver:
type: pool
pool: doctrine.query_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
</syntaxhighlight>
À partir de là le cache des métadonnées est utilisé partout.
====== Configuration par entité ======
Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
* Dans l'entité, avec un attribut <code>#[ORM\Cache(usage: 'READ_ONLY', region: 'write_rare')]</code> (anciennement <code>@ORM\Cache(usage="READ_ONLY", region="write_rare")</code><ref>https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6</ref>), utilisant la configuration doctrine.yaml :
<pre>
doctrine:
orm:
second_level_cache:
enabled: true
regions:
write_rare:
lifetime: 864000
cache_driver: { type: service, id: cache.app }
</pre>
* Dans le repository :
<syntaxhighlight lang=php>
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
</syntaxhighlight>
Dans cet exemple, on n'utilise pas <code>cache.system</code> pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. <code>cache.app</code> pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
Pour invalider le cache d'une entité afin que les findAll() renvoient la liste à jour depuis la base de données modifiée sur Doctrine 3 :
Une seule instance de cette entité :
<pre>
$em->getCache()->evictEntity(myEntity::class, $entityId);
</pre>
Toutes les instances de l'entité :
<pre>
$em->getCache()->getRegion(myEntity::class)->clear();
</pre>
{{(|Doctrine 2}}
<pre>
$em->getCache()->evictEntityRegion(myEntity::class);
</pre>
{{)}}
==== Expressions ====
Pour ajouter une expression en DQL, utilise <code>$qb->expr()</code>. Ex<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class</ref> :
* <code>$qb->expr()->count('u.id')</code>
* <code>$qb->expr()->between('u.id', 2, 10)</code> (entre 2 et 10)
* <code>$qb->expr()->gte('u.id', 2)</code> (plus grand ou égal à 2)
* <code>$qb->expr()->like('u.name', '%son')</code>
* <code>$qb->expr()->lower('u.name')</code>
* <code>$qb->expr()->substring('u.name', 0, 1)</code>
==== Injection de dépendances ====
Les repository DQL deoivent ''ServiceEntityRepository'' :
<syntaxhighlight lang=php>
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
</syntaxhighlight>
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
* Étendre une classe qui étend ''ServiceEntityRepository''.
* Le redéfinir dans services.yaml.
* Utiliser un trait.
== Transactions ==
Pour garantir d'intégrité d'une transaction<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly</ref> :
<syntaxhighlight lang=php>
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
</syntaxhighlight>
Il existe aussi une syntaxe alternative :
<syntaxhighlight lang=php>
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
</syntaxhighlight>
== Évènements ==
Pour ajouter des triggers sur la mise à jour d'une table, il y a deux solutions :
* ajouter dans son entité l'attribut <code>#[ORM\HasLifecycleCallbacks]</code>, et à ses méthodes l'attribut de l'évènement concerné. Ex :
<pre>
#[ORM\PrePersist]
public function setCreatedAt(): self
{
$this->createdAt = new DateTime();
return $this;
}
</pre>
* ajouter des tags dans services.yaml. Ex :
<pre>
App\EventListener\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: PrePersist }
</pre>
Voici les [[Programmation PHP avec Symfony/Évènement|évènements]] utilisables ensuite (dans les listeners / subscribers) :
=== prePersist ===
Se produit avant la persistance d'une entité (paramètre : <code>PrePersistEventArgs $args</code>).
=== postPersist ===
Se produit après la persistance d'une entité (<code>PostPersistEventArgs $args</code>).
=== preUpdate ===
Se produit avant l'update d'une entité (<code>PreUpdateEventArgs $args</code>).
=== postUpdate ===
Se produit après l'update d'une entité (<code>PostUpdateEventArgs $args</code>).
=== preRemove ===
Se produit avant l'update d'une entité (<code>PreRemoveEventArgs $args</code>).
=== postRemove ===
Se produit après l'update d'une entité (<code>PostRemoveEventArgs $args</code>).
=== preFlush ===
Se produit avant la sauvegarde d'une entité (<code>PreFlushEventArgs $args</code>).
{{attention|
* Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
* Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
* Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler <code>$unitOfWork->computeChangeSet($classMetadata, $entity)</code><ref>https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener</ref>.
}}
{{remarque|On peut aussi appeler computeChangeSet() depuis ailleurs pour savoir si une entité va occasionner une requête SQL lors de son flush<ref>https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2</ref>.}} Ex :
<pre>
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
//...
}
</pre>
{{remarque|On peut aussi utiliser le paramètre <code>LifecycleEventArgs $args</code> dans ces fonctions.}}
{{attention|Parfois le <code>$object::class</code> peut renvoyer <code>Proxies\__CG__\App\Entity\MyEntity</code> au lieu de <code>App\Entity\MyEntity</code>, selon le cache utilisé.}}
=== postFlush ===
Se produit après la sauvegarde d'une entité (<code>PostFlushEventArgs $args</code>).
== Migrations ==
Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
<syntaxhighlight lang=bash>
composer require doctrine/doctrine-migrations-bundle
</syntaxhighlight>
=== Création ===
Ensuite, on peut créer un squelette de "migration" :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:generate
</syntaxhighlight>
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée <code>migration_versions</code> enregistre leur succession).
==== Exemple SQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->connection->fetchAll('SHOW DATABASES;');
$this->addSql(<<<SQL
CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
}
public function down(Schema $schema) : void
{
$this->addSql('DROP TABLE ma_table');
}
}
</syntaxhighlight>
==== Exemple DQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$table = $schema->createTable('ma_table');
$table->addColumn('ma_colonne', 'string');
}
public function down(Schema $schema) : void
{
$schema->dropTable('ma_table');
}
}
</syntaxhighlight>
==== Exemple PHP ====
Depuis Symfony 7.0, il faut implémenter <code>MigrationFactory</code> pour injecter des dépendances dans les migrations (et on ne peut plus injecter tout le conteneur)<ref>https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies</ref>.
{{Boîte déroulante début|titre=Avant Symfony 7.0, il fallait juste utiliser <code>ContainerAwareTrait</code>}}
Exemple :
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function up(Schema $schema) : void
{
$em = $this->container->get('doctrine.orm.entity_manager');
$monEntite = new MonEntite();
$em->persist($monEntite);
$em->flush();
}
}
</syntaxhighlight>
{{Boîte déroulante fin}}
{{attention|Cette technique est déconseillée car les entités peuvent évoluer indépendamment de la migration. Mais elle peut s'avérer utile pour stocker des données dépendantes de l'environnement.}}
{{attention|<code>$this->container->getParameter()</code> ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple <code>$_SERVER['SUBAPI_URI']</code> renvoie la variable d'environnement et <code>$this->containergetParameter('env(SUBAPI_URI)')</code> sa valeur par défaut (définie dans services.yaml).}}
=== Exécution ===
La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate
</syntaxhighlight>
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --up 20170321095644
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
</syntaxhighlight>
Pour le rollback :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --down 20170321095644
</syntaxhighlight>
Pour éviter que Doctrine pose des questions durant les migrations, ajouter <code>--no-interaction</code> (ou <code>-n</code>).
Pour voir le code SQL au lieu de l'exécuter : <code>--write-sql</code>.
==== Sur plusieurs bases de données ====
Pour exécuter sur plusieurs bases :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
</syntaxhighlight>
Avec des migrations.yaml de type :
<syntaxhighlight lang=bash>
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
</syntaxhighlight>
=== Synchronisation ===
==== Vers le code ====
===== Vers les entités =====
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
</pre>
{{attention|Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer [[Programmation_PHP_avec_Symfony/Migration_de_Symfony_6_à_7#Rector|Rector]] pour convertir les annotations. Ex :
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable
vendor/bin/rector process src/Entity/MyNewEntity.php
</pre>
}}
===== Vers les migrations =====
Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
==== Vers la base ====
À contrario, pour mettre à jour la BDD à partir des entités :
php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
php bin/console doctrine:schema:update --dump-sql
== Fixtures ==
Il existe plusieurs bibliothèques pour créer des {{wt|fixture}}s, dont une de Doctrine<ref>https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html</ref> :
<syntaxhighlight lang=bash>
composer require --dev orm-fixtures
</syntaxhighlight>
Pour charger les fixtures du code dans la base :
<syntaxhighlight lang=bash>
php bin/console doctrine:fixtures:load -n
</syntaxhighlight>
== Types de champ ==
La liste des types de champ Doctrine se trouve dans <code>Doctrine\DBAL\Types</code>. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre <code>JsonType</code> pour surcharger le type JSON par défaut afin de lui faire faire <code>json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)</code> automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne<ref>https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23</ref>.
== Réplication SQL ==
Anciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml.
Ex<ref>https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35</ref> :
<pre>
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
replicas:
replica1:
url: '%env(resolve:REPLICA_DATABASE_URL)%'
</pre>
== Critique ==
# Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
# Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le {{wt|lazy loading}} est contourné par au choix :
## Avant l'appel null, un <code>ObjetJoint->get()</code>.
## Dans l'entité, un <code>@ManyToOne(…, fetch="EAGER")</code>.
## Dans le repository, un <code>$this->queryBuilder->addSelect()</code>. NB : si cela ajoute un problème N+1, joindre aussi la deuxième entité qui le provoque.
# Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
# Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
# Attention aux <code>$this->queryBuilder->setMaxResults()</code> et <code>$this->queryBuilder->setFirstResult()</code> en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar du <code>LIMIT</code> SQL). La solution consiste à ajouter un paginateur<ref>https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939</ref>.
# L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
# Pas de hints.
# Bug des <code>UNION ALL</code> quand on joint deux entités non liées dans le repo.
{{todo|
* Ajouter la connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL (patrons à copier-coller ?)
}}
== Références ==
{{Références}}
4z02dilm586u90bkqje5b4ice4ixyod
Photographie/Fabricants/Photo-Hall
0
73959
772365
628678
2026-09-17T21:10:16Z
Cjp24
4598
typo
772365
wikitext
text/x-wiki
{{Ph s Fabricants}}
'''Photo-Hall''' était un revendeur de matériel photographique installé 5 rue Scribe, Paris 9<sup>e</sup>.
== Photos à classer ==
<gallery>
Réclame Photo-Hall-1921.jpg|Réclame, 1921
</gallery>
{{Ph Fabricants}}
72dfioxgwxyuthbt38yft5ag7poci9c
Discussion utilisateur:SHB
3
76692
772387
739376
2026-09-18T02:31:37Z
Max
116893
Max a déplacé la page [[Discussion utilisateur:SHB2000]] vers [[Discussion utilisateur:SHB]] : Page automatiquement déplacée lors du renommage de l’utilisateur « [[Special:CentralAuth/SHB2000|SHB2000]] » en « [[Special:CentralAuth/SHB|SHB]] »
739376
wikitext
text/x-wiki
<div style="border: 1rem #dd0000 ridge; padding: 0.5em;">Hello! Please message me on [[m:User talk:SHB2000|meta.wikimedia.org]]. I may not notice messages on this page.</div>
bqe1mfal703untg34kx1aq7ncgg0n7d
Wikilivres:GroupesVertical
4
79059
772362
685792
2026-09-17T12:17:08Z
Lionel Scheepmans
20012
Retrait de la boite déroulante pour améliorer la mise en page de l'unique utilisation du modèle sur la page https://fr.wikibooks.org/wiki/Wikilivres:Accueil_de_la_communaut%C3%A9
772362
wikitext
text/x-wiki
{| class="wikitable flexible"
! Contributeurs
| [[Wikilivres:Utilisateurs|Groupe]]
| [[Wikilivres:Requêtes aux contributeurs|Requête]]
| [[Spécial:Liste des utilisateurs|Liste des membres]]
|-
! Utilisateurs confirmés
| [[Wikilivres:Utilisateurs enregistrés|Groupe]]
| [[Wikilivres:Requêtes aux contributeurs|Requête]]
| [[Spécial:Liste des utilisateurs/confirmed|Liste des membres]]
|-
! Patrouilleurs
| [[Wikilivres:Patrouilleurs|Groupe et candidature]]
| [[Wikilivres:Requêtes aux contributeurs|Requête]]
| [[Spécial:Liste des utilisateurs/patroller|Liste des membres]]
|-
! Bots
| [[Wikilivres:Bots|Groupe et candidature]]
| [[Wikilivres:Requêtes aux bots|Requête]]
| [[Spécial:Liste des utilisateurs/bot|Liste des membres]]
|-
! Modificateurs de filtre
| [[Wikilivres:Modificateurs de filtre anti-abus|Groupe et candidature]]
| [[Discussion Wikilivres:Modificateurs de filtre anti-abus|Requête]]
| [[Spécial:Liste des utilisateurs/abusefilter|Liste des membres]]
|-
! Administrateurs d'interface
| [[Wikilivres:Administrateurs d'interface|Groupe et candidature]]
| [[Wikilivres:Requêtes aux administrateurs d'interface|Requête]]
| [[Spécial:Liste des utilisateurs/interface-admin|Liste des membres]]
|-
! Administrateurs
| [[Wikilivres:Administrateurs|Groupe et candidature]]
| [[Wikilivres:Requêtes aux administrateurs|Requête]]
| [[Spécial:Liste des utilisateurs/sysop|Liste des membres]]
|-
! Bureaucrates
| [[Wikilivres:Bureaucrates|Groupe et candidature]]
| [[Wikilivres:Requêtes aux bureaucrates|Requête]]
| [[Spécial:Liste des utilisateurs/bureaucrat|Liste des membres]]
|}
* [[Spécial:Liste des droits de groupe|Liste des droits]]
<noinclude>
Ce modèle est une variante de [[Wikilivres:Groupes]] affiché en mode vertical
[[Catégorie:Wikilivres|{{SUBPAGENAME}}]]
[[Catégorie:Utilisateurs par droits]]
</noinclude>
ko0903nvoa4na7bjadkwz085hkqrgsn
Mathc initiation/a582
0
81064
772366
772140
2026-09-17T21:26:03Z
Xhungab
23827
772366
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du signal causale discret f(n) u(n).
'''Le signal : f(n)''' '''Transformée en Z : F(z)''' Domaine de convergence
'''δ(n) = 1''' '''1''' C
'''u(n) = 1''' '''z/(z-1)''' |z| > 1
'''r(n) = n''' '''z/(z-1)^2''' |z| > 1
'''c(n) = n^2''' '''z(z+1)/(z-1)^3''' |z| > 1
'''f(n) = a^n''' '''z/(z-a)''' |z| > |a|
'''g(n) = cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' |z| > 1
'''h(n) = sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' |z| > 1
:
{{AutoCat}}
qe2b4vgegzzwnsm95tn4fh53y2harar
772367
772366
2026-09-17T21:35:31Z
Xhungab
23827
772367
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du signal causale discret f(n) u(n).
'''Le signal : f(n)''' '''Transformée en Z : F(z)''' Domaine de convergence
'''δ(n) = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u(n) = 1''' '''z/(z-1)''' |z| > 1 '''* L'échelon unitaire'''
'''r(n) = n''' '''z/(z-1)^2''' |z| > 1 '''* La rampe'''
'''c(n) = n^2''' '''z(z+1)/(z-1)^3''' |z| > 1 '''* Le carré'''
'''f(n) = a^n''' '''z/(z-a)''' |z| > |a| '''* L'exponentiel'''
'''g(n) = cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction cosinus discret'''
'''h(n) = sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction sinus discret'''
:
{{AutoCat}}
j40hwhjq51laleivirqfvzoyeyidl2f
772368
772367
2026-09-17T21:40:21Z
Xhungab
23827
772368
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal : f(n)''' '''Transformée en Z : F(z)''' Domaine de convergence
'''δ(n) = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u(n) = 1''' '''z/(z-1)''' |z| > 1 '''* L'échelon unitaire'''
'''r(n) = n''' '''z/(z-1)^2''' |z| > 1 '''* La rampe'''
'''c(n) = n^2''' '''z(z+1)/(z-1)^3''' |z| > 1 '''* Le carré'''
'''f(n) = a^n''' '''z/(z-a)''' |z| > |a| '''* L'exponentiel'''
'''g(n) = cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction cosinus discret'''
'''h(n) = sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction sinus discret'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
r0pn1iz3xpzytt04s3xwshauyffuf9o
Mathc initiation/a585
0
81067
772369
719618
2026-09-17T21:54:57Z
Xhungab
23827
772369
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal f(n)''' '''Transformée en Z F(z)''' '''Z[a^n f(n)] = F(z/a)'''
'''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal f(n) : '''cos(kn)'''
Transformée en Z F(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a)^2-(z/a)cos(k)] / [(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[z^2-az cos(k)] / [z^2-2az cos(k)+a^2]'''
Le signal f(n) : '''sin(kn)'''
Transformée en Z F(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a) sin(k)] / [(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[az sin(k)] / [z^2-2az cos(k)+a^2]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
7tdwdw5qvkufs7lgchek20edox0kbla
772370
772369
2026-09-17T21:55:13Z
Xhungab
23827
772370
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal f(n)''' '''Transformée en Z F(z)''' '''Z[a^n f(n)] = F(z/a)'''
'''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal f(n) : '''cos(kn)'''
Transformée en Z F(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a)^2-(z/a)cos(k)] / [(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[z^2-az cos(k)] / [z^2-2az cos(k)+a^2]'''
Le signal f(n) : '''sin(kn)'''
Transformée en Z F(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a) sin(k)] / [(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[az sin(k)] / [z^2-2az cos(k)+a^2]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
htetspd4xfy0vwqli4qip63b3lk2dep
772371
772370
2026-09-17T21:58:07Z
Xhungab
23827
772371
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal f(n)''' '''Transformée en Z F(z)''' '''Z[a^n f(n)] = F(z/a)'''
'''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal f(n) : '''cos(kn)'''
Transformée en Z F(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a)^2-(z/a)cos(k)] / [(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[z^2-az cos(k)] / [z^2-2az cos(k)+a^2]'''
Le signal f(n) : '''sin(kn)'''
Transformée en Z F(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a) sin(k)] / [(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[az sin(k)] / [z^2-2az cos(k)+a^2]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
3s95t5hts7nddchifstndw7y0rui0mw
772378
772371
2026-09-17T22:39:04Z
Xhungab
23827
772378
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal f(n)''' '''Transformée en Z F(z)''' '''Z[a^n f(n)] = F(z/a)'''
'''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal f(n) : '''cos(kn)'''
Transformée en Z F(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a)^2-(z/a)cos(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[z^2-az cos(k)]/[z^2-2az cos(k)+a^2]'''
Le signal f(n) : '''sin(kn)'''
Transformée en Z F(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a) sin(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[az sin(k)]/[z^2-2az cos(k)+a^2]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
5gnwbgflznao87t0opx6ilm0c6gy24d
772379
772378
2026-09-17T22:46:36Z
Xhungab
23827
772379
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z'''
'''f(n)''' '''F(z)''' '''Z[a^n f(n)] = F(z/a)'''
'''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal f(n) : '''cos(kn)'''
Transformée en Z F(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a)^2-(z/a)cos(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[z^2-az cos(k)]/[z^2-2az cos(k)+a^2]'''
Le signal f(n) : '''sin(kn)'''
Transformée en Z F(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a) sin(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[az sin(k)]/[z^2-2az cos(k)+a^2]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
llmz8d2anls0jtwjslx0hx3eiuj57kx
772385
772379
2026-09-17T23:27:01Z
Xhungab
23827
772385
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z'''
'''f(n)''' '''F(z)''' '''Z[a^n f(n)] = F(z/a)'''
'''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal f(n) : '''cos(kn)'''
Transformée en Z F(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a)^2-(z/a)cos(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[z^2-az cos(k)]/[z^2-2az cos(k)+a^2]'''
Le signal f(n) : '''sin(kn)'''
Transformée en Z F(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n f(n)] : '''[(z/a) sin(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
F(z/a) : '''[az sin(k)]/[z^2-2az cos(k)+a^2]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
dww3860l5xk9l98i1cw1p87nevvqp5d
Mathc initiation/a588
0
81071
772374
719617
2026-09-17T22:29:37Z
Xhungab
23827
772374
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z''' '''Z[n f(n)] = -z F'(z)'''
'''f(n)''' '''F(z)''' '''Z[n f(n)]'''
'''u(n)''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [ [z^2-z cos(k)]/[z^2-2z cos(k)+1] ]' '''
'''-z F'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [ [z sin(k)]/[z^2-2z cos(k)+1] ]' '''
'''-z F'(z)''' : '''-z [ -[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2 ]'''
{{AutoCat}}
7ol7fopb5wlt2icl89gc6kd2pn8pcfv
772375
772374
2026-09-17T22:32:40Z
Xhungab
23827
772375
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z''' '''Z[n f(n)] = -z F'(z)'''
'''f(n)''' '''F(z)''' '''Z[n f(n)]'''
'''u(n)''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [ [z^2-z cos(k)]/[z^2-2z cos(k)+1] ]' '''
'''-z F'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
irsz53eo6rr69t15fvecr8j1xh0clkd
772376
772375
2026-09-17T22:35:46Z
Xhungab
23827
772376
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z''' '''Z[n f(n)] = -z F'(z)'''
'''f(n)''' '''F(z)''' '''Z[n f(n)]'''
'''u(n)''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
dywm80yqvaxxq5g5ft30rlzjw93sz30
772377
772376
2026-09-17T22:36:56Z
Xhungab
23827
772377
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z''' '''Z[n f(n)] = -z F'(z)'''
'''f(n)''' '''F(z)''' '''Z[n f(n)]'''
'''u(n)''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
1et6neerxfka99zg1fw7we5f4obpqfs
772380
772377
2026-09-17T22:48:29Z
Xhungab
23827
772380
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z'''
'''f(n)''' '''F(z)''' '''Z[n f(n)] = -z F'(z)'''
'''u(n)''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
m95oyezu2j0821m1focb70r82qcavri
772386
772380
2026-09-17T23:28:09Z
Xhungab
23827
772386
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z'''
'''f(n)''' '''F(z)''' '''Z[n f(n)] = -z F'(z)'''
'''u(n)''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
771uedof0gcrxmm7m6enwp6r8e2vu8z
Mathc initiation/a589
0
81072
772372
719621
2026-09-17T22:04:20Z
Xhungab
23827
772372
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Le retard de no unités : f(n-no) u(n-no)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal f(n)''' '''Transformée en Z F(z)''' '''Z[f(n-no)] = z^(-no) F(z)'''
'''u(n)''' '''z/(z-1)''' '''z^(-no) z/(z-1)'''
'''n''' '''z/(z-1)^2''' '''z^(-no) z/(z-1)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(-no) z(z+1)/(z-1)^3'''
'''a^n''' '''z/(z-a)''' '''z^(-no) z/(z-a)'''
'''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
thupt1ysj9z2r6vh37cj1x8vw3f3gx4
772373
772372
2026-09-17T22:07:47Z
Xhungab
23827
772373
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Le retard de no unités : f(n-no) u(n-no)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z ''' '''Z[f(n-no)] = z^(-no) F(z)'''
'''f(n)''' '''F(z)''' '''z^(-no) F(z)'''
'''u(n)''' '''z/(z-1)''' '''z^(-no) z/(z-1)'''
'''n''' '''z/(z-1)^2''' '''z^(-no) z/(z-1)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(-no) z(z+1)/(z-1)^3'''
'''a^n''' '''z/(z-a)''' '''z^(-no) z/(z-a)'''
'''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
d5auec6emocxytknqnddcz1ifh8c9me
772381
772373
2026-09-17T22:57:16Z
Xhungab
23827
772381
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Le retard de no unités : f(n-no) u(n-no)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z '''
'''f(n)''' '''F(z)''' '''Z[f(n-no)] = z^(-no) F(z)'''
'''u(n)''' '''z/(z-1)''' '''z^(-no) z/(z-1)'''
'''n''' '''z/(z-1)^2''' '''z^(-no) z/(z-1)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(-no) z(z+1)/(z-1)^3'''
'''a^n''' '''z/(z-a)''' '''z^(-no) z/(z-a)'''
'''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
aorbjjnqx1xc68fsb7pvlbdg6gy89s9
Mathc initiation/a590
0
81073
772382
719624
2026-09-17T23:11:23Z
Xhungab
23827
772382
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|L'avance d'une unité : f(n+1) u(n+1)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal ''' '''Transformée en Z''' '''Z[f(n+1)]'''
'''f(n)''' '''F(z)''' '''z^(1) [ F(z) -f(0)]'''
'''u(n)''' '''z/(z-1)''' '''z^(1) [ [z/(z-1)] - u(0)]'''
'''n''' '''z/(z-1)^2''' '''z^(1) [ [z/(z-1)^2] - 0]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(1) [ [z(z+1)/(z-1)^3] - 0^2]'''
'''a^n''' '''z/(z-a)''' '''z^(1) [ [z/(z-a)] - a^0]'''
'''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [[z^2-z cos(k)]/[z^2-2z cos(k)+1] - cos(k0)]'''
'''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [ [z sin(k)]/[z^2-2z cos(k)+1] - sin(k0)]'''
:
----
{{AutoCat}}
7bh7mojkint72e7k4rg626vlslzgp50
772383
772382
2026-09-17T23:13:02Z
Xhungab
23827
772383
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|L'avance d'une unité : f(n+1) u(n+1)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal ''' '''Transformée en Z''' '''Z[f(n+1)]'''
'''f(n)''' '''F(z)''' '''z^(1) [ F(z) - f(0)]'''
'''u(n)''' '''z/(z-1)''' '''z^(1) [ [z/(z-1)] - u(0)]'''
'''n''' '''z/(z-1)^2''' '''z^(1) [ [z/(z-1)^2] - 0]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(1) [ [z(z+1)/(z-1)^3] - 0^2]'''
'''a^n''' '''z/(z-a)''' '''z^(1) [ [z/(z-a)] - a^0]'''
'''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [[z^2-z cos(k)]/[z^2-2z cos(k)+1] - cos(k0)]'''
'''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [ [z sin(k)]/[z^2-2z cos(k)+1] - sin(k0)]'''
:
----
{{AutoCat}}
50sgvhv28uw9h6fcb038al88z29dmda
Mathc initiation/a591
0
81074
772384
719627
2026-09-17T23:24:24Z
Xhungab
23827
772384
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|L'avance de deux unités : f(n+2) u(n+2)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z ''' '''Z[f(n+2)]'''
'''f(n)''' '''F(z)''' '''z^2 [ F(z) -f(0) -f(1) z^(-1)]'''
'''u(n)''' '''z/(z-1)''' '''z^2 [ z/(z-1) -u(0) -u(1) z^(-1)]'''
'''n''' '''z/(z-1)^2''' '''z^2 [ z/(z-1)^2 -0 -1 z^(-1)]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^2 [z(z+1)/(z-1)^3 -0^2 -1^2 z^(-1)]'''
'''a^n''' '''z/(z-a)''' '''z^2 [ z/(z-a) -a^0 -a^1 z^(-1)]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z ''' : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[f(n+2)]''' : '''z^2 [[z^2-z cos(k)]/[z^2-2z cos(k)+1] -cos(k0) -cos(k1) z^(-1)]'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z ''' : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[f(n+2)]''' : '''z^2 [[z sin(k)]/[z^2-2z cos(k)+1] -sin(k0) -sin(k1) z^(-1)]'''
{{AutoCat}}
9tx3er56wuoex73coocggs0kd8bzmhf
Wikilivres:Le Bistro/2026
4
83439
772363
772354
2026-09-17T12:28:38Z
Xhungab
23827
/* == Je ne suis donc pas content tous seul. (2) == */ nouvelle section
772363
wikitext
text/x-wiki
<noinclude>{{Wikilivres:Le Bistro/En-tête}}</noinclude>
== Le meilleur à Wikilivres pour 2026 ! ==
Je crée la page du bistrot 2026 en transmettant mes vœux. Bonne année éditoriale à tous ! [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 janvier 2026 à 06:05 (CET)
:Merci, meilleurs vœux ! [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 16 janvier 2026 à 09:53 (CET)
::Meilleurs vœux pour 2026 !
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 janvier 2026 à 20:13 (CET)
:::Meilleurs vœux pour 2026
:::[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 janvier 2026 à 11:56 (CET)
::::Bonne année et bonnes lectures et écritures !
::::[[Utilisateur:Matthius|Matthius]]
== Page orpheline du livre : États généraux du multilinguisme dans les outre-mer ==
Bonjour,
Le livre "États généraux du multilinguisme dans les outre-mer" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons.
Je montre dans ce livre [[Wikilivres:Feuilles dupliquées orphelines]] cette suite de pages orphelines accompagnée par la page où elles ont été recopiées.
Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 10 février 2026 à 19:57 (CET)
:@[[Utilisateur:Xhungab|Xhungab]], @[[Utilisateur:JackPotte|JackPotte]], @[[Utilisateur:DavidL|DavidL]], bonjour. Je vois que l'on est en campagne de gestion de pages orphelines et je me pose la question de savoir si on doit les supprimer une fois réintégrées ailleurs ou les supprimer ? Je vois que les deux cas de figure ont été mis en œuvre précédemment. L'avantage de la fusion est de conserver l'historique des modifications, mais je ne suis pas habitué à le faire. Si quelqu'un d'entre vous pouvait m'indiquer une page d'explication, cela m'aiderait à m'y mettre. Ensuite, je me demande si la fusion a vraiment un sens lorsque c'est la même personne qui a créé la page orpheline et la nouvelle au contenu copié collé pour insertion ? Qu'en pensez-vous ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:02 (CET)
:::Ça fait plaisir de voir une personne s'investir dans la maintenance de Wikilivres =). Soit le ou la bienvenu.e. J'attends les avis des autres administrateurs pour te donner mon soutien @[[Utilisateur:Xhungab|Xhungab]]. @ bientôt. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:27 (CET)
:::: @[[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]] bonjour, je n'ai pas compris en quoi "deux cas de figure ont été mis en œuvre précédemment" : si la page est vide on la supprime et si elle a au moins une phrase valable on la fusionne. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 février 2026 à 16:26 (CET)
::::Concernant l'auteur pour moi cela importe peu, car on souhaite conserver chaque élément permettant de prouver une primauté du droit d'auteur (par exemple pour ne pas accuser un contributeur d'avoir copié un site miroir de sa page supprimée). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 février 2026 à 16:30 (CET)
:::::Désolé [[Utilisateur:JackPotte|JackPotte]], je n'avais pas vu que les pages que tu as supprimées étaient vides. La règle est donc supprimer si vide et fusionner si non. Tu peux me conseiller une page d'aide pour oppérer les fusions ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:34 (CET)
::::::Salut,
::::::Pour la fusion d'historique :
::::::* soit [[Wikilivres:Le guide de l'administrateur#Fusionner deux pages|Utiliser la page spéciale pour cela]] (cependant ne fonctionne pas toujours),
::::::* soit [https://fr.wikibooks.org/w/index.php?title=Wikilivres:Le_guide_de_l%27administrateur&oldid=588957#Fusionner_deux_pages utiliser l'ancienne méthode] qui reste toujours valable quand la première ne fonctionne pas.
::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 13 février 2026 à 19:34 (CET)
:::::::Salut @[[Utilisateur:DavidL|DavidL]]. Je viens de tester l'outil fusionner avec les deux pages suivantes
:::::::[[États généraux du multilinguisme dans les outre-mer/Annexes/Contexte]]
:::::::vers
:::::::[[États généraux du multilinguisme dans les outre-mer/Annexes]]
:::::::Et j'ai le message suivant :
:::::::« La période spécifiée chevauche les versions préexistantes de la page de destination. »
:::::::Tu peux m'expliquer si tu as le temps ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 février 2026 à 00:19 (CET)
::::::::Salut @[[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]]
::::::::Dans ce cas, j'utilise [https://fr.wikibooks.org/w/index.php?title=Wikilivres:Le_guide_de_l%27administrateur&oldid=588957#Fusionner_deux_pages l'ancienne méthode].
::::::::# Renommer A (page qui n'existera plus) vers B, sans laisser de redirection, et en cochant la case de suppression de B (case qui apparait après 1er clic sur le bouton Renommer) (→ l'historique de A est sur B, celui de B est supprimé)
::::::::# Supprimer B (→ l'historique de A et B sont supprimés)
::::::::# Restaurer B et cocher toutes les cases des versions (bouton "Inverser la sélection") (→ restaurer l'historique de A et B)
::::::::# Effectuer une modification de la version finale de la page à afficher, trouvée dans l'historique.
::::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 février 2026 à 08:29 (CET)
:::::::::Merci @[[Utilisateur:DavidL|DavidL]]. Ça m'a l'air bien compliqué tout ça. Et avec toute les pages qu'il faut faire, ça risque de prendre un certain temps. Je manque de courage en pensant que c'est juste pour ajouter une ligne dans l'historique des versions. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 février 2026 à 14:51 (CET)
::::::::::Finalement, dans l'exemple que j'ai pris, la fusion n'avait aucun sens puisque le contenu du doublon que j'ai finalement supprimé était intégralement repris dans l'autre doublon par le même auteur. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 22 août 2026 à 23:55 (CEST)
:::::::::::Désolé @[[Utilisateur:Xhungab|Xhungab]]. Je ne comprends rien à l'outil de fusion et je ne trouve rien qui me permet de comprendre sur le net. Une vidéo avec exemple des différents cas de figure serait bien, mais je ne trouve pas. Je vais donc laisser ce travail à ceux qui savent utiliser l'outil.
:::::::::::En revanche, j'ai remarqué que certains doublons, comme dit précédemment, ne nécessitent pas de fusion puisqu'il s'agit de pages qui ont été créées sans aucune autre modification, puis, copiées par le même auteur dans une autre page créée avec un autre nom, au lieu de faire un renommage. Il faudrait donc que je prenne le temps de parcourir la liste pour repérer ces cas de figure avant de supprimer le premier doublon. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 01:27 (CEST)
::::::::::::Merci pour ton aide. Actuellement ces pages ont été en quelque sorte neutralisées. Donc fais de ton mieux, mais ne te tracasse pas elles ne posent plus de problème. Encore merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 09:13 (CEST)
:::::::::::::OK @[[Utilisateur:Xhungab|Xhungab]], merci pour l'info. Au fait comment as-tu fais pour les détecter ? En parcourant les pages de la catégorie orphelines et en vérifiant celles qui sont doublons ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 12:36 (CEST)
::::::::::::::Dans la catégorie pages orphelines, il y avait un lien sur une page.
::::::::::::::Je choisissais une phrase et je recherchais avec la fonction recherche de wikibook où se trouvait cette phrase. Si c'était un doublon, j'avais un lien vers la nouvelle page. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 12:48 (CEST)
:::::::::::::::Intelligent. Tu poserais pas ta candidature d'admin !? Ici, avec @[[Utilisateur:Fourmidable|Fourmidable]] et puis sur wikiversité aussi. Ça pourrait aider pour le projet des 20 ans., si tu comote toujours t'investir. On est en sous effectif dans les deux projects et les communautés me semblent plus ouverte que sur Wikipédia. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 13:48 (CEST)
::::::::::::::::Je ne souhaite pas devenir un administrateur. Pourquoi ? Si on me donne du pouvoir, je l'utiliserais. Par la suite, ceux qui m'ont accordé le pouvoir auront le choix entre pleurer, nous quitter ou me mettre en arrêt de travail. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 14:59 (CEST)
:::::::::::::::::Ah ah ah. T'es si foireux que ça !? Ou peut-être as-tu eu des expériences antérieures ? Dans tous les cas, c'est super de ta part de pouvoir t'auto évaluer.
:::::::::::::::::Ceci dit, le système de demander au administrateurs de faire des tâches qu'il faut expliquer est une perte de temps et d'efficacité. On pourrait d'ailleurs penser à attribuer les droits temporairement pour effectuer certaines tâches. Je me demande si ça se fait déja quelque part ? Si oui, on pourrait s'en inspirer pour mettre le système en application sur Wikilibres et Wikobersité. T'en penses quoi @[[Utilisateur:Xhungab|Xhungab]] ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 15:40 (CEST)
::::::::::::::::::Oui, je pourrais obtenir des droits temporaires par rapport à un projet. Par exemple 2 projets
::::::::::::::::::Premier projet : Supprimer toutes les feuilles volantes. Ce sont des feuilles tests très bien faites mais qui n'appartiennent à aucun livre. Je crois qu'il y a actuellement, Feuilles volantes – 54 P.
::::::::::::::::::
:::::::::::::::::: Deuxième projet : [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Pages_anciennes Les pages les plus anciennement modifiées]
::::::::::::::::::Supprimer tous les livres entre 2006 et 2010 qui sont à l'abandon. Parfois il sera possible de sauvegarder un livre en supprimant les chapitres vides, ce qui nous permettra de créer un livre complet. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 16:19 (CEST)
Donc, tu as du temps à consacrer à ce genre de maintenance ? Ce serait bête de ne pas en profiter. Reste à voir à présent si les autres administrateurs et bureaucrates seraient d'accord de t'octroyer les droits, tout en approuvant les deux projets que tu proposes. Cependant, je ne suis pas un grand fan des suppressions, je préfère de loin renommer les pages pour les placer en dehors de l'espace principal de sorte à ce que les auteurs puissent toujours les retrouver. Pour ça, un bon espace, c'est les sous-pages des principaux auteurs. Cela pourrait ainsi s'appliquer aux feuilles volantes et aux projets d'écriture abandonnés qui sont suffisamment développés pour justifier une conservation. L'avantage de cette méthode est qu'elle ne demande pas d'avoir les outils d'administrateurs. Faudrait voir maintenant dans quel cas on supprime et dans quel cas on renomme. On pourrait par exemple définir un nombre d'octets ou de caractères, question d'avoir un critère standard et accepté par tous. Qu'en penses-tu [[Utilisateur:Xhungab|Xhungab]] ?
:En fait je préfère continuer comme actuellement. Proposer mes services pour améliorer wikibook sans avoir aucun droit. De cette manière, si je propose quelque chose d'incongru, comme cela est déjà arrivé, il suffit de neutraliser ma demande. Merci à ceux qui me suivent sur ce travail.[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 24 août 2026 à 01:52 (CEST)
:Je comprends @[[Utilisateur:Xhungab|Xhungab]]. Mais précisément, déplacer une page à l'abandon dans une sous-page l'espace utilisateur de l'auteur principal, cela ne demande justement aucun droit et tout le monde peut le faire, toi en premier puisque tu sembles actif à ce niveau. C'est pour ça que je te demande ton avis : que penses-tu de l'idée de déplacer les pages comme je viens de le décrire, plutôt que de les supprimer ? Je viens de soumettre l'idée dans une nouvelle discussion ci-dessous. Tu peux donner ton avis là-bas si tu veux. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 02:27 (CEST)
== archive.today ==
''Voir [[:w:Wikipédia:Le Bistro/21 février 2026#archive.today]].''
Ce site pose apparemment des problèmes de sécurité, il faudrait le remplacer au plus vite si possible, et sinon désactiver les liens.
[[Utilisateur:SyntaxTerror|SyntaxTerror]] ([[Discussion utilisateur:SyntaxTerror|discussion]]) 21 février 2026 à 13:07 (CET)
:Salut SyntaxTerror,
:* 0 liens vers archive.today : [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.today]
:* <s>18</s> 0 liens vers archive.is [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.is]
:* 0 liens vers archive.li [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.li]
:* 0 liens vers archive.ec [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.ec]
:* 0 liens vers archive.ph [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.ph]
:* 0 liens vers archive.fo [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.fo]
:* 0 liens vers archive.md [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.md]
:* <s>1</s> 0 liens vers archive.vn [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.vn]
:* 0 liens vers archive.closed.social [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.closed.social]
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 21 février 2026 à 16:14 (CET)
::Merci pour vos efforts ! [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 12 mai 2026 à 16:22 (CEST)
== demande aide pour sortir du mode "ébauche" ==
je viens de terminer mon livre ... ci dessous. J'ai compris qu'il fallait le signaler comme n'étant plus au stade "ébauche" commente faire Merci
https://fr.wikibooks.org/w/index.php?title=Essai_pour_un_mod%C3%A8le_de_psychisme_objectif&action=info#mw-pageinfo-header-basic [[Utilisateur:Clopeau|Clopeau]] ([[Discussion utilisateur:Clopeau|discussion]]) 17 mars 2026 à 08:20 (CET)
:Bonjour, quand je regarde ''[[Essai pour un modèle de psychisme objectif]]'', sa complétude permettrait en effet de le sortir des ébauches, mais il semble plutôt d'agir d'un travail de recherche que d'un livre pédagogique sur un sujet sourcé comme reconnu.
:Donc en vertu de [[Wikilivres:Principes fondateurs|notre charte]], je propose de le déplacer sur {{WV|Recherche:Accueil}}. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 18 mars 2026 à 09:06 (CET)
== Don à wikipédia depuis wikilivres ==
{{BlocCitation|La Wikimedia Foundation est l’organisation à but non lucratif qui soutient Wikipédia, '''les autres sites de connaissance libre de Wikimédia''', et sa mission de connaissance libre pour tous.|auteur=[https://wikimediafoundation.org/fr/give/donor-frequently-asked-questions/ FAQ], {{g|Qu'est-ce-que la Wikimedia Foundation ?}}}}
Le lien de donation https://donate.wikimedia.org/w/index.php?title=Special:LandingPage&country=FR&uselang=fr&wmf_medium=sidebar&wmf_source=donate&wmf_campaign=fr.wikibooks.org '''ne parle que de wikipédia'''. '''Où sont passés les autres projets ?''' Vont-ils être fermés sauvagement par la fondation comme Wikinews récemment ? La suite au prochain épisode d{{'}}''Highlander-Wikimedia'' : {{g|À la fin, un seul ''projet'' survivra.}} ... ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 12 mai 2026 à 11:26 (CEST)
:Bonjour,
:Je pense personnellement que les wikis ont des problèmes.
:Il y a une vingtaine d'années, il représentait l'innovation la plus ambitieuse.
:Aujourd'hui, les wikis me semblent dépassés de tous les côtés.
: wiki = Erreurs 404 ("Page introuvable")
:Problèmes:
:* 568 livres déclarés. 50 livres terminés (combien sont obsolètes)
:* 21 527 pages déclarées. 20 000 pages orphelines. (Papa t'est où?)
:Solution:
:* Où sont les cours sur YouTube qui utilisent un wikibook comme référence?
:Voilà mon idée sur les wiki.
:* https://fr.wikiversity.org/wiki/Facult%C3%A9:Droit
:* https://fr.wikiversity.org/wiki/D%C3%A9partement:Introduction_au_droit
:<nowiki>~~~~</nowiki> [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 12 mai 2026 à 13:50 (CEST)
::Ce serait intéressant de mentionner tous les projets actifs oui. [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 12 mai 2026 à 16:23 (CEST)
::: En même temps, le message est plutôt franc étant donné que l'argent des dons n'est pas investi dans le développement de Wikilivres (cf [[:meta:Community Wishlist Survey]] et les réponses à nos tickets Phabricator).
::: Par ailleurs, concernant la qualité des contenus, cela a toujours était un sujet depuis le début, et nous disposons tout de même de plusieurs pages spéciales dans [[Wikilivres:Maintenance]] pour y remédier afin que le lecteur n'ait pas la sensation de se faire empapaouter en se retrouvant sur des ébauches bien placées dans Google. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 mai 2026 à 08:48 (CEST)
== La catégorie SI est pleine ! ==
Bonjour tous,
Pour info, la [[:Catégorie:Suppressions immédiates demandées]] contient 9 pages.
Wikilivresquement, [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 14 juin 2026 à 15:36 (CEST)
:Elle est maintenant vide, merci à ceux qui s'y sont collés ! {{Sourire}} [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 24 juin 2026 à 17:17 (CEST)
== Un RAW bien frais pour juillet ==
{| style="width:100%;"
| valign="top" align="center" style="border:1px gray solid; padding:1em;" |
{| align="center"
|-
| style="text-align: center;" | <div style="background-color:#177860; border-radius:.2em; color:#FFF;padding: 1em;">
<div style="font-family: Century Gothic; font-size:4.5em; line-height:120%;text-align:center;color: #fff;">RAW</div>
<div style="margin-bottom:1.5em;text-align:center; color: #fff; font-style:italic;">Regards sur l’actualité du mouvement Wikimédia.</div>
<div style="text-align: center;">{{#ifeq:{{FULLPAGENAME}}|Wikipédia:RAW/Rédaction}}</div>
</div><br />
<hr />
<div style="font-size:12pt; font-family:Times New Roman; text-align:center;">[[w:fr:Wikipédia:RAW/2026-07-05|<span style="color:darkslategray;">Le numéro de juillet 2026 est sorti.</span>]]</div>
<hr /><br />
|- style="text-align: center;"
| <span style="font-size:12pt; font-family:Times New Roman;"> '''❖ Au menu de ce numéro ❖'''</span>
|- style="font-size:10pt; font-family:Times New Roman; text-align:center;"
| <div style="text-align:center; column-count:1; column-width:28em; vertical-align:top;">
'''L'Édito'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'Édito|Fêtons notre mouvement !]]
'''Échos francophones'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Résidence|Un nouveau vigneron dans la résidence]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Logo_WP25|Wikipédia en français aux couleurs des 25 ans]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#DAF|Wikipédia dans le ''Dictionnaire de l'Académie française'']]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Millésime_Wiktionnaire|Quelles sont les nouvelles entrées en français sur le Wiktionnaire ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#ADS|Relance de l'Article de la semaine]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#sans_pagEs|Le projet des sans pagEs fête ses dix ans]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#contributions_rémunérées|Faut-il faire évoluer les règles sur les contributions rémunérées ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#wikification|Le mois de la wikification fait son retour pour une septième édition]]
'''Ailleurs dans le mouvement'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Txikipedia|Txikipedia franchit le cap des 10 000 articles]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Concours_santé_2026|Un concours sur la santé]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Commons_et_IA|Commons se penche sur l'IA]]
'''Nouveautés techniques et outils'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#VIZWP|VIZWP : un outil pour visualiser les lacunes dans les sujets climatiques sur Wikipédia, mais pas que…]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#AI_Source_Verification|Examiner la vérifiabilité grâce à l'IA ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Icônes|Les icônes de l'interface ont changé]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Sous-références|Ça y est, les sous-références sont là]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#ConvoCompass|ConvoCompass, pour moins d'attaques personnelles]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#LSV-Tools|LSV-Tools : faciliter la gestion des anecdotes « Le saviez-vous ? »]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#LegendAndDot|Semi-automatiser l'ajout de ponctuation dans les légendes d'images]]
'''Du côté de la recherche'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#COMSAV|COMSAV : la contribution à Wikipédia fait-elle d'un ensemble d'individus un ensemble de pairs ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#JourneyMap|Où commence la contribution et où s'arrête-t-elle ?]]
'''Le POV'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Le_POV|Guide survie pour la Wikimania 2026]] par Trizek
'''L'Atelier'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'Atelier|Le point sur la diversité de genre dans les articles]] par PAC2
'''L'Analyse'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'Analyse|Larry Sanger is back... and fired!]] par Madelgarius, Trizek et Jules*
'''L'interview'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'interview|Pronoia : de Byzance aux réseaux sociaux, un œil sur Wikipédia et le monde]] par Antimuonium
'''Le wik’hit-parade'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Wikit|Articles les plus vus en juin 2026]] par Ælfgar
</div>
|-
| style="font-family:Times New Roman; text-align:center; font-size:90%;" | [[w:fr:Wikipédia:RAW/2026-07-05|Lire tout le numéro]]
|-
| valign="top" colspan="2" style="padding:0.5em; font-family:Times New Roman;text-align:center; font-size:100%;" |
Venez rédiger le prochain numéro, y parler de ce qui se fait dans votre communauté : [[w:fr:Discussion_Wikipédia:RAW/Rédaction|Salle de rédaction]].</br>Les anciens numéros sont retrouvables [[w:fr:Modèle:Palette RAW|ici]].
|-
|}
|}
<div style="margin-top:10px; font-size:90%; padding-left:5px; font-family:Georgia, Palatino, Palatino Linotype, Times, Times New Roman, serif;">[[w:fr:Wikipédia:RAW/Inscription|Abonnez-vous !]] [[Utilisateur:L'embellie|L'embellie]] ([[Discussion utilisateur:L'embellie|discussion]]) 5 juillet 2026 à 01:53 (CEST)</div>
== Les RAW en mode fiesta sur la Wikimania ==
<div style="background-color:#177860; border-radius:.2em; color:#FFF;padding: 1em;">
<div style="font-family: Century Gothic; font-size:3em; line-height:120%;text-align:center;color: #fff;">RAW</div>
<div style="margin-bottom:1.5em; margin-top:1em; text-align:center; color: #fff;">Le numéro d'août 2026 (n°295) est sorti.</div>
<div class="center">[[:w:Wikipédia:RAW/2026-08-05|<span class="cdx-button cdx-button--fake-button cdx-button--fake-button--enabled" style="white-space:break-spaces; text-align:center; gap:0;"role="button" aria-disabled="false">Lire l'infolettre</span>]]</div></div>
'''Un numéro exceptionnel'''
La conférence Wikimania s'est achevée le 25 juillet. Que vous ressentiez de la frustration (si vous n’avez pas pu y assister) ou du [[:w:Wikipédia:Wikispleen|wikispleen]] (si vous y étiez et que vous avez du mal à digérer que ce soit déjà fini), l’équipe des RAW n’a pas ménagé ses efforts pour vous proposer un numéro XXL qui revient en long, en large et en travers sur cet événement qui le mérite bien. Comptes-rendus, témoignages, entretiens, il y en aura pour tous les goûts ! Le numéro est aussi exceptionnel par le nombre de rédactrices et de rédacteurs qui y ont participé {{incise|merci !|stop}}
Au-delà de Wikimania, l’actualité wikimédienne est loin d’être paisible cet été. Nous vous proposons donc aussi de revenir en détail sur la crise qui secoue la fondation Wikimédia autour de la reconnaissance du syndicat Wiki Workers United (WWU). Nous aurons probablement l’occasion de couvrir la suite de cette histoire dans le prochain numéro. D’ici là, bonne lecture !
----
Au menu de ce numéro :
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">En bref</span>''' — Des actualités francophones et du mouvement Wikimédia et des nouveaux outils
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">Dossier spécial Wikimania</span>'''
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">Les POV</span>'''<br>— Ma Wikimania : la joie, l'inquiétude et la raison d'être<br>— Témoignages : regards sur la Wikimania
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">L'Analyse</span>''' — La crise s'intensifie après le refus de la WMF de reconnaître le syndicat WWU
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">Les Comptes-rendus</span>'''<br>— Une Wikimania sous le signe de la jeunesse<br>— Sur les conséquences de la définition de « wikimédien·ne »<br>— Petit tour d’horizon de la créativité wikimédienne<br>— CultureStrong : respecter les savoirs des Premières Nations sur les plateformes Wikimédia<br>— Des événements techniques à Wikimania
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">Les Interviews</span>'''<br>— Ses goûts, sa Wikimania, sa prochaine tournée... : Baby Globe nous dévoile tout !<br>— Jules*, notre Wikimédien de l'année : comment rendre compte du changement climatique tout en protégeant l'encyclopédie
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">Le wik'hit parade</span>''' — Une analyse des articles les plus vus le mois dernier
[[Fichier:Toicon-icon-sharp-corners-draw.svg|20px|classe=skin-invert-image]] Les RAW, c'est aussi vous : si un sujet vous tient à cœur, n'hésitez pas à proposer une idée d'article ou à participer à la rédaction du [[:w:Wikipédia:RAW/Rédaction|prochain numéro]]. Toutes les contributions sont les bienvenues, quel que soit votre wiki d'origine ! <span class="smiley">[[Image:Face-smile.svg|20px|Sourire|alt=Émoticône sourire]]</span>
[[Utilisateur:Antimuonium|Antimuonium]] ([[Discussion utilisateur:Antimuonium|discussion]]) 5 août 2026 à 00:28 (CEST)
== Bientôt les 20 ans de Wikiversité ==
Bonjour.
Un petit message de « débauchage » dans le cadre d'un projet d'amélioration de Wikiversité. Comme dit la chanson : « On n'a pas tous les jours vingt ans ! » Plus d'infos [[v:Wikiversité:La_salle_café/août_2026#Améliorer_Wikiversité_pour_ses_vingt_ans_d'existence|dans la salle café du projet]].
Belle fin de journée à tous ! [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 août 2026 à 00:26 (CEST)
== [[:Catégorie:Suppressions immédiates demandées|Série de demandes de suppression immédiate]] ==
Bonjour,
La [[:Catégorie:Suppressions immédiates demandées]] est de nouveau pleine, mais cette fois, il semble qu'elle ait été remplie par le seul {{U|Xhungab}}.
Cordialement, [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 16 août 2026 à 14:56 (CEST)
:{{fait}} Bonjour, oui c'était effectivement un ensemble d'ébauches abandonnées. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 17 août 2026 à 10:38 (CEST)
::Merci pour votre aide [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 août 2026 à 14:26 (CEST)
== Deux idées à débattre ==
Bonjour.
Vu le travail de traitement des pages et livres abandonnés et suite à une discussion avec @[[Utilisateur:Xhungab|Xhungab]] reprise ci-dessous, j'aimerais soumettre deux idées à la communauté Wikilivres.
'''La première idée''' est de déplacer toutes les pages et livres dont le développement est abandonné depuis longtemps en sous-page de l'auteur principal tout en laissant un message sur sa page de discussion avec un lien pointant vers la sous-page en question. Ce message pouvant faire l'objet d'un modèle.
Avantage : 1. Pas besoin d'être admin pour faire ce genre de maintenance. 2. Pas de risque de frustrer un auteur ni de perdre de vue des informations intéressantes. 3. Ceci dans le but d'améliorer l'apparence de Wikilivres au niveau du contenu visible sur l'espace de nom principal.
Nécessité : 1. Créer un modèle à placer sur la page de discussion de l'auteur principal après déplacement. 2. Définir la durée d'abandon des pages à déplacer.
'''La deuxième idée''' est de donner les outils d'administrateurs à des personnes qui ne veulent pas endosser cette responsabilité à long terme et de manière continue, mais qui sont d'accord d'en faire usage dans le cadre d'un projet de maintenance temporaire. Cela pourrait être utile pour @[[Utilisateur:Fourmidable|Fourmidable]] s'il le souhaite, à moins qu'il se décide à déposer sa candidature pour une nomination définitive. Ce qui serait une bonne idée, je trouve.
Qu'en pensez-vous @[[Utilisateur:JackPotte|JackPotte]], @[[Utilisateur:DavidL|DavidL]], @[[Utilisateur:Grondin|Grondin]] ?
Une prise de décision est-elle nécessaire si on décidait une mise en application ?
Bien à vous tous, [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 01:50 (CEST)
:Bonjour,
:J'ai proposé une liste de livres à supprimer
:https://fr.wikibooks.org/wiki/Wikilivres:Demande_de_suppression_imm%C3%A9diate
:Doit-on réellement les garder ? ;) [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 24 août 2026 à 10:21 (CEST)
::Quel est l'avantage de la suppression par rapport au déplacement @[[Utilisateur:Xhungab|Xhungab]]? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 10:56 (CEST)
:::En déterminant un contenu minimum, on peut faire le tri de ce que l'on supprime et ce que l'on déplace. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 10:58 (CEST)
::::Bonjour,
::::Les droits administrateurs temporaires sont difficiles à gérer : un bureaucrate peut attribuer le status administrateur mais pas le retirer.
::::Concernant les critères suppression ou renommage, je propose :
::::* (cas 1) Une page qui ne contient vraiment aucun texte (ex : seulement des liens rouges) --> suppression
::::* (cas 2) Une page avec quelques phrases significatives récupérables --> soit (a) tenter de fusionner dans un livre qui aborde le même sujet ou sujet connexe, soit (b) renommer en sous-page de l'auteur si aucun livre pour la fusion ne convient
::::* (cas 3) Une page avec plus de contenu que (cas 2) --> conserver
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 24 août 2026 à 19:33 (CEST)
:::::Salut @[[Utilisateur:DavidL|DavidL]]. Oui, c'est raisonnable. Reste que la fusion, c'est compliqué. J'ai essayé, mais je ne comprends rien. Une vidéo m'aiderait à comprendre, mais j'en ai pas trouvé. Ensuite, si on garde, genre un livre abandonné depuis plus de trois ans avec un chapitre entamé sur cinq prévus, par exemple, on le laisse dans l'espace principal, ou on le déplace vers l'espace utilisateur de l'auteur ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 25 août 2026 à 00:38 (CEST)
::::::Pour ce cas je propose de le fusionner si possible, comme (cas 2)(a) ; sinon le conserver, comme (cas3) afin qu'un utilisateur motivé puisse reprendre le livre.
::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 25 août 2026 à 08:36 (CEST)
:::::::Ok. Es-tu à l'aise avec la fusion @[[Utilisateur:DavidL|DavidL]] ? Car c'est une solution intéressante, mais je n'y arrive pas. Il y a toujours un message que je ne comprends pas et suite à quoi je préfère arrêter. Serais-tu dispo pour m'expliquer comment faire durant un appel vidéo ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 01:30 (CEST)
::::::::Bonjour {{Mention|Lionel Scheepmans}}, je suis très favorable à tes deux idées ! [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 26 août 2026 à 15:39 (CEST)
:::::::::Ok @[[Utilisateur:Fourmidable|Fourmidable]], je me dis qu'il serait encore bien d'avoir les avis de @[[Utilisateur:JackPotte|JackPotte]], voir de @[[Utilisateur:Grondin|Grondin]], s'ils trouvent le temps nécessaire pour le faire. Et puis, on peut penser tout doucement à créer une page pour fixer des décisions en votant si nécessaire pour confirmer l'adhésion générale ou grandement majoritaire.
:::::::::C'est curieux, je voulais travailler sur Wikiversité et finalement je me trouve plus actif sur Wikilivres. Mais les enjeux sont les mêmes et beaucoup de personnes actives ici le sont aussi sur Wikiversité, y compris parmi les administrateurs. En avançant dans Wikilivres, j'ai donc aussi l'impression que l'on avance sur Wikiversité.
:::::::::Au fait, fourmidable ? Tu ne serais pas intéressé, toi, de prendre un balai dans Wikilivres ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 17:27 (CEST)
== Demande d'aide pour amélioré Wikbook. Merci ==
* Actuellement j'ai proposé mes services pour améliorer wikibook. J'ai créé un partenariat informel avec des administrateurs, je propose des pages et des livres à supprimer. Ils décident soit de suivre ma demande, soit de la neutraliser. Je les remercie pour leur patience et leur aide.
- https://fr.wikibooks.org/wiki/Wikilivres:Demande_de_suppression_imm%C3%A9diate
*Certain livres semblent trop avancé pour suivre cette procédure.
- https://fr.wikibooks.org/wiki/Wikilivres:Demandes_de_suppression/2026
Dans cette page, je propose des livres trop avancés pour être traités rapidement. Il faut un vote pour que les demandes soient acceptées.
Aujourd'hui j'ai besoin de vous pour faire avancer mes propositions, sur des livres peut-être intéressants mais abandonnés depuis plus de trois ans, cinq ans, dix ans, vingt ans.
Si vous appréciez mon dévouement pour Wikibook et que vous souhaitez m'aider, ''il suffit chaque semaine de rajouter la commande'' {{VoteSupprimer}} ''sur les livres que je propose''. '''Entré {{}} puis à l'intérieur des parentthèses VoteSupprimer''' et signer votre accord. Merci pour votre aide. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 25 août 2026 à 10:33 (CEST)
:Salut @[[Utilisateur:Xhungab|Xhungab]], merci à toi pour ton engagement ! J'ai supprimé les pages qui étaient pratiquement vides dans ta liste. J'ai laissé les autres car la proposition de fusion est invoquée par @[[Utilisateur:DavidL|DavidL]] pour les pages avec du contenu. Malheureusement , je ne comprends pas comment fonctionne l'outil de fusion. J'espère que quelqu'un voudra bien m'expliquer le fonctionnement via un appel vidéo. Car je ne comprends pas les explications écrites. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 01:53 (CEST)
::Bonjour, Merci pour ton aide.
::.
::* J'ai travaillé sur la liste des pages les plus anciennement modifiées de l'année 2006. J'ai proposé tous les livres en errance à la suppression par vote.
::- https://fr.wikibooks.org/wiki/Wikilivres:Demandes_de_suppression/2026
::Je vais attendre qu'elle soit traitée. Soit en supprimant ces livres soit en supprimant ma demande. Ensuite je ferai l'année 2007.
::.
::*Tu m'a posé une question.
::Quel est l'avantage de la suppression par rapport au déplacement ?
::Je vais essayer de te répondre dans mon prochain message. (Ce sera juste un délire d'un mois d'août un peu chaud). [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 09:20 (CEST)
:::C'est motivant de voir des gens motivés =)
:::Prend ton temps, il n'y a aucune échéance dans les projets Wikimédia, c'est un processus en continu qui peut s'étendre à plusieurs générations... Et si c'est plus facile pour toi de passer à l'oral, sache que je suis partant pour une discussion audio ou vidéo. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 13:35 (CEST)
== J'ai fait un rêve. ==
J'ai fait un rêve.
Un lieu où des amateurs passionnés proposeraient de partager leurs travaux avec des étudiants curieux. Pas les meilleurs travaux du monde. Mais pas les pires non plus. Un endroit où les anciens passeraient le relais à ceux qui présentent leurs avenirs.
Ce serait une petite bibliothèque avec une cinquantaine de livres disponibles à l'étude. Cinq ou six livres en construction que les étudiants pourraient travailler avec l'auteur. Tous les ans quelques livres supplémentaires viendraient enrichir ce lieu d'étude.
Aujourd'hui, il existe Wikibook. Un lieu où des auteurs malveillants prouvent que le travail collaboratif ne fonctionne pas. Ils créent des livres parfaitement construits de quelques chapitres puis les abandonnent sans scrupule. Ces livres moisissent depuis cinq, dix, quinze, vingt ans.
Wikibook c'est:
* La bibliothèque de livres pédagogiques libres que chacun peut améliorer.
* 569 livres contenant 22 112 pages.
.
* 569 livres dont 450 livres à l'abandon depuis cinq, dix, quinze, vingt ans.
Oui, aujourd'hui Wikibook peut changer. Wikibook va changer. Wikibook pour les étudiants va renaître de ses cendres. Les cendres des vieux livres abandonnés. Les auteurs devront faire honnêtement leur travail, ou les étudiants feront le leur.
Ceci n'est juste qu'un rêve dans un mois d'août un peu plus chaud que d'habitude.
Merci pour m'avoir suivi jusqu'ici. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 10:13 (CEST)
:Je trouve que c'est un très beau rêve @[[Utilisateur:Xhungab|Xhungab]]. Dans ce rêve, on peut voir la partie remplie du verre au lieu de la partie vide. Sur Wikilivres, il y a plus de 100 ouvrages terminés entièrement libres d'usage et toujours améliorables collectivement. Deux fois plus que la cinquantaine de livres dont tu parles dans ton rêve.
:C'est normal d'abandonner un projet d'écriture. La vie est tellement imprévisible. Les ressources en temps et énergie tant limitées. Combien de personnes ont commencé un livre sur papier ou sur leur ordi sans jamais le terminer ?
:C'est pareil sur Wikilivres, sauf que c'est visible aux yeux de tous.
:Quand on commence un truc, c'est jamais dans l'intention de l'arrêter. Si on arrête, c'est qu'il y a des raisons à cela. Je n'ai donc pas envie de voir dans ces abandons des actes de malveillance, ni de critiquer leurs auteurs. J'ai d'ailleurs moi-même des projets que je n'arrive pas à concrétiser (exemple).
:Je pense au contraire que ce n'est pas un problème à partir du moment où nous avons la liberté et les outils (communication, suppression, fusion) qui nous permettent de faire le tri entre ce qui est terminé, voire même ce qui est de qualité, et le reste.
:Ce rêve est donc possible et mes démarches pour devenir wikimédien en résidence à l'université sont une démarche qui va dans ce sens. J'ai d'ailleurs apporté les preuves que l'intégration des projets Wikimédia dans le processus d'apprentissage universitaire est non seulement possible, mais hyper efficace. Malheureusement, j'ai toujours l'impression que cela n'intéresse personne. Ni à l'Unif, ni dans Wikimédia. Mais je ne désespère pas. Les changements de paradigmes sont lents en raison des habitudes. C'est une fatalité chez l'humain. Il faut faire avec.
:Mais donc. Concentrons-nous sur la manière d'organiser notre bibliothèque pour que les visiteurs puissent confortablement voir ce qui s'y trouve en séparant, livres terminés, livres de qualité (cela se fait bien avec les articles de Wikipédia), livres en cours d'édition et livres abandonnés, etc. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 13:32 (CEST)
::Je pense que nos points de vue sont irréconciliables en toute amitié. Pour moi, Wikibook est une bibliothèque de livres pédagogiques libres que chacun peut améliorer. Soit on peut mettre un étudiant sur un livre. Soit ce livre est inutile (Je parle de livre de plus de trois ans).
::.
::Wikibook n'est pas un entrepôt pour les auteurs. C'est une bibliothèque.[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 14:57 (CEST)
:::@[[Utilisateur:Xhungab|Xhungab]], c'est bien que l'on ne soit pas d'accord, car cela permet d'avancer vers une solution que tout le monde pourra approuver. C'est le principe du consensus et cela prend du temps en discussion. C'est normal et sain. En de plus, ce qui est dit ici est tout à fait pertinent pour Wikiversité qui est confronté à des problèmes similaires que Wikilivres.
:::J'exprime mes idées ci-dessous mes idées autrement pour voir si cela peut se rapprocher de ta propre vision des choses.
:::J'imagine la bibliothèque Wikilivres avec trois pièces :
:::# Une pièce principale à l'entrée qui reprend tous les livres terminés et prêts à l'emploi par une personne désireuse de les utiliser dans le cadre d'un apprentissage. Comme dans toutes les bonnes bibliothèques, ces ouvrages sont rangés par thèmes et certains d'entre eux sont mis en évidence en raison de leurs qualités, de leur pertinence par rapport à l'actualité, etc.
:::# Une pièce archives à l'arrière du bâtiment (un espace de nom que l'on pourrait choisir parmi ceux existants ou créer si nécessaire) dans laquelle on entrepose toute une série de livres abandonnés mais dont le contenu est archivé dans le but d'une éventuelle réutilisation. Cette réutilisation, comme le suggère @[[Utilisateur:DavidL|DavidL]], peut s'effectuer dans le cadre d'une fusion du contenu abandonné avec un autre contenu abandonné, ou mieux encore, si cela se présente, avec un livre terminé dans lequel les informations utiles du document abandonné sont manquantes. À noter que cela représente beaucoup de travail, tant pour la relecture que pour la mise en œuvre des fusions, qui, à mes yeux de personne expérimentée dans l'usage de MediaWiki, est un processus compliqué. De plus, il ne peut être fait que par des personnes ayant les outils d'administrateur. C'est-à-dire quatre personnes, dont deux ou trois actives dans ces récentes discussions.
:::# Dans la librairie Wikilivres, il y a ensuite un étage dans lequel chaque personne inscrite peut bénéficier d'un espace personnel, pour se présenter, stocker des informations, mais également travailler sur des projets personnels d'écriture. En tenant compte de ceci, on peut alors aussi imaginer de déplacer les ouvrages inachevés de la pièce principale à l'entrée de la librairie, vers ces différents locaux selon leurs auteurs respectifs.
:::Face à ces trois espaces, la question qu'il reste à se poser est de savoir si et dans quelles condition, on peut déplacer les ouvrages inachevés vers une salle d'archive, ou dans les locaux personnels des auteurs ? Cela en signalant que ce déplacement peut être fait très facilement et par toutes les personnes qui le souhaitent, même sans inscription à la bibliothèque. Ce qui est un grand avantage par rapport à la fusion.
:::Pendant ce temps, à l'accueil de la bibliothèque Wikibook, on invite les visiteurs à parcourir la pièce avec les ouvrages terminés, mais également la pièce archive, où ils peuvent trouver des ouvrages inachevés, au cas où ils veulent poursuivre leurs développements, ou trouver des ressources intéressantes pour leurs travaux personnels. On les informe ensuite aussi qu'avec leur inscription, ils bénéficient d'un local pour leurs activités personnelles et notamment pour y placer les ébauches d'ouvrages qu'ils désirent rédiger.
:::Que penses-tu de mon propre rêve ? Est-il si différent du tien ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 17:17 (CEST)
::::Et les autres ? @[[Utilisateur:DavidL|DavidL]], @[[Utilisateur:Fourmidable|Fourmidable]], @[[Utilisateur:JackPotte|JackPotte]], @[[Utilisateur:Grondin|Grondin]]. Que pensez-vous de nos rêves ? Et, si le mien vous parle, quelles seraient vos propositions pour orienter les déplacements de pages à l'abandon entre un espace d'archivage (qui pourrait être créé) et des espaces utilisateurs ? Le nombre d'octets ? La finalisation d'une section ou d'un chapitre ? Quelles autres possibilités ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 17:37 (CEST)
::::: Bonjour, a priori ça doit se régler au cas par cas sur [[Wikilivres:Demandes de suppression]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]])
::::Bonjour,
::::La structure de Wikibook est parfaite grâce à sa vitrine. Pour moi, il y a juste un problème sur les listes sur la droite : Arts, Loisir, Histoire, Langues, Technologie, Informatique, Sciences, Sciences humaines. Ces listes ne devraient donner accès qu'aux livres terminés.
::::Ces listes actuelles pourraient être caché dans un espace création qui pourrait être posé avec le lien Nouveauté en bas de la page.
::::Pour la gestion des livres, il suffirait qu'une ou deux personnes parcourent la bibliothèque et proposent quelques suppressions chaque semaine, pour en quelques mois éclaircir la situation. Je pense que l'on peut faire confiance aux administrateurs pour gérer la situation des livres, ceux qui sont à garder et les autres. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 18:30 (CEST)
:::::@[[Utilisateur:JackPotte|JackPotte]]. Le cas par cas est ce que nous faisons depuis toujours. Cela nous amène à la situation actuelle qui semble ne pas convenir à tous. En tout cas, c'est mon cas. Ici comme sur Wikiversité. Quand, en navigant sur le site, une personne tombe neuf fois sur dix sur des pages presque vides et inachevées, comme c'est le cas sur Wikilivre, elle se dit : c'est quoi ce site ? Elle passe alors à autre chose de plus vivant, genre https://zestedesavoir.com/ ou autre. Sans changements éditoriaux, je pense que la situation évoluera.
:::::@[[Utilisateur:Xhungab|Xhungab]], on peut faire comme tu dis. Cependant, la charge de travail repose sur une double action, la proposition d'abord et le traitement ensuite. Un traitement qui finalement repose sur les épaules des administrateurs. Nous ne sommes que des bénévoles peu nombreux et pas toujours disponibles. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 19:13 (CEST)
:Créer un livre, le ranger, en établir un plan, le catégoriser n'est pas un travail négligeable... Un travail que d'autres personnes intéressées pour écrire sur le sujet n'ont pas à refaire 🤷♂️ ...
== Petite proposition indécente à Lionel ==
Bonjour Lionel,
Je souhaiterais te proposer un petit projet.
a) Dans la page principale de wikibook tu créés une page "Espace pour les auteurs". Tu insères cette page devant le lien "Nouveautés".
.
b) Tu copies dans cette page la liste des liens qui se trouvent en haut à droite dans la nouvelle page. (Arts, Loisirs, ...) Tu ne les modifies pas dans la page principale.
.
c) Dans la nouvelle page tu peux écrire un texte du genre.
Dans cette page, vous trouverez la liste de tous les livres disponibles sur Wikibook. Vous trouverez en particulier des livres en construction et vous pourrez entrer en contact avec les auteurs. Vous trouverez aussi des livres en construction sans auteur actif que vous pourrez compléter.
Cela est la première étape, si l'aventure t'intéresse. En fait tu ajoutes juste une page, que tu pourras supprimer si les besoins se font sentir. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 27 août 2026 à 12:25 (CEST)
:La proposition est très décente @[[Utilisateur:Xhungab|Xhungab]]
:Mais avant toute choses le plus prudent est de lancer une prise dé décision pour s'assurer d'un concesnsus en faveurs des changements dont nous débatons actuellement. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 27 août 2026 à 15:40 (CEST)
::Toutes ces actions seront réversibles. On ajoute une page. Un problème, on la supprime.
::On fait rapidement les changements, on attend trois jours. En cas de problème, on supprime tout. Il suffira de s'excuser et de dire que l'on ne savait pas que l'on n'avait pas le droit.
:: Un espace pour les lecteurs et un espace pour les auteurs séparés.
::[https://fr.wikibooks.org/wiki/Utilisateur:Xhungab Nouvelle page wikibook][[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 27 août 2026 à 16:49 (CEST)
:@[[Utilisateur:Xhungab|Xhungab]] , ma grand mère disait : faire et défaire, c'est toujours travailler. Pour les grand changements, on a coutume de faire un prise de décision composée des propositions, suivies de débats et d'un vote.
:En plus, certaines décisions nécessite un changement technique et ce n'est pas toujours facilement réversible. Comme créer un nouvel espace de nom pour y transfèrer une catégorie de pages [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 28 août 2026 à 03:07 (CEST)
::Merci pour ton aide. Tu comprends maintenant pourquoi toi tu es un administrateur et moi pas.
::.
::'''Oui, aujourd'hui Wikibook peut changer. Wikibook va changer.'''
::.
::* '''Le changement c'est Maintenant :''' [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab '''Wikibook''']
::[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 28 août 2026 à 08:33 (CEST)
:::Ton enthousiasme fait plaisir @[[Utilisateur:Xhungab|Xhungab]], et je serais en faveur d'un style plus épuré de notre page d'accueil en plus d'une méthodes de classement et de séparation des ouvrages terminé, en cours et abandonnés. Je pense qu'à terme, nous pourrions faire une proposition de changement à soumettre au reste de la communauté. Par chance, nous ne sommes pas nombreux, cela simplifire les discussions et le processus de décision. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 19:26 (CEST)
::::Cette page [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab '''Wikibook'''] propose la vitrine, les livres disponibles, les mini-livres disponibles pour les lecteurs. Les lecteurs ne seront en contact qu'avec les livres terminés.
::::.
::::En bas à côté des nouveautés, il y a l' '''[https://fr.wikibooks.org/wiki/Discussion_utilisateur:Xhungab Espace pour les auteurs]''', avec tous les livres du site rangés par thème ou par niveau d'avancement si on clique sur l'image. Sinon, il y a une liste de catégories art,...
::::.
::::Cette proposition sépare donc deux espaces. Un pour les lecteurs et un pour les auteurs. Il suffirait maintenant plus qu'à nettoyer l'espace auteurs. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 20:22 (CEST)
L'usage des catégories me semble aussi une manière très pratique de trier le contenu de Wikilivre. Malheureusement, ces pages ne sont pas très sexy du coup l'usage de type de code pourrait être utile :
{ {Boîte déroulante|titre=Cliquez ici pour voir les livres terminés|alignT=left|contenu={ { #categorytree:Livres terminés|mode=pages} } } }
Avec le rendu ci-dessous :
{{Boîte déroulante|titre=Cliquez ici pour voir les livres terminés|alignT=left|contenu={{ #categorytree:Livres terminés|mode=pages}}}}
On pourrait ainsi imaginer d'intégrer directement ce code dans la page d'accueil pour afficher une boite déroulante au lieu d'envoyer le visiteur en dehors de la page d'accueil. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 21:08 (CEST)
:Tu devrais créer la page Lioneltest01 dans ta page utilisateur. Copier le code de la page principale de wikibook.
:Puis faire les changements que tu juges nécessaires. Tu peux éventuellement faire plusieurs pages. Ensuite, tu mets cette page dans le bistro pour que l'on puisse voir le résultat. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 21:22 (CEST)
::Je devrais, tu devrais, il devrait, nous devrions ... =) [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 21:28 (CEST)
:::Chaque chose en son temps.
:::1. Débattre ici des idées de changements.
:::2. Mettre les choses en place pour leur réalisation, comme la mise à jour du modèle:boite déroulante en s'inspirant de celui de Wikipédia ou d'autres sites.
:::3. Quand il n'y a plus de nouvelle idée ici, créer une page genre :[[Wikilivres:Prise de décision/Nouvelle page d'accueil et gestion des abandons]] avec une description précise des projets de changements, et une éventuelle [[Accueil/projet|page de présentation du projet d'un nouvelle page d'accueil]]
:::4. Poursuivre les discussions et propositions si nécessaire.
:::5. Quand on a fait le tour, passer au vote.
:::6. Mettre les décisions en application. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 22:03 (CEST)
::::Je propose actuellement un projet qui se trouve sur ma page utilisateur. Pourrais-tu lancer une prise de décision sur ce projet. Je ne sais pas comment on fait une telle demande. Je suis prêt à présenter mon projet et à recevoir les critiques. Merci pour ton aide. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 22:16 (CEST)
:::::''Aide-toi le ciel t'aidera''. La bureaucratie, c'est pas mon truc. Et puis ça m'irrite un peu que tu enchaines des demandes de faire des choses qui ne demande pas les outils d'administrateurs. Raisons pour lequelles, je pense finalement que l'on gagnerait à appliquer la [[w:Do-ocratie|Do-ocratie]] qui a fait ses preuves dans le monde du libre dont les projets Wikimédia sont issus.
:::::Si tu as une idée et qu'elle ne demande pas des droits de modification que tu n'as pas, fait la et on se retrouve en page de discussion de la page de tes changements en cas de problèmes et pour d'éventuelle commentaire. Sans réaction, pense à ''qui ne dit mot conscent''.
:::::Si tes idées demandes les outils d'administrateurs, tu peux alors demander à un admin de le faire. Libre à lui de le faire ou pas et si ça lui plait, si il a envie, si il prend le temps. Nous sommes bénévoles... Il y a bien des gens payés par la fondation, mais comment dire... Disons qu'ils ne sont pas des plus serviables. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 22:57 (CEST)
::::::Excuse moi je pensais qu'il faillait être administrateur pour faire une demande de prise de décision. Je vais donc essayer tous seul. Merci pour ton aide. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 23:06 (CEST)
:::::::Ok @[[Utilisateur:Xhungab|Xhungab]], pas de soucis. On ne peut pas tout savoir =) [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 30 août 2026 à 01:45 (CEST)
== Wikilivres:Prise de décision/Créer un espace lecteurs et un espace auteurs sur la page d'accueil ==
J'ai posé une demande de prise de décision concernant le sujet suivant.
'''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil Créer un espace lecteurs et un espace Les coulisses de Wikibook]'''
Merci pour votre participation
[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 30 août 2026 à 00:43 (CEST)
:Bonjour @[[Utilisateur:Xhungab|Xhungab]]. Content de te revoir dans les murs de Wikiversité après nos échanges sur Wikilivres. Je comprends ton sentiment de solitude, je le ressens aussi souvent. Il faut l'accepter et ne pas se décourager pour autant. Nous avons la chance de fonctionner dans une organisation bénévole, dans laquelle personne n'est tenu de répondre à des demandes. De ce fait, les choses évoluent lentement et en fonction des disponibilités. Dis-toi aussi que la période estivale est toujours un moment d'acalmie dans la participation. Moi par exemple, quand il fait bon, je préfère passer du temps sur des occupations extérieures que devant un écran.
:Cela dit, tu dois garder 3 règles implicites qui sont fréquemment appliquées dans les projets Wikimédia.
:1. ''Qui ne dit mot consent''. Si les personnes ne réagissent pas, cela ne veut pas dire qu'elles ne lisent pas. Par économie de temps et pour les personnes trop occupées ou pas assez motivées pour s'investir, ne pas réagir est un signe de consentement implicite.
:2. ''Les absents ont toujours tort.'' Lorsque l'on fait les choses dans les règles de l'art, par exemple comme tu le fais en lançant une prise de décision pour opérer un changement significatif dans Wikilivres, aucune personne absente durant un processus de décision clôturé viendra remettre en cause les choses par la suite.
:3. La doocratie. Les wikis sont des espaces de libertés au niveau de l'amélioration des contenus, mais également de l'organisation et des formes de contenus. En dehors du vandalisme qui est bien contrôlé par des personnes telles que @[[Utilisateur:Crochet.david|Crochet.david]], toute initiative est souvent perçue comme bienvenue. @[[Utilisateur:Fourmidable|Fourmidable]], par exemple, depuis qu'il est administrateur, a fait de nombreuses améliorations dans Wikiversité sans demander l'avis de la communauté. Personne ne lui a reproché son investissement. Si quelque chose qu'il a entrepris ne plaît pas à quelqu'un, une discussion commence entre lui et cette personne pour chercher une entente.
:Voilà tout.
:Je te rejoins aussi sur le fait que ce qui est décidé et mis en place sur Wikilivres peut tout à fait servir d'exemple pour Wikiversité et vice versa. Je ne suis pas le seul à être administrateur dans les deux projets et cela aide beaucoup à la création d'une synergie entre les deux projets qui sont finalement très complémentaires. Cela n'est pas étonnant d'ailleurs lorsque l'on sait que Wikiversité est né de Wikilivres.
:Une belle fin de journée à toi et à tous ceux qui liront ce message. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 6 septembre 2026 à 13:30 (CEST)
::Merci. Pour tes explications. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 6 septembre 2026 à 13:50 (CEST)
:::Avec plaisir. Pense aussi qu'il n'y a pas de combat dans les projets Wiki, mais un lent processus évolutif, sans fin programmée, et qui est pour moi l'un des meilleurs que je connaisse. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 6 septembre 2026 à 13:57 (CEST)
== Les RAW sont arrivés, la rentrée peut commencer ! ==
<div style="background-color:#177860; border-radius:.2em; color:#FFF;padding: 1em;">
<div style="font-family: Century Gothic; font-size:3em; line-height:120%;text-align:center;color: #fff;">RAW</div>
<div style="margin-bottom:1.5em; margin-top:1em; text-align:center; color: #fff;">Le numéro de septembre 2026 ({{numéro|296}}) est sorti.</div>
<div class="center">[[:w:Wikipédia:RAW/2026-09-05|<span class="cdx-button cdx-button--fake-button cdx-button--fake-button--enabled" style="white-space:break-spaces; text-align:center; gap:0;"role="button" aria-disabled="false">Lire l'infolettre</span>]]</div>
</div>
'''C'est la rentrée'''
Après un numéro très dense en août en raison de la Wikimania, celui-ci est un peu plus léger car l'équipe s'est octroyée quelques vacances. Cela n'empêche pas le mouvement de continuer à vivre, avec une rentrée déjà animée ! Du côté des projets francophones, le Wiktionnaire vient de franchir le cap symbolique des sept millions d’entrées, tandis que Wikipédia continue de voir fleurir sondages et discussions sur son fonctionnement.
Une rentrée, donc, placée sous le signe de la construction collective : de nouveaux outils pour contribuer, de nouveaux espaces pour échanger, de nouvelles formes d’organisation… Ce numéro des RAW vous propose de faire le tour de cette actualité, avant de reprendre tranquillement le chemin des projets Wikimédia.
L'équipe de la rédaction a également commencé à réfléchir à un nouveau nom pour remplacer l'énigmatique « RAW ». N'hésitez pas à venir donner votre avis dans la [[:w:Discussion Wikipédia:RAW/Rédaction#Changement de nom|section dédiée]] !
----
Au menu de ce numéro :
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">En bref</span>''' — Des actualités francophones et du mouvement Wikimédia, des nouveaux outils et des articles de recherche
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">Le wik'hit parade</span>''' — Une analyse des articles les plus vus le mois dernier
[[Fichier:Toicon-icon-sharp-corners-draw.svg|20px|classe=skin-invert-image]] Les RAW, c'est aussi vous : n'hésitez pas à suggérer des sujets ou participer à la rédaction du [[:w:Wikipédia:RAW/Rédaction|prochain numéro]] !
[[Utilisateur:Antimuonium|Antimuonium]] ([[Discussion utilisateur:Antimuonium|discussion]]) 5 septembre 2026 à 00:17 (CEST)
== Wikilivres:Prise de décision/Modifier le contenue de l'espace Nouveautés ==
J'ai posé une demande de prise de décision concernant le sujet suivant.
[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Modifier_le_contenue_de_l%27espace_Nouveaut%C3%A9s Wikilivres:Prise de décision/Modifier le contenue de l'espace Nouveautés]
Merci pour votre participation [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 9 septembre 2026 à 14:38 (CEST)
== '''Décision''':Prise de décision/Créer un espace lecteurs et un espace auteurs sur la page d'accueil ==
J'ai posé une demande de prise de décision concernant le sujet suivant.
'''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil Créer un espace lecteurs et un espace Les coulisses de Wikibook]'''
'''Décision pour : [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab '''Wikibook''']'''
Résultats : 3 + Pour, un Neutre Neutre et un - Contre soit 60 % de + Pour par rapport au total des votes exprimés : la proposition est adoptée. Quelques propositions de reformulations et de nuances ont été faites qui pourront être prise en compte. Cdlt, VIGNERON * discut. 13 septembre 2026 à 18:27 (CEST)
Merci pour votre participation. Ma première proposition était un peu malhabile, mais grâce à vos interventions je pense que l'on a créé quelque chose de correct. '''Merci à tous''' [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 13 septembre 2026 à 20:22 (CEST)
== Je ne suis donc pas content tous seul. (2) ==
Discutons dans la page d'accueil.
'''[https://fr.wikibooks.org/wiki/Discussion:Accueil#Je_ne_suis_donc_pas_content_tous_seul._%282%29 Je ne suis pas content tous seul]''' Version 2 dans la page d'accueil.
[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 septembre 2026 à 14:28 (CEST)
rwgbsl2k1qs1oolxaw1k368kbrs0dlb
Discussion utilisateur:SHB2000
3
84514
772388
2026-09-18T02:31:37Z
Max
116893
Max a déplacé la page [[Discussion utilisateur:SHB2000]] vers [[Discussion utilisateur:SHB]] : Page automatiquement déplacée lors du renommage de l’utilisateur « [[Special:CentralAuth/SHB2000|SHB2000]] » en « [[Special:CentralAuth/SHB|SHB]] »
772388
wikitext
text/x-wiki
#REDIRECTION [[Discussion utilisateur:SHB]]
k45bsf0rzjwfb2sqnpxuskri7d5tr1e