Programmation PHP/Version imprimable4
Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
https://fr.wikibooks.org/wiki/Programmation_PHP
DOMDocument
Introduction
[modifier | modifier le wikicode]Une des recherches majeures du développeur a toujours été de chercher à séparer les langages de programmation, clarifiant ainsi ses scripts et simplifiant sa tâche. Ainsi, il est possible d'enregistrer son CSS dans des fichiers externes comportant l'extension .css, de séparer le Javascript du HTML en l'enregistrant dans des fichiers .js.
Il reste cependant le problème de la séparation du PHP et du XML (incluant le HTML). La bibliothèque DOMDocument va repousser ces limites.
Qu'est-ce que DOMDocument ?
[modifier | modifier le wikicode]DOMDocument est une bibliothèque de fonctions apparue avec PHP5[1] et activée par défaut. Elle permet de concevoir des pages HTML sous forme d'objets.
Les avantages et les inconvénients
[modifier | modifier le wikicode]- Concevoir des pages HTML par cette méthode permet d’annihiler un problème majeur de la programmation procédurale : l'édition du code n'est plus en fonction de sa position dans le script. Pour être plus clair, chaque balise jusqu'à la DTD peut être modifiée à tout moment dans l'objet HTML.
- Il est possible d'enregistrer la page HTML dans un fichier sans l'afficher.
mais...
- Le code est plus long à éditer.
Principe du DOM
[modifier | modifier le wikicode]Cette bibliothèque présente de nombreuses similitudes avec le Javascript aussi bien dans le fonctionnement que dans le nom de ses fonctions.
Le DOM (Document Object Model) est basé sur un système de nodes (nœuds). Un node est un élément qui est - soit une balise (nodes Tag) - soit du texte - soit un attribut de balise Les nodes sont liés par un système hiérarchique :
<p>Ce texte est <strong>important</strong></p>
On dit alors que le node Tag <strong> est fils du node <p> Le node texte "Ce texte est" est également fils de <p> qui est parent du node texte.
Il existe un certain nombre de classes prédéfinies : DOMDocument, DOMNode, DOMElement, DOMText, DOMAttr, DOMList... Certaines sont très simples, d'autres possèdent des fonctionnalités très avancées.
Importer une page préexistante
[modifier | modifier le wikicode]Il est possible d'importer une page HTML. Cela simplifiera considérablement la tâche du programmeur qui n'aura qu'à apporter les modifications nécessaires avant de l'afficher ou de réenregistrer la page. Voici le code important la page.
<?php
$doc = DOMDocument::loadHTMLFile("fichier.html");
La variable $doc contient donc un objet DOMDocument avec toutes les balises sous formes de nodes. Il est maintenant possible d'accéder aux nodes par le biais de fonctions préexistantes.
NB : il est également possible d'avoir recours au code suivant.
<?php
$doc = new DOMDocument();
$doc->loadHTMLFile("fichier.html");
Il est également possible d'importer le code à partir d'une chaîne de caractères :
<?php $code = "<html><head></head><body></body></html>"; $doc = new DOMDocument(); $doc->loadHTML( $code );
Enregistrer une page
[modifier | modifier le wikicode]Un des grands avantages de cette bibliothèque est la capacité à enregistrer la page générée dans un fichier pour un affichage ultérieur. Il suffit d'avoir recours au code suivant :
$doc->saveHTMLFile("fichier.html");
Si vous voulez l'afficher, il vous suffit d'exécuter la fonction suivante :
<?php echo $doc->saveHTML();
La méthode retourne une chaîne de caractères que la fonction echo affiche.
Le résultat n'est pas en Unicode, donc les lettres avec diacritiques seront mal affichées par défaut, en français : àâçéèêëîïôöüù. Cela peut aussi générer un Warning: DOMDocumentFragment::appendXML(): Entity: line 1: parser error : Input is not proper UTF-8, indicate encoding !
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR"> <head> <meta http-equiv="Content-type" content="text/html; charset=UTF-8"/> </head> <body> <?php $code = 'Les àâçéèêëîïôöüù'; echo $code; // affichage normal $page = new DOMDocument(); $page->loadHTML($code); echo $page->saveHTML(); // Les à âçéèêëîïôöüù // Solution $page2 = new DOMDocument(); $page2->loadHTML(utf8_decode($code)); echo $page2->saveHTML(); // Les àâçéèêëîïôöüù ?> </body> </html>
La classe DOMNode
[modifier | modifier le wikicode]Les classes DOMElement, DOMText et DOMAttribute sont dérivées de cette classe. Ainsi, les méthodes et propriétés présentées ici seront disponibles pour leurs classes filles.
Attention : un nœud une fois créé ne se trouve pas dans le document. Ajouter un nœud va se dérouler en deux étapes :
- on crée le nœud
- on l'insère dans le nœud parent ou à la racine du document.
Voici les propriétés accessibles à tous les nœuds :
$node->nodeType // Type de nœud. Vaut 1 pour un élément XML, 3 pour un texte $node->childNodes //Retourne un objet NodeList qui contient tous les éléments enfants de ce nœud $node->firstChild //Retourne le premier nœud enfant $node->lastChild // Retourne le dernier nœud enfant $node->previousSibling // Retourne le nœud juste avant celui-ci $node->nextSibling // Retourne le nœud juste après celui-ci
Les méthodes sont les suivantes :
- AppendChild
- Le nœud $enfant devient enfant de $node
$node->appendChild( $enfant );
- RemoveChild
- Supprime le nœud $enfant du node $node
$node->removeChild( $enfant );
La classe DOMElement
[modifier | modifier le wikicode]- Les éléments possèdent les propriétés suivantes :
$node->tagName // par exemple "p" pour un paragraphe. Sa valeur ne peut être modifiée
Attention : seules les principales propriétés sont présentées. Si vous voulez en avoir la liste complète, vous pouvez consulter la documentation de référence sur http://php.net/manual/fr/class.domnode.php.
- Et les méthodes suivantes :
- Méthodes d'attributs
$node->hasAttribute(); //Renvoie true s'il possède des attributs
$node->getAttribute("name"); //Retourne la valeur de l'attribut
$node->removeAttribute("name"); //Supprime l'attribut ''name''
$node->setAttribute("name","value"); //Modifie un attribut
$node->getAttributeNode ("name" ); //Retourne un objet DOMAttr
- Autres méthodes
$newNode = $node->cloneNode(); //duplique un élément
$nodeList = $node->getElementsByTagName("strong");
Cette fonction retourne un objet nodeList qui contient une liste des balises <strong> enfants du nœud. Pour récupérer le n+1ème nœud de la liste, il suffit d'avoir recours à la méthode de l'objet nodeList suivante :
$strong5 = $nodeList->item(4); //Sélectionne la 5e balise <strong>
L'attribut length donne le nombre d'éléments de la liste. Exemple :
for ($i=0; $i<$nodeList->length; $i++)
{
echo $nodeList->item( $i )->tagName;
}
Comme vous le savez, tagName retourne le nom de la balise. Ici, le code retournera "strongstrongstrong…". En effet, seuls les nœuds strong ont "été sélectionnés. Comme vous avez pu le remarquer, il est possible d'exécuter plusieurs méthodes et propriétés en même temps. Voici l'ordre d'exécution :
- La méthode item($i) est exécutée et retourne un nodeTag.
- La propriété tagName du nœud est appelée. Attention : c'est celle de l'objet retourné.
- La fonction echo affiche le nom de la balise retournée par la propriété tagName.
La classe DOMText
[modifier | modifier le wikicode]La classe DOMText contient l'unique propriété suivante :
$node->wholeText
Elle n'est accessible qu'en lecture seule.
La classe DOMText admet deux méthodes :
/* Retourne true si la chaîne de caractère contient des espaces */ $node->isWhitespaceInElementContent();
/* Retourne dans $end un objet Text qui contient la fin du texte de $node. $node ne contiendra plus que les 5 premiers caractères de sa chaîne */
$end = $node->splitText(5)
Exemple :
<?php
$doc = new DOMDocument();
$text = $doc->createTextNode("Je suis celui qui est");
$text2 = $text->splitText(7);
echo $text->wholeText."<br />";
echo $text2->wholeText
Ce code retournera "je suis<br /> celui qui est"
La classe DOMAttr
[modifier | modifier le wikicode]La classe DOMAttr est comme son nom l'indique un attribut, qui est donc dépendant de la balise. Elle contient les propriétés suivantes :
$node->name // Nom de l'attribut $node->value // Valeur de l'attribut /* Nom de la balise qui contient l'attribut. La valeur retournée est un objet DOMElement */ $node->ownerElement
Seule la propriété value n'est pas en lecture seule, c'est à dire qu'il est possible d'avoir recours au code suivant :
$node->value = "maValeur";
Accéder à un nœud
[modifier | modifier le wikicode]Il existe plusieurs modes de recherche du nœud. Il est par exemple possible de le sélectionner par son id :
$node = $doc->getElementById("sonId"); // Retourne un objet nœud
Sachant qu'il ne peut y avoir qu'un nœud possédant l'id recherché, la méthode retournera un objet nœud au lieu d'un objet nodeList.
Il est cependant possible de récupérer une liste de nœuds en les sélectionnant par leur nom de balise. Évidemment, seuls les nœuds Tag peuvent être sélectionnés.
$nodeList = $doc->getElementsByTagName("acronym"); //Sélectionne toutes les balises <acronym>
Comme nous l'avons déjà dit, il faut, pour récupérer un nœud particulier de la liste, utiliser la méthode suivante :
$acronym5 = $nodeList->item(4); //Sélectionne le 5e nœud de la liste;
XML
[modifier | modifier le wikicode]Création :
$xml = new DOMDocument("1.0", "ISO-8859-15");
$xml_node1 = $xml->createElement("Node_1");
$xml_node2 = $xml->createElement("Node_2", "Feuille");
$xml_node2->setAttribute("Attribut_1", "Valeur_1");
$xml_node1->appendChild($xml_node2);
$xml->appendChild($xml_node1);
$xml->save('Fichier_1.xml');
Lecture :
$xml = new DomDocument;
$xml->load('Fichier_1.xml');
$fields = $xml->getElementsByTagName('fields');
foreach ($fields as $field) {
/** @var DOMElement|DomText $fieldChild */
foreach ($field->childNodes as $fieldChild) {
var_dump($fieldChild->nodeValue);
}
}
Références
[modifier | modifier le wikicode]
JSON
Bibliothèque PHP-JSON
[modifier | modifier le wikicode]Le format de données JavaScript Object Notation (JSON) peut être utilisé en PHP grâce à différentes fonctions natives depuis PHP 5.2.0.
Installation (pour PHP < 5.2)
[modifier | modifier le wikicode]Linux
[modifier | modifier le wikicode]apt-get install php5-json
Windows
[modifier | modifier le wikicode]- Télécharger le fichier json-1.2.1.tgz sur https://pecl.php.net/package/json.
- Décompresser et compiler le code source en json.so.
- Le copier dans le dossier des extensions PHP.
- Dans le php.ini (ex : C:\Program Files (x86)\EasyPHP\binaries\php\php_runningversion\php.ini), ajouter :
extension=json.so
json_encode()
[modifier | modifier le wikicode]Cette fonction convertit un objet PHP en JSON exploitable en JavaScript[1]. Ex :
$tableau = array('colonne 1' => 'valeur 1', 'colonne 2' => 'valeur 2', 'colonne 3' => 'valeur 3');
echo json_encode($tableau);
{"colonne 1":"valeur 1","colonne 2":"valeur 2","colonne 3":"valeur 3"}
json_encode() sur une instance de classe n'en n'affiche que les attributs publics. Pour encoder les privés, il faut que la classe implémente JsonSerializable en qui impose une méthode jsonSerialize()[2]. Ex :
public function jsonSerialize() {
return $this->array;
}
Options
[modifier | modifier le wikicode]
Par défaut json_encode() échappe les caractères spéciaux non ASCII (ex : "é" devient "\u00e9"). Pour éviter cela on utilise l'option suivante :
$a = json_encode($monTableau, JSON_UNESCAPED_UNICODE);
Par ailleurs, les erreurs de json_encode() sont accessibles avec json_last_error() ou json_last_error_msg(). Ex :
$a = json_encode($monTableau);
if (json_last_error() === JSON_ERROR_INF_OR_NAN) {
$this->logger->error(json_last_error_msg());
$a = json_encode($monTableau, JSON_PARTIAL_OUTPUT_ON_ERROR);
}
Pour afficher dans un format plus lisible par un humain (indenté), utiliser JSON_PRETTY_PRINT.
json_decode()
[modifier | modifier le wikicode]Convertit une chaine de caractères JSON en :
- Si aucun paramètre 2 n'est passé, un objet PHP dont chaque attribut correspond à une clé du tableau.
- Si le paramètre 2 vaut "true", un tableau associatif[3].
La gestion des erreurs est semblable à celle de json_encode().
json_validate()
[modifier | modifier le wikicode]Renvoie un booléen.
PEAR Services_JSON
[modifier | modifier le wikicode]Le framework PEAR possède aussi un package Services_JSON contenant des .php avec des exemples, à télécharger en tant que JSON.tar.gz sur http://pear.php.net/pepr/pepr-proposal-show.php?id=198.
Il requiert PHPUnit.phpTélécharger.
La classe Services_JSON de JSON.php peut s'utiliser comme décrit dans Test-JSON.php.
Références
[modifier | modifier le wikicode]
MING
Conceptions d'animations pour pages web
[modifier | modifier le wikicode]Créer les animations en Flash (.swf) se fait par des logiciels payants, cependant la librairie MING écrite en C, et utilisable en PHP, C++, Python et Ruby, permet de les générer gratuitement (mais pas d'éditer les .swf existant).
<?php
// Dessine deux boutons interactifs
function BoutonCarré($r, $g, $b)
{
$s = new SWFShape();
$s->setRightFill($s->addFill($r, $g, $b));
$s->movePenTo(-20,-20);
$s->drawLineTo(20,-20);
$s->drawLineTo(20,20);
$s->drawLineTo(-20,20);
$s->drawLineTo(-20,-20);
return $s;
}
function BoutonRond($r, $g, $b)
{
$s = new SWFShape();
$s->setRightFill($s->addFill($r, $g, $b));
$s->movePenTo(20, 20);
$s->drawCircle(20);
return $s;
}
$carré = new SWFButton();
$carré->setUp(BoutonCarré(0xff, 0, 0));
$carré->setOver(BoutonCarré(0, 0xff, 0));
$carré->setDown(BoutonCarré(0, 0, 0xff));
$carré->setHit(BoutonCarré(0, 0, 0));
$rond = new SWFButton();
$rond->setUp(BoutonRond(0xff, 0, 0));
$rond->setOver(BoutonRond(0, 0xff, 0));
$rond->setDown(BoutonRond(0, 0, 0xff));
$rond->setHit(BoutonRond(0, 0, 0));
$m = new SWFMovie();
$m->setDimension(320, 240);
$m->setBackground(0xff, 0xff, 0xff);
$i = $m->add($carré);
$i->moveTo(50, 50);
$i = $m->add($rond);
$i->moveTo(100, 50);
header('Content-type: application/x-shockwave-flash');
$m->output();
Depuis février 2016, Firefox bloque tous les contenus Flash par défaut, pour des raisons de sécurité. Cette technologie est donc amenée à être remplacée par JavaScript.
Liens externes
[modifier | modifier le wikicode]- Fonctions
- Tutoriel JournalDuNet
- Tutoriel GazbMing
- Tutoriel developpez.com
- Tutoriel supportduweb.com
- Bibliothèque de scripts
- Perl graphics programming, Shawn P. Wallace, 2002
SPL
La Standard PHP Library (SPL) est une bibliothèque intégrée depuis PHP 5[1].
Elle comprend les classes suivantes :
Structures de données
[modifier | modifier le wikicode]- SplDoublyLinkedList
- SplStack
- SplQueue
- SplHeap
- SplMaxHeap
- SplMinHeap
- SplPriorityQueue
- SplFixedArray
- SplObjectStorage
Itérateurs
[modifier | modifier le wikicode]Interfaces
[modifier | modifier le wikicode]Exceptions
[modifier | modifier le wikicode]Références
[modifier | modifier le wikicode]
ADOdb
Fonctionnalités
[modifier | modifier le wikicode]Cette librairie permet comme PEAR DB de supporter différents types de bases de données (MySQL, Oracle ...). Il est ainsi possible par le biais d'un fichier de configuration de modifier le type de base de données sans que cela n'aie d'impact dans le code de l'application en elle-même.
Il s'agit ici, comme pour les autres produits décrits plus haut, d'exploiter les possibilités de programmation orienté objet en ce sens qu'elles optimisent la programmation, la rendant plus efficiente.
Reformulons l'intérêt d'utilisation de cette classe. Nous savons par exemple que le langage Java permet "d'attaquer" différents types de bases de données sans qu'une ligne de programmation ne soit modifiée, déportant cette disparité en concentrant notre attention sur une seule ligne de configuration : la déclaration du driver de base de données.
C'est un peu ce que veut faire pour PHP ADODB, tout comme PEAR DB, qui furent des produits concurrents, dans le but de tester un développement PHP.
Installation
[modifier | modifier le wikicode]Les fichiers se téléchargent sur http://adodb.sourceforge.net/#download.
Exemple
[modifier | modifier le wikicode]Exemple sur une base Access :
include('adodb.inc.php'); # charge le code de ADOdb
$conn = &ADONewConnection('access'); # crée une connexion
$conn->PConnect('northwind'); # se connecte à MS-Access, northwind DSN
mais ensuite, que vous créez, puis exploitez en base réelle sur Postgres ou encore MySQL, les modifications sur les lignes de codes pour une version en exploitation seront alors des modifications de type paramétrage dans l'appel de la méthode d’accès, mais non point sur chaque ligne d'instruction, qu'il s'agisse d'un accès en lecture, en mise à jour, etc.
include('adodb.inc.php');
$conn = &ADONewConnection('mysql');
$conn->PConnect('localhost','userid','','agora');# se connecte à MySQL, agora db
Il existe aussi des méthodes permettant de passer rapidement en conversion HTML après exploitation en séquence de tuples de la base de données cible, ou encore de créer rapidement un fichier CSV, ce qui est très prisé en bureautique (pour les logiciel de type "Office").
Voir aussi
[modifier | modifier le wikicode]Un excellent tutoriel se trouve à l'adresse suivante :
- chez phplens : Tutorial ADODB français
- chez phpfreaks : Tutorial ADODB anglais
Une courte description en anglais au database journal : ADODB class library
DOMPDF
DOMPDF permet de générer des fichiers PDF à partir d'une page HTML. C'est une alternative à HTML2PDF, qui lui est basé sur TCPDF.
Exemples
[modifier | modifier le wikicode]use Dompdf\Dompdf;
class pdfGenerator
{
public function generate(string $html)
{
$dompdf = new Dompdf();
$dompdf->loadHtml($html);
$dompdf->render();
$dompdf->stream();
}
}
FPDF
FPDF est d'origine française, il est gratuit, et facile d'utilisation. Cette librairie permet d'exploiter les possibilités de production de documents en PDF à l'aide de PHP. Elle se télécharge sur http://www.fpdf.org/.
Exemples
[modifier | modifier le wikicode]use FPDF;
class pdfGenerator
{
public function generate(string $html)
{
$pdf = new FPDF();
$pdf->AddPage();
$pdf->Cell(10, 10, $html);
$pdf->Output();
}
}
PHPExcel
Introduction
[modifier | modifier le wikicode]Cette bibliothèque open source permet de lire et d'écrire dans des tableurs, XLS et XLSX. Mais il peut aussi générer des CSV, des PDF, et des HTML[1].
Elle comprend toute sorte de fonctions de manipulations de tableurs, telles que le changement de couleur des champs, l'ajout de graphiques et de filtres, la protection de feuilles...
Il faut la télécharger sur https://github.com/PHPOffice/PHPExcel :
composer require phpoffice/phpexcel
Pour l'utiliser, l'inclure en début de fichiers :
include 'PHPExcel/Classes/PHPExcel.php';
On appellera ses instances "$objPHPExcel", qui représentent les classeurs.
Création
[modifier | modifier le wikicode]Pour créer un fichier à partir de rien, soit CreateXLS.php un fichier situé à côté du répertoire de la bibliothèque nommé PHPExcel, brut de téléchargement (on appelle la feuille avec un nom très court car elle est souvent utilisée, "$s" pour "sheet") :
$objPHPExcel = new PHPExcel;
$s = $objPHPExcel->getActiveSheet();
$s->setCellValue('A1','Hello');
$s->setCellValueByColumnAndRow(2, 1, 'World!');
$writer = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
// Option 1 : fichier .xlsx apparaissant à côté du .php
$writer->save('./HelloWorld1.xlsx');
// Option 2 : fichier à télécharger par le navigateur
header('Content-Disposition: attachment;filename="HelloWorld2.xlsx"');
$writer->save('php://output');
Ouverture
[modifier | modifier le wikicode]Pour ouvrir et lire un fichier existant :
$objReader = PHPExcel_IOFactory::createReader('Excel2007');
$objPHPExcel = $objReader->load('./HelloWorld1.xlsx');
print $objPHPExcel->getActiveSheet()->getCell('A1')->getValue();
Conversion
[modifier | modifier le wikicode]Conversion d'un XLSX en CSV :
$xlsx = PHPExcel_IOFactory::load('./HelloWorld1.xlsx');
$writer = PHPExcel_IOFactory::createWriter($xlsx, 'CSV');
$writer->setDelimiter(";");
$writer->setEnclosure("");
$writer->save('./HelloWorld1.csv');
Conversion d'un CSV en XLSX :
$objReader = PHPExcel_IOFactory::createReader('CSV');
$objReader->setDelimiter(';');
$objReader->setEnclosure(' ');
$objPHPExcel = $objReader->load('./HelloWorld1.csv');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$objWriter->save('./HelloWorld3.xlsx');
Modifications
[modifier | modifier le wikicode]Les propriétés des cellules sont présentées sous forme de tableaux multidimensionnels :
$style = array(
'borders' => array(
'outline' => array(
'style' => PHPExcel_Style_Border::BORDER_THICK,
'color' => array('argb' => 'FFFF0000'),
),
),
'font' => array(
'bold' => true,
'name' => 'Tahoma',
'size' => 10,
'color' => array('rgb' => 'FF0000'),
),
'fill' => array(
'type' => PHPExcel_Style_Fill::FILL_SOLID,
'color' => array('rgb' => 'C3C3E5')
),
'alignment' => array(
'horizontal' => PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true // retour à la ligne automatique
)
);
// Ajout du style ci-dessus en feuille 2 d'un nouveau fichier
$objPHPExcel = new PHPExcel;
$writer = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$s = $objPHPExcel->createSheet();
$s->setTitle('Feuille style');
$s = $objPHPExcel->setActiveSheetIndex($objPHPExcel->getSheetCount()-1);
$s->setCellValue('A1','Hello style');
$s->getStyle('A1')->applyFromArray($style);
$s->getStyle('B1')->getNumberFormat()->setFormatCode( PHPExcel_Style_NumberFormat::FORMAT_TEXT );
$s->setCellValue('B1','9999999999999999999'); // Sans le format texte les nombres de plus de 15 chiffres sont arrondis
$writer->save('./HelloWorld4.xlsx');
On peut aussi :
// Récupérer la dernière ligne d'une feuille
$ligne = $s->getHighestRow();
// Insérer une ligne
$s->insertNewRowBefore($ligne + 1, 1);
Références
[modifier | modifier le wikicode]Voir aussi
[modifier | modifier le wikicode]- https://github.com/PHPOffice/PhpSpreadsheet (.xlsx et .ods)
- https://github.com/PHPOffice/PHPPresentation (PowerPoint)
PHPMailer
PHPMailer est une bibliothèque open source[1] pour envoyer des emails plus rapidement qu'à partir de la commande mail().
Installation
[modifier | modifier le wikicode]Télécharger sur GitHub ou bien ajouter à composer.json : "phpmailer/phpmailer": "~5.2".
Utilisation
[modifier | modifier le wikicode]// Pour la v5.0.0 (2009)
require_once('PHPMailer/class.phpmailer.php');
// Pour la v5.2.14 (2016)
require('PHPMailer/PHPMailerAutoload.php');
Exemple
[modifier | modifier le wikicode]Exemple de base :
$mail = new PHPMailer();
$mail->Subject = 'Hello World!';
$mail->SetFrom('expediteur@mon_domaine.com');
$mail->AddAddress('destinataire1@son_domaine.com');
$mail->MsgHTML('Corps de l\'email');
if (!$mail->Send()) {
echo 'Erreur : ' . $mail->ErrorInfo;
} else {
echo 'Message envoyé !';
}
Bien sûr, on peut ensuite ajouter en une ligne une pièce jointe, une copie cachée, une signature DKIM...
Références
[modifier | modifier le wikicode]
PHPWord
Introduction
[modifier | modifier le wikicode]Cette bibliothèque open source permet de lire et d'écrire des documents de traitement de texte.
Il faut la télécharger sur https://github.com/PHPOffice/PHPWord.
composer require phpoffice/phpword
RabbitMQ
RabbitMQ est un logiciel de messages en protocole AMQP. Il permet donc à des processus de produire des messages JSON dans des files d'attente pour que d'autres les consomme ensuite[1].
Installation
[modifier | modifier le wikicode]Client PHP
[modifier | modifier le wikicode]composer require php-amqplib/php-amqplib
Serveur
[modifier | modifier le wikicode]L'installation du serveur est multi-plateforme. Sous Linux[2] :
apt-get install rabbitmq-server
Test de fonctionnement :
telnet localhost 5672
Site de gestion
[modifier | modifier le wikicode]Une interface graphique existe pour lire et manipuler les messages manuellement, c'est le management plugin[3]. Pour l'activer :
/usr/sbin/rabbitmq-plugins enable rabbitmq_management
Test de fonctionnement depuis le serveur :
curl localhost:15672
Depuis le client : http://mon_serveur:15672
On la trouve aussi sur Docker[4].
Pour trouver le fichier de configuration : cat /usr/sbin/rabbitmq-server | grep RABBITMQ_ENV
Connexion
[modifier | modifier le wikicode]Les identifiants par défaut de RabbitMQ dépendent des versions. On trouve soit le login / mot de passe "user / password", soit "guest / guest". Pour tester :
curl -i -u guest:guest http://localhost:15672/api/whoami
Si cela ne fonctionne pas, configurer le serveur avec rabbitmqctl. Exemple sous Linux :
/usr/sbin/rabbitmqctl add_user userDev mon_mot_de_passe
/usr/sbin/rabbitmqctl set_permissions -p / userDev '.*' '.*' '.*'
/usr/sbin/rabbitmqctl set_user_tags userDev management
/usr/sbin/rabbitmqctl list_users
Connexion PHP
[modifier | modifier le wikicode] $connection = new AMQPStreamConnection($host, $port, $login, $password);
...
$connection->close();
Création de queue et routage
[modifier | modifier le wikicode]Pour créer une queue simple prête à recevoir des messages :
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue1', false, false, false, false);
Exchange
[modifier | modifier le wikicode]| Schéma des différents types de routage RabbitMQ sur le site : (en) Jyoti Sachdeva, « Getting Started With RabbitMQ: Python », | |
Une autre manière de poster des messages est en passant par un exchange. On en distingue plusieurs types[5] :
- direct : une seule queue recevra le message (patron de conception producteur/consommateur).
- fanout : toutes les queues liée à l’exchange recevront le message (patron de conception producteur/abonné).
- topic : les queues de l’exchange inscrites aux sujets concernés recevront le message (selon un motif dans la "routing key" où "*" représente un seul mot séparé par un point, et "#" au moins un)[6].
- headers : routage par en-tête de message plutôt que par "routing key".
Dans cet exemple, on rattache la queue à un exchange "Bus" :
$this->rabbitMqConnection->getChannel()->exchange_declare('Bus', 'fanout', false, true, false);
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue2', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue2', 'Bus');
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue3', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue3', 'Bus');
Exemple de topic : on ne publie pas dans la queue mais dans l’exchange qui leur routera ensuite le message.
$this->rabbitMqConnection->getChannel()->exchange_declare('Topic_bus', 'topic', false, false, false);
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue4', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue4', 'Topic_bus');
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue5', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue5', 'Topic_bus');
QoS
[modifier | modifier le wikicode]Pour demander à RabbitMQ de ne pas surcharger les consommateurs d'une queue en leur répartissant les messages que s'ils ont terminé de traiter le précédent :
$this->rabbitMqConnection->getChannel()->basic_qos(null, 1, null);
DLX
[modifier | modifier le wikicode]Le mode DLX (Dead Letter Exchanges) permet de transférer un message d'une queue dans une autre après un certain temps[7].
Production
[modifier | modifier le wikicode] $amqpMessage = new AMQPMessage(json_encode('Hello World!'),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$this->rabbitMqConnection->getChannel()->basic_publish($amqpMessage, 'Bus', 'Wikibooks.Queue1');
Consommation
[modifier | modifier le wikicode]Par défaut on consomme un seul message de la queue. Pour tous les lire un par un, utiliser basic_ack() après basic_consume().
$this->rabbitMqConnection->getChannel()->basic_consume(
'Wikibooks.Queue1',
gethostname() . '#' . rand(1, 9999),
false,
false,
false,
false,
[$this, 'consumeCallback']
);
while (count($this->rabbitMqConnection->getChannel()->callbacks)) {
$this->rabbitMqConnection->getChannel()->wait();
}
public function consumeCallback(?AMQPMessage $msg)
{
if (empty($msg)) {
return null;
}
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
var_dump(json_decode($msg->getBody()));
}
En mode "topic", on peut remplacer 'Wikibooks.Queue1' par 'Wikibooks.*' pour récupérer toutes les queues.
Références
[modifier | modifier le wikicode]- ↑ https://www.rabbitmq.com/tutorials/tutorial-one-php.html
- ↑ https://www.rabbitmq.com/install-debian.html
- ↑ https://www.rabbitmq.com/management.html
- ↑ https://hub.docker.com/_/rabbitmq
- ↑ https://www.rabbitmq.com/tutorials/tutorial-three-php.html
- ↑ https://www.rabbitmq.com/tutorials/tutorial-five-php.html
- ↑ https://www.rabbitmq.com/dlx.html
Cadriciels
Les cadriciels, plus connus sous le terme framework, sont un ensemble de bibliothèques, scripts destinés aux projets professionnels et au travail en équipe.
Issu d'une longue réflexion et de maturation, le cadriciel permet au développeur de s'affranchir des contingences de sécurité, ergonomie (bouton, habillage) afin de ne se concentrer que sur les fonctionnalités de son application.
Sur le modèle des cadriciels Java ou Ruby on rails, certains cadriciels PHP se démarquent comme CakePHP, PRADO et Symfony.
Modèle-vue-contrôleur
[modifier | modifier le wikicode]Ils se basent sur le plus utilisé des patrons de conception : le modèle-vue-contrôleur qui sépare le modèle de données, l'interface utilisateur et les traitements, ce qui donne trois parties fondamentales dans l'application finale : le modèle, la vue et le contrôleur.
- Le modèle ne s'occupe que du traitements des données, lire depuis une base de données, insérer des lignes dans une base, vérifier que les données sont bien formatées (validation).
- La vue correspond à tout ce qui concerne l'affichage pour l'utilisateur. Cela ne contient généralement que des modèles (template) de pages avec la logique de présentation. C'est une caractéristique forte de MVC, seule cette partie est à mettre-à-jour pour changer l'apparence de votre application.
- Le contrôleur prend en charge le déroulement du programme. La liste des actions sera dans le contrôleur.
Le programmeur peut être dérouté par la séparation en morceaux imposée mais le premier objectif de MVC est la maintenance à long terme.
Voir aussi
[modifier | modifier le wikicode]- Liste de frameworks PHP sur Wikipédia

CakePHP
Le cadriciel CakePHP
[modifier | modifier le wikicode]CakePHP est un framework de type Rapid Application Developpement (RAD) utilisant le motif de conception modèle-vue-contrôleur.
Le moyen d'appréhender l'intérêt d'un cadriciel est par l'exemple. Après l'installation nous l'emploierons pour créer un formulaire simple d'enregistrement des données d'un utilisateur (nom, prénom, email, mot de passe) stockée dans une table de base de données. Ensuite, ce formulaire sera amélioré par l'ajout de vérifications et de validation des données. Pour conclure, l'on ajoutera une page d'identification.
Ce chapitre va décrire les grandes étapes d'installation puis de configuration du cadriciel CakePHP dans un but pédagogique. Pour une installation de production la référence reste le manuel de l'éditeur[1].
Prérequis
[modifier | modifier le wikicode]Coté serveur l'installation nécessite un serveur HTTP comme Apache avec le module de réécriture d'URL activé (rewrite), le langage PHP 5 avec la bibliothèque des sessions et un système de base de données PostgreSQL ou MySQL.
Connaissances
[modifier | modifier le wikicode]- PHP 5 objet : Programmation PHP/La programmation orientée objet
- Avoir complété : Programmation_PHP/Exemples/Formulaire
- Être au moins débutant en programmation SQL
- Utilisation courante d'une base de donnée relationnelle : Découverte de MySQL, PostgreSQL et Oracle
Outils
[modifier | modifier le wikicode]Installation
[modifier | modifier le wikicode]Base de données
[modifier | modifier le wikicode]Créez une nouvelle base de données cake_monapp pour l'utilisateur propriétaire cakedev :
MySQL :
CREATE DATABASE cake_monapp;
GRANT ALL PRIVILEGES ON cake_monapp.* TO 'cakedev'@'localhost' IDENTIFIED BY 'plop';
PostgreSQL :
CREATE USER cakedev WITH PASSWORD 'plop';
CREATE DATABASE cake_monapp OWNER cakedev;
GRANT ALL PRIVILEGES ON DATABASE cake_monapp TO cakedev;
Dans /cake/app/config/ renommez database.php.default en database.php puis modifiez y les paramètres de connexion à la base de donnée (nom de serveur, nom de base, utilisateur, mot de passe).
/cake/app/config/database.php :
Pour MySQL :
var $default = array('driver' => 'mysql',
'connect' => 'mysql_connect',
'host' => 'localhost',
'login' => 'cakedev',
'password' => 'plop',
'database' => 'cake_monapp',
'prefix' => '');
Pour PostgreSQL :
var $default = array('driver' => 'postgres',
'connect' => 'pg_connect',
'host' => 'localhost',
'login' => 'cakedev',
'password' => 'plop',
'database' => 'cake_monapp',
'prefix' => '');
Configurations
[modifier | modifier le wikicode]Toujours dans /cake_xxx/app/config/ modifiez core.php en changeant la constante CAKE_SESSION_STRING par une chaîne aléatoire, avec par exemple en ligne de commande :
perl -e '@c=("A".."Z","a".."z",0..9);print join("",@c[map{rand @c}(1..36)]),"\n"'
Rendre /cake_xxx/app/tmp accessible en écriture.
$ chmod -R a+x tmp
Vérifiez que le module de réécriture d'URL est activé pour le serveur web Apache :
Configuration de votre EDI
[modifier | modifier le wikicode]Vous créerez un nouveau projet qui contiendra la copie locale de l'arborescence sous app/.
Notes et références
[modifier | modifier le wikicode]
PEAR
Qu'est-ce que PEAR ?
[modifier | modifier le wikicode]PEAR, acronyme de PHP Extension and Application Repository, est un framework PHP libre issu d'un groupe de développeurs qui proposent des extensions PHP en garantissant un code de qualité. La liste complète des extensions est téléchargeable gratuitement sur le site officiel[1].
Pour l'installer le framework, il y a quatre solutions :
- Télécharger les packages un par un sur http://pear.php.net/packages.php.
- Télécharger le gestionnaire sur https://pear.php.net/go-pear, et le lancer (ex : http://localhost/Frameworks/go-pear.php). Avec l'installation par défaut, les fichiers sont téléchargés dans un sous-dossier "PEAR".
- La commande
pear install Nom_du_package. - La commande
php pyrus.phar install pear/Nom_du_package.
DB
[modifier | modifier le wikicode]L'extension PEAR DB fournit une gamme de fonctions de gestion de base de données permettant d'utiliser le même code quel que soit la base de données. Cela permet, si vous décidez de changer de BDD de ne pas être obligé de modifier de nouveau tous vos scripts. Un simple changement de variable vous permettra de passer de MySQL à Oracle par exemple.
Elle n'est plus maintenue depuis 2015[2], ce qui offre un inconvénient par rapport à PEAR MDB2.
Connexion à la base
[modifier | modifier le wikicode]Se connecter à une base de données revêt la syntaxe suivante :
require_once('DB.php'); // Indispensable
$dbType = "mysql";
$host = "127.0.0.1";
$account = "Mon_Compte";
$pass = "Mon_Mot_de_passe";
$dbName = "Ma_Base";
$dsn = "$dbType://$account:$pass@$host/$dbName";
$db = DB::connect($dsn);
if (PEAR::isError($db)) {
echo "Erreur : ".$db->getMessage();
}
Il est également possible de remplacer la chaîne de caractères par un tableau contenant vos informations :
$dsn = array(
'phptype' => 'mysql',
'username' => 'myAccount',
'password' => '****',
'hostspec' => '127.0.0.1',
'database' => 'tests',
);
Vous êtes donc connectés à votre base de données. Il s'agit maintenant d'effectuer des opérations avec celle-ci.
Fermeture de la connexion
[modifier | modifier le wikicode]Il est important de fermer votre connexion une fois vos opérations terminées pour augmenter la sécurité de votre code, réduisant les risques d'atteinte à vos données par un individu mal intentionné. Voici donc le code détruisant la connexion :
$db->disconnect();
Envoyer une requête
[modifier | modifier le wikicode]Une fois connecté, vous allez pouvoir envoyer des requêtes à votre BDD comme suit :
$query = "SELECT * FROM table WHERE id=5";
$rsc = $db->query($query);
Récupérer des informations
[modifier | modifier le wikicode]Comme avec n'importe quelle base de données, vous aurez à récupérer le résultat de votre requête. Voici une fonction équivalente de mysql_fetch_array() :
$query = "SELECT * FROM table WHERE id=5";
$rsc = $db->query($query);
if ( DB::isError($rsc) )
die($rsc->getMessage());
while($result = $rsc->fetchRow(DB_FETCHMODE_ASSOC)) {
echo $result['id']."\n";
}
MDB2
[modifier | modifier le wikicode]Cette bibliothèque issue de la précédente, offre une API de gestion des SGBD. Elle se télécharge sur http://pear.php.net/package/MDB2.
Spreadsheet_Excel_Writer
[modifier | modifier le wikicode]Cette bibliothèque fournit des classes de manipulation de fichier .xls sont[3][4].
Pour l'installer, il faut simplement télécharger le paquetage Spreadsheet_Excel_Writer, qui utilise OLE et Getopt.
Pour l'utiliser :
include "Spreadsheet/Excel/Writer.php;
Limites
[modifier | modifier le wikicode]Cette bibliothèque n'est plus maintenue depuis 2012[5], on lui préfèrera donc PHPExcel[6][7], qui gère en plus l'auto-ajustement, les filtres, et les XLSX (plus de limite de 65 536 lignes par feuille).
D'autant plus qu'elle remplit rapidement les logs avec des centaines de warnings : Object of class Spreadsheet_Excel_Writer_Format could not be converted to int.
phpDocumentor
[modifier | modifier le wikicode]Ce générateur de documentation analyse aussi le code. Ainsi, on peut décrire dans l'annotation qui précède une méthode, les types de ses arguments ou de son résultat[8] :
/**
* @param bool $condition
*
* @return String|null
*/
Une méthode qui hérite d'une autre peut aussi hériter de sa doc : @inheritdoc
PECL
[modifier | modifier le wikicode]PECL (prononcé "pickle", pour PHP Extension Community Library), est un gestionnaire de paquets. Exemples :
sudo pecl install memcached
sudo pecl install redis
sudo pecl install apcu
sudo pecl install xdebug
sudo pecl install amqp
sudo pecl install igbinary
sudo pecl install imagick
Pour désinstaller :
sudo pecl uninstall memcached
Sur un serveur avec plusieurs versions de PHP actives, il peut être nécessaire de préciser sur laquelle installer le paquet au préalable[9]. Sinon l'erreur suivante survient : Unable to load dynamic library 'memcached.so'. Plusieurs solutions :
export PATH="/usr/local/opt/php@7.2/bin:$PATH"
export PATH="/usr/local/opt/php@7.2/sbin:$PATH"
sudo pecl uninstall memcached; sudo pecl install memcached
ou
sudo phpdismod -v 7.2 memcached; sudo phpenmod -v 7.2 memcached
Pour vérifier :
php7.0 -r "phpinfo();" |grep -i memcache
php7.1 -r "phpinfo();" |grep -i memcache
php7.2 -r "phpinfo();" |grep -i memcache
php7.3 -r "phpinfo();" |grep -i memcache
php7.4 -r "phpinfo();" |grep -i memcache
Références
[modifier | modifier le wikicode]
Symfony

- Pour plus de détails voir : Programmation PHP avec Symfony.
Présentation
[modifier | modifier le wikicode]Symfony (parfois abrégé SF) est un cadriciel MVC libre écrit en PHP (> 5). En tant que framework, il facilite et accélère le développement de sites et d'applications Internet et Intranet. Il propose en particulier :
- Une séparation du code en trois couches, selon le modèle MVC, pour une plus grande maintenabilité et évolutivité.
- Des performances optimisées et un système de cache pour garantir des temps de réponse optimums.
- Le support de l'Ajax.
- Une gestion des URL parlantes (liens permanents), qui permet de formater l'URL d'une page indépendamment de sa position dans l'arborescence fonctionnelle.
- Un système de configuration en cascade qui utilise de façon extensive le langage YAML.
- Un générateur de back-office et un "démarreur de module" (scaffolding).
- Un support de l'I18N - Symfony est nativement multi-langue.
- Une architecture extensible, permettant la création et l'utilisation de composants, par exemple un mailer ou un gestionnaire de fichiers .css et .js (minification).
- Des bundles :
- Un templating simple, basé sur PHP et des jeux de "helpers", ou fonctions additionnelles pour les gabarits... Comme alternative au PHP, on peut aussi utiliser le moteur de templates Twig dont la syntaxe est plus simples.
- Une couche de mapping objet-relationnel (ORM) et une couche d'abstraction de données (cf. Doctrine et son langage DQL[1]).
Utilisations
[modifier | modifier le wikicode]Plusieurs autres projets notables utilisent Symfony, parmi lesquels :
- https://github.com/drupal/drupal : le système de gestion de contenu (CMS) Drupal.
- https://github.com/joomla/joomla-cms : le CMS Joomla.
- https://github.com/sulu/sulu : le CMS Sulu.
- https://github.com/Sylius/Sylius : Sylius, un CMS d'e-commerce.
- https://github.com/x-tools/xtools : Xtools, un compteur d'éditions des wikis.
Différences entre les versions
[modifier | modifier le wikicode]Depuis la version 4, des pages récapitulant les nouvelles fonctionnalités sont mises à disposition :
- https://symfony.com/blog/symfony-4-1-curated-new-features (2018)
- https://symfony.com/blog/symfony-4-2-curated-new-features (2018)
- https://symfony.com/blog/symfony-4-3-curated-new-features (2019)
- https://symfony.com/blog/symfony-4-4-curated-new-features (2019)
- https://symfony.com/blog/symfony-5-0-curated-new-features (2019)
- https://symfony.com/blog/symfony-5-1-curated-new-features (2020)
- https://symfony.com/blog/symfony-5-2-curated-new-features (2020)
- https://symfony.com/blog/symfony-5-3-curated-new-features (2021)
- https://symfony.com/blog/symfony-6-1-curated-new-features (2022)
- https://symfony.com/blog/symfony-6-2-curated-new-features (2022)
- https://symfony.com/blog/symfony-6-3-curated-new-features (2023)
- https://symfony.com/blog/symfony-7-1-curated-new-features (2024)
- https://symfony.com/blog/symfony-7-2-curated-new-features (2024)
- https://symfony.com/blog/symfony-7-3-curated-new-features (2025)
Créer un projet
[modifier | modifier le wikicode]
Pour créer un nouveau projet sous Symfony, tapez la commande suivante :
composer create-project "symfony/skeleton:^7.3" mon_projet
ou avec Symfony CLI :
wget https://get.symfony.com/cli/installer -O - | bash symfony new mon_projet
Cette commande a pour effet la création d'un dossier contenant les bases du site web à développer.
Lancer le projet
[modifier | modifier le wikicode]On entend par cette expression le lancement d'un serveur web local pour le développement de l'application et le choix d'un hébergeur pour la déployer (autrement dit "la mettre en production").
Serveur web de développement
[modifier | modifier le wikicode]Symfony intègre un serveur web local qu'on peut lancer avec la commande (se placer dans le répertoire du projet auparavant) :
$ symfony server:start -d
En passant open:local en argument de la commande symfony, le projet s'ouvre dans un navigateur :
$ symfony open:local
Ou bien en utilisant le serveur web intégré à php
$ php -S localhost:8000 -t public
Serveur web de production
[modifier | modifier le wikicode]Pour le déploiement dans le monde "réel", il faut choisir un hébergeur web sur internet supportant PHP (nous l’appellerons "serveur web distant" pour le distinguer du précédent). Voici quelques exemples :
- https://www.lws.fr/hebergement_web.php
- https://www.hostinger.fr/hebergeur-web
- et surtout... https://symfony.com/cloud/
Autrement il est aussi possible d'installer un deuxième serveur web (autre que celui intégré à Symfony) sur sa machine pour se rendre compte du résultat final. Par exemple... Apache qui est très répandu chez les hébergeurs professionnels. Il faudra alors ajouter un vhost et un nom de domaine dédiés au site Symfony[2][3]. Pour le test, le domaine peut juste figurer dans /etc/hosts.
Le nom de domaine du site doit absolument rediriger vers le dossier /public. En effet, si on cherche à utiliser le site Symfony dans le sous-répertoire "public" d'un autre site, la page d'accueil s'affichera mais le routing ne fonctionnera pas.
Configurer le projet
[modifier | modifier le wikicode]Paramètres dev et prod
[modifier | modifier le wikicode]Les différences de configuration entre le site de développement et celui de production (par exemple les mots de passe) peuvent être définies de deux façons :
- Dans le dossier
config/packages. config.yml contient la configuration commune aux sites, config_dev.yml celle de développement et config_prod.yml celle de production. - Via le composant Symfony/Dotenv (abordé au chapitre suivant).
Par exemple, on constate l'absence de la barre de débogage (web_profiler) par défaut en prod. Une bonne pratique serait d'ajouter au config_dev.yml :
web_profiler:
toolbar: true
intercept_redirects: false
twig:
cache: false
# Pour voir tous les logs dans la console shell (sans paramètre -vvv)
monolog:
handlers:
console:
type: console
process_psr_3_messages: false
channels: ['!event', '!doctrine', '!console']
verbosity_levels:
VERBOSITY_NORMAL: DEBUG
Les fichiers .yml contenant les variables globales sont dans app\config\.
Par exemple en SF2 et 3, le mot de passe et l'adresse de la base de données sont modifiables en éditant parameters.yml (non versionné et créé à partir du parameters.yml.dist). L'environnement de test passe par web/app_dev.php, et le mode debug y est alors activé par la ligne Debug::enable(); (testable avec %kernel.debug% = 1).
Depuis SF4, il faut utiliser un fichier .env non versionné à la racine du projet, dont les lignes sont injectées ensuite dans les .yaml avec la syntaxe : '%env(APP_SECRET)%'. Le mode debug est activé avec APP_DEBUG=1 dans ce fichier .env.
Les variables d'environnement du système d'exploitation peuvent remplacer celles des .env.
Upgrade de version majeure
[modifier | modifier le wikicode]Installation ou mise à jour des versions précédentes :
Références
[modifier | modifier le wikicode]- « Wiki officiel »
- « Tutoriel openclassrooms.com »
- « Tutoriel developpez.com »
- (en) « Symfony 3.1 cookbook » : livre officiel de 500 pages
- (en) « Charming Development in Symfony 5 » (texte et vidéo)
Voir aussi
[modifier | modifier le wikicode]- #symfony : canal IRC (#symfony sur Freenode)
- #symfony-fr : canal IRC francophone (#symfony-fr sur Freenode)
- https://sonata-project.org/get-started : un CMS basé sur Symfony
Principe
[modifier | modifier le wikicode]Le principe des services Symfony est d'éviter d'instancier la plupart des classes manuellement (avec des "new" dispersés dans le code), mais de les déclarer une seule fois automatiquement, grâce au container. Elles sont alors instanciées uniquement si elles sont utilisées dans l'instance PHP (ex : sur la page web courante), grâce au lazy loading du container[1].
Les instanciations sont configurées par défaut dans config/services.yaml, mais peuvent aussi se faire en PHP.
On peut aussi baptiser chaque service individuellement (il peut y en avoir plusieurs par classe), et on appelle ses arguments par leur nom de service. Exemple :
services:
app.my_namespace.my_service:
class: App\myNamespace\myServiceClass
arguments:
- '%parameter%'
- '@app.my_namespace.my_other_service'
Les property hooks apparus avec PHP 8.4 ne sont pas compatibles avec l’injection de dépendances Symfony 7.3, car il les bypasse.
Pas de include ou require
[modifier | modifier le wikicode]Symfony gère toutes les inclusions grâce aux use.
Par exemple, les classes natives de PHP doivent être introduites par leur namespace ou bien par l'espace de nom global. Ex :
use DateTime;
echo new DateTime();
ou
echo new \DateTime();
Autowiring
[modifier | modifier le wikicode]Avant SF2.8, il était obligatoire de déclarer chaque service dans les fichiers de configuration .yml ou .yaml, en plus de leurs classes .php (qui peuvent se contredire), et de les mettre à jour à chaque changement de structure.
Depuis SF2.8, l'"autoconfigure: true" permet de déclarer automatiquement chaque service à partir de sa classe, et l'"autowiring: true" d'injecter automatiquement les arguments connus (ex : une autre classe appelée par son espace de nom et son nom), donc sans déclaration manuelle[2].
Depuis SF4, cette déclaration est par défaut sans le fichier services.yaml, mais on peut la placer dans un autre fichier qui sera importé par le premier, par exemple avec :
imports:
- { resource: services1.yaml }
- { resource: services2.yaml }
ou :
imports:
- { resource: services/* }
Cette séparation des services en plusieurs .yaml nécessite par contre d'exclure les dossiers de ces services de l'autowiring, et de reprendre la section _defaults dans le nouveau .yaml.
Exemple d'exclusion récursive de plusieurs dossiers de même nom, avec ** :
App\:
resource: '../src/*'
exclude:
- '../src/UnDossier'
- '../src/**/Entity' # Tous les sous-dossiers "Entity"
bind
[modifier | modifier le wikicode]Par défaut, l'autowiring ne fonctionne pas avec les classes avec des tags, ou ayant autre chose que des services dans leurs constructeurs[3]. Néanmoins pour injecter des scalaires automatiquement, il suffit que ces derniers soit déclarés aussi. Ex :
services:
_defaults:
bind:
$salt: 'ma_chaine_de_caractères'
$variableSymfony: '%kernel.project_dir%'
$variableDEnvironnement: '%env(resolve:APP_DEBUG)%'
_instanceof
[modifier | modifier le wikicode]Pour ajouter un tag ou injecter un service si on implémente une interface. Ex :
services:
_instanceof:
Psr\Log\LoggerAwareInterface:
calls:
- [ 'setLogger', [ '@logger' ] ]
Ici, toutes les classes qui implémentent LoggerAwareInterface verront leurs méthodes setLogger(LoggerInterface $logger) appelées automatiquement à l’instanciation.
En SF <2.8
[modifier | modifier le wikicode]Les contrôleurs sont des services qui peuvent en appeler avec la méthode héritée de leur classe mère :
$this->get('app.my_namespace.my_service')
Pour déterminer si un service existe depuis un contrôleur :
$this->getContainer->hasDefinition('app.my_namespace.my_service')
Paramètres
[modifier | modifier le wikicode]Chaque service doit donc être déclaré avec un paramètre "class", puis peut ensuite facultativement contenir les paramètres suivants :
| Nom | Rôle |
|---|---|
| class | Nom de la classe instanciée par le service. |
| arguments | Tableau des arguments du constructeur de la classe, services ou variables. |
| calls | Tableau des méthodes de la classe à lancer après l'instanciation, généralement des setters. |
| factory | Instancie la classe depuis une autre classe donnée. Méthode statique de la classe qui sera renvoyée par le service[4]. |
| configurator | Exécute un invocable donné après l'instanciation de la classe[5]. |
| alias | Crée un autre nom pour un service, qui peut alors être modifié par d'autres paramètres de déclaration (ex : créer une version publique d'un service privé dans services_test.yaml[6]). |
| parent | Nom de la superclasse. |
| abstract | Booléen indiquant si la méthode est abstraite. |
| public | Booléen indiquant une portée publique du service. |
| shared | Booléen indiquant un singleton. |
| tags | Quand on doit injecter un nombre indéterminé de services dans un autre, il est possible de le définir avec chacun des services à injecter, en y ajoutant un tag avec le nom du service qui peut les appeler. Ce tag doit néanmoins être défini dans un CompilerPass[7]. |
| autowire | Booléen vrai par défaut, spécifiant si le framework doit injecter automatiquement les arguments du constructeur. |
| decorates | Remplace un service par sa version décorée (mais l'ancien est toujours accessible an ajoutant le suffixe .inner au service décorateur)[8] |
Injecter des services tagués
[modifier | modifier le wikicode]Dans un constructeur :
App\Service\FactoriesHandler:
arguments:
- !tagged_iterator app.factory
Dans une autre méthode :
App\Service\FactoriesHandler:
calls:
- [ 'setFactories', [!tagged_iterator app.factory] ]
Par défaut, l'itérateur contient des clés numériques, mais on peut les personnaliser[9]. Ex :
App\Factory\FactoryOne:
tags:
- { name: 'app.factory', my_key: 'factory_one' }
App\Service\FactoriesHandler:
arguments:
- !tagged_iterator { tag: 'app.factory', key: 'my_key' }
Service abstrait
[modifier | modifier le wikicode]Un service abstrait est un système de factorisation des injections par l'intermédiaire d'une classe abstraite. Par exemple si on veut que tous les contrôleurs héritent du service logger (comme l'exemple _instanceof ci-dessus), plus la méthode setLogger() de leur classe abstraite, sans avoir à toucher à leurs constructeurs :
App\Controller\:
resource: '../src/Controller'
parent: App\Controller\AbstractEntitiesController
tags: ['controller.service_arguments']
App\Controller\AbstractEntitiesController:
abstract: true
autoconfigure: false
calls:
- [ 'setLogger', [ '@logger' ] ]
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/3.4/service_container.html
- ↑ https://symfony.com/doc/current/service_container/autowiring.html
- ↑ https://symfony.com/doc/current/service_container/autowiring.html#fixing-non-autowireable-arguments
- ↑ https://symfony.com/doc/current/service_container/factories.html
- ↑ https://symfony.com/doc/current/service_container/configurators.html
- ↑ https://symfony.com/doc/current/testing.html
- ↑ https://symfony.com/doc/current/service_container/compiler_passes.html
- ↑ https://symfony.com/doc/current/service_container/service_decoration.html
- ↑ https://symfony.com/doc/5.4/service_container/tags.html#tagged-services-with-index
Principe
[modifier | modifier le wikicode]Les contrôleurs Symfony sont les classes qui définissent les opérations à réaliser quand on visite les pages du sites[1] : elles transforment une requête HTTP en réponse (JSON, XML (dont HTML), etc.).
Par convention, leurs noms se terminent par Controller, les noms de leurs méthodes se terminent par "Action", et les URL qui provoquent leurs exécutions sont définies dans leurs annotations. L'exemple suivant affiche un texte quand on visite l'adresse "/" ou "/helloWorld" :
class HelloWorldController extends AbstractController
{
#[Route(path: '/', name: 'helloWorld')]
#[Route(path: '/helloWorld', name: 'helloWorld')]
public function indexAction(Request $request): Response
{
return new Response('Hello World!');
}
}
NB : en PHP < 8, remplacer l'attribut par une annotation :
/**
* @Route("/", name="helloWorld")
* @Route("/helloWorld")
*/
Retours
[modifier | modifier le wikicode]Ces méthodes peuvent déboucher sur plusieurs actions :
Response(): affiche un texte, et facultativement un code HTTP en deuxième paramètre (ex : erreur 404).JsonResponse()ou$this->json(): affiche du JSON.RedirectResponse(): renvoie vers une autre adresse. Si elle se trouve dans la même application, on peut aussi utiliser le$this->forward()hérité du contrôleur abstrait.BinaryFileResponse(): renvoie un fichier à télécharger (à partir de son chemin).
$this->redirect('mon_url'): redirige à une autre adresse.$this->redirectToRoute('nom_de_la_route');: redirige vers une route du site par son nom.$this->generateUrl('app_mon_chemin', []);: redirige vers une URL relative (ajouterUrlGeneratorInterface::ABSOLUTE_URLen paramètre 3 pour l'absolue, car il est àUrlGeneratorInterface::ABSOLUTE_PATHpar défaut dans SF3).$this->container->get('router')->generate('app_mon_chemin', ['paramètre' => 'mon_paramètre']);.$this->render(): affiche une page à partir d'un template, par exemple HTML ou Twig.
$response = new JsonResponse();
$response->setEncodingOptions(JSON_UNESCAPED_UNICODE);
$response->setData($data);
return $response;
Requêtes
[modifier | modifier le wikicode]L'objet Request est à préférer à la variable superglobale $_REQUEST, car il fournit une sécurité et des méthodes de manipulation. Ex :
- $request->getMethod() : la méthode HTTP utilisée.
- $request->query : les arguments $_GET (query param).
- $request->request : les arguments $_POST (lui préférer $request->getContent()).
- $request->files : les fichiers $_FILES (dans un itérable FileBag).
ParamConverter
[modifier | modifier le wikicode]On peut injecter un ID dans l'URL ou la requête pour le CRUD d'une entité, mais grâce au paramConverter on peut aussi injecter directement l'entité. Ex :
#[Route('/my_entity/{id}', methods: ['GET'])]
public function getProduct(MyEntity $myEntity): JsonResponse
{
return new JsonResponse($myEntity);
}
Avant Symfony 6.2 cela fonctionne avec un composer require sensio/framework-extra-bundle.
Flashbag
[modifier | modifier le wikicode]On peut aussi ajouter un bandeau de message temporaire en en-tête via :
$this->addflash('success', 'mon_message');
Le Twig peut les récupérer ensuite avec[2] :
{% for flashMessage in app.session.flashbag.get('success') %}
{{ flashMessage }}
{% endfor %}
En effet, ils sont stockés dans un Flashbag : un objet de session.
De plus, il en existe plusieurs types (chacun avec une couleur) : success, notice, info, warning, error.
Le fait de lire les flash (au moins depuis les Twig avec app.flashes) vide leur tableau.
Accès aux paramètres et services
[modifier | modifier le wikicode]Les contrôleurs étendent la classe abstraite Symfony\Bundle\FrameworkBundle\Controller\AbstractController. Cela leur permettait entre autres dans Symfony 2, de récupérer les services et paramètres ainsi :
dump($this->get('session'));
dump($this->getParameter('kernel.project_dir'));
Depuis Symfony 4, il faut injecter le service service_container pour accéder à la liste des services publics (public: true en YAML), mais la bonne pratique est d'injecter uniquement les services nécessaires dans le constructeur[3][4].
Les paramètres sont ceux des fichiers .yml du dossier "config", mais plusieurs autres paramètres sont fournis par Symfony :
bin/console debug:container --parameters
- kernel.debug : renvoie vrai si le site est en préprod et faux en prod.
- kernel.project_dir : dossier racine (qui contient bin/, config/, src/, var/, vendor/).
- kernel.build_dir.
- kernel.cache_dir.
- kernel.logs_dir.
- kernel.root_dir : deprecated en SF5.3. Chemin du site dans le système de fichier.
- kernel.bundles : liste JSON des bundles chargés.
Routing
[modifier | modifier le wikicode]Par exemple pour créer une nouvelle page sur l'URL :
http://localhost:8000/test
Installer le routage :
composer require sensio/framework-extra-bundle composer require symfony/routing
Par défaut, la page renvoie l'exception No route found for "GET /test". Pour la créer, il faut d'abord générer un fichier contrôleur (rôle MVC), qui fera le lien entre les URL, les données (modèle) et les pages (vue).
Les URL définies dans l'attribut (ou l'annotation) "route" d'une méthode exécuteront cette dernière :
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class TestController extends AbstractController
{
#[route('/test/{numero}', name: 'test', requirements: ['id' => '\d*'], methods: ['GET', 'POST'], priority: -1)]
public function HelloWorldAction(int $numero = 0)
{
return new Response('Hello World! '.$numero);
}
}
NB : en PHP < 8, remplacer l'attribut par une annotation :
/**
* @Route("/test/{numero}", name="test", requirements={"id"="\d*"}, methods={"GET|POST"}, priority=-1)
*/
Autres exemples de prérequis :
requirements={"id"="fr|en"}requirements={"id"="MaClasse::MA_CONSTANTE1|MaClasse::MA_CONSTANTE2"}requirements={"id"="(?!api/doc|_profiler).*"}
if ($request->get('_route') === 'test').Alias
[modifier | modifier le wikicode]Pour créer des alias, c'est-à-dire plusieurs autres URL pointant vers la page ci-dessus, on peut l'ajouter dans les annotations des contrôleurs, ou bien dans config/routes.yaml (anciennement app\config\routing.yml sur Symfony < 4) :
test:
path: /test/{numero}
defaults: { _controller: AppBundle:Test:HelloWorld }
À présent http://localhost:8000/test/1 ou http://localhost:8000/test/2 affichent "Hello World!".
- Une fois le YAML sauvegardé, l'URL fournie en annotation (/test) ne fonctionne plus.
- S'il y a des annotations précédant @Route dans le même bloc, cela peut inhiber son fonctionnement.
Redirection vers la dernière page visitée
[modifier | modifier le wikicode]Une astuce pour rediriger l'utilisateur vers la dernière page qu'il avait visité :
$router = $this->get('router');
$lastPage = $request->getSession()->get('last_view_page');
$parameterLastPage = $router->match($lastPage);
$routeLastPage = $parameterLastPage['_route'];
unset($parameterLastPage['_route']); // Pour ne pas la voir dans l'URL finale
return $this->redirect(
$this->generateUrl($routeLastPage, $parameterLastPage)
);
Annotations.yaml
[modifier | modifier le wikicode]Ce fichier permet de définir des groupes de contrôleurs, dont les routes sont préfixées. Ex :
back_controllers:
resource: ../../src/Controller/BackOffice
type: annotation
prefix: admin
front_controllers:
resource: ../../src/Controller/FrontOffice
type: annotation
prefix: api
Dans le cas où les contrôleurs ont des contrôles d'accès différents dans security.yaml, il est impératif de les préfixer ainsi pour éviter toute collision des gardiens.
Paramètres spéciaux
[modifier | modifier le wikicode]Il existe quatre paramètres spéciaux que l'on peut placer dans routes.yaml ou en argument des méthodes des contrôleurs[6] :
- _controller : contrôleur appelé par le chemin.
- _ format : format de requête (ex : html, xml).
- _fragment : partie de l'URL après "#".
- _locale : langue de la requête (code ISO, ex : fr, en).
Exemple :
#[Route('/controller_route', requirements: ['_locale' => 'en|fr'])]
class MyController extends AbstractController
Vue
[modifier | modifier le wikicode]Pour commencer à créer des pages plus complexes, il suffit de remplacer :
return new Response('Hello World!');
par une vue issue d'un moteur de template. Celui de Symfony est Twig :
return $this->render('helloWorld.html.twig');
Pour installer les bibliothèques JavaScript qui agiront sur ces pages, se positionner dans /public. Exemple :
cd public/ sudo apt-get install npm npm install --save jquery npm install --save bootstrap
Ensuite il suffit de les appeler dans /templates/helloWorld.html.twig pour pouvoir les utiliser :
<link rel="stylesheet" href="{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}">
<script type="text/javascript" src="{{ asset('node_modules/jquery/dist/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('node_modules/bootstrap/dist/js/bootstrap.min.js') }}"></script>
Modèle
[modifier | modifier le wikicode]Pour gérer le modèle du MVC, c'est-à-dire la structure des données stockées, l'ORM officiel de Symfony se nomme Doctrine.
Par défaut, ses classes sont :
- src/Entity : les entités, reflets des tables.
- src/Repository : les requêtes SELECT SQL (ou find MongoDB).
Tester un contrôleur
[modifier | modifier le wikicode]- Pour plus de détails voir : Programmation PHP avec Symfony/HttpClient#Tests.
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/controller.html
- ↑ https://stackoverflow.com/questions/14449967/symfony-setting-flash-and-checking-in-twig
- ↑ https://symfony.com/doc/current/best_practices.html#use-dependency-injection-to-get-services
- ↑ https://symfony.com/doc/4.0/best_practices/controllers.html#fetching-services
- ↑ https://symfony.com/doc/current/security/voters.html
- ↑ https://symfony.com/doc/current/routing.html#special-routing-parameters
Principe
[modifier | modifier le wikicode]Les commandes sont, avec les contrôleurs, les seuls points d'entrée permettant de lancer le programme. Ce sont aussi des services mais elles se lancent via la console (en CLI).
La liste des commandes disponibles en console est visible avec :
- Sur Linux :
bin\console
- Sur Windows :
php bin\console
Dans Symfony 2 c'était php app\console.
Parmi les principales commandes natives au framework et à ses bundles, on trouve[1] :
php bin/console list: liste toutes les commandes du projet.php bin/console debug:router: liste toutes les routes (URL) du site.php bin/console debug:container --show-hidden: liste tous les services avec leurs alias (qui sont des instanciations des classes).php bin/console debug:container --parameters: liste les paramètres.php bin/console debug:container --env-vars: liste les variables d'environnement.php bin/console debug:autowiring --all: liste tous les services automatiquement déclarés.php bin/console debug:config NomDuBundle: liste tous les paramètres disponibles pour paramétrer un bundle donné. Ex :bin/console debug:config FrameworkBundlephp bin/console cache:clear: vide la mémoire cache du framework.php bin/console generate:bundle: crée un bunble (surtout pour SF2).php bin/console generate:controller: crée un contrôleur (en SF2).php bin/console doctrine:migrations:generate; chown 1001:1001 -R app/DoctrineMigrations: génère un fichier vide de migration SQL ou DQL.php bin/console doctrine:migrations:list: liste les noms des migrations disponibles (utiles car selon la configuration on doit les appeler par leur namespace ou juste par numéro).
Toutes les commandes peuvent être abrégées, par exemple "doctrine:migrations:generate" fonctionne avec "d:m:g" ou "do:mi:ge".
Créer une commande
[modifier | modifier le wikicode]Lors du lancement d'une commande, on distingue deux types de paramètres[2] :
- Les arguments : non nommés
- Les options : nommées.
Exemple :
bin/console app:ma_commande argument1 --option1=test
#[AsCommand(name: 'app:ma_commande')]
class HelloWorldCommand extends Command
{
protected function configure(): void
{
$this
->addArgument(
'argument1',
InputArgument::OPTIONAL,
'Argument de test'
)
->addOption(
'option1',
null,
InputOption::VALUE_OPTIONAL,
'Option de test'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
echo 'Hello World! '.$input->getOption('option1').' '.$input->getArgument('argument1');
return self::SUCCESS;
}
}
NB : en SF < 6.1, remplacer l'attribut AsCommand par une propriété connue de la classe mère :
protected static $defaultName = 'app:ma_commande';
Pour définir un argument tableau, utiliser InputArgument::IS_ARRAY et séparer les valeurs par un espace. Ex :
bin/console app:my_command arg1.1 arg1.2 arg1.3
Ajout de logs
[modifier | modifier le wikicode]Pour que la commande logue ses actions, la documentation de Symfony propose deux solutions[3] :
- $output->writeln()
- $io = new SymfonyStyle($input, $output);
Cette deuxième option permet aussi d'afficher une barre de progression, ou d'interagir avec l'utilisateur :
$io->confirm(Êtes vous sûr de vouloir faire ça ? (Yes/No)');
$io->choice('Choisissez l\'option', ['première ligne', 'toutes les lignes'])
Ensuite il y a plusieurs niveaux de log pouvant colorer la console qui le permet :
$io->info('Commentaire');
$io->success('Succès');
$io->warning('Warning');
$io->error('Echec');
Toutefois ce n'est pas conforme à la PSR3[4] et si on veut utiliser ces logs comme ceux des autres services (pour les stocker ailleurs par exemple), mieux vaut utiliser LoggerInterface $logger (en plus c'est horodaté).
Pour affichage les logs dans la console, utiliser le paramètre -v :
- -v affiche tous les logs "NOTICE" ou supérieurs.
- -vv les "INFO".
- -vvv les "DEBUG", c'est le mode le plus verbeux possible.
Tester une commande
[modifier | modifier le wikicode]Ex :
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
/**
* @see https://symfony.com/doc/current/console.html#testing-commands
*/
class CommandTest extends KernelTestCase
{
public function testExecute()
{
$kernel = self::bootKernel();
$monService = static::getContainer()->get('test.mon_service_public'); // En Symfony 6.3 on n'est plus obligé de créer un service public pour le test
$application = new Application($kernel);
$command = $application->find('app:ma_commande');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'--option1' => 'option1',
'--dry-run' => 'true',
]
);
$commandTester->assertCommandIsSuccessful();
$output = $commandTester->getDisplay();
$hasErrors = str_contains($output, 'ERROR');
$this->assertFalse($hasErrors, $output);
}
Le getDisplay affiche ce que l'on voit sur le dernier écran de la console (cela n'affiche pas tout l'output). Pour voir les logs de Monolog, il faut ajouter les lignes suivantes dans la commande[5] :
if ($this->logger instanceof Logger) {
$this->logger->pushHandler(new ConsoleHandler($output));
}
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/service_container/debug.html
- ↑ https://symfony.com/doc/current/console/input.html
- ↑ https://symfony.com/doc/current/console/style.html
- ↑ https://www.php-fig.org/psr/psr-3/
- ↑ https://stackoverflow.com/questions/30664606/how-to-assert-a-line-is-logged-using-monolog-inside-symfony/31999172#31999172
Description
[modifier | modifier le wikicode]Le framework Symfony permet nativement les fonctionnalités minimum dans un souci de performances, à l'instar d'un micro-framework. Par exemple son compilateur permet d'utiliser plusieurs patrons de conception (design patterns) via des mots réservés dans services.yaml :
- arguments : Injection de dépendance
- decorator : Décorateur.
- shared : Singleton.
- factory : Fabrique[1].
Toutefois, on peut lui ajouter des composants[2], dont il convient de connaitre les fonctionnalités pour ne pas réinventer la roue. Pour les installer :
composer require symfony/nom_du_composant
Les quatre premiers ci-dessous sont inclus par défaut dans le microframework symfony/skeleton.
framework-bundle
[modifier | modifier le wikicode]Structure la configuration principale du framework sans laquelle aucun composant n'est installable[3].
console
[modifier | modifier le wikicode]Patrons de conception "Commande".
Fournit la possibilité d'exécuter le framework avec des commandes shell[4]. Par exemple pour obtenir la liste de toutes les commandes disponibles dans un projet :
php bin/console help list
dotenv
[modifier | modifier le wikicode]Gère les variables d'environnement non versionnées, contenues dans un fichier .env[5]. Elles peuvent aussi bénéficier de type checking en préfixant les types avec ":". Ex de .env :
IS_DEV_SERVER=1
Le services.yaml, parameters: récupère ensuite cette valeur et vérifie qu'il s'agit d'un booléen (via le processeur de variable d'environnement "bool") :
is_dev_server: '%env(bool:IS_DEV_SERVER)%'
Il existe plusieurs processeurs de variable d'environnement (en plus de "bool" et des autres types)[6] :
base64:encode en base64.default:remplace le deuxième paramètre par le premier si absent. Ex :$addTestValues: '%env(bool:default::ADD_TEST_VALUES)%'injecte "null" si ADD_TEST_VALUES n'est pas défini.$addTestValues: '%env(bool:default:ADD_TEST_VALUES2:ADD_TEST_VALUES1)%'injecte le contenu de ADD_TEST_VALUES2 si ADD_TEST_VALUES1 n'est pas défini.
file:remplace le chemin d'un fichier par son contenu.not:renvoie l'inverse.require:fait un require() PHP.resolve:remplace le nom d'une variable par sa valeur.trim:fait un trim() PHP.
Pour définir une valeur par défaut en cas de variable d'environnement manquante (sans utiliser default:), dans services.yaml, parameters: :
env(MY_MISSING_CONSTANT): '0'
yaml
[modifier | modifier le wikicode]Ajoute la conversion de fichier YAML en tableau PHP[7]. Ce format de données constitue une alternative plus lisible au XML pour renseigner la configuration des services. Par défaut le framework se configure avec config.yaml.
routing
[modifier | modifier le wikicode]patron de conception "Façade".
Installe les annotations permettant de router des URLs vers les classes des contrôleurs MVC.
- Pour plus de détails voir : Programmation PHP avec Symfony/Contrôleur#Routing.
serializer
[modifier | modifier le wikicode]Permet de convertir des objets en tableaux ou dans les principaux formats de notation : JSON, XML, YAML et CSV[8].
composer require symfony/serializer
Ce composant est notamment utilisé pour créer des APIs.
form
[modifier | modifier le wikicode]Construit des formulaires HTML.
- Pour plus de détails voir : Programmation PHP avec Symfony/Formulaire.
validator
[modifier | modifier le wikicode]Fournit des règles de validation pour les données telles que les adresses emails ou les codes postaux. Utile à coupler avec les formulaires pour contrôler les saisies.
Ces règles peuvent porter sur les propriétés ou les getters.
Il permet aussi de créer des groupes de validateurs, et de les ordonner par séquences. Par défaut chaque classe a automatiquement deux groupes de validateurs : "default" et celui de son nom. Si une séquence est définie, le groupe "default" n'est plus égal au groupe de la classe (celui par défaut) mais à la séquence par défaut[9].
Exemples
[modifier | modifier le wikicode]- Dans une entité :
use Symfony\Component\Validator\Constraints as Assert;
...
#[Assert\Email]
private ?string $email = null;
- Dans un formulaire (inutile à faire si c'est déjà dans l'entité) :
use Symfony\Component\Validator\Constraints\Email;
...
$builder->add('email', EmailType::class, [
'required' => false,
'constraints' => [new Email()],
])
translation
[modifier | modifier le wikicode]Les traductions sont stockées dans un fichier différent par domaine et par langue (code ISO 639). Les formats acceptés sont YAML, XML, PHP[10].
On peut ensuite récupérer ces dictionnaires en Twig (via le filtre "trans"), ou en PHP (via le service "translator").
Par exemple, le domaine par défaut étant "messages", le français se trouve donc dans translations/messages.fr.yml ou translations/messages.fr-FR.yml.
Installation
[modifier | modifier le wikicode]composer require symfony/translation
Pour avoir les traductions inutilisées en anglais :
bin/console debug:translation en --only-unused
Pour les traductions manquantes en anglais :
bin/console debug:translation en --only-missing
On peut restreindre à un seul domaine avec une option : --domain=mon_domaine
Traduction en PHP
[modifier | modifier le wikicode]Le domaine et la langue sont facultatifs (car ils ont des valeurs par défaut) :
$translator->trans('Hello World', domain: 'login', locale: 'fr_FR');
Traduction en Twig
[modifier | modifier le wikicode]Les traductions en Twig sont appelées par le filtre "trans" :
{% trans_default_domain 'login' %}
{{ 'Hello World' |trans }}
Ou :
{{ 'Hello World' |trans({}, 'login', 'fr-FR') }}
Variables
[modifier | modifier le wikicode]Hello World: 'Hello World name!'
- Twig :
{{ Hello World |trans({"name": userName}) }}
- PHP
$translator->trans('Hello World', ['name' => $userName]);
Dans un formulaire Symfony :
$builder
->add('hello', TextType::class,([
'label' => 'Hello World',
'label_translation_parameters' => [
'name' => $userName,
]
]))
;
Par défaut le domaine de traduction est "message" mais on peut désactiver ces dernières avec : choice_translation_domain => false.
event-dispatcher
[modifier | modifier le wikicode]Patrons de conception "Observateur"[12] et "Médiateur"[13].
Assure la possibilité d'écouter des évènements pour qu'ils déclenchent des actions.
- Pour plus de détails voir : Programmation PHP avec Symfony/Évènement.
process
[modifier | modifier le wikicode]Permet de lancer des sous-processus en parallèle[14]. Exemple qui lance une commande shell :
$process = new Process(['ls']);
$process->run();
En l'absence de $process->stop() ou de timeout, le sous-processus peut être stoppé en redémarrant le serveur PHP.
Exemple de requête SQL asynchrone[15] :
$sql = 'SELECT * FROM ma_table LIMIT 1';
$process = Process::fromShellCommandline(sprintf('../bin/console doctrine:query:sql "%s"', $sql));
$process->setTimeout(3600);
$process->start();
cache
[modifier | modifier le wikicode]Gère les connexions, lectures et écritures vers des serveurs de mémoire caches tels que Redis ou Memcached.
Il fournit une classe cacheItem conforme à la PSR, instanciable par plusieurs adaptateurs.
Le cache ne sert qu'à accélérer l'application donc une panne sur celui-ci ne doit pas la bloquer. C'est pourquoi il vaut mieux avoir un ou plusieurs caches de secours, même moins rapides, pour prendre le relais dans une chaine de caches.
Pour mettre cela en place sur Symfony, définir le chaine et ses composants dans cache.yaml.
- Pour plus de détails voir : Programmation PHP/Redis#Dans Symfony.
asset
[modifier | modifier le wikicode]Ajoute la fonction Twig asset() pour accéder aux fichiers CSS, JS ou images selon leurs versions[16].
webpack-encore
[modifier | modifier le wikicode]Intégration de Webpack pour gérer la partie front end (ex : minifications des CSS et JS).
Installation[17]
[modifier | modifier le wikicode]composer require symfony/webpack-encore-bundle yarn install yarn build
NB : si Yarn n'est pas installé, le faire avec npm : apt install nodejs npm; npm install --global yarn.
Cela crée les fichiers package.json et yarn.lock contenant les dépendances JavaScript, le dossier assets/ contenant les JS et CSS versionnés, et le fichier webpack.config.js dans lequel ils sont appelés.
De plus, des fonctions Twig permettent d'y accéder depuis les templates : encore_entry_link_tags() et encore_entry_script_tags().
Par ailleurs, cela installe le framework JS Stimulus, et interprète les attributs de données pour appeler ses contrôleurs ou méthodes.
Rebuild
[modifier | modifier le wikicode]Pour que le code se build en cours de frappe, deux solutions[18] :
- Avec Yarn :
- yarn watch
- yarn dev-server
- Avec npm :
- npm watch
- npm run dev-server
La différence entre les deux est que le dev-server peut mettre à jour la page sans même la rafraichir.
messenger
[modifier | modifier le wikicode]Patrons de conception "Chaîne de responsabilité".
Messenger permet d'utiliser des queues au protocole AMQP. En résumé, il gère l'envoi de messages dans des bus, ces messages transitent par d'éventuels middlewares puis arrivent à destination dans des handlers[19]. On peut aussi persister ces messages en les envoyant dans des transports via un DSN, par exemple dans RabbitMQ, Redis ou Doctrine (donc une table des SGBD les plus populaires).
php bin/console debug:messenger
Chaque middleware doit passer le relais au suivant ainsi :
return $stack->next()->handle($envelope, $stack);
Pour stopper le message dans un middleware sans qu'il arrive aux handlers :
return $envelope;
Chaque message peut être défini comme à traiter en synchrone ou asynchrone.
Pour le faire tourner en asynchrone en prod sans qu'il n'interrompe les traitements en cours à chaque MEP, il existe plusieurs solutions :
- Processus Linux supervisord : ne convient pas aux conteneurs car ils interrompent leurs traitements à chaque relance.
- Sidecar container Kubernetes : idem.
- Conteneur dédié qui se redéploie gracieusement après chaque MEP. Par exemple dans Kubernetes, l'option
ttlSecondsAfterFinished: 3600garantit que le conteneur attend la fin du traitement en cours jusqu'à 1h maximum avant de se redéployer.
workflow
[modifier | modifier le wikicode]Ce composant nécessite de créer (en YAML, XML ou PHP) la configuration d'un automate fini[20], c'est-à-dire la liste de ses transitions et états (appelés "places").
Ces graphes sont ensuite visualisables en image ainsi :
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Dumper\StateMachineGraphvizDumper;
class WorkflowDisplayer
...
$definition = new Definition($places, $transitions);
echo (new StateMachineGraphvizDumper())->dump($definition);
sudo apt install graphviz
php WorkflowDisplayer.php | dot -Tpng -o workflow.png
browser-kit
[modifier | modifier le wikicode]Simule un navigateur pour les tests d'intégration.
config
[modifier | modifier le wikicode]Permet de manipuler des fichiers de configurations.
contracts
[modifier | modifier le wikicode]Pour la programmation par contrat.
css-selector
[modifier | modifier le wikicode]Pour utiliser XPath.
debug
[modifier | modifier le wikicode]Fournit des méthodes statiques pour déboguer le PHP.
dependency-injection
[modifier | modifier le wikicode]Normalise l'utilisation du container de services.
Permet aussi d'exécuter du code pendant la compilation via un compiler pass, en implémentant l'interface CompilerPassInterface avec sa méthode process[21].
dom-crawler
[modifier | modifier le wikicode]Fournit des méthodes pour parcourir le DOM.
expression-language
[modifier | modifier le wikicode]Patrons de conception "Interpréteur".
Expression language sert à évaluer des expressions, ce qui peut permettre de définir des règles métier[22].
Installation : composer require symfony/expression-language
Exemple :
$el = new ExpressionLanguage();
$operation = '1 + 2';
echo(
sprintf(
"L'opération %s vaut %s",
$el->compile($operation));
$el->evaluate($operation));
)
);
// Affiche : L'opération 1 + 2 vaut 3
filesystem
[modifier | modifier le wikicode]Méthodes de lecture et écriture dans les dossiers et fichiers.
finder
[modifier | modifier le wikicode]Recherche dans les dossiers et fichiers.
security
[modifier | modifier le wikicode]Ensemble de sous-composants assurant la sécurité d'un site. Ex : authentification, anti-CSRF ou droit des utilisateurs d'accéder à une page.
Dans security.yaml, on peut par exemple définir les classes qui vont assurer l'authentification (guard), ou celle User qui sera instanciée après.
Pour obtenir l'utilisateur ou son token, on peut injecter : TokenStorageInterface $tokenStorage
pour avoir l'utilisateur courant avec $this->tokenStorage->getToken()->getUser().
guard
[modifier | modifier le wikicode]Extension de sécurité pour des authentifications complexes.
http-client
[modifier | modifier le wikicode]Pour lancer des requêtes HTTP depuis l'application.
- Pour plus de détails voir : Programmation PHP avec Symfony/HttpClient.
http-foundation
[modifier | modifier le wikicode]Fournit des classes pour manipuler les requêtes HTTP, comme Request et Response que l'on retrouve dans les contrôleurs.
Par exemple :
use Symfony\Component\HttpFoundation\Response;
//...
echo Response::HTTP_OK; // 200
echo Response::HTTP_NOT_FOUND; // 404
http-kernel
[modifier | modifier le wikicode]Permet d'utiliser des évènements lors des transformations des requêtes HTTP en réponses.
inflector
[modifier | modifier le wikicode]Deprecated depuis Symfony 5.
Accorde les mots anglais au pluriel à partir de leurs singuliers.
intl
[modifier | modifier le wikicode]Internationalisation, comme par exemple la classe "Locale" pour gérer une langue.
ldap
[modifier | modifier le wikicode]Connexion aux serveur LDAP.
lock
[modifier | modifier le wikicode]Pour verrouiller les accès aux ressources[23].
Par exemple, pour ne pas qu'une commande soit lancée deux fois simultanément, bien que le composant console aie aussi cette fonctionnalité :
use Symfony\Component\Console\Command\LockableTrait;
...
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->lock() === false) {
return Command::SUCCESS;
}
...
$this->release();
return Command::SUCCESS;
}
Maker bundle
[modifier | modifier le wikicode]Pour créer ou recréer des classes à partir de déductions[24].
composer require --dev symfony/maker-bundle
NB : ce composant ne permet pas de générer des entités à partir d'une base de données.
mailer
[modifier | modifier le wikicode]Pour envoyer des emails.
mime
[modifier | modifier le wikicode]Manipulation des messages MIME.
notifier
[modifier | modifier le wikicode]Pour envoyer des notifications telles que des emails, des SMS, des messages instantanés, etc.
options-resolver
[modifier | modifier le wikicode]Gère les remplacements de propriétés par d'autres, avec certaines par défaut.
phpunit-bridge
[modifier | modifier le wikicode]Patron de conception "Pont" qui apporte plusieurs fonctionnalités liées aux tests unitaires, telles que la liste des tests désuets ou des mocks de fonctions PHP natif.
property-access
[modifier | modifier le wikicode]Pour lire les attributs de classe à partir de leurs getters, ou des tableaux.
property-info
[modifier | modifier le wikicode]Pour lire les métadonnées des attributs de classe.
stopwatch
[modifier | modifier le wikicode]Chronomètre pour mesurer des temps d'exécution.
string
[modifier | modifier le wikicode]API convertissant certains objets en chaine de caractères. Ex :
use Symfony\Component\String\Slugger\AsciiSlugger;
$slugger = new AsciiSlugger();
echo $slugger->slug('caractères spéciaux € $');
Résultat : caracteres-speciaux-EUR
templating
[modifier | modifier le wikicode]Extension de construction de templates.
- Pour plus de détails voir : Programmation PHP avec Symfony/Templating.
var-dumper
[modifier | modifier le wikicode]Ajoute une fonction globale dump() pour déboguer des objets en les affichant avec une coloration syntaxique et des menus déroulant.
Ajoute aussi dd() pour dump() and die().
var-exporter
[modifier | modifier le wikicode]Permet d'instancier une classe sans utiliser son constructeur.
polyfill*
[modifier | modifier le wikicode]On trouve aussi une vingtaine de composants polyfill, fournissant des fonctions PHP retirées dans les versions les plus récentes.
Composants désuets
[modifier | modifier le wikicode]locale (<= v2.3)
[modifier | modifier le wikicode]Arrêté en 2011, car remplacé par le composant intl[25].
icu (<= v2.6)
[modifier | modifier le wikicode]Arrêté en 2014, car remplacé par le composant intl[26].
class-loader (<= v3.3)
[modifier | modifier le wikicode]Arrêté en 2011, car remplacé par composer.json[27].
Ajoutés en 2020
[modifier | modifier le wikicode]Uid (sic) (>= v5.1)
[modifier | modifier le wikicode]RateLimiter (>= v5.2)
[modifier | modifier le wikicode]Patron de conception "Proxy", qui permet de limiter la consommation de ressources du serveur par les clients[29]
Installation :
composer require symfony/rate-limiter
Pour l'activer, ajouter la ligne suivante dans les pare-feux de security.yaml concernés :
login_throttling: true
Semaphore (>= v5.2)
[modifier | modifier le wikicode]Pour donner l'exclusivité d'accès à une ressource[30].
Ajoutés en 2021
[modifier | modifier le wikicode]PasswordHasher (>= v5.3)
[modifier | modifier le wikicode]Pour gérer les chiffrements[31].
Runtime (>= v5.3)
[modifier | modifier le wikicode]Pour le démarrage (bootstrap) : permettre de découpler l'application de son code de retour. [32].
Ajoutés en 2022
[modifier | modifier le wikicode]HtmlSanitizer (>= v6.1)
[modifier | modifier le wikicode]Clock (>= v6.2)
[modifier | modifier le wikicode]Symfony UX (>= v5.4)
[modifier | modifier le wikicode]ux-autocomplete
[modifier | modifier le wikicode]ux-chartjs
[modifier | modifier le wikicode]Utilise Chart.js via Stimulus pour afficher des graphiques, via la fonction Twig render_chart()[33].
ux-react
[modifier | modifier le wikicode]Ajoute le framework React.js.
- Pour plus de détails voir : Programmation PHP avec Symfony/Stimulus.
ux-vue
[modifier | modifier le wikicode]Ajoute le framework Vue.js.
Ajoutés en 2023
[modifier | modifier le wikicode]Webhook et RemoteEvent (>= v6.3)
[modifier | modifier le wikicode]AssetMapper (>= v6.3)
[modifier | modifier le wikicode]Scheduler (>= v6.3)
[modifier | modifier le wikicode]Composants non listés comme tels
[modifier | modifier le wikicode]apache-pack
[modifier | modifier le wikicode]Pour faire tourner le site sans passer par le serveur symfony server:start.
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/service_container/factories.html
- ↑ https://symfony.com/components
- ↑ https://symfony.com/doc/current/reference/configuration/framework.html
- ↑ https://symfony.com/doc/current/components/console.html
- ↑ https://symfony.com/doc/current/components/dotenv.html
- ↑ https://symfony.com/doc/current/configuration/env_var_processors.html
- ↑ https://symfony.com/doc/current/components/yaml.html
- ↑ https://symfony.com/doc/current/components/serializer.html
- ↑ https://symfony.com/doc/current/validation/sequence_provider.html
- ↑ https://symfony.com/doc/current/translation.html
- ↑ https://symfony.com/doc/current/reference/formats/message_format.html
- ↑ http://www.jpsymfony.com/design_patterns/le-design-pattern-observer-avec-symfony2
- ↑ https://github.com/certificationy/symfony-pack/blob/babd3fee68a7e793767f67c6df140630f52e7f8d/data/architecture.yml#L13
- ↑ https://symfony.com/doc/current/components/process.html
- ↑ https://gist.github.com/appaydin/42eaf953172fc7ea6a8b193694645324
- ↑ https://symfony.com/doc/current/components/asset.html
- ↑ https://symfonycasts.com/screencast/stimulus/encore
- ↑ https://symfony.com/doc/current/frontend/encore/simple-example.html
- ↑ https://vria.eu/delve_into_the_heart_of_the_symfony_messenger/
- ↑ https://symfony.com/doc/current/workflow.html
- ↑ https://symfony.com/doc/current/components/dependency_injection/compilation.html
- ↑ https://symfony.com/doc/current/components/expression_language.html
- ↑ https://symfony.com/doc/current/components/lock.html
- ↑ https://symfony.com/bundles/SymfonyMakerBundle/current/index.html
- ↑ https://symfony.com/components/Locale
- ↑ https://symfony.com/components/Icu
- ↑ https://symfony.com/components/ClassLoader
- ↑ https://symfony.com/doc/current/components/uid.html
- ↑ https://symfony.com/doc/current/rate_limiter.html
- ↑ https://symfony.com/doc/current/components/semaphore.html
- ↑ https://symfony.com/blog/new-in-symfony-5-3-passwordhasher-component
- ↑ https://symfony.com/blog/new-in-symfony-5-3-runtime-component
- ↑ https://symfony.com/bundles/ux-chartjs/current/index.html
- ↑ https://symfony.com/blog/new-in-symfony-6-3-webhook-and-remoteevent-components
- ↑ https://symfony.com/blog/new-in-symfony-6-3-assetmapper-component
- ↑ https://symfony.com/blog/new-in-symfony-6-3-scheduler-component
Installation
[modifier | modifier le wikicode]Composant pour lancer des requêtes HTTP depuis l'application, avec gestion des timeouts, redirections, cache, protocole et en-tête HTTP. Il est configurable en PHP ou dans framework.yaml.
Depuis Symfony 4[1] :
composer require symfony/http-client
Utilisation
[modifier | modifier le wikicode]Deux solutions :
HttpClient::create();
ou
public function __construct(private readonly HttpClientInterface $httpClient)
Par défaut, l'appel statique à la classe HttpClient instancie un CurlHttpClient, alors que l'injection du service via HttpClientInterface récupère un TraceableHttpClient. Ce dernier est préférable puisqu'il affiche toutes les requêtes dans le profiler de Symfony.
GET
[modifier | modifier le wikicode]On peut forcer l'utilisation de HTTP 2 à la création :
$httpClient = HttpClient::create(['http_version' => '2.0']);
$response = $httpClient->request('GET', 'https://fr.wikibooks.org/');
if (200 === $response->getStatusCode()) {
dd($response->getContent());
} else {
dd($response->getInfo('error'));
}
- Ce code ne lève pas les exceptions de résolution DNS.
- Mieux vaut éviter de créer le client HTTP ainsi car il n'apparaitra pas dans les logs, il faut injecter son service à la place, à partir de
HttpClientInterface.
Exemple avec query parameters :
$response = $this->httpClient->request('GET', 'https://fr.wikibooks.org/', ['query' => ['debug' => 1]]);
Cache
[modifier | modifier le wikicode]Symfony peut stocker les résultats des requêtes HTTP pour ne pas avoir à le refaire.
Depuis SF 7.4[2] :
http_client:
scoped_clients:
example.client:
base_uri: 'https://example.com'
caching:
cache_pool: example_cache_pool
Avant SF 7.4 :
$client = new CachingHttpClient($client, $store);
POST
[modifier | modifier le wikicode]Exemple en POST avec authentification :
$response = $httpClient->request('POST', 'https://fr.wikibooks.org/w/api.php', [
'auth_bearer' => 'mon_token',
'body' => $keyValuePairs,
]);
Pour lancer plusieurs appels asynchrones, il suffit de placer leurs $response->getContent() ensemble, après tous les $httpClient->request().
Pour envoyer un fichier il y a plusieurs solutions :
- Utiliser le type MIME correspondant à son extension (ex : 'application/pdf', 'application/zip'...). Mais on ne peut envoyer que le fichier dans la requête.
- Utiliser le type MIME 'application/json' et l'encoder en base64. Il peut ainsi être envoyé avec d'autres données.
- Utiliser le type MIME 'multipart/form-data'[3].
Problèmes connus
[modifier | modifier le wikicode]Ce composant est relativement jeune et souffre d'incomplétudes.
- On peut avoir du "null given" à tort sur un mapping DNS, solvable en rajoutant une option :
$options = array_merge($options, [
'resolve' => ['localhost' => '127.0.0.1']
]);
$httpClient->request()renvoie uneSymfony\Contracts\HttpClient\ResponseInterface, mais en cas d'erreur, elle ne contient qu'une ligne de résumé, soit moins d'informations qu'un client comme Postman.
Tests
[modifier | modifier le wikicode]Ce composant peut aussi serveur aux tests fonctionnels via PhpUnit. On l'appelle alors avec static::createClient si le test extends WebTestCase. Dans le cas d'un projet API Platform, on l'appelle de la même manière mais le test extends ApiTestCase.
Exemple :
$client = static::createClient();
$client->request('GET', '/home');
var_dump($client->getResponse()->getContent());
Pour simuler plusieurs clients en parallèle : $client->insulate().
Pour simuler un utilisateur : $client->loginUser($monUser).
Pour un test de bundle, il faut créer une classe Kernel qui charge les routes en plus[4].
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/components/http_client.html
- ↑ https://symfony.com/blog/new-in-symfony-7-4-caching-http-client?utm_source=Symfony%20Blog%20Feed&utm_medium=feed
- ↑ https://dev.to/timoschinkel/sending-multipart-data-with-psr-18-2lb5
- ↑ https://symfonycasts.com/screencast/symfony-bundle/controller-functional-test
Un évènement est une action pouvant en déclencher d'autres qui l'attendaient, à la manière du patron de conception observateur, via un hook.
Installation
[modifier | modifier le wikicode]composer require symfony/event-dispatcher
Commande
[modifier | modifier le wikicode]Pour lister les évènements et écouteurs d'un projet (avec leurs priorités) :
php bin/console debug:event-dispatcher
Ex :
"console.terminate" event
-------------------------
------- ----------------------------------------------------------------------------- ----------
Order Callable Priority
------- ----------------------------------------------------------------------------- ----------
#1 Symfony\Component\Console\EventListener\ErrorListener::onConsoleTerminate() -128
#2 Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate() -255
------- ----------------------------------------------------------------------------- ----------
Event
[modifier | modifier le wikicode]Pour utiliser ce système, la première étape consiste à déterminer si on souhaite utiliser un évènement existant, ou en créer un nouveau.
- Pour un existant, son nom est obtenu par le commande ci-dessus.
- Pour un nouveau, voici un exemple de conception pilotée par le domaine où l'on souhaite qu'une condition du core soit traitée dans des modules en fonction du groupe utilisateur, sans les lister dans le core :
class AddExtraDataEvent
{
/** @var string */
private $userGroup;
public function __construct(string $userGroup)
{
$this->userGroup = $userGroup;
}
public function getUserGroup(): string
{
return $this->usernGroup;
}
public function setUserGroup(string $usernGroup): AddExtraDataEvent
{
$this->userGroup = $userGroup;
return $this;
}
}
Une fois la classe crée, il faut choisir où l'instancier :
use Symfony\Component\EventDispatcher\EventDispatcher;
...
$this->eventDispatcher->dispatch(new AddExtraDataEvent($userGroup));
Listener
[modifier | modifier le wikicode]Pour exécuter une ou plusieurs classes au moment du dispatch, il faut créer maintenant en créer une qui écoute l'évènement. Elle doit peut être reliée à son évènement, soit dans sa déclaration de service pour un écouter (event listener[1]), soit dans son constructeur pour un souscripteur (event subscriber).
Le listener a donc l'inconvénient de devoir être déclaré avec un tag, alors que le subscriber lui, est chargé à chaque exécution du programme, ce qui alourdit légèrement les performances mais évite de maintenir sa déclaration en autowiring.
Exemple de déclaration YAML
[modifier | modifier le wikicode] services:
App\EventListener\MyViewListener:
tags:
- { name: kernel.event_listener, event: kernel.view }
class MyViewListener
{
public function onKernelException(ExceptionEvent $event)
{
echo "Triggered!";
}
}
Subscriber
[modifier | modifier le wikicode]Un souscripteur doit forcément implémenter EventSubscriberInterface :
class ViewSubscriber implements EventSubscriberInterface
{
public function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['onView']
];
}
public function onView(ViewEvent $event): void
{
echo "Triggered!";
}
}
Autre exemple où on veut embarquer dans un évènement maison une information de ses souscripteurs :
class ClientXUserSubscriber implements EventSubscriberInterface
{
...
public static function getSubscribedEvents(): array
{
return [
ClientXEvent::class => 'getProperty',
];
}
public function getProperty(ClientXUserEvent $event): void
{
if ('X' === $this->user->getCompany()) {
$event->setProperty('XX');
}
}
}
Débogage
[modifier | modifier le wikicode]Les erreurs qui surviennent selon certains évènements ne sont pas faciles à provoquer ou visualiser. Pour les voir sans passer par le profiler, on peut ajouter temporairement dans un contrôleur :
$this->getEventDispatcher()->dispatch('mon_service');
Références
[modifier | modifier le wikicode]
Principe
[modifier | modifier le wikicode]Le principe est d'ajouter des champs de formulaire en PHP, qui seront automatiquement convertis en code HTML correspondant.
En effet, en HTML on utilise habituellement la balise <form> pour afficher les champs à remplir par le visiteur. Puis sur validation on récupère leurs valeurs en PHP avec la superglobale $_REQUEST (ou ses composantes $_GET et $_POST). Or ce système ne fonctionne pas en $_POST dans Symfony : si on affiche un tel formulaire et qu'on le valide, $_POST est vide, et l'équivalent Symfony de $_REQUEST, $request->request[1] aussi.
Les formulaires doivent donc nécessairement être préparés en PHP.
Installation
[modifier | modifier le wikicode]Form
[modifier | modifier le wikicode]
composer require symfony/form
Les formulaires présents sont ensuite listables avec :
bin/console debug:form
Et vérifiables individuellement :
bin/console debug:form "App\Service\Form\MyForm"
Avec le composant maker, on peut créer un formulaire pour chaque entité Doctrine à modifier :
composer require symfony/maker-bundle
bin/console make:form
Validator
[modifier | modifier le wikicode]Pour ajouter des contrôles sur les champs, il existe un deuxième composant Symfony[2] :
composer require symfony/validator
Contrôleur
[modifier | modifier le wikicode]Injection du formulaire dans un Twig
[modifier | modifier le wikicode] class HelloWorldType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class)
->add('save', SubmitType::class)
;
}
}
class HelloWorldController extends AbstractController
{
#[Route('/helloWorld/{id}, requirements: ['id' => '\d*']')]
public function indexAction(Request $request, ?HelloWorld $helloWorld = null): Response
{
$form = $this->createForm(HelloWorldType::class, $helloWorld);
return $this->render('helloWorld.html.twig', [
'form' => $form->createView(),
]);
}
}
Le second paramètre de createForm() est facultatif est sert à préciser des valeurs initiales dans le formulaire qui seront injectées en Twig, mais elles peuvent aussi l'être via le fichier du formulaire dans les paramètres de chaque champ.
Traitement post-validation
[modifier | modifier le wikicode]Dans la même méthode du contrôleur qui injecte le formulaire, il faut prévoir le traitement post-validation. Par exemple pour mettre à jour l'entité en base :
if (empty($myEntity)) {
$myEntity = new MyEntity();
}
$form = $this->createForm(MyEntityType::class, $myEntity);
$form->handleRequest($request); // Cette méthode remplit l'objet avec les valeurs postées dans $request pour les champs du formulaires mappés
if ($form->isSubmitted() && $form->isValid()) {
// Mise à jour d'un champ non mappé (ex : car absent de $myEntity)
$email = $form->get('email')->getData();
$this->em->persist($email);
$this->em->flush();
return $this->redirectToRoute('home');
}
Fichier du formulaire
[modifier | modifier le wikicode]Dans SF4, l'espace de nom Symfony\Component\Form\Extension\Core\Type propose 35 types de champ, tels que :
- Text
- TextArea
- Email (avec validation en option de la présence d'arrobase ou de domaine)
- Number
- Date
- Choice (menu déroulant)
- Checkbox (cases à cocher et boutons radio)
- Hidden (caché)
- Submit (bouton de validation).
TextType
[modifier | modifier le wikicode]Exemple[3] :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', TextType::class, [
'required' => true,
'empty_data' => 'valeur par défaut si vide à la validation',
'data' => 'valeur par défaut préremplie à la création',
'constraints' => [new Assert\NotBlank()],
'attr' => ['class' => 'ma_classe_CSS'],
]);
}
Pour préremplir des valeurs dans les champs :
$form->get('email')->setData($user->getEmail());
L'attribut "required" peut être interprété par les navigateurs comme un "NotBlank", mais il faut tout de même le compléter avec la contrainte sans quoi un simple retrait du "required" de la page web par la console du navigateur pourrait contourner l'obligation.
NumberType
[modifier | modifier le wikicode]Cette classe génère une balise input type="number", qui empêche donc les navigateurs d'écrire des lettres dedans en HTML5.
D'autre part, il y a aussi les problématiques des nombres minimum et maximum, et des séparateurs décimaux et de milliers.
Ex :
$builder
->add('email', NumberType::class, [
'html5' => true,
'constraints' => [new Assert\Positive()],
'attr' => [
'onkeypress' => 'return (event.charCode > 47 && event.charCode < 58) || event.charCode == 44 || event.charCode == 45',
],
]);
PercentType
[modifier | modifier le wikicode]
Bien définir le "scale" de 2 sans quoi une perte de précision survient par rapport au NumberType. Ex : un 1,27 va devenir 0.01.
ChoiceType
[modifier | modifier le wikicode]Il faut injecter le tableau des choix du menu déroulant dans la clé "choices", avec en clé ce qui sera visible dans la liste et en valeur ce qui sera envoyé à la soumission[4].
Ex :
$builder
->add('civility', ChoiceType::class, [
'choices' => ['Choisir' => null, 'M.' => 'M.', 'Mme' => 'Mme'],
])
Dans le cas où une valeur par défaut est définie dans 'data', elle doit appartenir aux valeurs du tableau de "choices", sans quoi elle ne sera pas prise en compte.
Si Symfony envoie une string vide au lieu du null de la liste, on peut mettre 0 dans la liste et 'empty_data' => null, dans le champ du formulaire.
Avec liste modifiable
[modifier | modifier le wikicode]Si une valeur absente de la liste des choix est envoyée à la soumission, on peut la faire accepter en l'ajoutant à la volée avec[5] :
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
...
})
EntityType
[modifier | modifier le wikicode]De plus, en installant Doctrine, il est possible d'ajouter un type de champ "entité" directement relié avec un champ de base de données[6].
Ex :
$builder->add('company', EntityType::class, ['class' => Company::class]);
En SF4, il n'y avait pas encore les types CheckboxType ou RadioType : il fallait jouer sur deux paramètres de EntityType ainsi :
| Élément | Expanded | Multiple |
|---|---|---|
| Sélecteur | false | false |
| Sélecteur multiple | false | true |
| Boutons radio | true | false |
| Cases à cocher | true | true |
Exemple :
$builder->add('gender', EntityType::class, ['expanded' => true, 'multiple' => false]);
Pour lui donner une valeur par défaut, il faut lui injecter un objet :
$builder->add('company', EntityType::class, [
'class' => Company::class,
'choice_label' => 'name',
'data' => $company,
]);
Sous-formulaire
[modifier | modifier le wikicode]Utiliser le nom du sous-formulaire comme type :
$builder->add('company', MySubformType::class, [
'label' => false,
]);
Validation
[modifier | modifier le wikicode]Validation depuis les entités
[modifier | modifier le wikicode]Le validateur de formulaire d'entité peut utiliser les annotations des entités. Ex :
use Symfony\Component\Validator\Constraints as Assert;
...
#[Assert\Type('string')]
#[Assert\NotBlank]
#[Assert\Length(
min: 1,
max: 255,
)]
En PHP < 8 :
use Symfony\Component\Validator\Constraints as Assert;
...
/**
* @Assert\Type("string")
* @Assert\NotBlank
* @Assert\Length(
* min = 2,
* max = 50
* )
*/
Plusieurs types de données sont déjà définis, comme l'email ou l'URL[7]. Ex :
@Assert\Email()
Validation depuis les formulaires
[modifier | modifier le wikicode]Sinon il permet aussi des contrôles plus personnalisés dans les types (qui étendent Symfony\Component\Form\AbstractType). Ex :
'constraints' => [
new Assert\NotBlank(),
new GreaterThanOrEqual(2),
new Assert\Callback([ProductChecker::class, 'check']),
],
Validation avec un service
[modifier | modifier le wikicode]Pour valider une entité depuis le service validateur[8] : use Symfony\Component\Validator\Validator\ValidatorInterface; ... $validator->validate( $entity, $entityConstraint ); NB : le second paramètre est optionnel.
Bien que l'on voit des services correspondant aux contraintes du validateur, on ne peut pas les injecter comme les autres services mais uniquement les utiliser via le validateur général.
Exemple pour valider un email :
php bin/console debug:container |grep -i validator |grep -i email validator.email Symfony\Component\Validator\Constraints\EmailValidator
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Validator\ValidatorInterface;
...
$this->validator->validate(
'mon_email@example.com',
new Email()
);
Appel du formulaire Symfony dans la vue
[modifier | modifier le wikicode]Les fonctions Twig permettant d'ajouter les éléments du formulaire sont :
- form_start
- form_errors
- form_row
- form_widget
- form_label
Pour afficher tout le formulaire, dans l'ordre où les champs ont été définis en PHP :
{{ form_start(form) }}
{{ form_end(form) }}
Pour n'afficher qu'un seul champ :
{{ form_widget(form.choosen_credit_card) }}
Les mêmes attributs qu'en PHP peuvent être définis en paramètre. Ex :
{{ form_widget(form.name, {'attr': {'class': 'address', 'placeholder': 'Entrer une adresse'} }) }}
{{ form_label(form.name, null, {'label_attr': {'class': 'address'}}) }}
Exemple complet :
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_label(form.name, 'Label du champ "name" écrasé ici') }}
{{ form_row(form.name) }}
{{ form_widget(form.message, {'attr': {'placeholder': 'Remplacez ce texte par votre message'} }) }}
{{ form_rest(form) }}
{{ form_row(form.submit, { 'label': 'Submit me' }) }}
{{ form_end(form) }}
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/components/http_foundation.html
- ↑ https://symfony.com/doc/current/forms.html#form-validation
- ↑ https://symfony.com/doc/current/reference/forms/types/form.html#empty-data
- ↑ https://symfony.com/doc/current/reference/forms/types/choice.html
- ↑ https://github.com/symfony/symfony/issues/42451
- ↑ https://symfony.com/doc/master/reference/forms/types/entity.html
- ↑ https://symfony.com/doc/current/validation.html#string-constraints
- ↑ https://symfony.com/doc/current/validation.html#using-the-validator-service
Mailer
[modifier | modifier le wikicode]Depuis Symfony 4.3, un composant Symfony Mailer a été ajouté.
Pour l'installer[1] :
composer require symfony/mailer
Ajouter ensuite le SMTP dans le .env :
MAILER_DSN=smtp://mon_utilisateur:mon_mot_de_passe@smtp.example.com
Utilisation
[modifier | modifier le wikicode] private MailerInterface $mailer;
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
public function send(string $message): void
{
$email = (new Email())
->from('no-reply@example.com')
->to('target@example.com')
->subject('Test Symfony Mailer')
->text($message)
;
$this->mailer->send($email);
}
Swift Mailer
[modifier | modifier le wikicode]Avant Symfony 4.3 et la création du composant Mailer[2], on pouvait utiliser Swift Mailer.
Swift Mailer est ensuite remplacé en novembre 2021 par le composant Mailer.
Installation
[modifier | modifier le wikicode]composer require symfony/swiftmailer-bundle
Utilisation
[modifier | modifier le wikicode]Par exemple, pour un envoi d'email sans passer par config.yml :
$transport = (new \Swift_SmtpTransport('mon_smtp.com', 25));
$mailer = new \Swift_Mailer($transport);
$message = (new \Swift_Message('Hello World from Controller'))
->setFrom('mon_email@example.com')
->setTo('mailcatcher@example.com')
->setBody('Hello World', 'text/html')
;
$mailer->send($message);
Templates
[modifier | modifier le wikicode]Pour simplifier les templates d'email, une alternative au HTML / CSS existe, il s'agit de Inky[4].
Elle utilise d'autres balises XML, comme callout ou spacer[5].
Installation : composer require twig/extra-bundle twig/inky-extra Utilisation : {% apply inky_to_html %} ...
Références
[modifier | modifier le wikicode]
Introduction
[modifier | modifier le wikicode]Stimulus est le framework JavaScript officiel de Symfony[1]. Il est installé avec Webpack :
composer require symfony/webpack-encore-bundle
Pour utiliser le framework React.js dans Symfony[2] :
composer require symfony/ux-react
Lancer ensuite npm run watch pour que le code JS exécuté soit toujours identique à celui écris. Cela va lancer le npm run build en cours de frappe.
Hello World on ready
[modifier | modifier le wikicode]Partie Twig
[modifier | modifier le wikicode]La première étape consiste à connecter un contrôleur Stimulus depuis un fichier Twig, en lui injectant les variables dont il a besoin. Ex :
<div {{ stimulus_controller('ticket', {
subject: 'Hello World'
} )}}>
</div>
Une syntaxe alternative est :
<div data-controller="ticket"
data-ticket-subject-value="Hello World"
>
</div>
Partie Stimulus
[modifier | modifier le wikicode]Dans le fichier assets/controllers/ticket_controller.js, créer une classe héritant de Stimulus :
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = {
subject: String,
body: String,
};
connect() {
alert(this.subjectValue);
}
}
Rafraichir la page du Twig pour voir le message du code exécuté par Stimulus.
Explication : la fonction connect est un mot réservé désignant une fonction prédéfinie qui s'exécute automatiquement quand le contrôleur Stimulus est connecté au DOM de la page[3]. C'est donc un mécanisme similaire à la méthode magique PHP __contruct. De plus, il existe aussi disconnect comparable à la méthode PHP __destruct.
Si le contrôleur Stimulus est dans un sous-dossier, la syntaxe des séparateurs de dossiers côté Twig n'est pas "/" mais "--".
Ex : stimulus_controller('sousDossier--ticket', ...) connectera le fichier assets/controllers/sousDossier/ticket_controller.js.
Hello World on click
[modifier | modifier le wikicode]On utilise l'action "click"[4].
Partie Twig
[modifier | modifier le wikicode]<div {{ stimulus_controller('ticket', {
subject: 'Hello World'
} )}}>
<button {{ stimulus_action('ticket', 'onCreate', 'click') }}>
Créer un ticket
</button>
</div>
Une syntaxe alternative est :
<div data-controller="ticket"
data-ticket-subject-value="Hello World"
>
<button data-action="click->ticket#onCreate" >
Créer un ticket
</button>
</div>
Partie Stimulus
[modifier | modifier le wikicode]Par rapport au premier exemple, on remplace juste "connect" par une méthode maison.
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = {
subject: String,
body: String,
};
onCreate() {
alert(this.subjectValue);
}
}
Rafraichir la page du Twig et cliquer sur le bouton pour voir le message du code exécuté par Stimulus.
Exemple où Stimulus appelle React
[modifier | modifier le wikicode]On veut maintenant déclencher l'ouverture d'une fenêtre modale React.js en cliquant sur un bouton de la page du Twig. Il faut donc que le contrôleur Stimulus appelle une classe React.
- ticket_controller.js :
import { Controller } from "@hotwired/stimulus";
import ReactDOM from "react-dom";
import React from "react";
import HelloWorld from "./HelloWorld";
export default class extends Controller {
static values = {
subject: String,
body: String,
};
onCreate() {
ReactDOM.render(<HelloWorld subject={this.subjectValue} />, this.element);
}
}
- HelloWorld.js :
export default function (props) {
alert(props.subject);
}
react_component().Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/blog/new-in-symfony-the-ux-initiative-a-new-javascript-ecosystem-for-symfony#symfony-ux-building-highly-interactive-applications-by-leveraging-javascript-giants
- ↑ https://symfony.com/bundles/ux-react/current/index.html
- ↑ https://stimulus.hotwired.dev/reference/lifecycle-callbacks
- ↑ https://stimulus.hotwired.dev/reference/actions
Dans Symfony, on appelle bundle une bibliothèque prévue pour être installée dans Symfony comme module complémentaire au framework.
Configurer un bundle
[modifier | modifier le wikicode]Après installation avec composer, il doit généralement être configuré dans le dossier config/ par un fichier YAML à son nom. Pour connaître les configurations possibles :
php bin/console config:dump mon_bundle
La classe du bundle est instanciée dans :
config/bundles.php
Pour l'activer ou le désactiver de certains environnements, il suffit de l'ajouter un paramètre. Ex :
<?php
return [
// À instancier tout le temps
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
// À instancier seulement si dans le .env, APP_ENV=dev ou APP_ENV=test (les autres sont "false" par défaut)
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
// À ne pas instancier dans les environnements de dev
Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
];
Créer un bundle
[modifier | modifier le wikicode]Par rapport à une application classique, la création d'un bundle possède des particularités du fait qu'il n'est prévu pour être utilisé que comme dépendance d'applications tierces[1]. Par exemple :
- Ses namespaces doivent démarrer par le nom du vendor et se terminer par le mot Bundle (ex : Symfony\Bundle\FrameworkBundle).
- Il doit contenir un fichier point d'entrée dans sa racine (ex : FrameworkBundle.php).
- Il peut avoir un .yaml de configuration dans config/packages à créer automatiquement à l'installation grâce à une classe étendant ConfigurationInterface[2].
Principaux bundles
[modifier | modifier le wikicode]Packagist propose une liste des bundles Symfony les plus utilisés[3].
SensioFrameworkExtraBundle
[modifier | modifier le wikicode]Permet de créer des annotations[4].
FOS
[modifier | modifier le wikicode]FriendsOfSymfony[5] propose plusieurs bundles intéressants, parmi lesquels :
- FOSUserBundle : pour gérer des utilisateurs.
- FOSRestBundle : pour les API REST.
KNP
[modifier | modifier le wikicode]KNP Labs offre également plusieurs bundles connus, dont un paginateur[6].
SonataAdmin
[modifier | modifier le wikicode]Ce bundle permet de créer rapidement un back-office pour lire ou modifier une base de données[7].
EasyAdmin
[modifier | modifier le wikicode]
Mêmes principales fonctions que SonataAdmin mais plus léger[8].
Installation
[modifier | modifier le wikicode]composer require easycorp/easyadmin-bundle
Configuration
[modifier | modifier le wikicode]Pour créer la page d'accueil :
bin/console make:admin:dashboard
Pour une liste paginée d'entités Doctrine modifiables, avec liens vers leurs CRUD :
bin/console make:admin:crud
Pour modifier les actions d'une page, dans son contrôleur :
public function configureActions(Actions $actions): Actions
{
$myCustomAction = Action::new('my_custom_action', 'Mon action personnalisée')
->linkToRoute('my_custom_action', function (MyEntity $myEntity): array {
return ['myEntityId' => $myEntity->getId()];
});
return $actions
->add('index', $myCustomAction);
->add('detail', $myCustomAction)
->update(Crud::PAGE_INDEX, Action::NEW, function (Action $action) {
return $action->setIcon('fa fa-file-alt')->setLabel('Créer mon entité');
})
;
The PHP League
[modifier | modifier le wikicode]Voir https://github.com/thephpleague.
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/bundles/best_practices.html
- ↑ https://symfony.com/doc/current/bundles/configuration.html#processing-the-configs-array
- ↑ https://packagist.org/packages/symfony/?query=symfony%20bundle&tags=symfony
- ↑ https://symfony.com/bundles/SensioFrameworkExtraBundle/current/index.html
- ↑ https://github.com/FriendsOfSymfony
- ↑ https://github.com/KnpLabs
- ↑ https://github.com/sonata-project/SonataAdminBundle
- ↑ https://symfony.com/bundles/EasyAdminBundle/current/index.html
Installation
[modifier | modifier le wikicode]Twig est un moteur de templates pour le langage de programmation PHP, utilisé par défaut par le framework Symfony. Son livre officiel faisant 156 pages[1], la présente pas aura plutôt un rôle d'aide mémoire et d'illustration.
Pour exécuter du code sans installer Twig, il existe https://twigfiddle.com/.
composer require symfony/templating
Anciennement on trouve aussi :
composer require symfony/twig-bundle
composer require twig/twig
Syntaxe native
[modifier | modifier le wikicode]Les mots réservés suivants s'ajoutent au HTML déjà interprété :
- {{ ... }} : appel à une variable ou une fonction PHP, ou un template Twig parent (
{{ parent() }}). - {# ... #} : commentaires.
- {% ... %} : commande, comme une affectation, une condition, une boucle ou un bloc HTML.
- {% set foo = 'bar' %} : assignation[2].
- {% if (i is defined and i == 1) or j is not defined or j is empty %} ... {% endif %} : condition.
- {% for i in 0..10 %} ... {% endfor %} : compteur dans une boucle.
- ' : caractère d'échappement.
Chaines de caractères
[modifier | modifier le wikicode]Concaténation
[modifier | modifier le wikicode]Il existe de multiples manière de concaténer des chaines[3]. Par exemple avec l'opérateur de concaténation ou par interpolation :
"{{ variable1 ~ variable2 }}"
"#{variable1} #{variable2}"
Les apostrophes ne fonctionnent pas avec l'interpolation.
Tableaux
[modifier | modifier le wikicode]Création
[modifier | modifier le wikicode]Pour créer un tableau itératif :
{% set myArray = [1, 2] %}
Un tableau associatif :
{% set myArray = {'key': 'value'} %}
À plusieurs lignes :
{% set months = {
1: 'janvier',
2: 'février',
3: 'mars',
} %}
{{ dump(months[1]) }} {# 'janvier' #}
Ajouter une ligne :
{% set months = months|merge({4: 'avril'}) %}
Ajouter une ligne avec clé variable :
{% set key = 5 %}
{% set months = months|merge({(key): 'mai'}) %}
Ajouter une ligne en préservant les clés numériques :
{% set key = 6 %}
{% set months = months + {(key): 'juin'} %}
Multidimensionnel :
{% set myArray = [
{'key1': 'value1'},
{'key2': 'value2'}
] %}
Dans un "for ... in", pour séparer chaque élément avec une virgule :
{% if loop.first != true %}
,
{% endif %}
Pour créer un tableau associatif JavaScript à partir d'un tableau Twig :
<script type="text/javascript">
const monTableauJs = JSON.parse('{{ monTableauTwig |json_encode |raw }}');
for (const maLigneJs in monTableauJs) {
console.log(maLigneJs);
console.log(monTableauJs[maLigneJs]);
}
</script>
Modification d'une ligne
[modifier | modifier le wikicode]Pour modifier une ligne, utiliser "merge()"[4]. Ex :
{% set tests = {'a': 1} %}
{% set tests = tests|merge({'b': 2}) %}
{{ dump(tests) }}
{% set tests = tests|merge({'b': 3}) %}
{{ dump(tests) }}
array:2 [▼ "a" => 1 "b" => 2 ] array:2 [▼ "a" => 1 "b" => 3 ]
La clé de la ligne ne doit pas être numérique (même convertie en chaine) sinon Twig modifie les clés, donc cela ajoute une ligne :
{% set tests = {'1': 1} %}
{% set tests = tests|merge({'2': 2}) %}
{{ dump(tests) }}
{% set tests = tests|merge({'2': 3}) %}
{{ dump(tests) }}
array:2 [▼ 0 => 1 1 => 2 ] array:3 [▼ 0 => 1 1 => 2 2 => 3 ]
Modification des lignes
[modifier | modifier le wikicode]Pour ajouter une ou plusieurs lignes à un tableau, utiliser "merge()" aussi :
{% set oldArray = [1] %}
{% set newArray = oldArray|merge([2,3]) %}
{{ dump(newArray) }}
0 => 1 1 => 2 2 => 3
Pour ajouter une ligne associative :
{% set oldArray = {'key1': 'value1'} %}
{% set newArray = oldArray|merge({'key2': 'value2'}) %}
{{ dump(newArray) }}
[ "key1" => "value1" "key2" => "value2" ]
Pour ajouter une ligne de sous-tableau :
{% set oldArray = [{'key1': 'value1'}] %}
{% set newArray = oldArray|merge([{'key2': 'value2'}]) %}
{{ dump(newArray) }}
[ 0 => ["key1" => "value1"] 1 => ["key2" => "value2"] ]
Lecture
[modifier | modifier le wikicode]Pour savoir si une variable est un tableau : if my_array is iterable
Pour savoir :
- si un tableau est vide, utiliser empty comme pour les chaines de caractères. Par exemple pour savoir si un tableau est vide ou null :
my_array is empty
- la taille du tableau :
my_array |length
- si un élément est dans un tableau :
my_item in my_array
- si un élément n'est pas dans un tableau :
my_item not in my_array
- si un élément est dans les clés d'un tableau :
my_item in my_array|keys
Pour filtrer le tableau, utiliser filter[5]. Par exemple pour savoir si un tableau multidimensionnel a ses sous-tableaux vides : my_array|filter(v => v is not empty) is empty
Précédence des opérateurs
[modifier | modifier le wikicode]Du moins au plus prioritaire[6] :
| Opérateur | Rôle |
|---|---|
| b-and | Et booléen |
| b-xor | Ou exclusif |
| b-or | Ou booléen |
| or | Ou |
| and | Et |
| == | Est-il égal |
| != | Est-il différent |
| < | Inférieur |
| > | Supérieur |
| >= | Supérieur ou égal |
| <= | Inférieur ou égal |
| in | Dans (ex : {% if x in [1, 2] %})
|
| matches | Correspond |
| starts with | Commence par |
| ends with | Se termine par |
| .. | Séquence (ex : 1..5)
|
| + | Plus |
| - | Moins |
| ~ | Concaténation |
| * | Multiplication |
| / | Division |
| // | Division arrondie à l'inférieur |
| % | Modulo |
| is | Test (ex : is defined ou is not empty)
|
| ** | Puissance |
| | | Filtre |
| [] | Entrée de tableau |
| . | Attribut ou méthode d'un objet (ex : country.name)
|
Pour afficher la valeur NULL dans un opérateur ternaire, il faut la mettre entre apostrophes :
{{ (myVariable is not empty) ? '"' ~ myVariable.value ~ '"' : 'null' }}
Fonctions usuelles
[modifier | modifier le wikicode]Chemins, routes et URLs
[modifier | modifier le wikicode]url('route_name'): affiche l'URL complète d'une route. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex :url('ma_route_de_controleur', {'parametre1': param1})).absolute_url('path'): affiche l'URL complète d'un chemin.path('route_name'): affiche le chemin, en absolu par défaut, mais il existe le paramètrerelative=true. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex :path('ma_route_de_controleur', {'parametre1': param1}).asset('path'): pointe le dossier des "assets" ("web" dans SF2, "public" dans SF4). Ex :<img src="{{ asset('images/mon_image.png') }}" />.controller('controller_name'): exécute la méthode d'un contrôleur. Ex :{{ render(controller('App\\Controller\\DefaultController:indexAction')) }}.
absolute_url() renvoie l'URL de l'application si l'appel provient d'un contrôleur, mais http://localhost s'il vient d'une commande (CLI)[7]. La solution est donc de définir l'URL de l'environnement dans une variable, soit default_uri de routing.yaml, soit maison et injectée par le contrôleur dans le Twig.
Divers
[modifier | modifier le wikicode]constant(constant_name): importe une constante d'une classe PHP[10].attribute(object, method): accède à l'attribut d'un objet PHP. C'est équivalent au "." mais la propriété peut être dynamique[11].date(): convertit en date, ce qui permet leur comparaison. Ex :{% if date(x) > date(y) %}. NB : comme en PHP, "d/m/Y" correspond au format "jj/mm/aaaa".- min() : renvoie le plus petit nombre de ceux en paramètres (ou dans un tableau en paramètre 1).
- max() : renvoie le plus grand nombre de ceux en paramètres (ou dans un tableau en paramètre 1).
Filtres
[modifier | modifier le wikicode]Les filtres fournissent des traitements sur une expression, si on les place après elle séparés par des pipes. Par exemple :
capitalize: équivaut au PHPucfirst(), met une majuscule à la première lettre d'une chaine de caractères, et passe les autres en minuscules.upper: équivaut au PHPstrtoupper(), met la chaine en lettres capitales. Exemple pour ne mettre la majuscule que sur la première lettre :{{ variable[:1]|upper ~ variable[1:] }}.lower: équivaut au PHPstrtolower().first: affiche la première ligne d'un tableau, ou la première lettre d'une chaine.length: équivaut au PHPsizeof(), renvoie la taille de la variable (chaine ou tableau).format: équivaut au PHPprintf().date: équivaut au PHPdate()mais son format est du type DateInterval[12].date_modify: équivaut au PHP DateTime->modify(). Ex :{% set tomorrow = 'now'|date_modify("+1 day") %}.replace: équivaut au PHPstr_replace(). Ex :{{ 'Mon titre %tag%.'|replace({'%tag%': '1'}) }}.join: équivaut au PHPimplode(): convertit un tableau en chaine avec un séparateur en paramètre.split: équivaut au PHPexplode(): convertit une chaine en tableau avec un séparateur en paramètre.slice(début, fin): équivaut au PHParray_slice()+substr(): découpe un tableau ou une chaine selon deux positions[13].trim: équivaut au PHPtrim().raw: ne pas échapper les balises HTML.json_encode: transforme un tableau en chaine de caractères JSON.default: ce filtre lève les exceptions sur les variables non définies ou vides[14]. Ex :
{{ variable1 |default(null) }}
Variables spéciales
[modifier | modifier le wikicode]loopcontient les informations de la boucle dans laquelle elle se trouve. Par exempleloop.indexdonne le nombre d'itérations déjà survenue (commence par 1 et pas par 0).- Les variables globales commencent par des underscores, par exemple[15] :
_route: partie de l'URL située après le domaine._self: nom de du fichier courant._charset: jeu de caractères de la page. Ex : UTF-8._context: variables injectées dans le template. Cela peut donc permettre d'y accéder en variables variables. Ex :{{ attribute(_context, 'constante'~variable) }}{{ attribute(form, 'constante'~variable) }}pour un champ de formulaire.
- Les variables d'environnement CGI, telles que
{{ app.request.server.get('SERVER_NAME') }}
Pour obtenir la route d'une page : {{ path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}
L'URL courante : {{ app.request.uri }}
La page d'accueil du site Web : url('homepage')
app.environment renvoie la valeur de APP_ENV.
Gestion des espaces
[modifier | modifier le wikicode]spaceless
[modifier | modifier le wikicode]Un Twig bien formaté ne correspond pas forcément au rendu qu'il doit apporter. Pour supprimer les espaces du formatage dans ce rendu :
{% apply spaceless %}
<b>
Hello World!
</b>
{% endspaceless %}
NB : en Twig < 2.7, c'était[16] :
{% spaceless %}
{% autoescape false %}
<b>
Hello World!
</b>
{% endspaceless %}
Par ailleurs, il existe un filtre |spaceless[17].
De plus, on peut apposer le symboles "-" aux endroits où ignorer les espacements (dont retours chariot) du formatage :
Hello {% ... -%}
{%- ... %} World!
Cela fonctionne aussi entre {{- -}}.
Utilisation du traducteur
[modifier | modifier le wikicode]Configuration
[modifier | modifier le wikicode]Le module de traduction Symfony s'installe avec : composer require translator
Quand une page peut apparaitre dans plusieurs langues, inutile d'injecter la locale dans le Twig depuis le contrôleur PHP, c'est une variable d'environnement que l'on peut récupérer avec :
{{ app.request.getLocale() }}
{{ app.request.get('mon_query_param') }}
Le fichier YAML contenant les traductions dans cette langue sera automatiquement utilisé s'il est placé dans le dossier "translations" apparu lors de l'installation. En effet, il est identifié par le code langue ISO de son suffixe (ex : le Twig de la page d'accueil pourra être traduit dans homepage.fr.yml, homepage.en.yml, etc.).
Pour définir le préfixe des YAML auquel un Twig fera appel, on le définit sans suffixe en début de fichier Twig :
{% trans_default_domain 'homepage' %}
Par ailleurs, la commande PHP pour lister les traductions les traductions d'une langue est[18] :
php bin/console debug:translation en --only-unused // Pour les inutilisées
php bin/console debug:translation en --only-missing // Pour les manquantes
Filtre trans
[modifier | modifier le wikicode]Une fois la configuration effectuée, on peut apposer le filtre trans aux textes traduis dans le Twig.
{{ MessageInMyLanguage |trans }}
Parfois, il peut être utile de factoriser les traductions de plusieurs Twig dans un seul YAML. Pour piocher dans un YAML qui n'est pas celui par défaut, il suffit de le nommer en second paramètre du filtre trans :
{{ 'punctuation_separator'|trans({}, 'common') }}
Si le YAML contient des balises HTML à interpréter, il faut apposer le filtre raw après trans.
Si une variable doit apparaitre dans une langue différente de celle de l'utilisateur, on le précisera dans le troisième paramètre du filtre trans :
{{ FrenchMessage |trans({}, 'common', 'fr') }}
Si le YAML doit contenir une variable, on la place entre pourcentages pour la remplacer en Twig avec le premier paramètre du filtre trans :
{{ variableMessage |trans({"%price%": formatPrice(myPrice)}) }}
Si la clé à traduire doit être variable, on ne peut pas réaliser la concaténation dans la même commande que la traduction : il faut décomposer en deux lignes :
{% set variableMessage = 'constante.' ~ variable %}
{{ variableMessage |trans }}
Opération trans
[modifier | modifier le wikicode]Il existe aussi une syntaxe alternative au filtre. Par exemple les deux paragraphes ci-dessous sont équivalents :
{{ 'punctuation_separator'|trans({}, 'common') }}
{% trans from 'common' %}
punctuation_separator
{% endtrans %}
De plus, on peut injecter une variable avec "with". Voici deux équivalents :
{{ 'Bonjour %name% !' |trans({"%name%": name}) }}
{% trans with {'%name%': name}%}Bonjour %name% !{% endtrans %}
Méthodes PHP appelables en Twig
[modifier | modifier le wikicode]En PHP, on peut définir des fonctions invocables en Twig, sous forme de fonction ou de filtre selon la méthode parente surchargée. Exemple :
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('getPrice', [$this, 'getPrice']),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('getPrice', [$this, 'getPrice']),
];
}
public function getPrice($value): string
{
return number_format($value, 2, ',', ' ') . ' €';
}
}
Héritages et inclusions
[modifier | modifier le wikicode]extends
[modifier | modifier le wikicode]Si une fichier appelé doit être inclus dans un tout, il doit en hériter avec le mot extends. Le cas typique est celui d'une "base.html.twig" qui contient l'en-tête et le pied de page HTML commun à toutes les pages d'un site. Ex :
{% extends "base.html.twig" %}
Twig ne supporte pas l'héritage multiple[19].
Il est possible de surcharger totalement ou en partie les blocs du template parent. Exemple depuis le template qui hérite :
{% block header %}
Mon en-tête qui écrase le parent
{% endblock %}
{% block footer %}
Mon pied de page qui complète le parent
{{ parent() }}
{% endblock %}
include
[modifier | modifier le wikicode]À contrario, si un fichier doit en inclure un autre (par exemple pour qu'un fragment de vue soit réutilisable dans plusieurs pages), on utilise le mot include. Ex :
{% include("partials/footer.html.twig") %}
En lui injectant des paramètres :
{% include("partials/footer.html.twig") with {'clé': 'valeur'} %}
embed
[modifier | modifier le wikicode]Enfin, embed combine les deux précédentes fonctions :
{% embed "footer.html.twig" %}
...
{% endembed %}
import
[modifier | modifier le wikicode]import récupère certaines fonctions d'un fichier en contenant plusieurs :
{% from 'mes_macros.html' import format_price as price, format_date %}
Macros
[modifier | modifier le wikicode]Les macros sont des fonctions globales, appelables depuis un fichier Twig[22].
Exemple :
{% macro format_price(price, currency = '€') %}
{% set locale = (app.request is null) ? 'fr_FR' : app.request.locale %}
{% if locale == 'fr_FR' %}
{{ price|number_format(2, ',', ' ') }} {{ currency }}
{% else %}
{{ price|number_format(2, '.', ' ') }}{{ currency }}
{% endif %}
{% endmacro %}
Lors de l'appel, les paramètres nommés ne fonctionnent que si 100 % des paramètres appelés le sont.
Exemples
[modifier | modifier le wikicode] {% extends "base.html.twig" %}
{% block navigation %}
<ul id="navigation">
{% for item in navigation %}
<li>
<a href="{{ item.href }}">
{% if item.level == 2 %} {% endif %}
{{ item.caption|upper }}
</a>
</li>
{% else %}
Aucun élément.
{% endfor %}
</ul>
{% endblock navigation %}
Pour ne pas qu'un bloc hérité écrase son parent, mais l'incrémente plutôt, utiliser :
{{ parent() }}
Bonnes pratiques
[modifier | modifier le wikicode]Les noms des fichiers .twig doivent être rédigés en snake_case[23].
Références
[modifier | modifier le wikicode]- ↑ https://twig.symfony.com/pdf/2.x/Twig.pdf
- ↑ https://twig.sensiolabs.org/doc/2.x/tags/set.html
- ↑ https://www.designcise.com/web/tutorial/how-to-concatenate-strings-and-variables-in-twig
- ↑ https://twig.symfony.com/doc/3.x/filters/merge.html
- ↑ https://twig.symfony.com/doc/3.x/filters/filter.html
- ↑ http://twig.sensiolabs.org/doc/templates.html
- ↑ https://stackoverflow.com/questions/73026340/absolute-url-in-template-returns-localhost-in-email-templates
- ↑ https://symfony.com/doc/current/reference/twig_reference.html
- ↑ https://symfony.com/doc/current/http_cache/esi.html
- ↑ https://twig.symfony.com/doc/2.x/functions/constant.html
- ↑ https://twig.symfony.com/doc/2.x/functions/attribute.html
- ↑ https://twig.symfony.com/doc/3.x/filters/date.html
- ↑ https://twig.symfony.com/doc/3.x/filters/slice.html
- ↑ https://twig.symfony.com/doc/2.x/filters/default.html
- ↑ https://twig.symfony.com/doc/3.x/templates.html#global-variables
- ↑ https://twig.symfony.com/doc/2.x/tags/spaceless.html
- ↑ https://twig.symfony.com/doc/2.x/filters/spaceless.html
- ↑ https://symfony.com/doc/current/translation/debug.html
- ↑ https://twig.symfony.com/doc/3.x/tags/extends.html
- ↑ https://twig.symfony.com/doc/1.x/functions/include.html
- ↑ https://twig.symfony.com/doc/2.x/tags/include.html
- ↑ https://twig.symfony.com/doc/2.x/tags/macro.html
- ↑ https://symfony.com/doc/current/contributing/code/standards.html
Installation
[modifier | modifier le wikicode]Doctrine est l'ORM par défaut de Symfony. Il utilise PDO. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la chaîne de responsabilité.
Installation en SF4[1] :
composer require symfony/orm-pack composer require symfony/maker-bundle --dev
Renseigner l'accès au SGBD dans le .env :
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
Ensuite la base de données doit être créée avec :
php bin/console doctrine:database:create
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
- doctrine/doctrine-bundle
- doctrine/doctrine-migrations-bundle
- doctrine/orm
- symfony/proxy-manager-bridge
Commandes Doctrine
[modifier | modifier le wikicode]Exemples de commandes :
php bin/console doctrine:query:sql "SELECT * FROM ma_table" php bin/console doctrine:query:sql "$(< mon_fichier.sql)" # Ces deux commandes sont équivalentes des précédentes php bin/console dbal:run-sql "SELECT * FROM ma_table" php bin/console dbal:run-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
Entity
[modifier | modifier le wikicode]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 getters et setters respectifs. Pour en générer une :
php bin/console generate:doctrine:entity
Cette association est définie par des attributs Doctrine. Pour les vérifier :
php bin/console doctrine:schema:validate
Exemple
[modifier | modifier le wikicode]Voici par exemple plusieurs types d'attributs :
#[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;
Exemple avec annotation (avant PHP 8)
/**
* @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;
Et leurs modificateurs (getters et setters) :
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;
}
}
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 $word->setLanguage($language).
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 #[ORM\JoinColumn (anciennement @ORM\JoinColumn) mentionnant la clé étrangère en base de données (et pas en PHP).
Bonnes pratiques
[modifier | modifier le wikicode]L'attribut #[ORM\Table(name: 'word')] é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 Table, mais ce n'est plus le cas avec les attributs :
#[ORM\UniqueConstraint(name: 'spelling-pronunciation', columns: ['spelling', 'pronunciation'])]
Exemple avec annotation (avant PHP 8)
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
Dans les relations *toMany :
- il faut initialiser l'attribut dans le constructeur en
ArrayCollection(). - 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).
Il faut ajouter le #[ORM\JoinColumn( dans les deux entités liées, car :
- dans aucune cela renvoie
Could not resolve type of column "id" - 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 length=0[2]. Le type "text" par contre n'a pas de limite.
ArrayCollection
[modifier | modifier le wikicode]Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
- Dans une entité :
#[ORM\OrderBy(['sort_order' => 'ASC'])](anciennement@ORM\OrderBy({"sort_order" = "ASC"})). - Sinon, instancier un critère :
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
GeneratedValue
[modifier | modifier le wikicode]L'annotation GeneratedValue peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID[3]. 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 :
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
Triggers
[modifier | modifier le wikicode]Les opérations en cascade sont définies sous deux formes d'attributs :
#[ORM\OneToMany(cascade: ['persist', 'remove'])]: au niveau ORM.#[ORM\JoinColumn(onDelete: 'CASCADE')]: 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
[modifier | modifier le wikicode]Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware[4].
Les types des attributs peuvent être quelque peu différents du SGBD[5].
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[6]).
Héritage
[modifier | modifier le wikicode]Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante[7] :
/** @MappedSuperclass */
class MyEntityParent
...
Tables sans classe
[modifier | modifier le wikicode]Doctrine peut créer des tables de mapping sans entité, si on précise son nom dans les deux tables reliées :
#[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)]
EntityManager
[modifier | modifier le wikicode]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 :
- persist() : prépare un INSERT SQL (rattache une entité à un entity manager).
- remove() : prépare un DELETE SQL.
- flush() : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
- merge() : fusionne une entité absent de l'em dedans.
- refresh() : 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
$createdDate:
$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);
Repository
[modifier | modifier le wikicode]On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de Doctrine\ORM\EntityRepository. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés NomDeLEntitéRepository.
CarFactory fera un new Car() mais aussi créera et lui associera ses composants : new Motor()...#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]
Exemple avec annotation (avant PHP 8)
@ORM\Entity(repositoryClass="App\Repository\WordRepository")
SQL
[modifier | modifier le wikicode]Depuis Doctrine
[modifier | modifier le wikicode]Utile pour exploiter les fonctionnalités du SGBD utilisé, absentes de Doctrine.
Par exemple, pour appeler une procédure stockée :
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
Ou tronquer une table :
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('TRUNCATE TABLE ma_table', $rsm)->getResult();
$connection = $this->_em->getConnection();
$databasePlatform = $connection->getDatabasePlatform();
$connection->executeStatement(
$databasePlatform->getTruncateTableSQL($this->_em->getClassMetadata(MonEntite::class)->getTableName(), true),
);
Sans Doctrine
[modifier | modifier le wikicode]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) :
return $this->connection->fetchAll($sql);
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) :
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
2. En injectant le service de connexion dans le constructeur ('@database_connection') :
use Doctrine\DBAL\Connection; ... return $this->dbalConnection->fetchAll($sql);
DQL
[modifier | modifier le wikicode]Méthodes magiques
[modifier | modifier le wikicode]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 :
$repo->find($id): cherche par la clé primaire définie dans l'entité.$repo->findAll(): récupère tous les enregistrements (sans clauseWHERE).$repo->findById($id): engendre automatiquement unSELECT * WHERE id = $iddans la table associée au repo.$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])engendre automatiquement unSELECT * WHERE lastname = $lastname AND firstname = $firstname.$repo->findOneById($id): engendre automatiquement unSELECT * WHERE id = $id LIMIT 1.$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname]): engendre automatiquement unSELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1.
Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "findById" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser findBy().
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
createQuery
[modifier | modifier le wikicode]DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec IDENTITY() pour les jointures. Ex :
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();
}
createQueryBuilder
[modifier | modifier le wikicode]L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel createQueryBuilder() :
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
Pour éviter le SELECT * dans cet exemple, on peut y ajouter la méthode ->select().
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
Jointures
[modifier | modifier le wikicode]Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
Pour filtrer quand une jointure toMany contient des résultats, utiliser EMPTY :
...
->andWhere('files IS NOT EMPTY')
Résultats
[modifier | modifier le wikicode]Doctrine peut renvoyer avec :
getResult(): un objet ArrayCollection (iterable, pour rechercher dedans :->contains()), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;getArrayResult()ougetScalarResult(): un tableau de tableaux (entité normalisée) ;getSingleColumnResult(): un tableau unidimensionnel.
Cache
[modifier | modifier le wikicode]Configuration globale
[modifier | modifier le wikicode]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 :
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
Puis dans doctrine.yaml, les utiliser :
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
À partir de là le cache des métadonnées est utilisé partout.
Configuration par entité
[modifier | modifier le wikicode]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
#[ORM\Cache(usage: 'READ_ONLY', region: 'write_rare')](anciennement@ORM\Cache(usage="READ_ONLY", region="write_rare")[8]), utilisant la configuration doctrine.yaml :
doctrine:
orm:
second_level_cache:
enabled: true
regions:
write_rare:
lifetime: 864000
cache_driver: { type: service, id: cache.app }
- Dans le repository :
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
Dans cet exemple, on n'utilise pas cache.system pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. cache.app 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 :
$em->getCache()->evictEntityRegion(myEntity::class);
Expressions
[modifier | modifier le wikicode]Pour ajouter une expression en DQL, utilise $qb->expr(). Ex[9] :
$qb->expr()->count('u.id')$qb->expr()->between('u.id', 2, 10)(entre 2 et 10)$qb->expr()->gte('u.id', 2)(plus grand ou égal à 2)$qb->expr()->like('u.name', '%son')$qb->expr()->lower('u.name')$qb->expr()->substring('u.name', 0, 1)
Injection de dépendances
[modifier | modifier le wikicode]Les repository DQL deoivent ServiceEntityRepository :
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
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
[modifier | modifier le wikicode]Pour garantir d'intégrité d'une transaction[10] :
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
Il existe aussi une syntaxe alternative :
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
Évènements
[modifier | modifier le wikicode]Pour ajouter des triggers sur la mise à jour d'une table, il y a deux solutions :
- ajouter dans son entité l'attribut
#[ORM\HasLifecycleCallbacks], et à ses méthodes l'attribut de l'évènement concerné. Ex :
#[ORM\PrePersist]
public function setCreatedAt(): self
{
$this->createdAt = new DateTime();
return $this;
}
- ajouter des tags dans services.yaml. Ex :
App\EventListener\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: PrePersist }
Voici les évènements utilisables ensuite (dans les listeners / subscribers) :
prePersist
[modifier | modifier le wikicode]Se produit avant la persistance d'une entité (paramètre : PrePersistEventArgs $args).
postPersist
[modifier | modifier le wikicode]Se produit après la persistance d'une entité (PostPersistEventArgs $args).
preUpdate
[modifier | modifier le wikicode]Se produit avant l'update d'une entité (PreUpdateEventArgs $args).
postUpdate
[modifier | modifier le wikicode]Se produit après l'update d'une entité (PostUpdateEventArgs $args).
preRemove
[modifier | modifier le wikicode]Se produit avant l'update d'une entité (PreRemoveEventArgs $args).
postRemove
[modifier | modifier le wikicode]Se produit après l'update d'une entité (PostRemoveEventArgs $args).
preFlush
[modifier | modifier le wikicode]Se produit avant la sauvegarde d'une entité (PreFlushEventArgs $args).
- 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
$unitOfWork->computeChangeSet($classMetadata, $entity)[11].
Ex :
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
//...
}
LifecycleEventArgs $args dans ces fonctions.
Parfois le $object::class peut renvoyer Proxies\__CG__\App\Entity\MyEntity au lieu de App\Entity\MyEntity, selon le cache utilisé.
postFlush
[modifier | modifier le wikicode]Se produit après la sauvegarde d'une entité (PostFlushEventArgs $args).
Migrations
[modifier | modifier le wikicode]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 :
composer require doctrine/doctrine-migrations-bundle
Création
[modifier | modifier le wikicode]Ensuite, on peut créer un squelette de "migration" :
php bin/console doctrine:migrations:generate
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 migration_versions enregistre leur succession).
Exemple SQL
[modifier | modifier le wikicode]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');
}
}
Exemple DQL
[modifier | modifier le wikicode]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');
}
}
Exemple PHP
[modifier | modifier le wikicode]Depuis Symfony 7.0, il faut implémenter MigrationFactory pour injecter des dépendances dans les migrations (et on ne peut plus injecter tout le conteneur)[13].
Avant Symfony 7.0, il fallait juste utiliser ContainerAwareTrait
Exemple :
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();
}
}
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.
$this->container->getParameter() ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple $_SERVER['SUBAPI_URI'] renvoie la variable d'environnement et $this->containergetParameter('env(SUBAPI_URI)') sa valeur par défaut (définie dans services.yaml).
Exécution
[modifier | modifier le wikicode]La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
php bin/console doctrine:migrations:migrate
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) :
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
Pour le rollback :
php bin/console doctrine:migrations:execute --down 20170321095644
Pour éviter que Doctrine pose des questions durant les migrations, ajouter --no-interaction (ou -n).
Pour voir le code SQL au lieu de l'exécuter : --write-sql.
Sur plusieurs bases de données
[modifier | modifier le wikicode]Pour exécuter sur plusieurs bases :
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
Avec des migrations.yaml de type :
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'
Synchronisation
[modifier | modifier le wikicode]Vers le code
[modifier | modifier le wikicode]Vers les entités
[modifier | modifier le wikicode]php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
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 Rector pour convertir les annotations. Ex :
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable vendor/bin/rector process src/Entity/MyNewEntity.php
Vers les migrations
[modifier | modifier le wikicode]Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
Vers la base
[modifier | modifier le wikicode]À 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
[modifier | modifier le wikicode]Il existe plusieurs bibliothèques pour créer des fixtures, dont une de Doctrine[14] :
composer require --dev orm-fixtures
Pour charger les fixtures du code dans la base :
php bin/console doctrine:fixtures:load -n
Types de champ
[modifier | modifier le wikicode]La liste des types de champ Doctrine se trouve dans Doctrine\DBAL\Types. 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 JsonType pour surcharger le type JSON par défaut afin de lui faire faire json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne[15].
Réplication SQL
[modifier | modifier le wikicode]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[16] :
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
replicas:
replica1:
url: '%env(resolve:REPLICA_DATABASE_URL)%'
Critique
[modifier | modifier le wikicode]- 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 lazy loading est contourné par au choix :
- Avant l'appel null, un
ObjetJoint->get(). - Dans l'entité, un
@ManyToOne(…, fetch="EAGER"). - Dans le repository, un
$this->queryBuilder->addSelect(). NB : si cela ajoute un problème N+1, joindre aussi la deuxième entité qui le provoque.
- Avant l'appel null, un
- 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
$this->queryBuilder->setMaxResults()et$this->queryBuilder->setFirstResult()en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar duLIMITSQL). La solution consiste à ajouter un paginateur[17]. - 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
UNION ALLquand on joint deux entités non liées dans le repo.
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/doctrine.html
- ↑ https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string
- ↑ https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations
- ↑ https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html
- ↑ https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html
- ↑ https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly
- ↑ https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener
- ↑ https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2
- ↑ https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies
- ↑ https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
- ↑ https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23
- ↑ https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35
- ↑ https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939
Pour créer une interface de programmation (API) REST avec Symfony, il existe plusieurs bibliothèques :
- API Platform[1], tout-en-un qui utilise les attributs PHP (ou des annotations en PHP < 8) des entités pour créer les APIs (donc pas besoin de créer des contrôleurs ou autres). Par défaut il permet de sérialiser les flux en JSON (dont JSON-LD, JSON-HAL, JSON:API), XML (dont HTML), CSV, YAML, et même en GraphQL[2].
- Sinon il faut combiner plusieurs éléments : routeur, générateur de doc en ligne et sérialiseur.
API Platform
[modifier | modifier le wikicode]Installation
[modifier | modifier le wikicode]composer require api
Utilisation
[modifier | modifier le wikicode]
Le GraphQL de la version 1.1 passe par le schéma REST, et ne bénéficie donc pas du gain de performances attendu sans overfetching.
En bref, les routes d'API sont définies depuis les entités Doctrine.
Pour ajouter des fonctionnalités supplémentaires aux create/read/update/delete, il faut passer par des data providers[3] ou des data persisters[4], pour transformer les données, respectivement à l'affichage et à la sauvegarde.
Sécurité
[modifier | modifier le wikicode]Par défaut toutes les routes sont accessibles sans identification (selon security.yaml). Pour changer cela, on peut utiliser les Custom Doctrine ORM Extension[5] :
class CurrentApiUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{ ... }
Attributs
[modifier | modifier le wikicode]ApiResource
[modifier | modifier le wikicode]Définit les noms et méthodes REST (GET, POST...) des routes de l'API.
Exemple sur la V3[6] :
#[ApiResource(
operations: [
new Get(),
new GetCollection()
]
)]
class MyEntity...
Avec personnalisation de la vue OpenAPI :
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
],
openapiContext: [
'summary' => '',
'tags' => ['Enums'],
]
)]
Idem sur la V2
#[ApiResource(
collectionOperations: [
'get' => [
'openapiContext' => [
'summary' => '',
'tags' => ['Enums'],
],
],
],
itemOperations: [
'get' => [
'openapiContext' => [
'summary' => '',
'tags' => ['Enums'],
],
],
],
)]
ApiProperty
[modifier | modifier le wikicode]Par exemple pour masquer un champ d'entité sur la route d'API :
#[ApiProperty(readable: false, writable: false, required: false, fetchable: false)]
MaxDepth
[modifier | modifier le wikicode]Définit le niveau de sérialisation d'un élément lié. Par exemple, si un client a plusieurs contrats et que ses contrats ont plusieurs produits, un MaxDepth(1) sur l'attribut client->contrat fera que la liste des clients comprendra tous les contrats mais pas leurs produits.
Évènements
[modifier | modifier le wikicode]API Platform ajoute plus d'une dizaine d'évènements donc les priorités sont définies dans EventPriorities[7].
Par exemple, pour modifier un JSON POST envoyé, utiliser EventPriorities::PRE_DESERIALIZE.
L'évènement suivant POST_DESERIALIZE contient les objets instanciés à partir du JSON.
Triplet de bibliothèques
[modifier | modifier le wikicode]Installation
[modifier | modifier le wikicode]FOS REST
[modifier | modifier le wikicode]FOSRestBundle apporte des annotations pour créer des contrôleurs d'API[8]. Installation :
composer require "friendsofsymfony/rest-bundle"
Puis dans config/packages/fos_rest.yaml :
fos_rest:
view:
view_response_listener: true
format_listener:
rules:
- { path: '^/', prefer_extension: true, fallback_format: ~, priorities: [ 'html', '*/*'] }
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] }
Documentation
[modifier | modifier le wikicode]Toute API doit exposer sa documentation avec ses routes et leurs paramètres. NelmioApiDocBundle est un de générateur de documentation automatique à partir du code[9], qui permet en plus de tester en ligne. En effet, pour éviter de tester les API en copiant-collant leurs chemins dans une commande cURL ou dans des logiciels plus complets comme Postman[10], on peut installer une interface graphique ergonomique qui allie documentation et test en ligne :
composer require "nelmio/api-doc-bundle"
Son URL se configure ensuite dans routes/nelmio_api_doc.yml :
app.swagger_ui:
path: /api/doc
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
À ce stade l'URL /api/doc affiche juste un lien NelmioApiDocBundle. Mais si les contrôleurs d'API sont identifiés dans annotations.yaml (avec un préfixe "api"), on peut voir une liste automatique de toutes leurs routes.
Pour documenter /api/* sauf /api/doc, il faut préciser l'exception en regex dans packages/nelmio_api_doc.yaml :
nelmio_api_doc:
areas:
path_patterns:
- ^/api/(?!/doc$)
Il faut vider le cache de Symfony à chaque modification de nelmio_api_doc.yaml.
Authentification
[modifier | modifier le wikicode]Pour tester depuis la documentation des routes nécessitant un token JWT, ajouter dans packages/nelmio_api_doc.yaml :
nelmio_api_doc:
documentation:
securityDefinitions:
Bearer:
type: apiKey
description: 'Value: Bearer {jwt}'
name: Authorization
in: header
security:
- Bearer: []
Il devient alors possible de renseigner le token avant de tester.
{{remarque|Si par défaut (sans configuration) on voit juste un champ "JWT (http, Bearer)" non envoyé depuis l'API doc, ajouter le paragraphe suivant pour qu'il le soit :
security:
- JWT: []
Exemple
[modifier | modifier le wikicode]Dans un contrôleur, au-dessus de l'attribut #[Route] d'un CRUD :
use OpenApi\Attributes as OA;
...
#[OA\Post(
requestBody: new OA\RequestBody(
required: true,
content: [
new OA\JsonContent(
examples: [
new OA\Examples('1', summary: 'By ID', value: '{ "myEntity": { "id": 1 }}'),
new OA\Examples('2', summary: 'By name', value: '{ "myEntity": { "name": "TEST" }}'),
],
type: 'object',
),
],
),
tags: ['MyEntities'],
responses: [
new OA\Response(
response: Response::HTTP_OK,
description: 'Returns myEntity information.',
content: new OA\JsonContent(
properties: [
new OA\Property(
property: "id",
type: "integer",
example: 1,
nullable: true
),
new OA\Property(
property: "name",
type: "string",
example: 'TEST',
nullable: true
),
]
)
),
new OA\Response(
response: Response::HTTP_NOT_FOUND,
description: 'Returns no myEntity.',
content: new OA\JsonContent(
properties: [
new OA\Property(
property: "id",
type: "integer",
example: null,
nullable: true
),
]
)
)
]
)]
Sérialiseur
[modifier | modifier le wikicode]Enfin pour la sérialisation, on distingue plusieurs solutions :
- symfony/serializer, qui donne des contrôleurs
extends AbstractFOSRestControlleret des méthodes aux annotations@Rest\Post()[11]. - jms/serializer-bundle, avec des contrôleurs
extends RestControlleret des méthodes aux annotations@ApiDoc(). - Le service
fos_rest.service.serializer.
symfony/serializer
[modifier | modifier le wikicode]composer require "symfony/serializer"
jms/serializer-bundle
[modifier | modifier le wikicode]composer require "jms/serializer-bundle"
Utilisation
[modifier | modifier le wikicode]Maintenant /api/doc affiche les méthodes des différents contrôleurs API. Voici un exemple :
<?php
namespace App\Controller;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class APIController extends AbstractFOSRestController
{
#[Route('/api/test', methods: ['GET'])]
public function testAction(Request $request): View
{
return View::create('ok');
}
}
Dans PHP < 8
...
/**
* @Route("/api/test", methods={"GET"})
*/
public function testAction(Request $request): View
...
}
Maintenant dans /api/doc, cliquer sur /api/test, puis "Ty it out" pour exécuter la méthode de test.
Sécurité
[modifier | modifier le wikicode]Une API étant stateless, l'authentification est assurée à chaque requête, par l'envoi par le client d'un token JSON Web Token (JWT) dans l'en-tête HTTP (clé Authorization). Côté serveur, on transforme ensuite le JWT reçu en objet utilisateur pour accéder à l'identité du client au sein du code. Ceci est fait en configurant security.yaml, pour qu'un évènement firewall appelle automatiquement un guard authenticator[12] :
composer require "symfony/security-bundle"
security:
firewalls:
main:
guard:
authenticators:
- App\Security\TokenAuthenticator
Manipuler les JWT
[modifier | modifier le wikicode]Pour décrypter le JWT :
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
...
public function __construct(
private readonly JWTEncoderInterface $jwtEncoder,
) {
}
public function decodeJwt(string $jwt): array
{
return $this->jwtEncoder->decode($jwt):
}
Résultat minimum :
array:6 [
"iat" => 1724677672
"exp" => 1724764072
"roles" => array:1 [
0 => "ROLE_INACTIF"
]
"username" => "test"
]
Si on n'a pas besoin de vérifier le JWT (validité, expiration et signature modifiée par un pirate), on peut se passer de Lexik ainsi :
private function decodeJwt(string $jwt): array|bool|null
{
$tokenParts = explode('.', $jwt);
if (empty($tokenParts[1])) {
return [];
}
$tokenPayload = base64_decode($tokenParts[1]);
return json_decode($tokenPayload, true);
}
Test
[modifier | modifier le wikicode]Pour tester en shell :
TOKEN=123
curl -H 'Accept: application/json' -H "Authorization: Bearer ${TOKEN}" http://localhost
Références
[modifier | modifier le wikicode]- ↑ https://api-platform.com/
- ↑ https://api-platform.com/docs/core/content-negotiation/
- ↑ https://api-platform.com/docs/core/data-providers/
- ↑ https://api-platform.com/docs/core/data-persisters/
- ↑ https://api-platform.com/docs/core/extensions/#custom-doctrine-orm-extension
- ↑ https://api-platform.com/docs/core/operations/
- ↑ https://api-platform.com/docs/core/events/
- ↑ https://github.com/FriendsOfSymfony/FOSRestBundle
- ↑ https://github.com/nelmio/NelmioApiDocBundle
- ↑ https://www.postman.com/
- ↑ https://www.thinktocode.com/2018/03/26/symfony-4-rest-api-part-1-fosrestbundle/
- ↑ https://symfony.com/doc/current/security/guard_authentication.html
PHPUnit
PHPUnit est utilisé dans un certain nombre de frameworks connus pour réaliser des tests unitaires. Sa documentation en anglais est disponible au format PDF[1].
Installation
[modifier | modifier le wikicode]Via composer
[modifier | modifier le wikicode]composer require --dev phpunit/phpunit ^8
Via wget
[modifier | modifier le wikicode]Une fois le .phar téléchargé depuis le site officiel[2], le copier dans le dossier où il sera toujours exécuté. Exemple :
Unix-like
[modifier | modifier le wikicode] wget https://phar.phpunit.de/phpunit-8.phar
mv phpunit.phar /usr/local/bin/phpunit
chmod +x phpunit.phar
Windows
[modifier | modifier le wikicode]- Ajouter à la variable d'environnement
PATH, le dossier où se trouve le fichier (ex :;C:\bin). - Créer un fichier exécutable à côté (ex :
C:\bin\phpunit.cmd) contenant le code :@php "%~dp0phpunit.phar" %*.
Par ailleurs, le code source de cet exécutable est sur GitHub[3].
Test
[modifier | modifier le wikicode]Test de l'installation :
phpunit --version
Utilisation
[modifier | modifier le wikicode]Il faut indiquer au programme les dossiers contenant des tests dans le fichier phpunit.xml.dist. Exemple sur Symfony[4] :
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.0/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="KERNEL_CLASS" value="AppKernel" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory suffix=".php">./tests</directory>
<exclude>tests/FunctionalTests/*</exclude>
</testsuite>
</testsuites>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
</phpunit>
Si on a plusieurs dossiers à exclure, mieux vaut sélectionner plutôt ceux à traiter :
<directory suffix=".php">tests/UnitTests</directory>
<directory suffix=".php">tests/FunctionalTests/QuickTests</directory>
Ensuite, pour tester tous les fichiers du dossier /test et ignorer ceux de /src, il suffit de lancer :
./bin/phpunit
Pour exclure un seul fichier ou une seule méthode des tests, lui mettre $this->markTestIncomplete('This test has to be fixed.');
Options
[modifier | modifier le wikicode]Ce .xml donne des options par défaut qui peuvent être modifiées dans les commandes. Par exemple stopOnFailure="true" dans la balise <phpunit> peut être par défaut, et phpunit --stop-on-failure seulement pour ce lancement.
Choisir les tests à lancer
[modifier | modifier le wikicode]Si les tests sont longs et qu'on ne travaille que sur un seul fichier, une seule classe ou une seule méthode, on peut demander à ne tester qu'elle en précisant son nom (ce qui évite d'afficher des dumps que l'on ne souhaite pas voir lors des autres tests) :
bin/phpunit tests/MaClasseTest.php
bin/phpunit --filter=MaClasseTest
bin/phpunit --filter=MaMethodeTest
Si une méthode dépend d'une autre, on ne n'appeler que ces deux-là (peu importe l'ordre) :
bin/phpunit --filter='test1|test2'
Détails de chaque test
[modifier | modifier le wikicode]Pour afficher les noms des tests et le temps qu'ils prennent, utiliser : --testdox
Rapports
[modifier | modifier le wikicode]Outre les résultats des tests, on peut avoir besoin de mesurer et suivre leur complétude, via le taux de couverture de code. PhpUnit permet d'afficher ce taux en installant Xdebug et en activant son option xdebug.mode = coverage.
Le calcul du taux de couverture peut ensuite être obtenu avec : bin/phpunit --coverage-text
Certains fichiers ne peuvent en aucun cas être testés, et doivent donc être exclus du calcul du taux de couverture dans phpunit.xml.dist. Par exemple pour les migrations et fixtures :
<exclude>
<directory suffix=".php">src/Migrations/</directory>
<file>src/DataFixtures/AppFixtures.php</file>
</exclude>
Dans un fichier
[modifier | modifier le wikicode]Le résultat des tests peut être sauvegardé dans un fichier de rapport XML avec l'option --log-junit phpunit.logfile.xml.
L'ajout de l'option --coverage-html reports/ générera un rapport du taux de couverture des tests en HTML (mais d'autres formats sont disponibles tels que l'XML ou le PHP), dans le dossier "reports" (créé automatiquement).
Exemple récupérable par l'outil d'analyse de code SonarQube : phpunit --coverage-clover phpunit.coverage.xml --log-junit phpunit.logfile.xml
Écriture des tests
[modifier | modifier le wikicode]use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class SuiteDeTests1 extends TestCase
{
private MockObject $monMock1;
protected function setUp(): void
{
// Création des mocks et instanciation de la classe à tester...
$this->monMock1 = $this->getMockBuilder(maMockInterface::class)->getMock();
}
protected function tearDown(): void
{
// Libération des ressources après les tests...
}
public static function setUpBeforeClass(): void
{
// Pour réinitialiser une connexion déclarée dans setUp()
}
public static function tearDownAfterClass(): void
{
// Pour fermer une connexion déclarée dans setUp()
}
protected function test1()
{
// Lancement du premier test...
$this->assertTrue($condition);
}
}
La classe de test PHPUnit propose des dizaines d'assertions différentes.
$this->fail()
[modifier | modifier le wikicode]PhpUnit distingue pour chaque test, les erreurs (ex : division par zéro) des échecs (assertion fausse).
Dans le cas où on on souhaiterait transformer les erreurs en échecs, on peut utiliser $this->fail() :
try {
$response = $this->MonTestEnErreur();
} catch (\Throwable $e) {
$this->fail($e->getMessage());
}
MockObject
[modifier | modifier le wikicode]Les mocks sont des objets PhpUnit qui permettent de simuler des résultats de classes existantes[5].
Psr\Log\NullLogger qui peut être instancié depuis les tests des classes utilisant Psr\Log\LoggerInterface.
Les méthodes statiques ne peuvent pas être mockée, il faut les encapsuler dans une dynamique.
willReturn()
[modifier | modifier le wikicode]Par exemple, pour simuler le résultat de deux classes imbriquées (en appelant la méthode d'une méthode), on leur crée une méthode de test chacune :
public function mainTest()
{
$this->monMock1
->expects($this->once())
->method('MaMéthode1')
->willReturn($this->mockProvider())
;
$this->assertEquals(null, $this->monMock1->MaMéthode1()->MaMéthode2());
}
private function mockProvider()
{
$monMock = $this
->getMockBuilder('MaClasse1')
->getMock()
;
$monMock->method('MaMéthode2')
->willReturn('MonRésultat1')
;
return $monMock;
}
Pour qu'une méthode de mock réalise un "set" quand elle est appelée, il ne faut pas le faire directement dans le willReturn, auquel cas il s'effectue lors de sa définition, mais dans un callback. Ex :
$monMock->method('MaMéthode3')
->will($this->returnCallback(function($item) use ($quantity) {
return $item->setQuantity($quantity);
}));
willReturnArgument()
[modifier | modifier le wikicode]Renvoie l'argument dont le numéro est en paramètre.
willThrowException()
[modifier | modifier le wikicode]Pour qu'un mock simule une erreur. Ex :
$monMock->method('MaMéthode3')->willThrowException(new Exception());
expects()
[modifier | modifier le wikicode]Dans l'exemple précédent, expects() est un espion qui compte le nombre de passage dans la méthode, et le test échoue si ce résultat n'est pas 1. Ses valeurs possibles sont :
$this->never(): 0.$this->once(): 1.$this->exactly(x): x.$this->any().
De plus, on trouve $this->at() pour définir un comportement dépendant du passage.
onConsecutiveCalls
[modifier | modifier le wikicode]Si la valeur retournée par le mock doit changer à chaque appel, il faut remplacer willReturn() par onConsecutiveCalls().
Exemple :
$this->enumProvider->method('getEnumFromVariable')
->will($this->onConsecutiveCalls(
ProductStatusEnum::ON_LINE,
OrderStatusEnum::VALIDATED
)
);
with()
[modifier | modifier le wikicode]Cette méthode permet de définir les paramètres avec lesquels doit être lancé une méthode mock. Ex :
$this->enumProvider->method('getEnumFromVariable')
->with($this->equalTo('variable 1'))
disableOriginalConstructor()
[modifier | modifier le wikicode]Cette méthode s'emploie quand il est inutile de passer par le constructeur du mock.
expectException()
[modifier | modifier le wikicode]S'utilise quand le test unitaire doit provoquer une exception dans le code testé (ex : s'il contient un throw).
$this->expectException(Exception::class);
$monObjetTesté->method('MaMéthodeQuiPète');
Si au contraire on veut vérifier que le code testé ne renvoie pas d'exception, on peut le lancer suivi d'une incrémentation des assertions :
$monObjetTesté->method('MaMéthodeSansErreur');
$this->addToAssertionCount(1);
Attributs
[modifier | modifier le wikicode]PHPUnit depuis sa version 10 offre plusieurs attributs pour influencer les tests. Exemples :
#[DataProvider()]: indique un tableau d'entrées et de sorties attendues lors d'un test[6].#[Depends()]: spécifie qu'une méthode récupère le résultat d'une autre (son return) dans ses arguments.
Annotations
[modifier | modifier le wikicode]PHPUnit offre plusieurs annotations pour influencer les tests[7]. Exemples :
@covers: renseigne la méthode testée par une méthode de test afin de calculer le taux de couverture du programme par les tests.@uses: indique les classes instanciées par le test.@dataProvider: indique un tableau d'entrées et de sorties attendues lors d'un test[8].@depends: spécifie qu'une méthode récupère le résultat d'une autre (son return) dans ses arguments. Si elle appartient à un autre fichier, il faut renseigner son namespace :@depends App\Tests\FirstTest::testOne. Et comme PhpUnit exécute les tests dans l'ordre alphabétique des fichiers, il faut que le test se trouve après celui dont il dépend.
JavaScript
[modifier | modifier le wikicode]En PHP, Selenium peut s'interfacer avec PHPUnit[9] pour tester du JavaScript.
Avec Symfony, il existe aussi Panther[10].
Symfony
[modifier | modifier le wikicode]Pour récupérer une variable d'environnement ou un service dans un test unitaire Symfony, il faut passer par setUpBeforeClass() pour booter le kernel du framework :
private static string $maVariableYaml;
private static Translator $translator;
public static function setUpBeforeClass(): void
{
$kernel = static::createKernel();
$kernel->boot();
self::$maVariableYaml = $kernel->getContainer()->getParameter('ma_variable');
self::$translator = $kernel->getContainer()->get('translator');
}
Seuls les services publics seront accessibles. Mais il est possible de créer des alias publics des services accessibles uniquement en environnement de test grâce au fichier de config services_test.yaml.
Test fonctionnel :
public function testPost(): void
{
$route = '/api/test';
$body = [
'data' => ['post_parameter_1' => 'value 1'],
];
static::$client->request('POST', $route, [], [], [], json_encode($body));
$response = static::$client->getResponse();
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertTrue($response->isSuccessful(), $response);
$content = json_decode($response->getContent(), true);
$this->assertNotEmpty($content['message'], json_encode($content));
}
Références
[modifier | modifier le wikicode]- ↑ https://phpunit.de/manual/current/en/phpunit-book.pdf
- ↑ https://phpunit.de/
- ↑ https://github.com/sebastianbergmann/phpunit
- ↑ https://symfony.com/doc/current/testing.html
- ↑ https://phpunit.de/manual/current/en/test-doubles.html
- ↑ https://docs.phpunit.de/en/10.5/writing-tests-for-phpunit.html#data-providers
- ↑ https://phpunit.readthedocs.io/fr/latest/annotations.html
- ↑ https://blog.martinhujer.cz/how-to-use-data-providers-in-phpunit/
- ↑ Chaine complète de test avec Selenium IDE, Selenium RC et PHPUnit
- ↑ https://github.com/symfony/panther
SimpleTest
SimpleTest est un framework de test open source en PHP qui possède une documentation en français sur http://www.simpletest.org/fr/.
Une fois le .gz téléchargé depuis le site officiel, le décompresser dans un répertoire lisible par Apache.
Il existe également sous la forme d'un plugin Eclipse[1].
HelloWorld
[modifier | modifier le wikicode]Soit le fichier HelloWorld.php situé dans le répertoire du framework :
<?php
require_once('autorun.php');
class TestHelloWorld extends UnitTestCase
{
function TestExitenceFichiers()
{
$this->assertTrue(file_exists($_SERVER['SCRIPT_FILENAME']));
$this->assertFalse(file_exists('HelloWikibooks.php'));
}
}
En exécutant ce script dans un navigateur, toutes les méthodes des classes de test sont exécutées séquentiellement. Il devrait donc comme prévu, se trouver lui-même, puis ne pas trouver un fichier HelloWikibooks avec succès.
Les nombres de tests réussis et échoués sont comptabilisés en bas de page, mais seuls les noms des fonctions en échec sont affichés.
Test d'un formulaire web
[modifier | modifier le wikicode]Plusieurs méthodes sont disponibles pour interagir avec les formulaires[2]. Voici un exemple qui recherche certains mots sur un célèbre site, tente de s'y authentifier, et d'écrire dedans :
<?php
require_once('autorun.php');
require_once('web_tester.php');
class TestWikibooks extends WebTestCase
{
function TestTextesSurPage()
{
$this->assertTrue($this->get('http://fr.wikibooks.org/wiki/Accueil'));
$this->assertTitle('Wikilivres');
$this->assertText('licence');
$this->assertPattern('/Bienvenue/i');
$this->authenticate('MonLogin', 'MonMDP');
$this->assertField('search', 'test');
$this->clickSubmit('Lire');
$this->assertText('Introduction au test logiciel');
}
}
Sous réserve que le site possède bien un champ "name=search".
Références
[modifier | modifier le wikicode]
Behat
Behat est un framework de test pour faire du behavior-driven development. Cela consiste à rédiger plusieurs scénarios en langage Gherkin, proche de l'anglais naturel, avec indentation comme syntaxe, dans des fichiers .feature. Ces tests peuvent également tester du JavaScript.
Installation
[modifier | modifier le wikicode]Lancer les tests avec en ligne de commande.
Syntaxe
[modifier | modifier le wikicode]Feature: Function to test description
Texte libre
Scenario: Scenario 1
Given preconditions
When actions
Then results
Scenario: Scenario 2
...
Les préconditions après "Given" correspondent au nom de la méthode PHP à exécuter.
Exemples
[modifier | modifier le wikicode]use Behat\Behat\Context\Context;
class Context1 implements Context
{
public function iAmOnTheHomePage()
{
echo 'ok';
throw new PendingException();
}
}
Feature: Visit the homepage
Scenario: Click a link from the homepage
Given I am on the homepage
Compléments
[modifier | modifier le wikicode]Mink[1] est une bibliothèque PHP permettant de simuler un navigateur Web, ce qui permet à Behat de tester du JavaScript avec Selenium[2].
Références
[modifier | modifier le wikicode]- ↑ http://mink.behat.org/en/latest/
- ↑ (en) Junade Ali, Mastering PHP Design Patterns, Packt Publishing Ltd, (lire en ligne)
Problèmes connus
Le processus de débogage est relativement le même d'un bug à l'autre :
- En cas d'erreur 400, regarder d'abord les logs les plus spécifiques (ex : le var/log de l'application), puis les logs des serveurs Web (ex : /var/log/nginx), puis ceux du système (/var/log).
- En cas d'erreur 500 ou de non réponse, passer directement aux logs des serveurs.
- En cas d'absence de log, localiser le problème avec :
La liste suivante doit permettre de gagner du temps pour solutionner les erreurs que l'on peut trouver dans les logs.
Erreurs sans message PHP
[modifier | modifier le wikicode]IIS tourne dans le vide après la mise à jour de PHP
[modifier | modifier le wikicode]Vérifier que les dépendances sont bien installées (ex : Visual C++)[1].
Les modifications du fichier php.ini ne sont pas prises en compte dans phpinfo
[modifier | modifier le wikicode]Sur Wamp, redémarrer PHP et Apache ne suffit pas car Apache contient une copie du fichier php.ini dans C:\wamp64\bin\apache\apache2.4.54.2\bin, créée lors de la sélection de la version de PHP (au clic sur PHP/Version).
La connexion a été réinitialisée
[modifier | modifier le wikicode]Erreur sous Firefox provenant d'un mysql_close() ou d'une directive Apache.
La page n'est pas redirigée correctement
[modifier | modifier le wikicode]Un header revient en boucle après une suite de conditions. S'il est local, le remplacer par chdir().
Le code PHP n'est pas interprété (et est affiché)
[modifier | modifier le wikicode]Si a2enmod php7.4 indique que le module est déjà installé, c'est peut-être lié à a2enmod userdir. Cela peut se régler avec :
vim /etc/apache2/mods-enabled/php7.4.conf
Commenter les lignes :
<IfModule mod_userdir.c>
...
</IfModule>
Et relancer Apache.
Sinon c'est peut-être le vhost utilisé qui n'est pas le bon (voir /var/log/apache2) ou qu'il ne contient pas :
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
Require all granted
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Allow from all
</Directory>
Les dernières lignes d'un POST sont ignorées
[modifier | modifier le wikicode]Il faut soit augmenter la variable PHP max_input_vars (ce qui ne peut pas être fait avec ini_set()[2]), soit il faut fragmenter en plusieurs requêtes, par exemple toutes les 300 lignes.
Une expression régulière (regex) marche sur https://regex101.com/ mais pas en local
[modifier | modifier le wikicode]Si la machine est Windows, tenir compte de la différence de retour chariot : \r\n au lieu de \n.
Une addition ou soustraction de dates ajoute ou retire un jour
[modifier | modifier le wikicode]Cela survient quand on part d'un mois à 31 jours pour arriver à un mois qui en a moins.
Par exemple :
var_dump((new DateTime('2024-12-31'))->add(new DateInterval("P6M"))); // 2025-07-01 00:00:00
var_dump((new DateTime('2024-12-31'))->add(new DateInterval("P12M"))); // 2025-12-31 00:00:00
Pour avoir un calcul juste, on peut ajuster le jour du résultat ainsi :
$startDate = new DateTime('2024-12-31');
$originalDay = $startDate->format('d');
$startDate->add(new DateInterval('P6M'));
if ($startDate->format('d') !== $originalDay) {
// Ajuster au dernier jour du mois précédent
$startDate->modify('last day of previous month');
}
var_dump($startDate); // 2025-06-30 00:00:00
Une addition ou soustraction de dates ajoute ou retire une heure
[modifier | modifier le wikicode]C'est à cause des changements d'heure d'hiver et heure été.
Par exemple :
var_dump((new DateTime('2024-12-31'))->add(new DateInterval("PT4320H"))); // 2025-06-29 01:00:00
var_dump((new DateTime('2024-12-31'))->add(new DateInterval("PT8640H"))); // 2025-12-26 00:00:00
Pour avoir un calcul juste, on peut changer de fuseau horaire le temps du calcul, vers un qui n'est pas soumis aux changements d'heures[3] :
var_dump((new DateTime('2024-12-31 UTC'))->add(new DateInterval("PT4320H"))); // 2025-06-29 00:00:00
var_dump((new DateTime('2024-12-31 UTC'))->add(new DateInterval("PT8640H"))); // 2025-12-26 00:00:00
ou à l'échelle du serveur :
$timeZone = date_default_timezone_get();
date_default_timezone_set('UTC');
// calcul
date_default_timezone_set($timeZone);
PHP natif
[modifier | modifier le wikicode]Connect Error, 2002: Aucune connexion n'a pu être établie car l'ordinateur cible l'a expressément refusée.
[modifier | modifier le wikicode]Relancer le serveur de base de données.
Connect Error, 2002: Une tentative de connexion a échoué car le parti connecté n'a pas répondu convenablement au-delà d'une certaine durée ou une connexion établie a échoué car l'hôte de connexion n'a pas répondu.
[modifier | modifier le wikicode]Ouvrir les pare-feux.
Invalid body indentation level (expecting an indentation level of at least 8)
[modifier | modifier le wikicode]En PHP7.3, la syntaxe heredoc impose de supprimer l'indentation de la balise fermante : elle soit suivre "\n".
json_decode renvoie NULL (aléatoirement)
[modifier | modifier le wikicode]Ajouter le paramètre 4 : JSON_THROW_ON_ERROR.
json_decode throw "Control character error, possibly incorrectly encoded"
[modifier | modifier le wikicode]La string à décoder est trop longue.
Malformed UTF-8 characters, possibly incorrectly encoded
[modifier | modifier le wikicode]Changer l'encodage de la chaine avant son encodage en JSON, avec[4] : $chaine = mb_convert_encoding($chaine, 'UTF-8', 'auto');
MySQL server has gone away
[modifier | modifier le wikicode]La limite des 61 jointures a peut-être été atteinte dans une requête. Sinon vérifier les limites des ressources (du .ini) : par défaut default_socket_timeout égal 60 s.
This extension requires the Microsoft ODBC Driver 11 for SQL Server
[modifier | modifier le wikicode]Installer le pilote depuis https://www.microsoft.com/en-us/download/details.aspx?id=36434.
Unable to initialize module. Module compiled with module API=x. PHP compiled with module API=y. These options need to match
[modifier | modifier le wikicode]Se procurer une autre DLL à renseigner dans le fichier php.ini.
You can only iterate a generator by-reference if it declared that it yields by-reference
[modifier | modifier le wikicode]Se produit quand on itère sur la référence d'un générateur PHP :
foreach ($generator as &$item) {
...
}
Il faut donc retirer l'opérateur de référence (&) de l'itération...
Cannot traverse an already closed generator
[modifier | modifier le wikicode]Un iterator_to_array($generator) le ferme en le convertissant en tableau.
child exited on signal 7 (SIGBUS)
[modifier | modifier le wikicode]Modifier le fichier php.ini[5] :
pm.max_children = 80
pm.max_spare_servers = 20
pm.max_requests = 200
apc.stat = 0
max_children est calculé en divisant la RAM par la taille des processus[6].
Invalid resource type: unknown type
[modifier | modifier le wikicode]Le paramètre n°2 de fopen() ne permet pas la lecture[7].
Each stream must be readable
[modifier | modifier le wikicode]On essaie de lire un fichier fermé : déplacer le fclose() après s'il y en a un avant.
fclose(): supplied resource is not a valid stream resource
[modifier | modifier le wikicode]On essaie de fermer un fichier fermé : utiliser if (is_resource($f)) avant.
SSL peer certificate or SSH remote key was not OK
[modifier | modifier le wikicode]Lors d'un curl_exec, ajouter :
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
ou :
$options['verify_host'] = false;
$options['verify_peer'] = false;
Fatal error
[modifier | modifier le wikicode][] operator not supported for strings
[modifier | modifier le wikicode] // On récupère une variable dont on ne connait pas le type pour en faire un tableau
if (!isset($tableau1)) {
$tableau1 = array();
} elseif (is_string($tableau1)) {
$tableau1 = array($tableau1);
}
$tableau1[] = 'paramètre suivant';
Allowed memory size of x bytes exhausted
[modifier | modifier le wikicode]Modifier le fichier php.ini ou bien ajouter une autre limite dans le programme :
ini_set('memory_limit', '100M');
Pour faire sauter la limitation :
ini_set('memory_limit', '-1');
Si c'est dans une commande : php -d memory_limit=-1 ma_commande
Si c'est Composer : COMPOSER_MEMORY_LIMIT=-1 ./composer.phar update
Sinon, utiliser Xdebug en mode pas à pas pour visualiser les variables à supprimer (avec unset()).
Call to a member function ... on a non-object
[modifier | modifier le wikicode]La méthode est invoquée sur une variable qui n'est pas une classe.
Call to undefined function
[modifier | modifier le wikicode]Si une fonction est définie mais qu'on ne peut pas l'invoquer dans une méthode de classe, il faut préalablement l'importer en tenant compte du polymorphisme.
Call to undefined function sqlsrv_connect()
[modifier | modifier le wikicode]Installer le pilote correspondant à la version de PHP du serveur Web :
- Télécharger sur https://www.microsoft.com/en-us/download/details.aspx?id=20098.
- Copier dans le dossier PHP (ex : C:\Program Files (x86)\EasyPHP\binaries\php\php_runningversion\ext).
- Ajouter au fichier
php.ini. - Redémarrer le serveur Web.
Call to undefined method
[modifier | modifier le wikicode]La méthode est invoquée sur une classe qui ne l'a pas.
Si elle est censée l'avoir c'est qu'elle n'est pas récupérée, ce qui peut arriver avec les jointures entre entités d'ORM.
Cannot access empty property
[modifier | modifier le wikicode]Une variable non définie ne peut pas fournir de propriété. Si elle était définie ailleurs c'est qu'elle est inaccessible, et donc qu'il faut la récupérer (ex : avec global).
Error connecting to the ODBC database: [Microsoft][SQL Server Native Client 10.0][SQL Server]échec de l'ouverture de session de l'utilisateur
[modifier | modifier le wikicode]La précédente connexion n'a pas dû être fermée proprement avant une tentative de reconnexion. Essayer au choix :
mysql_close($conn); // MySQL
sqlsrv_close($conn); // MS-SQL
odbc_close($conn); // ODBC
Out of memory
[modifier | modifier le wikicode]Éditer le fichier php.ini pour augmenter la limite mémoire :
memory_limit = 256M
Si cela ne suffit pas, vérifier la taille des structures et tableaux alloués, probablement par une boucle avec trop d'itérations ou une boucle infinie, ou le chargement d'une ressource trop volumineuse en mémoire.
pdo_sqlsrv_db_handle_factory: Unknown exception caught
[modifier | modifier le wikicode]Installer et configurer le paquet suivant :
apt-get install -y locales
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
Uncaught exception 'com_exception' with message 'Failed to create COM object `xxx.application': Accès refusé.
[modifier | modifier le wikicode]Sur le serveur Web Windows, dans démarrer, exécuter dcomcnfg (sinon y aller dans Outils d'administration, Service de composants), puis Ordinateurs, Poste de travail, Configuration DCOM, aller dans les propriétés de l'application mentionnée, puis dans l'onglet Sécurité, définir la première permission (Autorisations d'exécution et d'activation à Personnaliser et ajouter le compte du serveur ou site qui exécute le script (ex pour ISS : IIS_IUSRS ou IUSR).
Par exemple pour Word l'application se prénomme "Document Microsoft Office Word", et Excel "Microsoft Excel Application".
Uncaught exception 'com_exception' with message 'Source: Microsoft Office Excel Description: Mémoire insuffisante.
[modifier | modifier le wikicode]Il faut certainement rerégler la Configuration DCOM voire le serveur Web.
Uncaught exception 'PDOException' with message
[modifier | modifier le wikicode]could not find driver
[modifier | modifier le wikicode]Se référer à la liste des pilotes connus dans Programmation PHP/PDO#Installation.
SQLSTATE[HY000]: General error
[modifier | modifier le wikicode]PDO ne gère pas le code SQL "set @ma_variable" ou "if". Donc il faut plutôt faire ces calculs en PHP.
SQLSTATE[28000] [1045] Access denied for user
[modifier | modifier le wikicode]Si l'utilisateur existe avec les privilèges nécessaires, y compris depuis toute localisation (@%), il faut quand-même créer un deuxième compte homonyme pour l'emplacement distant (ex : 'username'@'example.com'). Par exemple dans phpMyAdmin, remplir la provenance mentionnée en erreur ('example.com') dans le champ "Client".
SQLSTATE[28000] SQLConnect: 18456 [Microsoft][ODBC SQL Server Driver][SQL Server] Échec de l'ouverture de session de l'utilisateur
[modifier | modifier le wikicode]Se référer à Programmation PHP/PDO#Accès à la base de données avec PDO : la source de données ODBC doit être suivie du compte pour y accéder.
SQLSTATE[IM002] SQLConnect: 0 [Microsoft][Gestionnaire de pilotes ODBC] Source de données introuvable et nom de pilote non spécifié
[modifier | modifier le wikicode]Se référer à Programmation PHP/PDO#Accès à la base de données avec PDO : la source de données ODBC doit être créée dans C:\Windows\SysWOW64\odbcad32.exe.
SQLSTATE[IMSSP]: An invalid keyword 'host' was specified in the DSN string
[modifier | modifier le wikicode]Se référer à Programmation PHP/PDO#Accès à la base de données avec PDO : le paramètre 'host' est valide pour MySQL mais pas pour MS-SQL.
SQLSTATE[IMSSP]: The DSN string ended unexpectedly
[modifier | modifier le wikicode]Se référer à Programmation PHP/PDO#Accès à la base de données avec PDO : les virgules et points-virgules changent d'un pilote à l'autre.
SQLSTATE[IMSSP]: This extension requires the Microsoft SQL Server 2012 Native Client ODBC Driver to communicate with SQL Server
[modifier | modifier le wikicode]Se référer à Programmation PHP/PDO#Accès à la base de données avec PDO : utiliser la syntaxe ODBC.
Notice
[modifier | modifier le wikicode]Undefined property
[modifier | modifier le wikicode]La propriété de la variable n'est pas déclarée.
Undefined index: SERVER_NAME in ...php
[modifier | modifier le wikicode]Certaines versions de PHP utilisent $_SERVER['HTTP_HOST'] au lieu de $_SERVER['SERVER_NAME'].
Undefined variable
[modifier | modifier le wikicode]La variable n'est pas déclarée.
Use of undefined constant
[modifier | modifier le wikicode]La constante n'est pas déclarée.
Parse error
[modifier | modifier le wikicode]syntax error, unexpected '(', expecting variable (T_VARIABLE) or '$' in...
[modifier | modifier le wikicode]Séparer le $ dans la chaine (ex : {$ -> { $).
syntax error, unexpected '' (T_ENCAPSED_AND_WHITESPACE), expecting identifier (T_STRING) or variable (T_VARIABLE) or number (T_NUM_STRING)
[modifier | modifier le wikicode]Remplacer les variables incluses dans des chaines. Ex :
$query="select $contact['member']"; // pas bien
$query="select ".$contact['member']; // bien
syntax error, unexpected '$Variable' (T_VARIABLE), expecting function (T_FUNCTION)
[modifier | modifier le wikicode]Dans une classe en dehors des méthodes, il faut déclarer les variables avec leur portée :
private $Variable;
Strict Standards
[modifier | modifier le wikicode]Declaration of extFunctions::logs() should be compatible with functions::logs($chaine)
[modifier | modifier le wikicode]La fonction (logs() dans l'exemple) existe déjà, peut-être avec des majuscules (peu importe les arguments).
Only variables should be passed by reference
[modifier | modifier le wikicode]Il faut appliquer la fonction end() sur une variable au lieu du résultat d'une opération[8]. Ex :
// $extension = end(explode('.', $fichier));
$ext = explode('.', $fichier);
$extension = end($ext);
Warning
[modifier | modifier le wikicode]Cannot use a scalar value as an array
[modifier | modifier le wikicode]Un tableau de valeurs ne peut pas être redéfini en tableau de tableaux si elles existent. Remplacer les cas ainsi :
$tab['1'] = 1;
//$tab['1']['un'] = 'un';
$tab['un']['un'] = 'un';
Creating default object from empty value
[modifier | modifier le wikicode]Se produit quand on appelle l'attribut d'un objet NULL.
date_diff() expects parameter 1 to be DateTimeInterface
[modifier | modifier le wikicode]La classe native DateTime() est plus pratique que la fonction date_diff() :
$Avant = new DateTime('20140101');
$Apres = new DateTime();
print $Avant->diff($Apres)->format("%d");
Illegal string offset
[modifier | modifier le wikicode]On invoque une entrée inexistante dans un tableau associatif. Lever l'exception avec Try ou if (!isset(.
mkdir(): File exists
[modifier | modifier le wikicode]Sous Docker Desktop pour Windows, mkdir() appelle file_exists(), et ce dernier renvoie true si le dossier a existé.
PHP Startup: Unable to load dynamic library 'memcached.so'
[modifier | modifier le wikicode]sudo pecl install memcached
Si cela persiste : sudo apt-get install php-igbinary sudo apt-get install php-msgpack sudo service php7.2-fpm reload
PHP Startup: Unable to load dynamic library 'redis.so'
[modifier | modifier le wikicode]sudo pecl install redis
Erreurs SMTP
[modifier | modifier le wikicode]La connexion a échoué
[modifier | modifier le wikicode]Vérifier le serveur HTTP qui interprète le fichier .php.
SMTP Error: Could not connect to SMTP host
[modifier | modifier le wikicode]Changer de SMTP, ex : http://www.commentcamarche.net/faq/893-parametres-de-serveurs-pop-imap-et-smtp-des-principaux-fai
Si les mails partent sans arriver
[modifier | modifier le wikicode]- Vérifier que l'IP de l'expéditeur n'est pas blacklistée : http://whatismyipaddress.com/blacklist-check
- Définir un reverse DNS si absent
- Veiller à ce que le mail ne soit pas présumé spam, en évitant les sujets vides par exemple, ou les pièces jointes exécutables non compressées (.exe, .cmd, .vbs...).
Composer
[modifier | modifier le wikicode]1 package has known vulnerabilities
[modifier | modifier le wikicode]Exemple avec un guzzlehttp/guzzle (7.4.3) absent du composer.json :
composer depends guzzlehttp/guzzle
puis composer update du résultat.
Si cela ne suffit pas, installer Guzzle puis le désinstaller pour mettre à jour sa dépendance indirecte : composer require guzzlehttp/guzzle && composer remove guzzlehttp/guzzle
"./composer.json" does not contain valid JSON
[modifier | modifier le wikicode]Lors d'un composer install, si ce message survient à tort, c'est qu'un autre fichier .json du projet contient le problème.
Si cela persiste malgré la correction, il se peut qu'il faille redémarrer Docker Desktop sur Windows.
Conclusion: don't install xxx
[modifier | modifier le wikicode]Lors d'un composer require, spécifier une version inférieure du paquet requis.
No driver found to handle VCS repository
[modifier | modifier le wikicode]VCS fonctionne en protocole git, vérifier que l'URL est bien au format "git@repo:bundle.git".
Sinon il y a deux alternatives :
- Pour HTTPS, remplacer la dépendance de type "vcs" par une de type "package"[9].
- Pour décompresser un .zip, utiliser le type "artifact".
no matching package found
[modifier | modifier le wikicode]Ajouter le paramètre suivant : composer require mon_paquet --update-with-all-dependencies
Permission denied (public key)
[modifier | modifier le wikicode]Si le dépôt privé se clone bien sans passer par "composer" : voir la page Programmation PHP/Composer.
You must be using the interactive console to authenticate
[modifier | modifier le wikicode]Pour installer cette bibliothèque, il faut que Composer puisse se loguer. Pour ce faire, il utilise auth.json qui peut se trouver dans[10] :
$HOME/.composer/auth.json- ou à côté du
composer.json
Exemple de auth.json[11] :
{
"github-oauth": {
"github.com": "<snip>"
},
"http-basic": {
"repo.magento.com": {
"username": "<snip>",
"password": "<snip>"
}
}
}
Your Composer dependencies require a PHP version ">= 8.0.0".
[modifier | modifier le wikicode]Ajouter au paragraphe "config" :
"platform-check": false
Puis relancer composer install pour que cela soit pris en compte.
Your requirements could not be resolved to an installable set of packages
[modifier | modifier le wikicode]Si deux dépendances s'empêchent mutuellement de se mettre à jour, les demander dans la même commande :
composer require mon-bundle ^1.0 symfony/http-client 5.3.* -W
Timeout sur composer install
[modifier | modifier le wikicode]Désactiver Xdebug et relancer.
PHPUnit
[modifier | modifier le wikicode]Les tests ne se lancent pas
[modifier | modifier le wikicode]Si phpunit.xml.dist utiliser un bootstrap.php, y ajouter error_reporting(E_ALL);.
Sinon, si un var_dump() fonctionne dans le setUp() du test unitaire mais pas dans ses méthodes de test, c'est peut-être une exception qui se lance dans un trait ou dans le vendor PHPUnit. Pour la trouver, lancer l'application et regarder les logs (par exemple depuis un contrôleur).
Sinon, si ça fonctionne en commentant le "extends", tester la classe mère pour y trouver l'exception.
Sinon, dans Symfony, tail var/log/test/mon_log.log.
Sinon, lancer Xdebug pour comprendre.
et echo() ou var_dump() dans les tests n'affiche rien
[modifier | modifier le wikicode]Lancer le test en mode le plus verbeux :
- Avec le paramètre : -vvv
- Modifier phpunit.xml.dist avec :
- <server name="SHELL_VERBOSITY" value="3" />[12]
- <ini name="error_reporting" value="true" />
Les tests fonctionnels renvoient toujours 404
[modifier | modifier le wikicode]Sur Symfony, self::createClient() appelle directement l'API sans serveur HTTP. Si on utilise phpunit dans symfony/phpunit-bridge, il va chercher sur example.com.
Sinon il manque peut-être un trailing slash dans la route appelée.
Les tests se lancent mais s'arrêtent sans explication, en renvoyant "killed"
[modifier | modifier le wikicode]Un var_dump() sature la mémoire.
Did you forget a "use" statement for MaClasse ou Class 'MaClasse' not found
[modifier | modifier le wikicode]Si des classes existent mais que PHPUnit n'arrive pas à les charger :
- Vérifier les namespaces racines définis dans composer.json par autoload et autoload-dev.
- Retirer suffix=".php" du phpunit.xml utilisé.
The .git directory is missing from...
[modifier | modifier le wikicode]Supprimer vendor/ et relancer composer.
THE ERROR HANDLER HAS CHANGED!
[modifier | modifier le wikicode]Plusieurs solutions possibles :
- phpunit --self-update
- Dans Symfony, changer phpunit.xml.dist avec SYMFONY_DEPRECATIONS_HELPER = weak_vendors
- set_error_handler(array(&$this, 'handleGeoError'));
- Si le projet Symfony a Sentry, on peut le retirer des tests dans bundles.php.
Trying to configure method "get" which cannot be configured because it does not exist, has not been specified, is final, or is static
[modifier | modifier le wikicode]Un mock ne récupère aucune méthode de sa classe car elle n'a pas pu être instanciée.
- La casse du nom de la classe ou du namespace de son
usen'est probablement pas exacte. - Sinon c'est la portée de la méthode mockée qui n'a pas publique. Si un dump du mock montre les attributs mais pas les méthodes, remplacer
->getMockForAbstractClass()par->getMock(). - S'il s'agit d'une méthode finale, il faut la définir lors de l'instanciation du mock. Ex :
$this->serializerMock = $this
->getMockBuilder(SerializerInterface::class)
->setMethods(['serialize', 'deserialize', 'decode'])
->getMock()
;
$this->serializerMock
->method('decode')
->willReturn('')
;
Trying to @cover or @use not existing method
[modifier | modifier le wikicode]Si la méthode existe bien, c'est que la classe testée n'a pas été définie en annotation (avec son namespace) :
/**
* @coversDefaultClass App\Service\MyService
*/
class MyServiceTest extends TestCase
{
...
}
TypeError: Argument 1 passed to PHPUnit\Framework\TestCase::registerMockObjectsFromTestArguments() must be of the type array, null given
[modifier | modifier le wikicode]Si cela survient dans tous les tests qui invoquent un trait, retirer le constructeur de ce trait.
Warning No tests found in class "Xxx".
[modifier | modifier le wikicode]Si les méthodes de tests contiennent des assertions invisibles de PHPUnit, leur ajouter /** @test */ pour afficher pourquoi ils ne se lancent pas. Par exemple, il peut s'agir d'un mock qui demande un constructeur.
Si c'est normal de ne pas lancer de test dans une classe mère, la rendre abstraite ou statique.
Symfony
[modifier | modifier le wikicode]
En cas d'erreur, un composant de débogage appelé "Profiler", est accessible en bas à gauche de la page d'erreur, avec des logs et mesures de performances. Installation :
composer require --dev symfony/profiler-pack
On peut par exemple accéder au phpinfo() via l'URL .../_profiler/phpinfo.
Le profiler fonctionne pour les contrôleurs mais pas pour les commandes.
La première soumission d'un formulaire ne marche pas, mais les suivantes oui
[modifier | modifier le wikicode]Retirer le :
$uow = $em->getUnitOfWork(); $uow->computeChangeSets();
Le contrôle de formulaire avec name=‘xxx’ ne peut recevoir le focus.
[modifier | modifier le wikicode]Il s'agit généralement d'un champ caché : le passer en required=false ou faire en sorte qu'il soit toujours rempli même caché.
Un champ de formulaire ChoiceType n'affiche pas sa valeur par défaut, qui est pourtant dans la liste
[modifier | modifier le wikicode]La liste contient une clé d'un type different. Exemple de solution :
'data' => (string) $myInteger,
Une route de contrôleur fonctionne dans un client HTTP mais pas dans un autre
[modifier | modifier le wikicode]Si $request est vide avec certains clients HTTP :
- Utiliser HTTPS au lieu de HTTP.
- Vérifier les paramètres d'en-tête HTTP (Accept: 'application/json' et Content-Type: 'application/json').
- Utiliser $request->getContent() ou ou $request->toArray() au lieu de $request->request[13] (ça ne marche pas à la place de $request->query).
Une route de contrôleur fonctionne dans un client HTTP mais pas en PhpUnit, ex : No matching accepted Response format could be determined (406 Not Acceptable)
[modifier | modifier le wikicode]composer remove friendsofsymfony/rest-bundle
Un fichier téléchargé est toujours à 0
[modifier | modifier le wikicode]Il est probablement trop volumineux et il faut donc le streamer sans le charger dans la RAM, en remplaçant $fileContent = file_get_contents($localFilePath); new Response($fileContent); par $response = new BinaryFileResponse($localFilePath);.
Attempted to load class "WebProfilerBundle" from namespace "Symfony\Bundle\WebProfilerBundle"
[modifier | modifier le wikicode]Si cela fonctionne avec "composer install" mais pas avec " composer install --no-dev", il faut définir APP_ENV=prod dans le .env.
Attribute "autowire" on service "xxx" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.
[modifier | modifier le wikicode]Lors d'utilisation de service abstrait, on ne peut pas utiliser l'autowiring. On peut alors transformer ce service abstrait en dépendance à injecter plutôt qu'à hériter.
Cannot autowire service... alors qu'il existe
[modifier | modifier le wikicode]Peut se produire :
- quand le dossier de la classe est dans "App\.exclude" du services.yaml.
- quand il n'est pas exclus et qu'on le déclare dans un .yaml importé dans services.yaml (donc en doublon de l'autowiring). Il faut alors soit le désactiver, soit exclure les namespaces concernés, soit déplacer ces déclarations dans services.yaml.
- dans un bundle, si
$loader->load('services.yaml');est bien effectué[14], alors le déclarer dans le services.yaml du bundle. Sinon, vérifier qu'il n'y a pas un chemin erroné dans l'extension prepend(). - Depuis Symfony 6.1 on peut utiliser l'attribut
#[Autowire][15].
#[AsEventListener(event: KernelEvents::CONTROLLER)]
class MyListener
{
public function __construct(
private readonly Security $security, // exemple
private readonly ControllerResolverInterface $controllerResolver
) {
}
}
L'erreur est :
Cannot autowire service "App\EventListener\MyListener": argument "$controllerResolver" of method "__construct()" references interface "Symfony\Component\HttpKernel\Controller\ControllerResolverInterface" but no such service exists. You should maybe alias this interface to one of these existing services: "debug.controller_resolver", "debug.controller_resolver.inner".
La solution est d'écrire :
#[AsEventListener(event: KernelEvents::CONTROLLER)]
class MyListener
{
public function __construct(
private readonly Security $security, // exemple
#[Autowire(service: 'controller_resolver')] private readonly ControllerResolverInterface $controllerResolver
) {
}
}
Cannot load resource "../../src/Controller/". Make sure to use PHP 8+ or that annotations are installed and enabled.
[modifier | modifier le wikicode]composer require sensio/framework-extra-bundle
Circular reference detected
[modifier | modifier le wikicode]L'autoloader se heurte à un argument du constructeur d'une classe : il faut le sortir de la méthode __construct() pour le définir dans une méthode portant son nom. Exemple de déclaration en YAML :
app.ma_classe:
class: App\MaClasse
arguments:
- '@service.sans.probleme'
calls:
- method: setServiceAvecProbleme
arguments:
- '@service.avec.probleme'
tags:
- { name: doctrine.event_subscriber }
A circular reference has been detected when serializing the object
[modifier | modifier le wikicode]Idem en fetch="EXTRA_LAZY" dans l'entité.
Pour résoudre cela sans changer de relation entre les entités, il y a plusieurs solutions :
- Dans l'entité,
use Symfony\Component\Serializer\Annotation\Ignore;et annotation/** @Ignore() */au dessus de l'attribut en erreur. - Dans le contrôleur, éviter de renvoyer la réponse brute, mais filtrer les attributs avec
use Symfony\Component\Serializer\SerializerInterface;et$this->serializer->serialize($data, 'json', $context).
curl error 6 while downloading https://flex.symfony.com/versions.json: Could not resolve host: flex.symfony.com
[modifier | modifier le wikicode]composer update symfony/flex --no-plugins --no-scripts
CURLPIPE_HTTP1 is no longer supported
[modifier | modifier le wikicode]Si cela se produit sur un projet Symfony avec composer, il faut juste :
composer global require symfony/flex ^1.5
rm -Rf vendor/symfony/flex
Environment variables "xxx" are never used. Please, check your container's configuration.
[modifier | modifier le wikicode]Pazsser par des variables intermédiaires dans services.yaml :
yyy: '%env(xxx)%'
Error 400 Bad Request Your browser sent a request that this server could not understand.
[modifier | modifier le wikicode]Les règles de réécriture d'URL Apache sont erronées (voir ci-dessous).
Error 404 Not Found The requested URL /xxx was not found on this server.
[modifier | modifier le wikicode]Si la page d'accueil fonctionne mais pas les sous-pages (alors qu'un nom de domaine est déjà dédié au site dans le vhost), les règles de réécriture d'URL de la configuration Apache sont manquantes ou erronées. Il faut donc créer public/.htaccess :
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
Invalid header: CR/LF/NUL found
[modifier | modifier le wikicode]Un JSON envoyé en POST dans symfony/http-client contient des retours chariots inattendus : les retirer. Ex :
$json = preg_replace("(\r?\n)", '', $json);
manifest.json does not exist
[modifier | modifier le wikicode]yarn install && yarn add --dev @symfony/webpack-encore && yarn build
Maximum function nesting level of '6000' reached
[modifier | modifier le wikicode]Si cela se produit par exemple en vidant le cache, c'est que deux services se renvoient la balle (même indirectement) depuis leurs constructeurs.
NetworkError when attempting to fetch resource (Firefox) / Failed to fetch (Chrome)
[modifier | modifier le wikicode]Dans l'API avec Swagger UI : violation du CQRS empêchant l'affichage du résultat de l'API dans /api/doc, à causes des domaines. Sur un domaine local, cela peut être résolu en changeant l'URL avant /api/doc pour qu'elle soit valide (ex : passer de wikibooks/api/doc à wikibooks.org/api/doc).
Sinon réessayer en HTTP au lieu de HTTPS.
Sinon c'est la clé renseignée (du .env) qui est différente de celle en BDD.
Si le serveur Web est Nginx, retirer du vhost "add_header Access-Control-Allow-Origin *;" et "limit_exept".
request->request et request->query sont vides à tort
[modifier | modifier le wikicode]Utiliser $request->getContent() ou $request->toArray() à la place[16].
SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known
[modifier | modifier le wikicode]Un service inexistant ou avec des paramètres en erreur est appelé. Cela peut être provoqué par une variable d'environnement d'URL manquante ou erronée (ex : retirer le protocole, ou ajouter la version du SGBD).
SSL certificate problem: unable to get local issuer certificate
[modifier | modifier le wikicode]Utiliser l'installation avec "Composer.phar" plutôt que "Symfony.phar".
InvalidArgumentException Invalid env(bool:xxx) name: only "word" characters are allowed
[modifier | modifier le wikicode]Soit retirer la résolution "bool:" sur la variable d'environnement concernée, soit la renommer si elle contient des symboles autres que des lettres, des nombres ou underscore (ex : "=", "%", ":", etc.).
The autoloader expected class "App\MaClasse" to be defined in file "/var/www/.../MaClasse.php". The file was found but the class was not in it, the class name or namespace probably has a typo.
[modifier | modifier le wikicode]Revérifier que :
- le fichier a bien l'extension .php
- la balise ouvrante
<?php - comporte bien la classe du même nom à la majuscule prêt
- avec le bon namespace et qui correspond aux dossiers (vérifiable dans la déclaration avec CTRL + clic dans PhpStorm).
Uncaught ReflectionException
[modifier | modifier le wikicode]Si c'est lors du passage de PHP 7 à 8, essayer de lancer le script sans Symfony pour avoir le détail (ex : utiliser PhpUnit directement au lieu de PhpUnit bridge). Cela peut par exemple provenir de return type différents entre une interface (ex : ArrayAccess) et son implémentation.
You have requested a non-existent parameter "kernel.secret". Did you mean this: "kernel.charset"?
[modifier | modifier le wikicode]À l'installation d'une dépendance, si les champs sont renseignés dans config/packages, alors c'est qu'ils ne sont pas chargé dans le bundle par Kernel.php.
Peut se traduire aussi par des erreurs de chargement de bundle du type : The child config "x" under "y" must be configured.
"The controller for URI \"/api/ma_route/123\" is not callable: Controller \"MonController\" does neither exist a service nor as class."
[modifier | modifier le wikicode]Si bin/console debug:autowiring --all montre le service et bin/console debug:route la route, renseigner routes.yaml et vider le cache. Ex :
index:
methods: DELETE
path: /api/ma_route/{id}
controller: App\Controller\MonController::__invoke
Si ça ne fonctionne pas, passer par un évènement sur la méthode générique.
Doctrine
[modifier | modifier le wikicode]Le champ ne se sauvegarde pas en base
[modifier | modifier le wikicode]- Est-ce qu'il y a le flush après la modification du champ ?
- Le cache Doctrine a-t-il bien été vidé depuis l'ajout du champ ?
- Est-il bien dans la variable, sinon est-il mappé dans un formulaire ?
- Est-il en annotation PHP alors que Symfony est configuré pour les attributs ?
- Est-il qu'il y a eu un clear() avant le flush(), ce qui aurait détaché l'entité à sauvegarder ?
Le champ sauvegardé en base est toujours 0
[modifier | modifier le wikicode]Se produit quand on utilise l'annotation @ORM\GeneratedValue(strategy="IDENTITY") sur un champ qui n'est pas AUTOINCREMENT en base de données.
A new entity was found through the relationship 'X' that was not configured to cascade persist operations for entity: X. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"})
[modifier | modifier le wikicode]- Dans le cas d'un Doctrine\ORM\ORMInvalidArgumentException:
- Si on est en train de supprimer une entité, il manque son retrait d'une collection au préalable (ex :
$entityToKeep->removeEntityToDelete($entityToDelete)); - Un service a un tag et un argument constructeur incompatible. Ce service en argument fait un $em->clear() au lieu de clear(object), ou persist(object) au lieu de merge(object).
- Si on est en train de supprimer une entité, il manque son retrait d'une collection au préalable (ex :
- Dans le cas "Multiple non-persisted new entities were found through the given association graph"
- Ajouter un persist dans l'entité ou avant le flush.
- Sinon, si c'est que le même flush qui est réalisé plusieurs fois : ajouter du cache d'instance pour ne pas ré-exécuter ce code.
- Sinon, si on tente de flusher une entité (directement ou à travers une jointure) récupérée par l’Entity Manager d'un replica SQL (donc pas encore répliquée), il faut utiliser la base de données maîtresse à la place, ou mieux : faire en sorte de ne pas récupérer les données qui viennent d'être flushées de la base, mais du code.
- Sinon, si un flush immédiatement après n'importe quel set provoque cela, diagnostiquer la cause avec :
// Find previous creations (to flush before)
$uow = $this->em->getUnitOfWork();
$uow->computeChangeSets();
foreach ($uow->getScheduledEntityInsertions() as $e) {
if ($e instanceof X) {
dump($e);
}
}
// Find unmanaged objects in collection (to retrieve otherwise)
foreach ($x->getYs() as $y) {
dump(
'y',
'id=' . ($y->getId() ?? 'null'),
'managed=' . (int) $this->em->contains($y),
'oid=' . spl_object_id($y)
);
}
Par exemple le deuxième paragraphe peut voir des managed=1 avant un formulaire sans 'data_class' => X::class, qui deviennent managed=0 après.
Binding an entity with a composite primary key to a query is not supported
[modifier | modifier le wikicode]Se produit quand on utilise la méthode magique find() d'un repository sur une entité qui a une clé composite (au moins deux attributs avec @ORM\Id). Il faut alors utiliser findBy(['id' => xxx]).
Call to undefined function Closure at EventDispatcher.php:299
[modifier | modifier le wikicode]Ajouter dans composer.json :
"conflict": {
"symfony/symfony": "*",
"doctrine/common": ">=3.0",
"doctrine/persistence": "<1.3"
},
Puis lancer "composer update".
Cannot autowire service "App\Components\CRM\Repository\MonRepository": argument "$class" of method "Doctrine\ORM\EntityRepository::__construct()" references class "Doctrine\ORM\Mapping\ClassMetadata" but no such service exists.
[modifier | modifier le wikicode]Se produit avec l'autowiring, quand on ajoute l'annotation vers un repo dans une entité. Ex :
@ORM\Entity(repositoryClass="App\MonRepository")
En fait depuis Doctrine 1.8[17], le repository doit étendre "ServiceEntityRepository" au lieu de "EntityRepository", et avoir un constructeur. Par exemple :
use App\Entity\MonEntite;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\ManagerRegistry; // anciennement Doctrine\Persistence\ManagerRegistry;
class MonEntiteRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MonEntite::class);
}
Cannot select entity through identification variables without choosing at least one root entity alias
[modifier | modifier le wikicode]Retirer les ->addSelect() du queryBuilder, ou utiliser ->from().
Class "App\Entity\X" seems not to be a managed Doctrine entity. Did you forget to map it?
[modifier | modifier le wikicode]On fait appel à une classe PHP comme si c'était une entité Doctrine : il faut simplement ajouter les annotations Doctrine à la classe, ou bien recréer la table à partir du code PHP.
Column not found: 1054 Unknown column 't0.xxx_id' in 'field list'
[modifier | modifier le wikicode]Il faut juse ajouter la colonne de jointure sous l'annotation *To*. Ex :
@ORM\JoinColumn(name="id", referencedColumnName="id_personne")
Could not convert database value "2026-01-01 00:00:00" to Doctrine Type date. Expected format: Y-m-d
[modifier | modifier le wikicode]Se produit quand on sélectionne des données avec un type "DATETIME" dans la table MySQL mais "date" dans l'entité Doctrine.
Il faut alors le passer en "datetime" aussi.
Could not find the entity manager for class '...'
[modifier | modifier le wikicode]Dans doctrine.yaml, retirer type: annotation.
Entity has to be managed or scheduled for removal for single computation
[modifier | modifier le wikicode]- Si remove : retirer le ON CASCADE DELETE de l’entité supprimée.
- Si update : faire le add avant.
Entity of type 'App\\MonEntite' for IDs id(1) was not found
[modifier | modifier le wikicode]S'il s'agit de l'ID d'une entité jointe, le rendre nullable (@ORM\JoinColumn(nullable=true))[18].
S'il s'agit d'une entité dont la clé primaire est une clé étrangère, lui ajouter :
@ORM\GeneratedValue(strategy="NONE")
Entity of type App\\MonEntite is missing an assigned ID for field 'id'.
[modifier | modifier le wikicode]Une entité n'arrive pas à être sauvegardée avec un ID null (non auto-incrémenté). Il faut donc le générer (par exemple avec un new UuidV4()).
Sinon vérifier que l'erreur ne vient pas d'un listener en sur Doctrine\ORM\Events::prePersist qui tente de persister l'entité récupérée via $eventArgs->getEntity().
Invalid PathExpression. Must be a StateFieldPathExpression
[modifier | modifier le wikicode]Cela peut arriver quand on ajoute une clé étrangère dans un select sans sa jointure :
$this->createQueryBuilder('user')
->select(['user.id', 'user.company'])
On peut donc le remplacer par :
$this->createQueryBuilder('user')
->select('user.id')
->leftJoin('user.company', 'u')
Par contre, si on ne veut pas la jointure, utiliser "identity" :
$this->createQueryBuilder('user')
->select(['user.id', 'identity(user.company) company'])
Iterate with fetch join in class xxx using association xxx not allowed
[modifier | modifier le wikicode]Soit on faut retirer les leftJoin des relations toMany(), soit remplacer PHP Generator par une pagination maison.
No alias was set before invoking getRootAlias()
[modifier | modifier le wikicode]Se produit soit :
- Lors d'un
$qb = $this->entityManager->getEntityManager()->createQueryBuilder();(sans alias comme dans :$this->createQueryBuilder('me')hérité deEntityRepository).
Il faut alors rajouter un alias pour l'entité courante ainsi :
$em->createQueryBuilder()
->select('me')
->from(MonEntite::class, 'me')
- Ou pour un update où on répète à tort l'alias ('me') dans
update()(ce qui peut aussi donnerError: Class 'pn' is not definedquand il n'y a pas de jointure) :
$this->createQueryBuilder('me')
->update()
->set('me.isDeleted', 1)
Property \"metadata\" on resource \"App\\MonEntite\" is declared as a subresource, but its type could not be determined in
[modifier | modifier le wikicode]Survient quand une entité étend une autre sans discriminator[19].
Single id is not allowed on composite primary key
[modifier | modifier le wikicode]Il est recommandé de remplacer la clé composite en ManyToOne par une clé autoincrémentée.
The association mon_entité1#entité2 refers to the owning side field mon_entité2#id which does not exist
[modifier | modifier le wikicode]Si l'entité 2 ne fait pas référence à l'entité 1, supprimer dans l'entité 1, champ entité2, le mappedBy=.
The class 'Doctrine\Common\Collections\ArrayCollection' was not found in the chain configured namespaces App\Entity
[modifier | modifier le wikicode]Il peut y avoir une entité avec une relation ManyToMany dans laquelle on met un ArrayCollection au lieu de l'entité demandée[20].
Sinon vérifier qu'on attend pas un PersistentCollection au lieu d'un ArrayCollection (ou vice-versa avec ->unwrap()).
The EntityManager is closed
[modifier | modifier le wikicode]Cela survient quand l'EntityManager rencontre une exception. On peut[21] :
- la lever avec :
if ($this->em->isOpen()) {
$this->em->persist($entity);
$this->em->flush($entity);
}
- recréer l'entityManager :
if (!$this->em->isOpen()) {
$this->em = $this->em->create(
$this->em->getConnection(),
$this->em->getConfiguration()
);
}
The field xxx has the property type 'bool' that differs from the metadata field type 'int' returned by the 'integer' DBAL type
[modifier | modifier le wikicode]Se produit avec :
bin/console doctrine:schema:validate
Quand il trouve par exemple une option par défaut manquante. Ex :
#[ORM\Column(type: 'boolean', nullable: true, options: ['default' => null])]
The identifier id is missing for a query
[modifier | modifier le wikicode]Se produit lors d'un repository->find(null).
The metadata storage is not up to date, please run the sync-metadata-storage command to fix this issue
[modifier | modifier le wikicode]Si le paramètre "server_version" est présent dans le DSN "DATABASE_URL", l'ajouter, sinon le retirer.
The referenced column name 'xxx' has to be a primary key column on the target entity class
[modifier | modifier le wikicode]S'il n'est pas possible d'utiliser la clé étrangère comme clé primaire, retirer simplement la ligne :
#[ORM\JoinColumn(name: 'my_id', referencedColumnName: 'my_id')]
Mais ça peut provoquer un problème N+1...
The table with name 'xxx' already exists
[modifier | modifier le wikicode]Si cela survient lors du bin/console doctrine:schema:validate, create ou update sur une base vide, et qu'il n'y a pas de doublon dans le dossier "Entity"[22] :
- Chercher les doublons dans d'autres namespaces.
- Retirer les appels directs à la table de l'entité mal placés (
#[ORM\JoinTable(name: xxx)). - Forcer la version du SGBD dans
DATABASE_URL, en l'ajoutant dans?serverVersion=. - Dans le cas du validate, on peut ajouter
--skip-syncpour bypasser cette partie du test.
Transaction commit failed because the transaction has been marked for rollback only
[modifier | modifier le wikicode]Se produit quand un flush() rencontre une erreur SQLSTATE (ex : colonne manquante, même d'une autre table, ou locks à cause d'UPDATE du même champ qui se répètent).
- Le commenter pour la voir apparaitre.
- Si elle n'apparait pas (ex : timeout), regarder directement dans les logs de la base de données.
Cela peut être dû à la présence de deux attributs dans une entité, pointant vers la même colonne de sa table (ex : un ID de clé étrangère et son objet correspondant).
Uncaught PHP Exception Doctrine\Common\Proxy\Exception\UnexpectedValueException: "Your proxy directory "var/cache/prod/doctrine/orm/Proxies" must be writable"
[modifier | modifier le wikicode]Si cela se produit avec APP_ENV=prod et pas dev dans le .env :
rm -Rf var/cache/ var/log
Uncaught Symfony\Component\Debug\Exception\UndefinedFunctionException: Attempted to call function "apc_fetch"
[modifier | modifier le wikicode]sudo pecl install apcu sudo pecl install apcu_bc sudo apt-get install -y php7.2-apcu php7.2-apcu-bc
Unexpected non-iterable value for to-many relation
[modifier | modifier le wikicode]Modifier la déclaration du champ en erreur avec un itérable. Ex :
public $mesObjets = new ArrayCollection();
S'il n'y a pas de champ en erreur, il faut le retrouver avec dd($type) dans AbstractItemNormalizer.
Unknown database
[modifier | modifier le wikicode]Lancer doctrine:database:create :
bin/console d:d:c
Puis rajouter les éventuelles tables :
bin/console doctrine:schema:update --force
Unrecognized field (ORMException)
[modifier | modifier le wikicode]Se produit quand un findBy de repository ne trouve pas un champ de son entité. Cela peut être résolu avec :
bin/console cache:clear- Vérifier si on ne recherche pas à tort une valeur sans sa clé (si Unrecognized field: 0).
- Passer par un QueryBuilder plutôt que par un find.
- Ne pas faire hériter le repo de ServiceEntityRepository.
WARNING [cache] Failed to save key... "cache-adapter" => "Symfony\Component\Cache\Adapter\ApcuAdapter"
[modifier | modifier le wikicode]Ajouter apc.enable_cli=1 dans le fichier php.ini.
DoctrineMigrations
[modifier | modifier le wikicode]S'il n'exécute pas tout (sans logs même en -vvv)
[modifier | modifier le wikicode]Séparer en plusieurs migrations, notamment les créations de tables et de fonctions.
The schema provider is not available
[modifier | modifier le wikicode]Remplacer "connection" par "em" dans doctrine_migrations.yaml :
doctrine_migrations:
em: default
Syntax error or access violation
[modifier | modifier le wikicode]Il faut probablement échapper des caractères, par exemple avec la syntaxe heredoc ou $this->connection->quote().
API Platform
[modifier | modifier le wikicode]Invalid IRI
[modifier | modifier le wikicode]Ajouter le getId() dans l'entité récupérée par IRI.
InvalidArgumentException: "No item route associated with the type xxx
[modifier | modifier le wikicode]Se produit quand on a pas une route POST sans route GET[23], ou si le GET n'a pas d'ID pour créer des URI. Il faut donc en créer une, mais pas forcément besoin de créer un contrôleur :
* @ApiResource(
* itemOperations={
* "get"={
* "method"="GET",
* "controller"=NotFoundAction::class,
* "read"=false,
* "output"=false,
* },
* },
* )
No identifiers defined for resource of type
[modifier | modifier le wikicode]/** * @ApiProperty(identifier=true) */ private $id;
Si aucun ID n'est possible, en renvoyer "1" par exemple.
Unable to generate an IRI for
[modifier | modifier le wikicode]En général, rajouter un getId() dans l'entité.
The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the \"api_platform.eager_loading.max_joins\" configuration key, or limit the maximum serialization depth using the \"enable_max_depth\" option of the Symfony serializer
[modifier | modifier le wikicode]Sur MySQL (ou MariaDB) il existe un maximum de 64 jointures. Donc s'il n'est pas possible d'augmenter max_joins, il faut limiter les jointures par les deux annotations suivantes :
* @API\ApiSubresource(maxDepth=1) * @ORM\OneToMany(targetEntity="MonEntité", mappedBy="monChamp", fetch="EXTRA_LAZY")
Twig
[modifier | modifier le wikicode]Un template Twig ne se rafraichit pas dans la navigateur
[modifier | modifier le wikicode]En local, dans .env, passer de APP_ENV=prod à APP_ENV=dev.
Sinon vider le cache Symfony.
A hash key must be followed by a colon (:)
[modifier | modifier le wikicode]Il faut probablement mettre des parenthèses autour des variables dans un merge de tableau. Ex :
myArray|merge([{(myKey): (myValue)}])
Array to string conversion
[modifier | modifier le wikicode]Plusieurs solutions sont possibles pour afficher un tableau dans un template JSON.
- Pour avoir un tableau, ajouter (et retirer les guillemets autour de la valeur si besoin) :
"my_key": {{ my_value |json_encode(constant('JSON_PRETTY_PRINT'))|raw }},
voire :
"my_key": {{ my_value |join(', ') }},
- Pour avoir un objet en chaine de caractères, faire le json_encode en amont puis laisser les guillemets :
"my_key": "{{ my_value }}",
double quoted property
[modifier | modifier le wikicode]Une virgule de trop après une clé.
key \"id\" for array with keys \"0\" does not exist.
[modifier | modifier le wikicode]Appel d'une clé absente d'un tableau.
The CSRF token is invalid. Please try to resubmit the form
[modifier | modifier le wikicode]Cela peut arriver quand la session expire pendant qu'un formulaire est rempli, ou bien qu'un formulaire soit particulièrement long à être rempli.
- Sur Symfony on peut changer la durée des sessions avec framework.session.cookie_lifetime:
- Sinon dans php.ini : session.cookie_lifetime
- Mais pour éviter ça dans un seul formulaire, on peut lui ajouter la ligne :
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
...
'csrf_protection' => false,
]);
}
unexpected token "punctuation" of value "{"
[modifier | modifier le wikicode]Au choix :
- Ajouter les apostrophes aux clés du tableau concerné.
- Ajouter des parenthèses à ses expressions.
- Remplacer les accolades par des crochets pour le premier niveau.
xmlParseEntityRef: no name
[modifier | modifier le wikicode]Un "&" n'est pas échappé en XML. Il faut ajouter un filtre "| escape" en Twig pour le faire.
PhpStorm
[modifier | modifier le wikicode]Les logs sont accessibles par :
tail -f ~/.PhpStorm2019.2/system/log/idea.log
Certains dossiers sont rouges (exclus) dans la navigation et l'indexation, alors qu'ils ne sont pas censés l'être
[modifier | modifier le wikicode]Fermer le projet, supprimer le .idea, et le rouvrir.
Impossible de CTRL + clic dans un .twig (chemin introuvable)
[modifier | modifier le wikicode]Vérifier que le plugin Symfony de PhpStorm est bien installé et activé.
Si cela persiste, dans File\Settings\PHP\Symfony\Twig \ Template, ajouter le chemin vers les .twig importés du bundle concerné.
L'historique Git d'un dossier est tronqué
[modifier | modifier le wikicode]Si git log . montre plus de commits que le clic droit / Git / Show history, alors cliquer sur Files / Invalidate Caches, et tout cocher.
Si cela ne fonctionne pas après redémarrage, et que le projet fait partie d'un groupe de projets ouverts, le fermer (clic droit dessus et "Remove from project view"), puis le rouvrir.
Dans l'onglet Git, on ne voit pas la liste des fichiers modifiés
[modifier | modifier le wikicode]Dans Settings, Version Control, Commit, décocher "Use non-modal commit interface".
Un fichier a disparu des onglets, a un icône de point d'interrogation, et ne peut être rouvert
[modifier | modifier le wikicode]Il n'est pas ou plus associé à un type de fichier, aller dans File\Associate with File Type.
incoming connection from xdebug
[modifier | modifier le wikicode]Lors d'une erreur au remplissage de cette pop-up, on peut la corriger dans .idea/workspace.xml.
No differences files that files have differences only in line separators
[modifier | modifier le wikicode]Les scripts lancés depuis PhpStorm sous Windows modifient les retours à la ligne (CLRF to LF).
Changer l'option dans Settings/Preferences | Editor | Code Style | Line separator[24] vers "System dependent".
Sinon, configurer git :
git config --global core.autocrlf true
Undefined class xxx
[modifier | modifier le wikicode]Une classe existe mais PhpStorm ne la voit pas : ajouter son dossier dans File\Settings\Directories, retirer l'exclusion (rouge) du dossier concerné.
Xdebug
[modifier | modifier le wikicode]Des logs Xdebug sont ajoutables dans le fichier php.ini :
xdebug.remote_log = /var/www/xdebug.log xdebug.show_error_trace = 1 ; Profiling (enable via cookie or GET/POST variable: XDEBUG_PROFILE=1). xdebug.profiler_enable = 1 xdebug.profiler_enable_trigger = 0 xdebug.profiler_output_dir = /tmp/ ; var_dump() settings. xdebug.overload_var_dump = 1 xdebug.cli_color = 1
En CLI, la console s'ouvre sur default_prepend.php et on ne voit pas l'exécution de la commande
[modifier | modifier le wikicode]Cela se produit quand plusieurs projets sont ouverts dans PhpStorm et que la commande n'appartient pas au principal.
Le navigateur déclenche bien le débogage pas à pas, mais on ne voit pas le fichier PHP dans l'IDE
[modifier | modifier le wikicode]Il manque le mapping (par exemple avec les routes du conteneur). Par exemple dans PhpStorm, Servers, cocher "Use path mappings" et le renseigner.
Messages d'erreur
[modifier | modifier le wikicode]Cannot find file '/usr/local/php/php/auto_prepends/default_prepend.php' locally
[modifier | modifier le wikicode]Sous PhpStorm avec Docker, cliquer sous l'erreur pour modifier le path mapping.
Code coverage needs to be enabled in php.ini by setting 'xdebug.mode' to 'coverage'
[modifier | modifier le wikicode]Pour éviter d'éditer le .ini, on peut remplacer :
bin/phpunit --coverage-text
par :
php -dxdebug.mode=coverage bin/phpunit --coverage-text
Could not connect to client
[modifier | modifier le wikicode]Le serveur est introuvable, changer :
- En V2, xdebug.remote_host ou remote_port.
- En V3, xdebug.client_host ou xdebug.client_port.
Gateway Timeout
[modifier | modifier le wikicode]Cette erreur du navigateur généralement due au serveur Web.
- Sur Apache, dans httpd.conf augmenter le nombre de secondes par défaut dans
Timeout 60[25]. - Sur Nginx, augmenter
fastcgi_read_timeout 60s;[26]. - Sur IIS, étendre la valeur du paramètre Activity Timeout des FastCGI Settings[27].
- Cela peut aussi provenir d'un load balancer en amont du serveur HTTP.
De plus, on peut aussi revoir les variables du fichier php.ini. Ex :
max_execution_time=30 # 30 s par défaut max_input_time=-1 # Utilisera "max_execution_time" si -1, sinon la valeur indiquée
Failed to read FastCGI header
[modifier | modifier le wikicode]Si le log Apache affiche cela lors d'un Gateway Timeout, il faut ajouter à httpd.conf[28] :
ProxyTimeout 6000
<IfModule mod_fcgi.c>
FcgidProcessLifeTime 6000
FcgidBusyTimeout 6000
FcgidConnectTimeout 6000
FcgidIdleTimeout 6000
FcgidInitialEnv 6000
FcgidIOTimeout 6000
IdleTimeout 6000
IPCConnectTimeout 6000
IPCCommTimeout 6000
IdleScanInterval 6000
</IfModule>
Parfois il convient aussi de modifier le fichier php.ini[29] :
opcache.optimization_level=0xFFFFFBFF xdebug.remote_cookie_expire_time=6000
Enfin, sur PhpStorm on rencontre cela dans le cas de sous-requêtes qui dépassent la valeur du paramètre "Max. simultaneous connections".
No code coverage driver is available
[modifier | modifier le wikicode]Installer ou activer Xdebug. Il doit apparaitre ensuite dans php -v.
Remote file path 'default_prepend.php' is not mapped to any file path in project
[modifier | modifier le wikicode]Sous PhpStorm il faut décocher dans les settings, debug, les deux cases Force break..., puis de redémarrer l'IDE[30].
Time-out connecting to client
[modifier | modifier le wikicode]Changer xdebug.remote_host ou xdebug.remote_port car le serveur existe mais n'écoute pas le port spécifié.
Si le navigateur ne déclenche plus le débogage sur l'IDE, alors que ce dernier écoute et que la clé du navigateur est bien définie, c'est peut-être le pare-feu qui bloque. Exemple de reset sur Linux avec iptables[31] :
iptables --policy INPUT ACCEPT; iptables --policy OUTPUT ACCEPT; iptables --policy FORWARD ACCEPT; iptables -Z; # zero counters iptables -F; # flush rules iptables -X; # delete all extra chains
Waiting for incoming connection with ide key 'xxx'
[modifier | modifier le wikicode]Xdebug peut marcher pour certains sites, sauf un qui ne déclenche rien dans l'IDE. Ce message apparait alors dans PhpStorm si on clique sur "Debug".
- Vérifier le serveur et son port) associé à l'URL[32].
- Voir la page Programmation PHP/Xdebug.
GraphQL
[modifier | modifier le wikicode]Fields "maRoute" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.
[modifier | modifier le wikicode]Si on appelle plusieurs fois la même route dans la même requête, il faut définir des alias. Ex :
mutation {
maRoute(...)
aliasMaRoute: maRoute(...)
}
Le résultat de la requête sera donc un tableau avec ['data' => [[maRoute => ...], [aliasMaRoute => ...]].
Windows
[modifier | modifier le wikicode]imagecreatefromstring(): gd-png: libpng warning: Interlace handling should be turned on when using png_read_image
[modifier | modifier le wikicode]Remplacer le php_gd2.dll du PHP 7.4.33 par celui du 7.4.2 téléchargé depuis https://www.pconlife.com/viewfileinfo/php-gd2-dll/.
Exemple du chemin par défaut :
C:\wamp64\bin\php\php7.4.33\ext\
Références
[modifier | modifier le wikicode]- ↑ http://windows.php.net/download/
- ↑ https://stackoverflow.com/questions/9973555/setting-max-input-vars-php-ini-directive-using-ini-set
- ↑ https://stackoverflow.com/questions/804571/how-to-subtract-two-dates-ignoring-daylight-savings-time-in-php
- ↑ https://stackoverflow.com/questions/31115982/malformed-utf-8-characters-possibly-incorrectly-encoded-in-laravel
- ↑ https://whynhow.info/17522/How-to-get-rid-of-SIGBUS-when-running-php-fpm?
- ↑ https://www.kinamo.fr/fr/support/faq/determiner-le-nombre-de-processes-valide-pour-php-fpm-sur-nginx
- ↑ https://www.php.net/manual/fr/function.fopen.php
- ↑ http://stackoverflow.com/questions/4636166/only-variables-should-be-passed-by-reference
- ↑ https://stackoverflow.com/questions/24443318/getting-error-no-driver-found-to-handle-vcs-repository-on-composer-and-svn?answertab=votes#tab-top
- ↑ https://getcomposer.org/doc/articles/http-basic-authentication.md
- ↑ https://github.com/magento/magento2/issues/2523#issuecomment-159884152
- ↑ https://symfony.com/doc/current/console/verbosity.html
- ↑ https://silex.symfony.com/doc/2.0/cookbook/json_request_body.html
- ↑ https://symfony.com/doc/current/bundles/configuration.html
- ↑ https://symfony.com/doc/current/service_container/autowiring.html
- ↑ https://symfony.com/doc/5.4/components/http_foundation.html
- ↑ https://www.it-swarm.dev/fr/php/service-autowire-impossible-largument-fait-reference-la-classe-mais-ce-service-nexiste-pas/836794307/
- ↑ https://cilefen.github.io/symfony/2016/12/29/doctrine-orm-nulls.html
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/inheritance-mapping.html
- ↑ https://openclassrooms.com/forum/sujet/symfony2-collection-erreur-de-namespace
- ↑ https://www.kerstner.at/2014/09/doctrine-2-exception-entitymanager-closed/
- ↑ https://openclassrooms.com/forum/sujet/doctrine-schema-update-impossible
- ↑ https://github.com/api-platform/core/issues/3501
- ↑ https://stackoverflow.com/questions/40470895/phpstorm-saving-with-linux-line-ending-on-windows
- ↑ https://www.h3xed.com/web-development/php-and-apache-504-gateway-timeout-troubleshooting-and-solutions
- ↑ http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_read_timeout
- ↑ https://www.leighton.com/blog/php-debugging-in-phpstorm-6-0-with-xdebug/
- ↑ https://support.plesk.com/hc/en-us/articles/115000064929-Website-is-not-accessible-The-timeout-specified-has-expired-Error-dispatching-request-to
- ↑ https://www.reddit.com/r/drupal/comments/ase67i/for_issue_reference_service_unavailable_error/
- ↑ https://www.jetbrains.com/help/phpstorm/troubleshooting-php-debugging.html
- ↑ https://ubuntuforums.org/showthread.php?t=1381516
- ↑ https://stackoverflow.com/questions/17715128/xdebug-phpstorm-waiting-for-incoming-connection-with-ide-key
| Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture. |
