Programmation PHP/Version imprimable

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
Introduction
Historique
PHP est un langage de script créé par Rasmus Lerdorf en 1995. Principalement utilisé pour la programmation Web, on pourrait le situer entre les SSI (Server Side Includes) et le langage de script Perl. Il est utilisable sur tous les systèmes d'exploitation, donc sur Windows, MacOS, GNU-Linux ou autre Unix commercial, ce qui en fait un langage très portatif.
La sortie de PHP 5 en 2004 a permis au langage d'atteindre une certaine maturité, pour être reconnu comme un serveur d'application à part entière tel que JEE ou .Net.
PHP a ensuite acquis une place incontournable dans le développement Web Open Source. Sa popularité vient de sa syntaxe, proche du C, de sa vitesse et de sa simplicité. En 2013, on estime qu'il y a plus de 244 millions de serveurs qui utilisent le langage. En 2016 il est utilisé dans plus de 80 % des sites Internet, et toujours 77,5 % en août 2022[1].
PHP 5
Les nouvelles fonctionnalités du PHP 5 (paru en 2004) concernent surtout la programmation orientée objet[2] :
- interfaces
- classes abstraites
- constructeurs et destructeurs de classes (ainsi que d'autres méthodes magiques)
- portée des attributs et méthodes (public, protected, private)
- attributs et méthodes statiques
- attributs et méthodes finaux
- type hinting de classe.
PHP 7
Les principales fonctionnalités apportées par PHP 7 (depuis 2015) sont[3] :
- typage strict par classe
- paramètres typés
- retours de méthode typés
- opérateur de coalescence null (??)
- opérateur vaisseau spatial (<=>)
- tableaux de constantes
- classes anonymes
- groupage des déclarations (use, avec des accolades).
PHP 8
Nouvelles fonctionnalités du PHP 8.0 en 2020[4][5] :
- paramètres nommés
- types d'union (X|Y)
- propriétés promues (déclarations dans le constructeur)
- autorisation d'une virgule de fin dans les paramètres
- autorisation des méthodes abstraites dans les traits
- autorisation des indices négatifs dans les tableaux autoincrémentés
- l'instruction
match
- opérateur null-safe (?->)
- fonction
str_contains()
- fonction
str_starts_with()
etstr_ends_with()
- fonction
get_debug_type()
: il s'agit d'ungettype()
plus précis, car il renvoie le nom de la classe au lieu de "object
" - exécution juste-à-temps (JIT), pour améliorer les performances : plus de 20 % de requêtes en plus par seconde[6].
8.1
Pour PHP 8.1, sorti en novembre 2021 :
- types d'intersection (X&Y)
- type de retour
never
- constantes de classe finales
- énumérations
- fibres (threads virtuels pilotés par la classe Fiber)
- fonction array_is_list()
- attributs en lecture seule (readonly).
8.2
PHP 8.2 est sorti le 8 décembre 2022[7] :
- classes en lecture seule :
readonly class
signifie que tous les arguments de son constructeur son implicitement enreadonly
. - types
null
,true
etfalse
. - constantes dans les traits.
- Forme normale disjonctive de type (combinaison de l'union et de l'intersection).
8.3
PHP 8.3 sort le 23 novembre 2023[8] :
- classes anonymes en lecture seule.
- constantes typées.
- fonction "json_validate".
8.4
PHP 8.4 date du 21 novembre 2024 :
- Property Hooks : définition des accesseurs depuis leur attribut.
- Visibilité asymétrique (aviz) : définition de la portée des accesseurs depuis la promotion de propriété.
- Objets paresseux (lazy objects)[9] : objets instanciés au moment où ils sont appelés.
- Attribut
#[\Deprecated]
(pour remplacer l'annotation)[10].
Sites Web statiques ou dynamiques
À l'origine du Web, les sites Web étaient des sites statiques : constitués d'un ensemble de pages écrites dans le langage HTML. L'information présente sur ces pages était toujours identique et leur mise à jour était particulièrement fastidieuse. Le serveur Web se contentait de diffuser les pages telles quelles à l'utilisateur. L'interaction entre le site et l'utilisateur était très sommaire : l'utilisateur demandait une page web et le serveur la lui fournissait.
Aujourd'hui la plupart des sites sont dynamiques : à l'intérieur des pages HTML, le concepteur du site a inséré des programmes. Ces programmes permettent une plus grande souplesse dans la gestion du site, sa mise à jour et ses fonctionnalités. La possibilité d'insérer des programmes a permis de décupler les fonctionnalités des sites Web.
Pour vous en convaincre prenons quelques exemples :
- Vous voulez écrire un site qui présente une centaine de produits. Vous n'allez pas écrire 100 pages différentes, une pour chacun des produits ! Mais plutôt une seule page (page type) permettant de présenter n'importe quel produit. Cette page va contenir un programme qui interagira avec une base de données. Dans la base de données, seront stockées les informations utiles pour chaque produit : le nom du produit, sa présentation, sa référence, son prix, etc. Le programme aura donc pour rôle d'aller chercher l'information utile dans la base de données et de l'afficher en HTML. De plus, pour ajouter un produit, il suffira d'ajouter un élément dans la base de données. Il est même possible d'avoir des programmes permettant de passer une commande pour vos différents produits !
- Vous voulez diffuser rapidement des informations sur Internet : vous voulez avoir un outil convivial qui vous permet d'ajouter un article, de le modifier, de le supprimer... Vous allez donc écrire un programme permettant de modifier à volonté les différents articles. Un exemple bien connu de ce type de programme est le blog : il s'agit d'un programme permettant à n'importe quel utilisateur non informaticien de gérer ses différents articles.
- L'encyclopédie Wikipédia est réalisée avec un programme (en PHP d'ailleurs) qui permet à chaque utilisateur de créer et de modifier les articles tout en gardant un historique complet des différentes versions des articles.
- les forums de discussion sont des lieux d'échange permettant une interaction étroite entre le serveur et l'utilisateur. Grâce aux programmes qu'ils utilisent, il est possible de se connecter, de consulter les messages des différents forums, d'y répondre. Les modérateurs de ces forums peuvent modifier les messages, les supprimer, interdire un utilisateur indélicat.
Dans chacun des exemples précédents il a été nécessaire d'incorporer un programme à l'intérieur des pages du site afin de réaliser des fonctionnalités de haut niveau. Aujourd'hui la quasi-totalité des sites professionnels sont dynamiques et il est quasi inconcevable de réaliser un site statique. Le langage PHP est un des langages utilisables pour réaliser facilement les sites Web dynamiques, ne serait-ce que parce qu'il est disponible sur la plupart des serveurs hébergeant des sites.
Possibilités
Si vous êtes déjà allés sur un site qui vous demandait de vous connecter, vous avez utilisé un script côté serveur. Ce script était certainement écrit en PHP, en raison de la popularité de ce dernier.
PHP transforme une page statique (fichier HTML par exemple), en une suite d'instructions interprétables par PHP, installée sur un serveur Web comme Apache - ça peut-être simplement un "Hello World" 50 fois dans une colonne, ou une interaction avec un système de base de données, comme MySQL, fréquemment couplé à PHP.
Mais PHP peut aussi servir à programmer des batchs sans page web aucune.
Les premières versions de PHP étaient faiblement typées, mais depuis la version 7 il est possible de forcer un typage fort dans un fichier en lui ajoutant :
declare(strict_types = 1);
Références
- ↑ https://w3techs.com/technologies/details/pl-php
- ↑ https://www.web24.com.au/tutorials/features-of-php5
- ↑ https://www.php.net/manual/fr/migration70.new-features.php
- ↑ https://kinsta.com/fr/blog/php-8/
- ↑ https://php.developpez.com/actu/335682/PHP-8-2-est-disponible-en-beta-2-Cette-version-propose-null-true-et-false-en-tant-que-types-autonomes-ainsi-que-des-constantes-dans-les-traits/
- ↑ https://thecodingmachine.com/php-8-nouveautes-compatibilites-migration/
- ↑ https://stitcher.io/blog/new-in-php-82
- ↑ https://www.phparch.com/2023/08/whats-new-and-exciting-in-php-8-3/
- ↑ https://les-tilleuls.coop/blog/ce-quil-faut-retenir-des-nouveautes-de-php-8-4
- ↑ https://fr.siteground.com/blog/php-8-4/
Installer PHP
Principe
Dans la cadre d'un apprentissage, des sites comme http://phpfiddle.org/ ou Tutorialspoint permettent d'exécuter ponctuellement des petits scripts PHP sans rien installer.
Par ailleurs, installer PHP seul permet d'exécuter des commandes shell (ex : php -v
donne la version, et php -i
toutes les infos telles que les paramètres de limite mémoire), mais le réel intérêt est d'installer PHP en complément d'un serveur HTTP, pour y publier des sites web.
Unix / Linux
LAMP
Logiciel tout-en-un pour Linux (Apache + MySQL + PHP), comme WAMP pour Windows.
# apt-get install tasksel
# tasksel install lamp-server
Installation manuelle
L'installation des logiciels séparément permet plus de personnalisation, par exemple de remplacer Apache par Nginx.
Apache sur Debian / Ubuntu
# apt-get install apache2
Le service peut ne pas être lancé par défaut, mais même s'il l'est on peut quand-même essayer de l'activer avec :
# /etc/init.d/apache2 start
On peut ensuite tester le serveur, pour voir si une page s'affiche ou s'il refuse la connexion :
$ lynx http://localhost/
Cette adresse est le rebouclage, elle peut aussi être rentrée directement dans tout navigateur web.
Si Apache était déjà installé vérifier le fichier pour indiquer le démarrage automatique d'Apache 2 /etc/default/apache2 :
# vi /etc/default/apache2 ... NO_START=0
Installer PHP
On distingue principalement deux versions de PHP : celle dont le binaire est appelé par le serveur Web, et php-fpm qui possède son propre service daemon (aussi appelé par le serveur Web) testable ainsi :
telnet localhost 9000 CTRL + ALT + ] quit
FPM signifie FastCGI Process Manager, puisque le processus PHP-fpm écoute les requêtes CGI[1]. Cela peut se traduire soit par des requêtes TCP/IP, soit par un socket Unix (.sock dans le vhost).
PHP peut-être installé avec toutes les déclinaisons de la distribution Debian (stable, testing, unstable). Il suffit pour cela d'insérer vos lignes préférées dans le fichier /etc/apt/sources.list :
deb http://ftp.fr.debian.org/debian/ stable main non-free contrib deb-src http://ftp.fr.debian.org/debian/ stable main non-free contrib
Ce qui suit suppose que le serveur Web a bien été installé ; exécuter les commandes suivantes :
sudo apt-get update && apt-get install php8.4 && apt-get install libapache2-mod-php8.4
Une fois ces commandes exécutées, redémarrer le serveur Web. Dans le cas d'Apache cela s'effectue avec la commande suivante :
/etc/init.d/apache2 restart
Si tout s'est bien passé, vous disposez maintenant d'un serveur Web qui a la capacité d'exécuter des scripts PHP dans votre navigateur.
Testons :
$ lynx http://localhost/test.php
Pour débugger :
$ tail /var/log/apache2/error.log
Une fois les serveurs Web installés, ils se lancent automatiquement à chaque démarrage de la machine, ce qui est souhaitable pour un serveur, mais pas toujours pour un PC. Pour éviter cela, il suffit d'y désactiver les daemons :
sudo update-rc.d apache2 disable sudo update-rc.d mysql disable
Mise à jour
Pour PHP 7 ou 8 sur Ubuntu :
sudo add-apt-repository ppa:ondrej/php
Sur Debian :
sudo wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg sudo sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
Puis :
sudo apt update sudo apt install php8.4 php8.4-common php8.4-cli php8.4-fpm sudo a2enmod php8.4 sudo systemctl reload apache2
Bibliothèques
Voici une liste de bibliothèques fréquemment utilisées dans les applications :
# apt-get install -y \ php8.4-mysql \ php8.4-gd \ php8.4-curl \ php8.4-mbstring \ php8.4-xml
D'autres s'installent avec pecl au lieu de apt.
Pour les activer après installation, on peut éditer le php.ini ou lancer : phpenmod nom_du_module_php. Ex : sudo phpenmod gd
.
Pour les désactiver : phpdismod nom_du_module_php
Pour détecter l'emplacement du php.ini de la version de PHP par défaut : php --ini
.
Désinstaller PHP
Pour éviter de désinstaller tous les paquets PHP un par un (par exemple après une bascule de PHP7.0 vers PHP7.1), il existe "ppa-purge" :
sudo apt-get install ppa-purge sudo ppa-purge ppa:ondrej/php-7.0
Apache sur Gentoo
Premièrement il faut installer Apache :
emerge apache
Ensuite, il faut installer PHP :
emerge dev-lang/php
Puis il faut qu'apache utilise PHP dans sa configuration.
Code : Configuration de apache |
# nano -w /etc/conf.d/apache2 APACHE2_OPTS="-D PHP5" |
MySQL seul
MySQL est disponible sur http://dev.mysql.com/downloads/gui-tools/5.0.html au format :
- .msi (Windows)
- .dmg (Mac)
- .rpm (Linux)
- .tar
En l'absence de gestionnaire de paquets, utiliser le .tar ainsi :
shell> groupadd mysql shell> useradd -r -g mysql mysql shell> cd /usr/local shell> tar zxvf /path/to/mysql-VERSION-OS.tar.gz shell> ln -s full-path-to-mysql-VERSION-OS mysql shell> cd mysql shell> chown -R mysql . shell> chgrp -R mysql . shell> scripts/mysql_install_db --user=mysql shell> chown -R root . shell> chown -R mysql data shell> bin/mysqld_safe --user=mysql &
APT
$ sudo apt-get install mysql-server mysql_secure_installation
Variante
La dénomination des paquets mentionnés peut varier légèrement selon la version. Dans un terminal, entrez :
$ sudo apt-get install mysql-server
et confirmez.
(Remarque : il semblerait qu'en installant le paquet "mysql-server-5.0", au lieu du paquet mentionné plus haut, certaines personnes rencontrent des problèmes. Il est donc préférable d'installer ce paquet, ou d'installer la dernière version 4 stable avec : $ sudo apt-get install mysql-server-4.1. Consultez le forum pour plus d'informations : )
Lancez ensuite la commande :
cd && sudo mysql_secure_installation
Appuyez sur Entrée lorsqu'il vous demande le mot de passe root MySQL : pour le moment il n'y en a pas.
Le script vous demande alors si vous voulez mettre un mot de passe pour l'utilisateur root. Répondez Y, et entrez (2 fois le nouveau mot de passe du root MySQL). Il vous pose ensuite une série de questions. Si vous ne savez pas quoi répondre, acceptez les choix par défaut en appuyant simplement sur Enter.
Votre serveur MySQL est prêt. Par défaut il se lance à chaque démarrage du système, si vous ne le souhaitez pas, il vous suffit de lancer :
$ sudo dpkg-reconfigure mysql-server
et de répondre "Non" à la question du démarrage systématique de MySQL.
Sur Gentoo
emerge mysql
Modules
De nombreux modules complémentaires peuvent être installés sur Apache.
Pour les lister, on utilise apachectl
(parfois apache2ctl
) :
apachectl -t -D DUMP_MODULES
ou
apache2ctl -M
Pour activer un module :
a2enmod Nom_du_module
Un fichier est alors créé dans /etc/apache2/mods-enabled/.
Exemple pour la réécriture d'URL :
a2enmod rewrite
Pour le désactiver :
a2dismod Nom_du_module
La configuration du module reste toutefois disponible dans /etc/apache2/mods-available/.
phpenmod mbstring
Sites
Pour lister les sites du serveur :
apachectl -S
Pour activer un site :
a2ensite Nom_du_site
Le fichier du site est alors visible dans /etc/apache2/sites-enabled/.
Pour le désactiver :
a2dissite Nom_du_site
Le site est dans /etc/apache2/sites-available/.
Problème d'encodage d'Apache2
Si vous rencontrez un problème d'encodage des caractères de vos pages, par exemple les caractères accentués apparaissant sous la forme "�" (<?>), c'est probablement parce qu'Apache2 déclare dans les en-têtes HTTP qui accompagnent les pages visionnées un encodage par défaut en Unicode (UTF-8) :
Content-Type: text/html; charset=UTF-8
Tandis que les pages visionnées utilisent un autre encodage des caractères, comme par exemple Latin1 (ISO-8859-1). Même si vos documents indiquent le jeu de caractères utilisé, le paramètre donné par le serveur dans les en-têtes HTTP est prioritaire !
Pour corriger ce problème, il faudra éditer /etc/apache2/apache2.conf :
$ sudo gedit /etc/apache2/apache2.conf
Encodage par défaut en Latin1 (ISO-8859-1)
Cherchez la ligne suivante :
#AddDefaultCharset ISO-8859-1
Décommentez-la en enlevant le # :
AddDefaultCharset ISO-8859-1
Pour ceux qui ont la locale iso-8859-15 (sinon vous pouvez faire "sudo dpkg-reconfigure locales" pour l'ajouter) et qui désirent l'utiliser par défaut, ajoutez un 5 en fin de ligne :
AddDefaultCharset ISO-8859-15
ainsi que la ligne suivante dans le paragraphe en-dessous :
AddCharset ISO-8859-15 .iso8859-15 .latin15 .fr
Il ne vous reste plus qu'à mettre "fr" en première position dans la ligne LanguagePriority (juste au-dessus), et à demander à apache de relire sa configuration :
$ sudo /etc/init.d/apache2 reload
Aucun encodage par défaut
Il est également possible de s'affranchir de tout encodage par défaut, de la manière suivante :
Cherchez la directive AddDefaultCharset :
AddDefaultCharset ISO-8859-1
Remplacez l'attribut par la valeur Off :
AddDefaultCharset Off
Là encore, on demandera à Apache de relire sa configuration :
$ sudo /etc/init.d/apache2 reload
Maintenant, les en-têtes HTTP ne contiendront plus d'indication d'encodage des caractères. Attention : il faudra alors que chaque page indique l'encodage utilisé, car s'en remettre à la détection automatique par les navigateurs peut s'avérer assez aléatoire !
Windows
Tout-en-un
Des logiciels tout-en-un (serveur Web, base de donnée MySQL, et PHP) permettent de s'affranchir d'une installation fastidieuse et rédhibitoire pour le débutant :
- EasyPHPtéléchargement : n'a pas vocation à être installé pour de la production, mais pour le développement. Il stocke les bases de données dans C:\Program Files (x86)\EasyPHP\binaries\mysql\data.
- WAMPtéléchargement : est du même type qu'EasyPHP : ce logiciel installe facilement un serveur Web Apache, une base de données MySQL et PHP 4 et 5. Il a l'avantage de permettre de passer facilement de PHP 4 à PHP 5, sans avoir à refaire une installation ou une compilation. Tout comme EasyPHP, c'est un environnement de développement, et non un environnement de production. Attention : la résolution des noms d'hôtes se réalise séparément. Les installations WAMP servent à tester en local sur votre PC. Dans la plupart des cas, il suffit d'utiliser le fichier Hosts local, comme on le ferait sur une machine Linux, afin de lier des noms aux adresses IP. Dans Windows XP, Vista et 7, ce fichier se trouve dans le répertoire systemroot\System32\Drivers\Etc. Il peut se faire que le service ait déjà été configuré. Lorsque vous vous en doutez, contactez votre administrateur réseau. Remarque : vous trouverez une liste des possibilités de résolution de noms avec MS Windows sur Microsoft.com.
- XAMPPtéléchargement : est du même type qu'EasyPHP ou WAMP, le deuxième P étant pour Perl. Son usage est recommandé avec PHPEclipse, et il fournit aussi un serveur Apache Tomcat par défaut.
- The Uniform Servertéléchargement : en anglais seulement avec Apache2, Perl5, PHP5, MySQL5, phpMyAdmin.
Sur Windows 10 pro, le serveur IIS est installé par défaut, et oblige Apache à changer de port (888 au lieu de 80) lors de l'installation. Pour résoudre cela il suffit de décocher Internet Information Services dans Programmes et fonctionnalités, Activer ou désactiver des fonctionnalités Windows. De même, le port MySQL est susceptible de passer de 3306 à 3388.
Sur Windows 10, EasyPHP development server (alias Devserver, la version rouge) ne fonctionne pas (il manque MSVCR110.dll), mais EasyPHP hosting server (alias Webserver, la bleue) tourne normalement. Or, elle se lance automatiquement à chaque démarrage, ce qui le ralentit significativement. Pour éviter cela, exécuter services.msc, puis passer les trois services ci-dessous en démarrage manuel. Ensuite pour les lancer à souhait (en tant qu'administrateur), créer un script MySQL.cmd contenant les lignes suivantes :
net start ews-dbserver net start ews-httpserver net start ews-dashboard pause net stop ews-dashboard net stop ews-httpserver net stop ews-dbserver

Message d'erreur relatif à SSL
Pour l'instant, WAMP ne supporte pas encore le Secure Socket Layer (SSL). L'installation se finit par un message qui vous informe de ce fait. Afin de pouvoir travailler sans problèmes, éditez le fichier c:\windows\php.ini. Cherchez dans ce fichier la ligne qui commence avec extension=php_openssl.dll. Commentez cette ligne en la faisant précéder d'un point-virgule :
;extensions=php_openssl.dll
Si tout se passe bien, vous pouvez ouvrir la page de test dans votre navigateur.
Installation manuelle
- Apache est disponible sur le site Web de Apache Software Foundation apache.org.
- PHP est téléchargeable sur le site officiel de php. Choisissez le fichier au format ZIP.
- Enfin, vous trouverez MySQL sur mysql.com.
Installer Apache
Pour installer Apache, double-cliquez sur le fichier exécutable, et suivez les instructions d'installation automatique.
Si vous installez Apache sur un ordinateur de développement, renseignez le champ "nom de domaine" avec la valeur localhost
.
Si vous installez un serveur de production et que vous disposez d'un nom de domaine, vous devriez disposer des informations nécessaires concernant votre nom de domaine, fournies par le registrar.
Une fois l'installation terminée, il faut encore indiquer à Apache qu'il doit fonctionner conjointement avec PHP, car il ne sait pas les traiter par défaut. Pour cela, il faut modifier les informations de configuration d'Apache, contenues dans le fichier httpd.conf
, qui se trouve dans le dossier d'installation d'Apache, dans le sous-dossier conf
.
Installer PHP
Une fois l'archive téléchargée, décompressez-la à la racine de votre disque dur et renommez le dossier en 'PHP'
.
Dans le dossier PHP
, vous trouverez deux fichiers: php.ini-dist
et php.ini-recommended
. Copiez php.ini-recommended
dans votre dossier C:\Windows
ou C:\winnt
(le nom du dossier dépend de la version de votre système.
renommez-le en php.ini
.
Ce fichier est le fichier de configuration qui contrôle les options dont vous disposerez. Par exemple :
PHP.ini | PHP | Rôle |
---|---|---|
error_reporting E_ALL | error_reporting(E_ALL); | Affiche tous les avertissements et erreurs directement sur le site. C'est utile pour la préproduction car cela évite de rechercher d'éventuels messages dans les logs, mais peut perturber la mise en page pour des avertissements bénins. |
error_reporting 0 | error_reporting(0); | N'affiche aucun message sur le site relatif à son exécution |
max_execution_time = 300 | set_time_limit(300); | Définit le "timeout", c'est-à-dire le temps maximum en secondes autorisé pour exécuter un script PHP. |
post_max_size = 80M | ini_set('post_max_size', '80M'); | Définit la taille maximum d'un fichier que l'on peut envoyer au serveur en HTTP. |
MySQL
Télécharger et installer le .msi sur http://dev.mysql.com/downloads/gui-tools/5.0.html.
Pour arrêter, démarrer, démarrer automatiquement le serveur MySQL vous devez aller dans la gestion des services (Démarrer/Exécuter/services.msc).
Apache CGI ou module ?
On peut configurer Apache pour utiliser PHP comme binaire CGI, ou comme module.
L'installation en module offre de meilleures garanties en matière de sécurité, de meilleures performances, et certaines fonctionnalités absentes de l'installation en CGI. Cette installation est cependant un peu plus difficile (mais rassurez-vous, pas tellement plus que l'installation CGI), aussi nous intéresserons nous à celle-ci.
- Dans le répertoire de PHP, trouvez la DLL
php5ts.dll
, et copiez-la dans le répertoire d'Apache. - Ouvrez ensuite le fichier
httpd.conf
. On en a déjà parlé plus tôt, vous vous souvenez où il est, n'est-ce pas ? - Dans ce fichier, ajouter cette ligne qui permet à Apache de savoir que l'extension
.php
concerne l'utilisation du module PHP :AddType application/x-httpd-php .php
- Puis, ajoutez ces deux lignes qui charge et exécute (respectivement) le module PHP5 au démarrage du serveur :
LoadModule php5_module c:\php\php5apache.dll
AddModule mod_php5.c
Enregistrez le fichier httpd.conf
et fermez-le.
IIS
IIS est activé automatiquement sur Windows Server et Windows 10, et peut l'être manuellement pour les versions antérieures, dans le panneau de configuration, Activer ou désactiver des fonctionnalités de Windows. C'est également là que l'on peut le désactiver pour donner le port 80 à Apache.
Son processus est issu de C:\Windows\System32\inetsrv\w3wp.exe et est lancé en tant que "Service réseau". Ce compte local devra donc avoir le droit de lecture sur les fichiers du site web.
Sur Windows Server uniquement, lors de la première utilisation il faut ajouter le rôle Serveur Web dans le Gestionnaire de serveur, accessible dans le menu démarrer, Tous les programmes, Outils d'administration[2]. Puis pour ce rôle il faut cocher plusieurs services de rôle :
- CGI[3] (sous peine d'erreurs 500 à chaque commande PHP).
- ODBC pour les connexions aux bases de données.
- ...
Télécharger et installer ensuite le manager PHP[4] pour pouvoir configurer PHP dans :
- Le Gestionnaire des services Internet (%windir%\system32\inetsrv\InetMgr.exe) sur Windows Server.
- Les Services Internet sur Windows PC.
Si ensuite IIS renvoie systématiquement des erreurs 500 par la suite (même sans appel PHP), installer Visual C++ 32 bits[5] (PHP ne fonctionne pas en 64 bits en 2016).
Docker
Il existe de nombreuses images de conteneur Docker pouvant fournir PHP. Exemple de Dockerfile[6] :
FROM php:8.2-cli
FROM php:8.2-fpm
PHP 7.4 FPM
Exemple avec GD[7] :
FROM php:7.4-fpm RUN apt-get update && apt-get install -y \ libfreetype6-dev \ libjpeg62-turbo-dev \ libpng-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j$(nproc) gd
IDE
Pour développer efficacement en PHP il est indispensable d'utiliser un environnement de développement intégré (IDE) plutôt qu'un éditeur de texte sur le PC client. Ils seront abordés dans les chapitres suivants.
vim
vim dispose de plugins pour programmer en PHP[8].
Références
- ↑ https://www.tecmint.com/connect-nginx-to-php-fpm/
- ↑ https://technet.microsoft.com/fr-fr/library/cc753077(v=ws.10).aspx
- ↑ http://blog.bobbyallen.me/2016/05/02/installing-php7-on-windows-server-2012-r2-and-iis-8/
- ↑ https://phpmanager.codeplex.com/
- ↑ https://www.microsoft.com/fr-fr/download/details.aspx?id=48145
- ↑ https://hub.docker.com/_/php
- ↑ https://itecnote.com/tecnote/php-installing-gd-extension-in-docker/
- ↑ https://www.octopuce.fr/debugger-php-en-cli-avec-xdebug-vim/
Composer
Composer est un logiciel de gestion des bibliothèques PHP open source. Celles-ci sont aussi accessibles sur https://packagist.org/.
Installation
Pour l'installer il y a trois solutions :
- Avec les commandes PHP décrites sur https://getcomposer.org/download/.
- En téléchargeant le package par navigateur à la même adresse.
- En le téléchargeant avec la commande cURL[1], comme le montre la vidéo suivante :
À la fin il faut juste disposer du fichier composer.phar.
Pour le lancer, on peut utiliser la commande composer
s'il est installé avec PHP, ou php composer.phar
s'il est juste téléchargé.
Pour l'installer à partir du téléchargement :
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
Enfin, pour tester l'installation, lancer :
composer -v
Utilisation
Le programme Composer lit et modifie la liste des bibliothèques du projet dans le fichier composer.json.
init
Pour créer le fichier composer.json :
composer init
create-project
Alternative à init pour les apps initialisée par un framework.
composer create-project "symfony/skeleton:^5" mon_projet
require
Pour installer un paquet, par exemple client client HTTP Guzzle :
composer guzzlehttp/guzzle
Si l'installation réussit, il ajoute le nom et la version du paquet dans composer.json, ceux de ses dépendances dans composer.lock, et télécharge leurs fichiers dans un dossier "vendors". Il informe ensuite des problèmes de dépendances, par exemple si la version de PHP locale est inférieure à celle recommandée pour le paquet. En cas d'erreur il retire tout ce qu'il a fait.
Paramètres
On peut aussi préciser le numéro de version, par exemple MediaWiki :
composer require mediawiki/semantic-media-wiki "1.9.*,>=1.9.0.1"
On peut aussi installer plusieurs paquets à la suite :
composer require symfony/framework-bundle symfony/console
Pour éviter de télécharger une bibliothèque destinée au développement en environnement de production, l'ajouter en mode "dev", puis lancer composer install
en développement, et composer install --no-dev
en production :
composer require --dev phpunit/phpunit ^8
Pour utiliser une branche du dépôt de la dépendance, , utiliser le mot réservé ":dev-" suivi de son nom. Ex :
composer require mediawiki/semantic-media-wiki:dev-123_ma_branche
Pour utiliser un commit particulier du dépôt de la dépendance, utiliser le mot réservé "dev-master". Ex :
composer require mediawiki/semantic-media-wiki dev-master#hash_du_commit
install
Généralement quand on clone un projet git existant contenant un composer.json, il suffit ensuite pour le faire fonctionner, d'installer ses dépendances ainsi :
composer install
Si plusieurs versions de PHP sont installées, il faut préciser laquelle exécute composer ainsi :
/usr/bin/php8.4 /usr/local/bin/composer install
update
Ne jamais lancer de composer update
sur un projet existant, sous peine de devoir tout retester. En effet, mieux vaut ne mettre à jour qu'une seule bibliothèque en précisant son nom :
php composer.phar update mediawiki/semantic-media-wiki --with-dependencies
Il faut parfois éditer le composer.json avec la règle de la nouvelle version souhaitée avant de lancer l'update.
show
Affiche toutes les bibliothèques installées sur une ligne chacune, ce qui est plus lisible que composer.lock.
depends
Affiche les bibliothèques qui dépendent du package donné en paramètre.
clear-cache
composer clear-cache
Syntaxe du composer.json
La syntaxe JSON de ce fichier contient quelques extensions[2] :
name
Le nom de l'application, telle qu'elle sera appelée par composer.
Il existe une syntaxe pour le nommage des bibliothèques sous peine de warnings lors des manipulations avec composer. Elles doivent être de la forme vendor name/package name, uniquement avec des caractères alphanumériques plus "-", "." ou "_".
require
Le paragraphe "require" contient les noms des dépendances ainsi que leurs numéros de version (SemVer).
Symbole | Rôle (placé avant un numéro de version) | Exemple |
---|---|---|
>= | permet d'en étendre le numéro. De même on trouve les symboles >, <, <=. | "php": ">=5.5.9" inclut PHP 7. |
!= | exclut une version. | |
- | définit une plage de versions. | |
¦¦ | ajoute des versions possibles. | "symfony/symfony": "2.8 ¦¦ 3.0" regroupe uniquement ces deux versions. |
* | étend à toutes les sous-versions. | "symfony/symfony": "3.1.*" comprend la 3.1.1. |
~ | étend aux versions suivantes du même niveau. | "doctrine/orm": "~2.5" concerne aussi la 2.6 mais pas la 2.4 ni la 3.0. |
^ | fait la même chose que tilde sous réserve qu'il y ait une compatibilité ascendante. |
Stability Flag
Après la version à trois nombres, on peut suffixer un arobase puis un Stability Flag[3]. Exemples :
@dev
@stable
require-dev
On y trouve ce qui a été installé avec require --dev
, donc généralement des bibliothèques de tests et d'analyse de code.
config
Ensemble de paramètres de composer[4], par exemple le timeout ou les identifiants vers certains dépôts.
autoload
Mapping des namespaces vers les classes. Cela permet de plus d'inclure tous les fichiers des dossiers automatiquement (et donc d'éviter les fonctions require()
et include()
)[5].
Exemple :
"autoload": { "psr-4": { "App\\": "src/" } },
De plus, un mapping spécifique aux environnements dev est possible avec :
"autoload-dev": { "psr-4": { "App\\Tests\\": "tests/" } },
scripts
Scripts qui s’exécutent quand on lance "composer"[6].
- Soit automatiquement si présents dans "auto-scripts".
- Soit manuellement. Exemple avec Doctrine dans Symfony :
... "scripts": { "doctrine-cc": "php bin/console doctrine:cache:clear-metadata && php bin/console doctrine:cache:clear-query && php bin/console doctrine:cache:clear-result" }, ...
Utilisation :
composer doctrine-cc
extra
Complément de "scripts".
conflict
Liste automatiquement les dépendances en conflit, c'est-à-dire qui ne peuvent pas être installées avec l'application courante dans une tierce.
repositories
Pour spécifier des dépôts privés (qui ne sont pas accessibles sans communiquer leur URL).
Exemple complet
Exemple de mediawiki/core[7] :
{
"name": "mediawiki/core",
"description": "Free software wiki application developed by the Wikimedia Foundation and others",
"type": "mediawiki-core",
"keywords": [
"mediawiki",
"wiki"
],
"homepage": "https://www.mediawiki.org/",
"authors": [
{
"name": "MediaWiki Community",
"homepage": "https://www.mediawiki.org/wiki/Special:Version/Credits"
}
],
"license": "GPL-2.0-or-later",
"support": {
"issues": "https://bugs.mediawiki.org/",
"irc": "irc://irc.libera.chat/mediawiki",
"wiki": "https://www.mediawiki.org/"
},
"prefer-stable": true,
"require": {
"composer/semver": "3.2.4",
"cssjanus/cssjanus": "2.0.0",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-iconv": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"guzzlehttp/guzzle": "7.2.0",
"liuggio/statsd-php-client": "1.0.18",
"monolog/monolog": "2.2.0",
"oojs/oojs-ui": "0.42.0",
"pear/mail": "1.4.1",
"pear/mail_mime": "1.10.9",
"pear/net_smtp": "1.9.2",
"php": ">=7.2.22",
"psr/container": "1.1.1",
"psr/log": "1.1.3",
"ralouphie/getallheaders": "3.0.3",
"wikimedia/assert": "0.5.0",
"wikimedia/at-ease": "2.1.0",
"wikimedia/base-convert": "2.0.1",
"wikimedia/cdb": "1.4.1",
"wikimedia/cldr-plural-rule-parser": "2.0.0",
"wikimedia/common-passwords": "0.3.0",
"wikimedia/composer-merge-plugin": "2.0.1",
"wikimedia/html-formatter": "3.0.1",
"wikimedia/ip-set": "3.0.0",
"wikimedia/ip-utils": "3.0.2",
"wikimedia/less.php": "3.1.0",
"wikimedia/minify": "2.2.4",
"wikimedia/normalized-exception": "1.0.1",
"wikimedia/object-factory": "3.0.2",
"wikimedia/parsoid": "^0.14.0-a14@alpha",
"wikimedia/php-session-serializer": "2.0.0",
"wikimedia/purtle": "1.0.7",
"wikimedia/relpath": "3.0.0",
"wikimedia/remex-html": "2.3.2",
"wikimedia/request-timeout": "1.1.0",
"wikimedia/running-stat": "1.2.1",
"wikimedia/scoped-callback": "3.0.0",
"wikimedia/services": "2.0.1",
"wikimedia/shellbox": "2.0.0",
"wikimedia/utfnormal": "3.0.2",
"wikimedia/timestamp": "3.0.0",
"wikimedia/wait-condition-loop": "2.0.2",
"wikimedia/wrappedstring": "3.2.0",
"wikimedia/xmp-reader": "0.8.1",
"zordius/lightncandy": "1.2.5"
},
"require-dev": {
"composer/spdx-licenses": "1.5.4",
"doctrine/dbal": "2.10.4||3.0.0",
"doctrine/sql-formatter": "1.1.1",
"giorgiosironi/eris": "^0.10.0",
"hamcrest/hamcrest-php": "^2.0",
"johnkary/phpunit-speedtrap": "^3.1",
"justinrainbow/json-schema": "~5.2",
"mediawiki/mediawiki-codesniffer": "37.0.0",
"mediawiki/mediawiki-phan-config": "0.10.6",
"nikic/php-parser": "4.10.2",
"nmred/kafka-php": "0.1.5",
"php-parallel-lint/php-console-highlighter": "0.5",
"php-parallel-lint/php-parallel-lint": "1.3.0",
"phpunit/phpunit": "^8.5",
"psy/psysh": "0.10.5",
"seld/jsonlint": "1.8.3",
"symfony/yaml": "~3.4|~5.1",
"wikimedia/testing-access-wrapper": "~2.0",
"wmde/hamcrest-html-matchers": "^1.0.0"
},
"replace": {
"symfony/polyfill-ctype": "1.99",
"symfony/polyfill-mbstring": "1.99"
},
"suggest": {
"ext-apcu": "Local data cache for greatly improved performance",
"ext-curl": "Improved http communication abilities",
"ext-openssl": "Cryptographical functions",
"ext-wikidiff2": "Diff accelerator",
"monolog/monolog": "Flexible debug logging system",
"nmred/kafka-php": "Send debug log events to kafka"
},
"autoload": {
"psr-0": {
"ComposerHookHandler": "includes/composer",
"ComposerVendorHtaccessCreator": "includes/composer",
"ComposerPhpunitXmlCoverageEdit": "includes/composer"
}
},
"autoload-dev": {
"files": [
"vendor/hamcrest/hamcrest-php/hamcrest/Hamcrest.php",
"vendor/wmde/hamcrest-html-matchers/src/functions.php"
]
},
"scripts": {
"mw-install:sqlite": "php maintenance/install.php --server=http://localhost:4000 --dbtype sqlite --dbpath cache/ --scriptpath '' --pass adminpassword MediaWiki Admin",
"serve": "php -S localhost:4000",
"lint": "parallel-lint --exclude vendor",
"phan": "phan -d . --long-progress-bar",
"phpcs": "phpcs -p -s --cache",
"fix": [
"phpcbf"
],
"pre-install-cmd": "ComposerHookHandler::onPreInstall",
"pre-update-cmd": "ComposerHookHandler::onPreUpdate",
"post-install-cmd": "ComposerVendorHtaccessCreator::onEvent",
"post-update-cmd": "ComposerVendorHtaccessCreator::onEvent",
"test": [
"composer lint .",
"composer phpcs ."
],
"test-some": [
"composer lint",
"composer phpcs"
],
"phpunit": "phpunit",
"phpunit:unit": "phpunit --colors=always --testsuite=core:unit,extensions:unit,skins:unit",
"phpunit:integration": "phpunit --colors=always --testsuite=core:integration,extensions:integration,skins:integration",
"phpunit:coverage": "phpunit --testsuite=core:unit --exclude-group Dump,Broken",
"phpunit:coverage-edit": "ComposerPhpunitXmlCoverageEdit::onEvent",
"phpunit:entrypoint": "php tests/phpunit/phpunit.php"
},
"config": {
"optimize-autoloader": true,
"prepend-autoloader": false
},
"extra": {
"merge-plugin": {
"include": [
"composer.local.json"
],
"merge-dev": false
}
}
}
Changer de version de PHP
Composer ne permet pas de changer de version de PHP en ligne de commande. Pour ce faire, il faut éditer le composer.json en mettant la version cible souhaitée, puis lancer composer update
.
Références
- ↑ https://curl.haxx.se/download.html
- ↑ https://getcomposer.org/doc/articles/versions.md
- ↑ https://igor.io/2013/02/07/composer-stability-flags.html
- ↑ https://getcomposer.org/doc/06-config.md
- ↑ https://coopernet.fr/formation/php/autoload
- ↑ https://getcomposer.org/doc/articles/scripts.md
- ↑ https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/master/composer.json
PhpStorm
IDE en freemium réputé le meilleur pour le langage PHP (compter 10 € par mois pour l'utiliser au-delà d'un mois), PhpStorm permet entre autres des recherches relativement rapides dans le code grâce à son indexation, de l'autocomplétion des langages Web, de nombreuses propositions d'optimisations de code et des plugins (ex : Git, Docker, frameworks PHP...).
Installation
Le logiciel est multi-plateforme est ses prérequis figure avec les liens de téléchargement sur le site officiel : https://www.jetbrains.com/help/phpstorm/installation-guide.html.
Configuration
Pour un projet git, il est recommandé d'ajouter le dossier .idea/ créé par PhpStorm dans le fichier .gitignore.
Pour éviter de modifier la totalité des fichiers d'un projet mal formaté, il faut désactiver le reformatage automatique lors du copier-coller sans Settings, Editor, Smart Keys, Reformat on paste : None.
Exclure les fichiers minifiés de l'indexation
Idéalement l'indexation des fichiers par PhpStorm devrait prendre moins d'une minute, et ses recherches ne devraient renvoyer que du code source lisible par un humain (par les fichiers minifiés).
Pour ce faire, il faut exclure certains dossiers dans File\Settings\Directories.
Par exemple sur un projet Symfony on exclura :
- public/build (pour les .js optimisés)
- var (pour les .php optimisés, et les logs)
Fonctionnalités
Refactorisation
Par rapport à ses concurrents, il offre de nombreuses options de refactorisation. Par exemple, quand on renomme une variable en passant par le menu "Refactor", il peut le répercuter dans tout le code du projet qui l'appelle, y compris dans les getters et setters. De plus, il peut ajouter ces derniers automatiquement, ainsi que le constructeur d'une classe selon ses attributs (raccourci ALT + Ins), avec un formatage très personnalisable, par exemple pour les retours à la ligne après chaque attributs ou selon une largeur.
À ce propos, afin de respecter la PSR-1 lors de l'insertion de setters, il convient de paramétrer dans Editor\Code Style\PHP\Blank lines, Before return statement = 1.
Autoformatage
Il fournit aussi de nombreuses options d'autoformatage et son analyse de code permet par exemple de trouver les variables non utilisées. Quand on appelle une méthode avec du type hinting, il apparait sans avoir besoin d'ouvrir le fichier de cette dernière. Depuis la version 2019.3, il affiche les méthodes mortes en couleur plus sombre (en plus des variables mortes qu'il signalait déjà).
Par ailleurs, pour le pretty-print XML ou JSON : Code\Reformat Code.
Terminal
Par ailleurs, il possède un lien vers un terminal shell intégré dans une fenêtre du footer, et peut aussi exécuter des requêtes SQL sur des bases si on lui ajoute les sources de données. À ce propos, il permet de naviguer dans une base de données nativement avec une interface, comme le fait PhpMyAdmin, Adminer ou MySQL Workbench.
L'interpréteur de commande est modifiable dans Settings\Tools\Terminal. Par exemple pour utiliser du shell Unix depuis Windows, on peut mettre : C:\Program Files\Git\bin\sh.exe
Gestion de version
Son système VCS est compatible avec git et permet de voir l'historique des modifications des fichiers en couleur. Par exemple avec un clic droit dans la marge on peut afficher les annotations pour retrouver l'auteur d'un passage, puis réitérer l'opération en affichant les annotations précédentes pour remonter tout l'historique.
De plus, quand on regarde le différentiel des fichiers modifiés depuis le dernier commit (onglet "Version Control" en bas, "resolve", puis "merge..."), en cas de conflit il propose un outil de résolution à trois colonnes très ergonomique.
Enfin, son outil de rebase interactif permet des squash et fixup en masse (dans Log, sélectionner la branche, sélectionner les commits à squasher dans la liste).
Xdebug
Xdebug peut être déclenché depuis une page Web ou une commande shell. S'il est déjà installé sur le serveur PHP, et pour les pages Web dans le navigateur, voici ce qu'il reste à faire pour le faire fonctionner dans PhpStorm :
- Cliquer sur l'icône en haut à droite Listen for debug connect.
- Lancer le script à déboguer (afficher la page Web ou entrer la commande shell).
Une fenêtre apparait alors dans PhpStorm Incoming Connection From Xdebug, demandant quel est l'index.php correspondant au signal reçu. Cocher "Import mappings from deployment" si les fichiers sont exécutés sur un serveur distant, et "Manually choose local file or project" s'il est local. Cette deuxième option dresse une liste des index.php trouvés dans le projet, mais pour conserver celui par défaut, choisir leur dossier parent (le nom du projet), et cliquer sur "Accept".
En cas de problème, un outil de diagnostic se trouve dans File, Settings, chercher "Xdebug".
Docker
Si le serveur PHP est dans un conteneur Docker, il faut le stipuler à PhpStorm :
- Ajouter un serveur : dans File, (Languages & Frameworks,) PHP, Servers, ajouter une URL pour le port 80, et une pour le 443. Pour chacun, cocher "Use path mappings" et y mettre le chemin de chaque projet accessible dans le conteneur PHP.
- Ajouter une configuration de lancement : en haut à droite dans Add Configuration... ou Edit Configurations..., créer une entrée PHP Remote Debug, dans laquelle il faut renseigner le serveur précédemment créé, et la clé de session envoyée par le module de son navigateur.
Le php.ini dépend de l'OS hôte[1] :
- Linux :
xdebug.remote_host = 172.170.0.1
- Windows :
xdebug.remote_host = "docker.for.win.host.internal"
- Mac :
xdebug.remote_host = "docker.for.mac.host.internal"
Exemple de Dockerfile :
RUN pecl install -f xdebug \ && docker-php-ext-enable xdebug COPY xdebug.ini $PHP_INI_DIR/conf.d # ou s'il y a peu de lignes à ajouter dans le .ini : RUN echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
Raccourcis clavier
Maj Maj | Rechercher un fichier |
Ctrl + Maj + N | Ouvrir un fichier (ex : quand on le copie-colle depuis le terminal) |
Ctrl + Maj + F | Rechercher dans les fichiers |
Ctrl + G | Aller à la ligne n°X |
Ctrl + D | Dupliquer la ligne courante |
Ctrl + Maj + Alt | Sélectionner (en clic, double-clic ou glisser) plusieurs morceaux de codes pour pouvoir les copier. |
Ctrl + Maj + Alt + T | Renommer en refactorant |
Ctrl + B | Trouver les utilisations de l'objet courant (équivalent au clic droit, find usages) |
Ctrl + E | Fichiers récents |
Ctrl + K | Commiter |
Ctrl + L | Vider le terminal (utile pour afficher uniquement les derniers logs) |
Alt + Insert | Générer du code, par exemple le constructeur ou les getters et setters à partir des attributs. |
Alt + clic | Décupler le curseur pour écrire la même chose à plusieurs endroits. |
F2 | Se rendre sur l'erreur de compilation du fichier courant |
Plugins
PhpStorm dispose de plusieurs plugins installables dans un deuxième temps, classables par thèmes. Certains sont payants, mais en voici des gratuits utiles :
- Modal commit interface : pour visualiser le git status et rajouter des fichiers à commiter (après installation, cocher "Use modal commit interface" pour l'activer).
- EditorConfig récupère automatiquement la configuration d'IDE par projet (par exemple les conventions de codage)[2] à partir d'un fichier .editorconfig versionné. NB : ce format de fichier fonctionne dans la plupart des autres IDEs[3].
- SonarLint analyse de code statique en cours de frappe, synchronisable avec SonarQube.
- Prise en charge de plusieurs frameworks PHP :
- Symfony donne :
- Un accès aux fichiers YAML de déclaration de services depuis les classes PHP et vice-versa en un clic[4]
- Un accès aux fichiers Twig entre eux ou depuis les classes PHP en un clic. Il faut toutefois ajouter au préalable chaque dossier "template" dans Settings, PHP, Symfony, Twig / Template (Namespace = __main__, Type = ADD_PATH).
- WordPress
- Drupal
- Joomla
- Symfony donne :
- Des frameworks JS :
- Node.js
- Angular
- Next.js
- Vue.js
- Docker
Critique
- Il est possible que l'application freeze le PC. Pour éviter cela aller dans le menu Help/Change Memory Settings et diminuer la valeur.
- Début 2019, il considère à tort un fichier accessible par deux liens symboliques Linux comme deux fichiers.
- Depuis au moins 2020, quand plusieurs projets sont ouverts ensemble, il mélange leurs espaces de nom (namespaces) comme si c'était le même (notamment dans les CTRL + clic).
- Quand on exclut un dossier de l'indexation, si l'application crée un sous-dossier, il est indexé à tort. Par exemple, dans Symfony var/ est exclu mais var/cache/ apparait dans les recherches si on ne l'exclut pas manuellement après exécution de l'application.
Références
NetBeans

Installation de NetBeans
NetBeans est un environnement de développement intégré (EDI), générique et extensible. Il permet nativement de développer en Java, C, C++, JavaScript, XML, Groovy, PHP et HTML.
De plus, il possède son propre wiki officiel[1] (en anglais). La présente page est donc destinée à le synthétiser au maximum.
Pour démarrer l'installation, le télécharger la version LTS sur https://netbeans.org/downloads/, ou bien la toute dernière sur http://bits.netbeans.org/dev/nightly/latest/.
Configuration
- Pour installer les plugins comme PHP ou Python : https://netbeans.org/downloads/index.html.
- Par défaut le chemin d'accès d'un fichier ouvert apparait en survol de son nom. Pour l'afficher en permanence sous la barre de menu : Tools, Options, Appearance, Show full file path.
- L'autoformatage est réglable dans Tools, Options, Editor, Formatting.
- Pour déplacer la barre verticale rouge qui se trouve par défaut à 80 caractères de la gauche : Tools, Options, Editor, Formatting, Right Margin. Par exemple sur GitHub, la taille des écrans est de 120 caractères, il vaut donc mieux ne pas dépasser cette largeur pour éviter lors des relectures de codes, de descendre chercher un des ascenseurs pour remonter lire une fin de ligne.
- Pour bénéficier des avantages propres à un framework (ex : autocompletion des objets du projet ou ouverture d'une déclaration de méthode avec "CTRL + clic" sur un appel), il convient, après installation dans "Tools\Plugins" de spécifier manuellement son emplacement dans "Tools\Options", puis pour PHP par exemple "Framework & Tools".
Utilisation

NetBeans permet de comparer deux fichiers : Tools, Diff...
Gestion de versions
Les gestions de version sont regroupées dans le menu Team. NetBeans peut donc tout à fait remplacer des clients Git purs tels que GitHub Desktop ou Smartgit.
Pour éviter de commiter fichier par fichier, ajouter le bouton Commit All - Repository en le faisant glisser depuis View, Toolbars, Customize.


Principaux raccourcis claviers
Liste exhaustive en anglais : http://wiki.netbeans.org/KeymapProfileFor60.
- Alt + Maj + o : ouvre le fichier recherché.
- Ctrl + o : ouvre le fichier déclarant l’objet sélectionné (recherche de classe).
- Ctrl + Maj + f : cherche un texte dans les fichiers le dépôt.
- Ctrl + b : cherche la déclaration d'un élément (dans le même fichier ou pas).
- Ctrl + espace : propose une liste de complétion (mots-réservés et squelettes de fonctions).
- Ctrl + Maj + espace : affiche la documentation d'une méthode (Javadoc, PHPDoc...).
- Ctrl + p : affiche uniquement les arguments possibles d'une méthode.
- Ctrl + Maj + c : commente les lignes sélectionnées.
Plugins
Certains plugins peuvent se révéler utiles :
- http://plugins.netbeans.org/plugin/45925/sort-line-tools : trie les lignes dans l'ordre alphabétique.
- http://plugins.netbeans.org/plugin/37077/autosave-module-for-6-9-and-later : sauvegarde automatiquement chaque fichier modifié en temps réel.
Références
Eclipse

Eclipse est un environnement de développement intégré (EDI), générique et extensible (site officiel http://www.eclipse.org). Son système de plugins permet d'ajouter des fonctionnalités diverses.
Initialement prévu pour développer en Java, grâce aux plugins il peut maintenant également gérer des projets développés avec d'autres langages de programmation tels que :
- Le C et le C++ grâce à l'ensemble de plugins CDT (C Development Toolkit)[1] (compilateur non intégré).
- Le Python via PyDev[2].
- Avant l'arrivée d'Android Studio, le développement pour Android se faisait avec Eclipse grâce à l'ensemble de plugins ADT (Android Development Toolkit).
Certains IDE sont basés sur Eclipse, et permettent par exemple le développement de logiciel embarqués pour des systèmes temps réel.
Installation de Eclipse
La page de téléchargement d'Eclipse permet de récupérer une version déjà adaptée au langage ciblé sur http://www.eclipse.org/downloads/. Mais pour installer un plugin manuellement, il faut :
- Lancer Eclipse, puis dans le menu déroulant :
Help>Software Updates>Find and Install...
- Cocher Search for new features to install, bouton Next. Bouton New Remote Site..., entrer l'adresse de téléchargement :
Name: Nom du plugin URL: adresse du plugin, ex : http://www.eclipse.org/cdt/downloads.php
- Bouton Finish, choisir un miroir proche puis continuer l'installation.
Utilisation de Eclipse
L'interface de l'IDE Eclipse est basée sur différentes perspectives.
Une seule perspective n'est visible à la fois, et se compose de plusieurs vues.
Exemples :
- La perspective "Java" se compose par défaut de la vue "Package Explorer", de la vue éditeur de code en Java avec un onglet par fichier ouvert, de la vue "Outline" donnant la hiérarchie des éléments composant la classe du fichier ouvert.
- La perspective "Debug" est ouverte automatiquement au lancement d'une application en mode débogage et se compose par défaut de la vue "Debug" affichant la pile d'appel, de la vue des points d'arrêt nommée "Breakpoints", de la vue éditeur de code en Java avec un onglet par fichier ouvert, de la vue "Outline" donnant la hiérarchie des éléments composant la classe du fichier ouvert.
- Deux ou plusieurs perspectives peuvent être affichées conjointement.
Chaque vue est une sous-fenêtre qui a un titre et se place dans un cadre particulier de la fenêtre de l'IDE. Les vues peuvent être déplacées à la souris par drag and drop pour changer la disposition de la perspective. Plusieurs vues peuvent partager le même cadre, auquel cas, une barre d'onglets permet de basculer entre les vues. Un double clic sur le titre d'une vue provoque l'affichage du cadre qui la contient en pleine fenêtre, réduisant les autres cadres à une icône sur les côtés. Un second double clic restaure les cadres.
Le menu "Window" permet de changer de perspective, et d'ajouter des vues à la perspective courante. Une vue peut également être retirée de la perspective affichée en utilisant la croix à droite du titre de la vue.

Édition de lignes
L'éditeur de code possède des raccourcis clavier pratiques rendant l'édition des lignes de code plus rapide :
Touches | Effet | |
---|---|---|
Shift | ↵ Enter | Ajouter une nouvelle ligne après la ligne courante. |
Ctrl | ↑/↓ | Faire défiler la vue vers le haut/le bas. |
CtrlShift | ↑/↓ | Déplacer le curseur sur le membre précédent/suivant de la classe. |
Alt | ↑/↓ | Déplacer la ligne courante ou les lignes sélectionnées vers le haut/le bas dans le texte. |
CtrlAlt | ↑/↓ | Dupliquer la ligne courante ou les lignes sélectionnées vers le haut/le bas. |
CtrlShift | : | Commenter/Décommenter la ligne courante. |
Complétion de code
L'éditeur de code peut compléter automatiquement le code là où se trouve le curseur :
Touches | Effet | |
---|---|---|
Ctrl | Espace | Ouvrir la liste des suggestions de complétion.
Une fois la suggestion choisie, la validation se fait par l'une des touches suivantes :
Toute autre touche produit le caractère sans valider (annuler la complétion). |
AltShift | : | Complète avec la seule possibilité, ou produit un bip s'il y a plusieurs possibilités. |
Navigation et autres astuces
- Défilement rapide avec la molette de la souris
- Maintenez la touche Ctrl enfoncée pendant l'utilisation de la molette de la souris pour faire défiler le contenu d'un fichier page par page plutôt que ligne par ligne.
- Visualiser l'ouverture du bloc correspondant à la fermeture
- Dans les codes sources dépassant la hauteur de fenêtre de l'éditeur, placez le pointeur de la souris sur l'accolade fermante d'un bloc pour voir apparaître un résumé du code d'ouverture du bloc en bulle d'aide. Ceci est fort utile pour vérifier quel bloc est fermé par l'accolade sans avoir à faire défiler le code.
- Aller à la définition d'un membre de classe
- Placez le pointeur de la souris sur un identifiant de classe, méthode ou variable et enfoncez la touche Ctrl pour faire apparaître un lien cliquable vers la définition.
- Localiser toutes les occurrences dans la classe
- Cliquez sur un identifiant de membre de classe pour faire apparaître toutes les occurrences d'utilisation dans le fichier : à la fois dans le texte et dans la barre de défilement. Les blocs apparaissant dans la barre de défilement sont cliquables pour faire défiler le code à la position de l’occurrence. Il y a deux couleurs distinctes pour les occurrences utilisées en lecture (accès, appel, ...) et celles utilisées en écriture (affectation).
- Il peut arriver que cela ne semble pas fonctionner car l'éditeur peut être dans un mode autre que celui par défaut. Il faut dans ce cas appuyer une ou deux fois la touche Échap pour que cela fonctionne.
Inconvénients
L'IDE Eclipse montre beaucoup de choses à ne pas faire quand on conçoit une interface graphique ; notamment concernant les raccourcis clavier :
- Certains raccourcis standard dans la grande majorité des logiciels ne sont pas supportés par Eclipse (exemple : Ctrl+O pour ouvrir un fichier),
- Il est possible de modifier les raccourcis clavier via une boîte de dialogue cachée dans le dédale des menus de l'IDE (qui change à chaque version), toutefois le champ "When" doit être rempli avec la condition d'activation du raccourci (activation contextuelle). Il est impossible de définir un raccourci valable quel que soit le contexte.
L'historique de navigation entre les fichiers est également mal géré : après une action de navigation avant (CTRL+clic sur une méthode, par exemple) changeant le fichier courant, plusieurs retours en arrière sont généralement nécessaires pour revenir à la position précédente.
Références
- ↑ https://eclipse.org/cdt/
- ↑ (anglais) http://pydev.org/
Visual Studio Code

Visual Studio Code (ou simplement nommé "code") est un logiciel éditeur de texte graphique, gratuit fonctionnant sous Windows. Il est capable de prendre en charge le débogage, d'effectuer la coloration syntaxique, l'auto-complétion et surtout le pliage de code (code folding), c'est à dire le masquage à volonté de différents blocs d'instructions (contenu d'une classe, d'une fonction, d'une boucle, etc.) : cette fonctionnalité se révèle extrêmement pratique lorsque vos scripts commencent à s'allonger... Il intègre également la gestion de version (notamment Git et SVN)[1], une fenêtre de terminal ainsi qu'un raccourci pour lancement des scripts.
Cet éditeur est disponible pour Windows sur https://code.visualstudio.com .
Langages supportés
Visual Studio Code prend immédiatement en charge presque tous les principaux langages informatiques.
Plusieurs d'entre eux sont inclus par défaut, comme HTML et CSS, et aussi JavaScript, TypeScript, Java, PHP, mais d'autres extensions de langage peuvent être trouvées et téléchargées gratuitement à partir de VS Code Marketplace[2].
Utilisation

Édition de lignes
L'éditeur de code possède des raccourcis clavier pratiques rendant l'édition des lignes de code plus rapide :
Touches | Effet | |
---|---|---|
Ctrl | ↵ Enter | Ajouter une nouvelle ligne après la ligne courante. |
Ctrl | ↑/↓ | Faire défiler la vue vers le haut/le bas. |
Alt | ↑/↓ | Déplacer la ligne courante ou les lignes sélectionnées vers le haut/le bas dans le texte. |
AltShift | ↑/↓ | Dupliquer la ligne courante ou les lignes sélectionnées vers le haut/le bas. |
Ctrl | : | Commenter/Décommenter la ligne courante. |
Références
Bases du langage
PHP est un langage dont les programmes s'exécutent côté serveur pour produire la réponse à une requête HTTP, ou une commande shell.
En général, il produit une réponse dans un langage de notation (ex : HTML, XML, ou JSON), et le code PHP peut s'intégrer dedans, par exemple dans les balises HTML comme montré dans ce chapitre introductif.
Il peut également produire une sortie dans un format binaire, par exemple une image PNG produite dynamiquement pour une représentation graphique des données, ou un fichier quelconque pour filtrer les accès à des fichiers téléchargeables.
Dans les premiers chapitres, seul le cas de la sortie en HTML est abordé.
Intégration du code
Pour que PHP interprète votre code, vous devez remplir deux conditions :
- Votre code doit être placé dans un fichier d'extension .php (selon la configuration du serveur cela peut varier), ouvert via un serveur web en HTTP.
- Votre code doit être compris entre deux balises :
<?php ... ?>
. Tout ce qui n'est pas compris entre ces balises n'est pas interprété par PHP :
<!DOCTYPE html> <html> <head> <title>Le titre de l'onglet</title> <meta charset="utf8" /> </head> <body> <p>Du texte en html</p> <? echo 'un exemple de texte en php'; ?> <p>Encore du texte en html</p> //Écriture recommandée et universelle <?php echo "Encore du texte en php"; ?> <body> </html>
L'utilisation des balises <? ?>
(sans le mot "php") peut poser des problèmes de compatibilité. Il faut en effet que pour cela, la directive short_open_tags
soit activée dans la configuration de l'environnement, ce qui n'est pas le cas sur la plupart des serveurs mutualisés et hébergements gratuits. Imaginez que vous changiez de serveur avec un interpréteur qui ne reconnaisse pas les balises courtes, il vous faudra alors modifier les balises de chaque fichier, travail fastidieux. Il est donc conseillé de prendre l'habitude d'utiliser les balises de la forme <?php ?>
, reconnue universellement.
Pour regarder si votre configuration prend en compte ce type de balises, entrez le code suivant :
<?php phpinfo();
Cela affichera le contenu du php.ini. Pour l'obtenir en CLI :
php -r "phpinfo();"
Autre exemple : trouver le .ini utilisé :
php8.4 -r "echo php_ini_loaded_file().PHP_EOL;"
Il est possible de trouver des extensions de fichiers comme .phtml, .php3 ou autres. Il est cependant conseillé d'utiliser l'extension .php qui garantit son interprétation par PHP.
<?php
et finissent par ?>
, la balise fermante est optionnelle voire déconseillée.Les directives
Il est possible de définir certains comportements de PHP lors de la compilation par des directives inscrites dans la commande declare()
[1]. Par exemple :
declare(encoding = 'UTF-8');
Références
Commentaires
Principe
Les commentaires sont en réalité des portions de texte qui ne seront pas interprétées par PHP et ne seront visibles que dans le code source. Ils jouent un rôle très important dans la réalisation et la mise à jour d'un script : en effet, les commentaires rendent le code plus lisible et peuvent aider les éventuelles personnes qui souhaitent retravailler votre script. En effet, si les commentaires sont très utiles aux programmeurs seuls, ils le sont encore plus lors du travail en équipe.
Il existe trois façons différentes d'ajouter des commentaires à son script PHP :
- La méthode avec les symboles
//
pour ajouter un commentaire sur une ligne. - La méthode avec le sigle
#
pour ajouter un commentaire sur une ligne également. - La méthode avec les caractères
/* */
pour désigner un bloc de commentaires.
<?php # un commentaire PHP // encore un commentaire PHP /* et encore un autre, mais sur plusieurs lignes cette fois-ci ! */
Il est important de ne pas emboîter les commentaires. Exemple à ne pas suivre :
<?php /*blabla /* hihi*/ blalbal*/
L'interpréteur comprendra que le commentaire s'arrête à hihi*/ et il tentera d'interpréter blalbal*/. Il en résultera donc une erreur.
Annotations
Il existe des logiciels qui génèrent une documentation complète à partir des commentaires insérés dans le code du programme. De là est apparue une certaine forme de standardisation de ceux-ci afin de faciliter la génération de documentation, appelée PHPDoc. On peut en effet ajouter des commentaires structurés pour permettre de générer une documentation automatique via PEAR[1] ou PhpDocumentor. En pratique, cela se traduit par des mots-clés interprétés, précédés d'un arobase, appelés "annotations".
Exemple[2] :
/** * Commentaires sur le rôle du fichier courant * * date modifier : 13 mars 2013 * @since 13 mars 2013 * @author Prénom Nom (courriel) * @copyright PHPascal.com * @version 1.2 * */
Les annotations permettent de plus, aux IDE d'en déduire l'autocomplétion, et aident les analyseurs de code statique à garantir la qualité du code. Elles étaient les seules à pouvoir préciser certains types de variables avant l'apparition des type hinting et type checking en PHP7.
Exemple de commentaires indispensables avant PHP7 :
class MyEntity { /** @var int|null */ private $id; /** * @return int|null */ public function getId() { return $this->id; } }
Depuis PHP7 :
class MyEntity { private int $id; public function getId(): ?int { return $this->id; } }
Attributs
Depuis PHP8, il existe des attributs, représentés par des mots-clés suivant un croisillon, pour préciser des informations (métadonnées) sur les classes, fonctions, constantes ou variables[3]. Ex :
<?php #[\ReturnTypeWillChange] public function getMixedData() { return $this->mixedData; }
Cet attribut sert à ne pas ajouter de warning si le type retourné ne peut pas être déterminé[4].
Il en existe aussi d'autres comme :
- "Attribute" : pour déclarer un nouvel attribut (en précisant par exemple s'il est restreint aux fonctions).
- "Route" : pour relier une URL à une méthode.
Les attributs d'une classe peuvent être récupérés à l'exécution avec : ReflexionClass()->getAttributes().
Références
Premier programme
Hello World
Écrivons notre premier programme PHP. Pour le moment nous nous contenterons d'afficher le fameux "Hello World".
La commande shell est[1] :
php -r "print 'Hello World!';"
Dans un fichier
Soit le fichier HelloWorld.php
ci-dessous, il peut être lancé par :
- les commandes shell
php -q HelloWorld.php
ouphp -f HelloWorld.php
- un navigateur Web, ex : http://localhost/HelloWorld.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>Mon premier script PHP !</title> </head> <body> <p><?php echo 'Hello World!'; ?></p> </body> </html>
Étude du code
Le terme echo
représente une fonction propre à PHP qui a pour rôle d'afficher du texte, mais on peut aussi utiliser la fonction print
à la place.
Quant à 'Hello World', il est écrit entre apostrophes pour indiquer qu'il s'agit de texte, mais on peut aussi utiliser des guillemets à la place.
Enfin, le point-virgule (;) sert à indiquer la fin de l'instruction. On peut donc placer plusieurs commandes sur une seule ligne.
Parse error
Il s'agit d'une erreur fréquente quand on débute en PHP. Elle est généralement due à une erreur de syntaxe. Cela peut être simplement à cause de l'omission d'un point-virgule ou bien par ce que vous avez une apostrophe qui gène.
Exemple de code erroné :
<?php // Parse error assurée echo 'J'irais bien boire un coup.';
Ne s'affichera pas, pour ce faire il faut juste mettre un antislash (\
) devant notre apostrophe.
Exemple de code opérationnel :
<?php // Antislash et c'est bon echo 'J\'irais bien boire un coup.';
On peut aussi rendre plus lisible le code source en utilisant les caractères suivants :
- \n : saut de ligne,
- \t : tabulation,
- \r : retour de chariot.
Attention : Ces caractères ne sont interprétés par le moteur PHP que s'ils sont introduits à l'aide de double apostrophe (guillemets) :
echo "Retourne à la ligne\n";
Vous pouvez remarquer que l'exemple précédent est constitué de deux langages HTML et PHP, vous remarquerez aussi que le script est placé entre les balises <body></body>
ce n'est pas une obligation ainsi le code suivant retourne la même chose :
<?php
$texte= 'Hello World';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Mon premier script PHP !</title>
</head>
<body>
<?php
echo ($texte);
?>
</body>
</html>
Particularités de PHP CLI
PHP en ligne de commande possède plusieurs différences avec PHP sur serveur Web :
- Les variables superglobales sont différentes : il n'y a pas de
$_GET
,$_POST
et$_COOKIE
. - Les paramètres (après le nom du script) sont récupérables par la variable globale
$argv
, et leur nombre par$argc
(ou$_SERVER['argc']
si "register_globals = off;" dans php.ini).
global $argv; global $argc;
Références
Catégorie:Bonjour le monde#PHP
Variables
Définition
En PHP, les variables sont représentées par un signe dollar "$" suivi du nom de la variable, ou d'un underscore "_" pour les constantes (un warning apparait quand ces dernières sont redéfinies).
Le nom est sensible à la casse (par exemple $MaVariable
est différent de $mavariable
). Dans tous les cas, les variables doivent commencer par une lettre (a-z, A-Z) :
$1MaVariable // incorrect $MaVariable // correct $_MaVariable // correct $àéè // correct MA_CONSTANTE // correct _MA_CONSTANTE // correct
Exemple
<?php // Initialisation des variables $Prenom = 'Romuald'; $Age = '23 ans'; $Profession = 'informaticien'; // Affichage echo 'Bonjour ' . $Prenom . ', tu as ' . $Age . ' et ta profession est ' . $Profession . '.'; // Ce qui affichera sur votre navigateur : Bonjour Romuald, tu as 23 ans et ta profession est informaticien. // Une autre manière de faire echo "Bonjour $Prenom, tu as $Age et ta profession est $Profession"; // Ce qui affichera sur votre navigateur : Bonjour Romuald, tu as 23 ans et ta profession est informaticien. // Subtilité des " et des ' echo 'Bonjour $Prenom, tu as $Age et ta profession est $Profession'; // Ce qui affichera sur votre navigateur : Bonjour $Prenom, tu as $Age et ta profession est $Profession // Le contenu d'une chaîne construite avec des " sera interprété par PHP et les variables // éventuellement utilisées seront remplacées par leurs valeurs.
Nota bene : une variable accolée à du texte est précédée d'un point. Ce dernier sert à la concaténation et dans le cas présent est surtout utile pour indiquer à l'interpréteur que l'on passe du texte, puis à une variable. On lui évite ainsi un temps de réflexion qu'il aurait si l'on avait écrit :
echo "Bonjour $Prenom, tu as $Age et ta profession est $Profession";
- Pour citer du texte contenant uniquement des apostrophes ou des guillemets, on peut encadrer les uns par les autres :
echo 'Hello "World"!';
- Pour citer du texte contenant les deux symboles, on peut utiliser le caractère d'échappement "\" :
echo 'Hello \'the\' "World" !';
Here document
Pour éviter de s'encombrer des caractères d'échappement, il existe aussi les syntaxes heredoc et nowdoc avec l'opérateur "<<<"[1]. Ex :
$v = 1; echo <<<Test 'Hello' "World" \\ $v heredoc Test; // 'Hello' "World" \ 1 heredoc echo <<<'Test' 'Hello' "World" \\ $v nowdoc Test; // 'Hello' "World" \\ $v nowdoc
heredoc interprète similairement à une chaine entre guillemets et nowdoc entre apostrophes.
Le mot de fin ("Test" dans l'exemple) des syntaxes heredoc ne doit pas être indenté ni suivi d'autres caractères que le ";", dans les versions PHP antérieures à 7.3[2].
echo <<<HTML
a un rendu différent de echo <<<SQL
.$v1 = new stdClass(); $v1->name = "World"; $v2 = ['one', 'two']; echo <<<Test Hello $v1->name $v2[1] Test;
Mais si cela pose problème, utiliser les accolades pour forcer l'interpolation :
$v1 = new stdClass(); $v1->name = ['one', 'two']; echo <<<Test Hello $v1->name[1] // Hello Array[1] Hello {$v1->name[1]} // Hello two Test;
Chaines binaires
A ne pas confondre avec les nombres binaires qui sont composés de 0 et de 1, les chaines binaires sont apparues en PHP 5.2.1 et indiquées avec le préfixe "b". Ex :
b'ma_chaine'
Elles se distinguent des autres chaines par leur encodage, qui ne permet pas à ses caractères non ASCII d'être affichés brutes. Par exemple, print(b'é');
donne : �.
On peut alors la convertir avec :
print(iconv('ISO-8859-1', 'UTF-8', b'é'));
Types de variables
Contrairement à de nombreux langages de programmation, en PHP il ne faut pas prédéclarer une variable mais celle-ci est typée lors de son instanciation.
$x = 1; // indique implicitement que $x est un entier $mot = 'test'; // indique que $mot est une chaîne de caractères
En PHP il y a donc quatre types de variables scalaires :
- entiers (integer) : nombres naturels sans décimale (sans virgule) ;
- réels (float) : nombres décimaux (on parle généralement de type "double", car il s'agit de nombre décimaux à double précision). On peut y introduire des puissances de 10 avec la lettre "e" (ex : 2e1 = 20, 4e-3 = 0.004) ;
- booléens (boolean) : deux valeurs possibles : 1 ou 0. Elles sont équivalentes aux mots réservés true ou false (en français, vrai ou faux) ;
- chaînes de caractères (string) : ensembles de caractères (des phrases).
De plus, on trouve les types complexes :
- tableaux (array) : listes d'éléments ordonnés.
- objets (object) : classe instanciée.
Pratique sur les nombres flottant
Il faut toujours arrondir les nombres flottant à l'affichage, mais aussi lors des comparaisons[4].
Exemple :
$ php -r "var_dump(8 - 6.4 == 1.6);" bool(false) $ php -r "var_dump(round(8 - 6.4, 1) == 1.6);" bool(true)
Lors de la division de nombres à virgules flottantes, on peut facilement avoir une imprécision à cause du mode de leur stockage en mémoire :
echo PHP_VERSION.PHP_EOL; var_dump(fmod(9999.00000, 9998.00001));
donne :
7.3.14 float(0.99999000000025)
Variables dynamiques
Il est pratique d'avoir parfois des noms de variables qui sont variables. C'est-à-dire un nom de variable qui est affecté et utilisé dynamiquement.
La valeur maVariable
est affectée à une variable classique appelée a
avec l'instruction suivante :
$a = "maVariable";
Une variable dynamique, ou variable variable, utilise le nom d'une autre variable. Dans l'exemple ci-dessous, la valeur bonjour
est affectée à la variable dynamique $$a
, c'est-à-dire à une variable dont le nom est la valeur $a
de la variable a
. La variable classique a
contient la valeur maVariable
donc le code :
$$a = "bonjour";
est équivalent à : $maVariable="bonjour";
, ou encore ${$a} = "bonjour";
.
Attention : cette syntaxe est inutile pour les fonctions. Ex :
<?php function direBonjour() { echo "Bonjour !"; } $a = 'direBonjour'; $a();
Dans le cas des tableaux variables, il faut préciser si l'index s'applique à la variable ou à la variable dynamique à l'aide d'accolades[5] :
<?php $a = ['Hello', 'World']; echo ${$a[0]}; // Undefined variable: Hello echo ${$a}[0]; // Array to string conversion
Variable de classe variable
Les variables de classe seront introduites en détails dans un chapitre ultérieur. Mais il faut savoir que leurs noms peuvent aussi être variables avec ces notations :
echo $this->$myVariableName; echo $this->{'variable'.$i}; echo self::$myStaticVariableName; echo constant(self::class.'::'.strtoupper($myConstantName));
Autres variables
Les variables présentées dans ce paragraphe sont des tableaux indicés par le nom de la valeur accédée (une chaîne de caractère).
Variables superglobales
- Pour plus de détails voir : Programmation PHP/Variables superglobales.
Variables de sessions
- Pour plus de détails voir : Programmation PHP/Sessions.
Lors de la création d'une session (session_start()
), il est possible d'enregistrer des variables (par session_register('nom_variable') = $variable
).
On peut aussi utiliser le tableau $_SESSION
pour créer et modifier une variable de session (par exemple : $_SESSION['ma_variable'] = 3;
)
Il est également possible de supprimer les sessions courantes dans le code PHP en utilisant la fonction session_destroy()
. La destruction de la session en cours peut aussi se faire par la fermeture du navigateur.
Pour supprimer une variable de session sans supprimer la session entière, il suffit d'utiliser la fonction unset($_SESSION['ma_variable'])
.
Variables de cookie
- Pour plus de détails voir : Programmation PHP/Cookies.
Le tableau $_COOKIE
permet de gérer les cookies (définis avec setcookie()
[6]).
Ces cookies sont d'une très grande importance mais sont limités à 20 dans la configuration par défaut de PHP.
Ne pas mettre d'informations privées (mots de passe du serveur...) dans ces variables car elles sont stockées dans un fichier non protégé, sur le disque dur de l'utilisateur.
Variables de requêtes
$_REQUEST est un tableau associatif constitué du contenu des variables $_GET, $_POST, $_COOKIE.
Variables de fichiers
- Pour plus de détails voir : Programmation PHP/Fichiers.
Lors d'un téléchargement de fichiers vers le serveur, une variable est assignée aux données de ce fichier. Il s'agit de $_FILES
. Elle permet de récupérer le nom du fichier envoyé (exemple : mon_image.png
), le nom du fichier temporaire où PHP a copié les données et où il est donc possible de les lire (exemple: C:\temp\T0001AF7.tmp
).
Exemple :
$_FILES['nom_fichier']
Variables mixtes
Le mot clé mixed
permet de définir une variable mixte.
Variables de serveurs
$_SERVER permet d'obtenir des renseignements sous forme d'un tableau sur le serveur.
'PHP_SELF'
- Le nom du fichier du script en cours d'exécution, par rapport à la racine web. Par exemple,
$_SERVER['PHP_SELF']
dans le script situé à l'adressehttp://www.monsite.com/test.php/foo.bar
sera/test.php/foo.bar
. La constante__FILE__
contient le chemin complet ainsi que le nom du fichier courant. 'argv'
- Tableau des arguments passées au script. Lorsque le script est appelé en ligne de commande, cela donne accès aux arguments, comme en langage C. Lorsque le script est appelé avec la méthode GET, ce tableau contiendra la chaîne de requête.
'argc'
- Contient le nombre de paramètres de la ligne de commande passés au script (si le script fonctionne en ligne de commande).
'GATEWAY_INTERFACE'
- Numéro de révision de l'interface CGI du serveur : i.e. ' CGI/1.1 '.
'SERVER_NAME'
- Le nom du serveur hôte qui exécute le script suivant. Si le script est exécuté sur un hôte virtuel, ce sera la valeur définie pour cet hôte virtuel.
'SERVER_SOFTWARE'
- Chaîne d'identification du serveur, qui est donnée dans les en-têtes lors de la réponse aux requêtes.
'SERVER_PROTOCOL'
- Nom et révision du protocole de communication : i.e. ' HTTP/1.0 ';
'REQUEST_METHOD'
- Méthode de requête utilisée pour accéder à la page; i.e. ' GET ', ' HEAD ', ' POST ', ' PUT '.
'REQUEST_TIME'
- Le temps Unix depuis le début de la requête. Disponible depuis PHP 5.1.0.
'QUERY_STRING'
- La chaîne de requête, si elle existe, qui est utilisée pour accéder à la page.
'DOCUMENT_ROOT'
- La racine sous laquelle le script courant est exécuté, comme défini dans la configuration du serveur.
'HTTP_ACCEPT'
- Contenu de l'en-tête Accept: de la requête courante, s'il y en a une.
'HTTP_ACCEPT_CHARSET'
- Contenu de l'en-tête Accept-Charset: de la requête courante, si elle existe. Par exemple : ' iso-8859-1,*,utf-8 '.
'HTTP_ACCEPT_ENCODING'
- Contenu de l'en-tête Accept-Encoding: de la requête courante, si elle existe. Par exemple : ' gzip '.
'HTTP_ACCEPT_LANGUAGE'
- Contenu de l'en-tête Accept-Language: de la requête courante, si elle existe. Par exemple : ' fr '.
'HTTP_CONNECTION'
- Contenu de l'en-tête Connection: de la requête courante, si elle existe. Par exemple : ' Keep-Alive '.
'HTTP_HOST'
- Contenu de l'en-tête Host: de la requête courante, si elle existe.
'HTTP_REFERER'
- L'adresse de la page (si elle existe) qui a conduit le client à la page courante. Cette valeur est affectée par le client, et tous les clients ne le font pas. Certains navigateur permettent même de modifier la valeur de HTTP_REFERER sous forme de fonctionnalité. En bref, ce n'est pas une valeur de confiance.
'HTTP_USER_AGENT'
- Contenu de l'en-tête User_Agent: de la requête courante, si elle existe. C'est une chaîne qui décrit le client HTML utilisé pour voir la page courante. Par exemple : Mozilla/4.5 [en] (X11; U; Linux 2.2.9 i586). Entre autres choses, vous pouvez utiliser cette valeur avec get_browser pour optimiser votre page en fonction des capacités du client.
'HTTP_header'
- Valeur de l'en-tête HTTP correspondant envoyé dans la requête du client.
'HTTPS'
- Définissez à une valeur non-vide si le script nécessite d'utiliser le protocole HTTPS.
'REMOTE_ADDR'
- L'adresse IP du client qui demande la page courante.
'REMOTE_HOST'
- Le nom de l'hôte qui lit le script courant. La résolution DNS inverse est basée sur la valeur de REMOTE_ADDR
'REMOTE_PORT'
- Le port utilisé par la machine cliente pour communiquer avec le serveur web.
'SCRIPT_FILENAME'
- Le chemin absolu vers le fichier contenant le script en cours d'exécution.
'SERVER_ADMIN'
- La valeur donnée à la directive SERVER_ADMIN (pour Apache), dans le fichier de configuration. Si le script est exécuté par un hôte virtuel, ce sera la valeur définie par l'hôte virtuel.
'SERVER_PORT'
- Le port de la machine serveur utilisé pour les communications. Par défaut, c'est '80'. En utilisant SSL, par exemple, il sera remplacé par le numéro de port HTTP sécurisé.
'SERVER_SIGNATURE'
- Chaîne contenant le numéro de version du serveur et le nom d'hôte virtuel, qui sont ajoutés aux pages générées par le serveur, si cette option est activée.
'PATH_TRANSLATED'
- Chemin dans le système de fichier (pas le document-root) jusqu'au script courant, une fois que le serveur a fait une traduction chemin virtuel -> réel.
'SCRIPT_NAME'
- Contient le nom du script courant. Cela sert lorsque les pages doivent s'appeler elles-mêmes. La constante __FILE__ contient le chemin complet ainsi que le nom du fichier (i.e. inclut) courant.
'REQUEST_URI'
- L'URI qui a été fourni pour accéder à cette page. Par exemple : ' /index.html '.
'PHP_AUTH_DIGEST'
- Lorsque vous utilisez PHP avec Apache en tant que module faisant une identification HTTP Digest cette variable est définie dans l'en-tête 'Authorization' envoyé par le client (que vous devez donc utiliser pour réaliser la validation appropriée).
'PHP_AUTH_USER'
- Lorsque vous utilisez PHP avec Apache ou IIS (ISAPI en PHP 5) en tant que module faisant une identification HTTP, cette variable est définie à l'utilisateur fourni par l'utilisateur.
'PHP_AUTH_PW'
- Lorsque vous utilisez PHP avec Apache ou IIS (ISAPI en PHP 5) en tant que module faisant une identification HTTP, cette variable est définie au mot de passe fourni par l'utilisateur.
'AUTH_TYPE'
- Lorsque vous utilisez PHP avec Apache en tant que module faisant une identification HTTP, cette variable est définie au type d'identification.
Détection du protocole
Pour déterminer si l'utilisateur se connecte en HTTP ou HTTPS à partir de la superglobale $_SERVER
:
$_SERVER['REQUEST_SCHEME']
semble la meilleure option, car renvoie "http" ou "https".$_SERVER['SERVER_PORT']
renvoie 80 ou 443.$_SERVER['HTTPS']
donne true en HTTPS ou null en HTTP (provoque un warning unset).$_SERVER['SERVER_PROTOCOL']
ne convient pas du tout car renvoie "HTTP/1.1" dans les deux cas.$_SERVER['HTTP_X_FORWARDED_PROTO']
et$_SERVER['HTTP_FRONT_END_HTTPS']
: à creuser...
Si ces variables renvoient les caractéristiques HTTP en connexion HTTPS, c'est qu'il faut activer SSL pour le site.
Tester les types
Plusieurs fonctions permettent de déterminer les types des objets.
Soit une variable $v :
gettype($v)
donne son type simple ("object" si c'est une instance de classe).get_class($v)
donne son type complexe si c'est une instance de classe.is_null($v)
teste si un objet est null. Équivaut à "=== null"[7].is_bool($v)
est vrai si booléen.is_numeric($v)
est vrai si numérique.is_int($v)
est vrai si entier.is_float($v)
est vrai si décimal.is_string($v)
est vrai si chaine.is_array($v)
est vrai si tableau.is_object($v)
est vrai si objet.$v instanceof MaClasse
est vrai si l'objet est de type "MaClasse".
Conversions
En PHP, il n'y a pas de déclaration de variable selon un type figé. Pour le forcer, il faut donc utiliser les fonctions suivantes, ou un cast (vers un type entre parenthèses).
Soit une variable $v, et $s un séparateur :
intval($v)
ou(int) $v
convertit en entier.floatval($v)
ou(float) $v
convertit en flottant.chr($v)
convertit en caractère.strval($v)
ou(string) $v
convertit un scalaire en chaine de caractères.implode($s, $v)
oujoin($s, $v)
transforme un tableau en chaine selon un séparateur donné.explode($s, $v)
convertit une chaine en tableau avec un séparateur donné entre les valeurs.
De plus, il est possible de réaliser un transtypage en préfixant la variable avec le type entre parenthèses. Exemple :
$a = 1; $b = (bool) $a; print gettype($a); // integer print gettype($b); // boolean
(bool) "false"
renvoie true
car toute string non vide est vraie.
URLs
Tous les caractères Unicode n'étant pas admis dans les URL, on utilisera urlencode()
pour convertir une chaine dans ce format.
De plus, si l'URL contient des paramètres HTTP, http_build_query()
permet, en plus de l'encodage, de les générer à partir d'un tableau associatif.
Références
- ↑ http://fr2.php.net/manual/fr/language.types.string.php#language.types.string.syntax.heredoc
- ↑ https://www.php.net/manual/fr/language.types.string.php#language.types.string.syntax.heredoc
- ↑ https://github.com/microsoft/vscode/issues/137539
- ↑ https://www.php.net/manual/en/language.types.float.php
- ↑ http://php.net/manual/fr/language.variables.variable.php
- ↑ http://www.php.net/manual/fr/function.setcookie.php
- ↑ http://php.net/manual/fr/function.is-null.php
Variables superglobales
Les variables superglobales
Ces variables sont prédéfinies à PHP et sont destinées à stocker des informations bien spécifiques. Elles se présentent généralement sous la forme d'un tableau associatif à une ou deux dimensions. Elles sont disponibles dans tous les contextes d'exécution (fonctions ou méthodes).
Voici une liste non-exhaustive (comprenant les cas d'utilisation les plus courants) :
Variables de serveur : $_SERVER
Elle contient des informations sur le serveur (nom, IP, logiciels installés…)
Par exemple pour extraire l'URL de la page courante :
echo 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
Références
- « Extraire l’url de la page en cours » [html]
- (en) « $_SERVER » [html]
Variables d'environnement : $_ENV
Elle contient des informations sur l'environnement d'exécution du script PHP (c'est-à-dire sur le serveur). Elle est donc directement liée au serveur et à son système.
Prenons comme exemple, le prénom 'Paul' :
<?php echo 'Mon nom d\'utilisateur est ' .$_ENV["USER"] . '!';
En imaginant que l'utilisateur 'Paul' exécute ce script, on aura alors :
Exemple |
Mon nom d'utilisateur est Paul ! |
Cookies : $_COOKIE
Elle stocke les informations sur les cookies envoyés aux clients.
Un petit exemple pour illustre cela :
<?php echo 'Bonjour ' . htmlspecialchars($_COOKIE["name"]) . '!';
On suppose que le cookie "name" a été défini précédemment, (on prendra Paul encore une fois)
L'exemple ci-dessus va afficher alors :
Exemple |
Bonjour Paul ! |
GET variables : $_GET
Elle stocke les valeurs des arguments passés par URL. Ses clés sont donc par conséquent variables. Pour illustrer ces propos, imaginons que l'utilisateur ai entrée l'URL suivant : http://example.com/?name=Paul
<?php echo 'Bonjour ' . htmlspecialchars($_GET["name"]) . '!';
L'exemple ci-dessus va afficher alors :
Exemple |
Bonjour Paul ! |
POST variables : $_POST
Elle stocke les valeurs des informations passées par formulaire avec la méthode="post". Ses clés sont donc par conséquent variables.
L'exemple ci dessous est un formulaire comportant un champ de saisie et un bouton de soumission.
Quand un utilisateur soumet des données en cliquant sur "Soumettre", les données du formulaire sont envoyées dans un fichier spécial dans l'attribut d'action de la balise <form>
.
Puis, on peut utiliser $_POST
pour recueillir la valeur du champ de saisie.
Exemple |
<html> <body> <form method="post" action="<?php echo $_SERVER['PHP_SELF'];?>"> Nom : <input type="text" nom="fnom"> <input type="soumettre"> </form> <?php if ($_SERVER["REQUEST_METHOD"] == "POST") { // collecte la valeur du champ de saisie $nom = $_REQUEST['fnom']; if (empty($nom)) { echo "Le nom est vide"; } else { echo $nom; } } ?> </body> </html> |
Variables de requête : $_REQUEST
Un tableau associatif constitué du contenu des variables $_GET
, $_POST
, $_COOKIE
.
L'exemple ci dessous est un formulaire comportant un champ de saisie et un bouton de soumission.
Quand un utilisateur soumet des données en cliquant sur "Soumettre", les données du formulaire sont envoyées dans un fichier spécial dans l'attribut d'action de la balise <form>
.
Puis, on peut utiliser $_REQUEST
pour recueillir la valeur du champ de saisie.
Exemple |
<html> <body> <form method="post" action="<?php echo $_SERVER['PHP_SELF'];?>"> Nom : <input type="text" nom="fnom"> <input type="soumettre"> </form> <?php if ($_SERVER["REQUEST_METHOD"] == "POST") { // collecte la valeur du champ de saisie $nom = $_REQUEST['fnom']; if (empty($nom)) { echo "Le nom est vide"; } else { echo $nom; } } ?> </body> </html> |
Variable de téléchargement : $_FILES
Elle stocke les informations sur un fichier envoyé via HTTP par le client par un formulaire.
Variables de session : $_SESSION
Elle contient les valeurs de la session en cours pour le client.
Variables globales : $GLOBALS
Elle stocke les variables globales de la page. Ses clés sont donc variables.
Ci-dessous, un exemple d'utilisation de la variable $GLOBALS
.
Exemple |
<?php $x = 75; $y = 25; function addition() { $GLOBALS['z'] = $GLOBALS['x'] + $GLOBALS['y']; } addition(); echo $z; |
Lorsqu'on exécute ce code, on obtient "100" (=> résultat de la variable 'z' + 'y').
filter_input()
Une bonne pratique consiste à filtrer les contenus des variables superglobales, qui sont vulnérables aux injections de code[1].
Pour des raisons de sécurité, on évitera donc d'appeler directement les tableaux des variables superglobales comme dans les paragraphes précédents, pour leur préférer la syntaxe filter_input()
[2] :
<?php echo filter_input(INPUT_GET, 'password'); // bien echo $_GET['password']; // pas bien
Ne pas confondre avec la fonction filter_var() qui permet d'appliquer des validations ou nettoyages de certaines données[3] (ex : email, URL, adresse IP, booléen...).
Références
Constantes
Tout comme en C, PHP peut utiliser des variables dont on précise la valeur une fois pour toutes et qui ne pourra être modifiée jusqu'à la fin de l'exécution du code.
Nom
Le nom d'une constante suit les mêmes règles que celles pour les variables, mis à part qu'il n'est pas précédé par $. Par convention on les baptise avec des lettres capitales.
// Noms valides CONSTANTE1 CONSTANTE_2 // Noms invalides 0CONSTANTE
Déclaration
Pour déclarer une constante, on utilise la fonction define()
(depuis PHP3) qui renvoie un booléen true
en cas de réussite de déclaration et false
en cas d'échec[1].
La syntaxe de define
est la suivante :
define(chaine_de_caractere, valeur);
La chaîne de caractère est le nom de la constante, pouvant contenir lettre, tiret, underscore et chiffre (la première lettre de la chaîne est une lettre dans [a-zA-Z]).
Utilisation
L'utilisation est semblable à celle des variables.
<?php define("CONSTANTE", "Texte ici"); echo CONSTANTE; // affiche "Texte ici"
Depuis PHP 5, la déclaration de constantes à l'intérieur d'une classe peut se faire avec le mot-clé const de la façon suivante :
<?php class MaClasse { const CONSTANTE = "Texte ici"; function afficher() { echo self::CONSTANTE; } } $instance = new MaClasse; $instance->afficher();
Ces constantes sont publiques par défaut, mais PHP 7.1 introduit la possibilité de les rendre privée avec : private const
.
Informations supplémentaires
Type de données
Une constante, si elle peut être de type booléen, entier, nombre à virgule flottante ou chaîne de caractère, mais en aucun cas un objet
Depuis PHP 7, elle peut aussi être un tableau de scalaires.
<?php // Le code suivant va générer une erreur en PHP 5 (mais pas en PHP 7) define ("CONSTANTE", ["a", "b", "c"]); print_r(CONSTANTE);
Depuis PHP7.4, les nombres entiers peuvent être rendus plus lisibles dans le code en ajoutant des "_" comme séparateurs. Ex :
echo 10_00_000; // affiche 10000000
Unicité de la déclaration
La redéclaration d'une constante portant le même nom est ignorée, et la valeur de la première déclaration reste valable.
<?php define ("CONSTANTE", "première définition"); define ("CONSTANTE", "deuxième définition"); // ignoré echo CONSTANTE; // retournera toujours "première définition"
D'où l'utilisation de defined()
pour déterminer si une constante est déjà définie ou pas[2] :
define("CONSTANTE", "première définition"); if (!defined("CONSTANTE")) { //...
Conflit de nom
Il existe des constantes intégrées à PHP, or on ne peut créer une constante portant leurs noms, cela entraînerait une erreur. En général, évitez la syntaxe suivante pour le nom d'une variable ou d'une constante :
__NOM__
Constantes prédéfinies
Il en existe de deux types[3] :
- fixes en valeur
- dont la valeur est dynamique
Constantes intégrées à valeur fixe
Les constantes suivantes ont des valeurs fixes, comme celles définies avec define
:
TRUE
: vrai (booléen) = 1.FALSE
: faux (booléen) = 0.INF
: l'infinie.PHP_VERSION
: version de PHP du serveur exécutant le script.PHP_OS
: nom du système d'exploitation du serveur exécutant le script.PHP_EOL
: end of line = \n.DIRECTORY_SEPARATOR
: '/' sur Linux et MacOS, '\' sur Windows.
Constantes intégrées à valeur dynamique
Les constantes magiques[4] :
__DIR__
: dossier courant.__NAMESPACE__
: namespace courant.__FILE__
: chemin complet du fichier qui est actuellement exécuté par le serveur (exemple : /la/ou/est/le/fichier.php).__CLASS__
: nom de la classe dans laquelle on se trouve.__TRAIT__
: trait courant.__FUNCTION__
: nom de la fonction dans laquelle on se trouve.__METHOD__
: nom de la méthode dans laquelle on se trouve.__LINE__
: ligne du fichier qui est actuellement exécuté par le serveur.
Exemple d'utilisation :
<?php
echo 'Fichier: ' . __FILE__ . "\n";
echo 'Ligne: ' . __LINE__ . "\n";
class test
{
function foo()
{
echo 'Fonction: ' . __FUNCTION__ . "\n";
echo 'Classe: ' . __CLASS__ . "\n";
}
}
test::foo();
// ou alors
$test = new test();
$test->foo();
Qui affichera :
Fichier: /home/ze/toto.php Ligne: 3 Fonction: foo Classe: test Fonction: foo Classe: test
L'affichage de la première ligne, par exemple, dépend d'où est situé le fichier dans l'arborescence des fichiers.
Références
Opérateurs
Les opérateurs sont des symboles qui permettent de manipuler les variables.
Opérateurs arithmétiques
= Opérateur d'affectation
<?php $un_chiffre = 4; //affectation de l'entier 4 à la variable un_chiffre $un_mot = 'je suis fan des pâtes'; //affectation de la chaîne de caractères à la variable "un_mot"
+ Opérateur d'addition
<?php $mon_premier_chiffre = 3; //affectation du premier chiffre $mon_deuxieme_chiffre = 4; //affectation du deuxième chiffre $resultat = $mon_premier_chiffre + $mon_deuxieme_chiffre; //affectation de l'addition des deux chiffres echo $resultat; //affiche 3+4 soit 7
Si les opérandes sont des tableaux, "+" est l'opérateur union[1].
- Opérateur de soustraction
<?php $mon_premier_chiffre = 3; $mon_deuxieme_chiffre = 4; $resultat = $mon_premier_chiffre - $mon_deuxieme_chiffre; echo $resultat; //affiche 3-4 soit -1
* Opérateur de multiplication
<?php $mon_premier_chiffre = 3; $mon_deuxieme_chiffre = 4; $resultat = $mon_premier_chiffre * $mon_deuxieme_chiffre; echo $resultat; //affiche 3*4 soit 12
** Opérateur d’exponentiation
<?php $mon_premier_chiffre = 2; $mon_deuxieme_chiffre = 3; $resultat = $mon_premier_chiffre ** $mon_deuxieme_chiffre; echo $resultat; //affiche 2^3 soit 8
/ Opérateur de division
<?php $mon_premier_chiffre = 3; $mon_deuxieme_chiffre = 4; $resultat = $mon_premier_chiffre / $mon_deuxieme_chiffre; echo $resultat; //affiche 3/4 soit 0,75
% Opérateur modulo
<?php $mon_premier_chiffre = 3; $mon_deuxieme_chiffre = 4; $resultat = $mon_premier_chiffre % $mon_deuxieme_chiffre; echo $resultat; //affiche 3 modulo 4 soit 3 (le modulo est le reste entier de la division euclidienne)
Il sert à tester si un nombre est le multiple d'un autre :
<?php if ($nb % 3 == 0) { // $nb est un multiple de trois }
Opérateurs logiques booléens
Les opérateurs logiques agissent sur les types booléens (true
ou false
).
- ! : négation. Transforme
false
entrue
ettrue
enfalse
. - && : opérateur "et".
- and : opérateur "et" de moindre priorité.
- || : opérateur "ou"
- or : opérateur "ou" de moindre priorité.
Opérateurs logiques bit à bit
Les opérateurs logiques bit à bit agissent sur chaque bit des valeurs entières.
- ~ Négation :
0
->1
et1
->0
- & Opérateur et (and)
- | Opérateur ou (or)
- ^ Opérateur ou exclusif (xor)
- >> : décalage à droite (division par deux)
- << : décalage à gauche (multiplication par deux).
Opérateurs d'assignation
Ils permettent de simplifier l'écriture des assignations.
- ??= : opérateur de coalescence nul[3]
- += additionne deux valeurs et stocke le résultat dans la variable ($x += 5 équivaut à $x = $x+5)
- -= soustrait deux valeurs et stocke le résultat dans la variable ($x -= 5 équivaut à $x = $x-5)
- *= multiplie deux valeurs et stocke le résultat dans la variable ($x *= 5 équivaut à $x = $x*5)
- /= divise deux valeurs et stocke le résultat dans la variable ($x /= 5 équivaut à $x = $x/5)
- %= donne le reste de la division de deux valeurs et stocke le résultat dans la variable ($x %=5 équivaut à $x = $x%5)
- |= effectue un OU logique entre deux valeurs et stocke le résultat dans la variable ($x |=5 équivaut à $x = $x|5)
- ^= effectue un OU exclusif entre deux valeurs et stocke le résultat dans la variable ($x ^= 5 équivaut à $x = $x^5)
- &= effectue un ET logique entre deux valeurs et stocke le résultat dans la variable ($x &= 5 équivaut à $x = $x&5)
- .= concatène deux chaînes et stocke le résultat dans la variable ($x .= 'test' équivaut à $x = $x.'test')
Opérateurs d'incrémentation
- ++ : incrémente de 1 la valeur de la variable si "$x = 1 ", "$x++" vaut "++$x" qui vaut "2".
- -- : décrémente de 1 la valeur de la variable si "$x = 2", "$x--" vaut "--$x" qui vaut "1".
Ces opérateurs sont très utiles dans les boucles (notamment for).
Opérateurs de comparaison
- == renvoie un booléen pour indiquer l'égalité (ce n'est pas =) : $x == 1 teste si la valeur $x est égale à 1.
- === renvoie un booléen pour indiquer à l'identique, c'est-à-dire l'égalité à la fois de la valeur et du type.
- != (alias <>) renvoie un booléen pour indiquer la différence : $x != 1 teste si la valeur $x est différente de 1.
- !== renvoie un booléen pour indiquer la différence ou de la valeur ou du type.
- > renvoie un booléen pour indiquer la supériorité stricte si $x > 1 teste si la valeur $x est strictement supérieure à 1.
- < renvoie un booléen pour indiquer l'infériorité stricte : $x < 1 teste si la valeur $x est strictement inférieure à 1.
- >= renvoie un booléen pour indiquer la supériorité-égalité si $x >= 1 teste si la valeur $x est supérieure ou égale à 1.
- <= renvoie un booléen pour indiquer l'infériorité-égalité si $x <= 1 teste si la valeur $x est inférieure ou égale à 1.
- <=>[4] renvoie 1 si le premier opérande est supérieur, 0 s'il est égal, et -1 s'il est inférieur au deuxième.
Si les opérandes sont des tableaux :
- "==" est l'opérateur d'égalité (mêmes paires clés/valeurs).
- "===" vérifie aussi le type et l'ordre des paires clés/valeurs.
- "!=" (alias "<>") est l'opérateur d'inégalité.
- "!==" est l'opérateur non identique.
Opérateurs divers
Opérateur ?
L'opérateur ?
permet de renvoyer une valeur ou une autre en fonction d'un test.
??
(depuis PHP 7) [5] (Null Coalescing Operator) opérateur binaire qui renvoie l'opérande qui le précède s'il existe (isset), sinon l'opérande qui le suit.?:
(depuis PHP 7) opérateur ternaire qui renvoie l'opérande qui le précède s'il existe et est non vide (empty), sinon l'opérande qui le suit.
Par exemple :
$appreciation = ($note>10) ? "bon" : "mauvais";
qui est équivalent au bloc PHP suivant :
if ($note > 10) { $appreciation = "bon"; } else { $appreciation = "mauvais"; }
Toutefois il présente un autre avantage que sa concision : la stratégie d'évaluation de cet opérateur ne calcule pas le "else" si le script n'a pas besoin d'y aller. Il devient donc moins gourmand en ressources.
En l'absence de premier résultat, le ternaire renvoie l'expression comparée si non nulle ou vide. Exemple :
print 1 ?: 2; // 1 print 0 ?: 2; // 2 print null ?: 2; // 2
print 'Un est plus ' . (1 > 2 ? ' grand' : ' petit' ). ' que deux.';
-> Opérateur objet
L'opérateur objet -> permet d'accéder aux propriétés d'un objet (variables et méthodes de classe). Exemple :
print $voiture->couleur; $voiture->repeindre('bleu');
Depuis PHP 8 il a une version qui renvoie null si l'objet est null :
$voiture = null; print $voiture?->couleur;
:: Opérateur de résolution de portée
L'opérateur ::, également appelé en PHP Paamayim Nekudotayim, permet d'accéder aux membres statiques ou constants d'une classe. Exemple :
print $voiture::couleur;
@ Opérateur de contrôle d'erreur
L'arobase permet d'ignorer les erreurs de l'expression qui le suit. Ex :
print 1/0; //Warning: Division by zero, puis INF print @(1/0); // INF
& Opérateur de référence
"&" accolé avant une variable désigne sa référence[6], qui est en quelque sorte un pointeur en lecture seule. Elles sont utiles par exemple pour éviter copier un grand tableau en mémoire quand il est en paramètre d'une fonction. Exemple :
public function archive(&$db) { var_dump($db); }
Son utilisation pour les fonctions sera détaillée dans le chapitre sur les fonctions, mais on peut aussi l'utiliser dans les boucles.
... Opérateur de fonction variadique
Depuis PHP 5.6, "..." dans la liste des arguments d'une fonction indique que ceux-ci sont d'un nombre variable.
Exemple :
public function somme(...$nombres) { $somme = 0; foreach ($nombres as $n) { $somme += $n; } return $somme; } echo somme(1, 2, 3); echo somme(1, 2, 3, 4);
Précédence des opérateurs
Quand une opérande est entourée de deux opérateurs (ex : 1 + 2 * 3 = 7), des règles de priorités s'appliquent systématiquement pour résoudre les opérations[7].
Voici du plus au moins prioritaire :
$$ |
$ |
-> |
clone, new |
** |
+ - ++ -- ~ |
instanceof |
! |
* / % |
+ - . |
<< >> |
. |
< <= > >= |
== != === !== <> <=> |
& |
^ |
| |
&& |
|| |
?? |
? : |
= ^= <<= >>= ??= |
yield from |
yield |
and |
xor |
or |
Références
- ↑ https://www.php.net/manual/fr/language.operators.array.php
- ↑ https://www.php.net/manual/fr/function.fmod.php
- ↑ https://www.php.net/manual/fr/language.operators.assignment.php
- ↑ PHP>=7
- ↑ https://wiki.php.net/rfc/isset_ternary
- ↑ http://php.net/manual/fr/language.references.php
- ↑ https://www.php.net/manual/fr/language.operators.precedence.php
Fonctions
Définition de fonction
Outre les dizaines de fonctions natives[1], PHP permet bien entendu d'écrire ses propres fonctions. Pour en définir une, il suffit d’utiliser le mot-clef function
. Comme le langage est faiblement typé, une fonction peut retourner n’importe quel type de valeur (chaîne, entier…) ou ne rien retourner du tout. Enfin, ses arguments peuvent avoir des valeurs par défaut, être nommés (depuis PHP8) et sont limités à 12 par fonction.
// foo retourne le résultat de la somme du deuxième paramètre et de 4. // Si aucun second paramètre n'est donné, la fonction utilisera la valeur 0 par défaut. function foo($arg1, $arg2 = 0) { print 'Fonction foo(' . $arg1 . ',' . $arg2 . ') donne : '; return $arg2+4; // tout ce qui suit ne sera jamais exécuté } //appel à la fonction print foo(1,3); //affichera: Fonction foo(1,3) donne 7 print foo(5); //affichera: Fonction foo(5,0) donne 4
Notez que lorsqu'une fonction arrive à un return
, elle l'effectue puis se termine, même s'il y a d'autres instructions après.
Contrairement à PHP 8, PHP 7 et antérieur ne permet pas d'appeler certains arguments par leurs noms : si l'on souhaite appeler un des derniers arguments, il faut donc définir tous ceux avant lui, même les optionnels. Toutefois pour éviter ce désagrément, on peut utiliser la classe ReflectionMethod
.
NB : il existe aussi ReflectionClass
[2].
Generator
Par ailleurs, en cas de gros volume de données à retourner, on peut utiliser yield
au lieu de return
pour les décomposer à l'aide du générateur PHP[3] (classe Generator).
Ne jamais faire de condition "yield" sinon "return" car PHP renverra toujours un Generator mais si elle est fausse (si besoin il faut les séparer dans deux fonctions).
Portée des variables
Le problème de portée des variables est assez réduit en PHP. Une fonction n'a accès qu’à ses arguments, ses propres variables et aux variables globales importées statiquement (mot clé global
). De ce fait, il y a peu de confusion.
Toujours suivant le même principe, les variables utilisées dans une fonction sont toutes détruites à sa sortie (les variables globales non, bien entendu).
Exemple |
$valeur1=10; $valeur2=20; function exemple($valeur) { global $valeur1; // récupération de la valeur globale de $valeur1 $valeur3=5; $calcul=$valeur1+$valeur2+$valeur3+$valeur; // 10 + 0 + 5 + le paramètre qui sera entre parenthèses. //$valeur2 n'ayant pas été définie comme valeur globale, la variable $valeur2 est donc vide. return $calcul; } echo exemple(2); // affiche 17 |
On peut aussi trouver un peu plus compliqué si vous utilisez deux fichiers. L'un pour les variables, l'autre pour les traitements.
Espaces de noms
Depuis PHP 5.3.0, des espaces de noms peuvent être définis pour cloisonner certains objets, à l'aide du mot clé namespace
utilisé en début de fichier[4]. Exemple de définition :
<?php namespace Projet1; ...
Soit un fichier TestNS.php suivant :
<?php namespace Projet1\SousProjet2; function Fonction1() { echo "Fonction exécutée.\n"; echo __NAMESPACE__; }
Le fichier doit être encodé en ANSI ou Unicode sans BOM, mais pas en Unicode seul, sous peine d’avoir l'erreur Namespace declaration statement has to be the very first statement in the script, et d'être obligé de coller le mot "namespace" à "?php").
Exemple d’utilisation :
<?php require 'TestNS.php'; Projet1\SousProjet2\Fonction1();
require 'Projet1\SousProjet2\TestNS.php';
Références de variables
Les références sont utiles lorsque l’on souhaite retourner plusieurs valeurs dans une fonction. On utilise alors le passage d'argument par référence, qui fait que quand une fonction modifie un argument, la valeur de la variable du programme principale change aussi.
Pour utiliser un argument en tant que référence, il suffit d'y mettre l'opérateur de référence, le symbole &
devant, dans la déclaration de la fonction.
Un exemple concret devrait vous faire comprendre :
Exemple |
function foo(&$arg1, &$arg2, $arg3) { $arg1 = 4; $arg2 = 6; $arg3 = 8; } foo($var1, $var2, $var3); print $var1; //affichera 4 print $var2; //affichera 6 print $var3; //affichera NULL car $arg3 n'est pas une référence (pas de &) |
Fonctions variables
On appelle "fonction variable" une fonction dont on ne peut prédire le nombre d'arguments. Ce genre de fonction pourra se révéler pratique pour exécuter certains codes répétitifs ou le programmeur n'a pas envie de recopier le nom de la fonction pour n valeurs.
Pour cela, il faut retenir deux fonctions importantes :
- func_num_args : permet de compter le nombre d'arguments (retourne true ou false).
- func_get_args : permet de récupérer la valeur d’un argument (retourne un tableau de valeur)[5].
Ces deux fonctions ne peuvent s'utiliser qu’à l'intérieur d’une fonction; dans le cas contraire un message d'erreur s'affichera.
Exemple |
function afficher_variables () { $nb_args = func_num_args(); $list_args = func_get_args(); for ($i = 0; $i < $nb_args; $i++) { echo $list_args[$i].' '; } } $var1 = 'programmeur'; afficher_variables('Je suis', $var1, ', c\'est utile', ', c\'est intéressant.'); // Et on peut en rajouter autant que nécessaire. |
Le code se comprend de lui-même. Il affichera : Je suis programmeur, c’est utile, c’est intéressant.
Fonctions de rappel
Pour passer une fonction en argument d'une autre, il faut utiliser call_user_func()
. Ex :
function fonctionAppelante(string $nomDeFonctionAppelee) { return call_user_func($nomDeFonctionAppelee); }
Fonctions anonymes
Une fonction sans nom peut être stockée dans une variable permettant de l'appeler. Le mot clé use
permet d'injecter des variables globales dedans. Ex :
$variableGlobale = 'Hello'; $fonctionAnonyme = function ($argument) use ($variableGlobale) { return $variableGlobale.' '.$argument.PHP_EOL; }; echo $fonctionAnonyme('World'); // Hello World echo $fonctionAnonyme('You'); // Hello You
Fonctions fléchées
Depuis PHP7.4, on peut définir une fonction anonyme avec l'opérateur "=>" et le mot réservé fn
. Ex :
$fonctionFlechee = fn($argument) => $variableGlobale.' '.$argument.PHP_EOL; echo $fonctionFlechee('World'); // Hello World echo $fonctionFlechee('You'); // Hello You
Fonctions prédéfinies
Voici une liste des fonctions globales prédéfinies en PHP les plus usuelles.
print()
(aliasecho
) : affiche le contenu de la variable placée en paramètre.printf()
et dérivés (ex :sprintf
) : print formaté, affiche un texte dont on remplace les marqueurs, numérotables, par les variables en paramètre[6]. Exemple :
printf('Compteur : %s. ', 1); // Compteur : 1. printf('Compteur : %s. ', 2); // Compteur : 2. printf('Compteur : %s, taille : %s. ', 3, 1); // Compteur : 3, taille : 1. printf('Compteur : %2$s, taille : %1$d. ', 1, 4); // Compteur : 4, taille : 1. printf('Compteur : %1$d, taille : %1$d. ', 5); // Compteur : 5, taille : 5. echo vsprintf('Tailles : %d, %d, %d, %d', [1, 2, 3, 4]); // Tailles : 1, 2, 3, 4
Les types suivants sont généralement utilisés :
- d : digit
- f : float
- s : string
- Pour échapper les "%" pour sont interprétés par ces fonctions, ce n'est pas "\" mais "%".
- Ne pas utiliser les doubles quotes sous peine de
Undefined variable: d
.
sleep($secondes)
: attend un certain nombre de secondes (utilisation déconseillée quand un humain doit attendre un résultat).call_user_func_array('maFonction', 'mesArguments')
: exécute une fonction à partir de son nom en chaîne de caractères.getenv()
: affiche toutes les variables d'environnement, ou celle demandée en paramètre.ignore_user_abort(true)
: ("false" par défaut) continue l'exécution d'un script lancé par un utilisateur, même s'il change de page ou clique annule le chargement.header()
: ajoute une clé dans l'en-tête HTTP de la réponse.ini_get()
: lit le php.ini.ini_set()
: modifie le php.ini le temps de l'instance.set_time_limit()
: redéfinit la limite de temps du php.ini.require()
: importe le code PHP d'un autre fichier, avec une erreur si c'est impossible[7].require()_once
: idem en garantissant que chaque fichier n'est ajouté qu'une fois.include()
: importe le code PHP d'un autre fichier, sans erreur si c'est impossible.include_once()
: idem en garantissant que chaque fichier n'est ajouté qu'une fois.getmypid()
: retourne l'ID du processus courant dans l'OS.
Sur les chaînes
trim($string)
: supprimer les espaces et retours chariots en début et fin de chaîne par défaut. Son second paramètre permet de remplacer les symboles à retirer.strip_tags()
: supprime les balises HTML de la chaîne mentionnée en paramètre 1, en conservant ceux en paramètre 2 (sous la forme d'une chaine comme '<p><br>').ucfirst()
: met une majuscule en début de chaîne.lcfirst()
: met une minuscule en début de chaîne.strtoupper()
: met en lettres capitales toute la chaîne.strtolower()
: met en bas de casse toute la chaîne.strlen()
: compte la taille d'une chaîne en octets. Pour avoir le nombre de caractères, utilisermb_strlen()
(pour multibyte).str_contains($chaine, $cle)
: cherche si une chaîne contient une sous-chaîne.- <codestr_starts_with($chaine, $cle) : vérifie si une chaîne commence par une sous-chaîne.
str_replace($ancien, $nouveau, $texte)
: remplace des caractères par d'autres dans un texte ou un tableau[8].str_ireplace($ancien, $nouveau, $texte)
: fait la même chose en ignorant la casse.strtr($texte, $ancien, $nouveau)
(string translate) : réputée plus rapide questr_replace
[9].strpos($meubleDeFoin, $aiguille)
[10] : première position d'une sous-chaine. Attention : ne jamais utiliser comme si elle renvoyait un booléen () car si la recherche est en première position (0) elle sera considérée comme fausse avec le typage faible. Il faut tester l'existence avecif (strpos())
if (false !== strpos())
.stripos($meubleDeFoin, $aiguille)
: fait la même chose en ignorant la casse.strrpos($meubleDeFoin, $aiguille)
: dernière position d'une sous-chaine.substr($texte, $debut , $fin)
: tronque un texte en sous-chaine. Utilisermb_substr
en Unicode.- Pour accéder à un seul caractère d'une chaine, PHP peut la considérer comme un tableau (ex :
$chaine[0]
). - Ex :
echo substr('Hello World', 0, 2); // "He" : les deux premiers caractères echo substr('Hello World', -2); // "ld" : les deux derniers caractères echo substr('Hello World', 0, -1); // "Hello Worl" : tout sauf le dernier caractère
substr_count($chaine, $sous-chaine)
: compte le nombre de sous-chaine dans une chaine.str_pad()
: complète une chaine avec un caractère pour qu'elle atteigne la taille demandée si ce n'était pas le cas. Ex :
echo str_pad(10, 2, '0'); // 10 echo str_pad(9, 2, '0'); // 90 echo str_pad(9, 2, '0', STR_PAD_LEFT); // 09
eval($chaine)
: exécute une chaine comme un script PHP.count_chars($chaine)
: renvoie un tableau avec en clés chaque caractère de la chaine, et en valeur son nombre d'occurrences. Pratique pour détecter les anagrammes.strcspn($chaine, $caracteres)
: (complementary span) renvoie la taille de la sous-chaine située avant les caractères mentionnés[11].
Encodage
- Pour plus de détails voir : Coder avec Unicode/Conversion#PHP.
utf8_encode($chaine)
etutf8_decode($chaine)
sont dépréciée en PHP 8.2, il faut utiliser à la placemb_convert_encoding($chaine, 'UTF-8')
etmb_convert_encoding($chaine, 'ISO-8859-1')
.
URLs
parse_url()
: découpe une chaîne en partie d'une adresse URL (protocole, domaine et chemin d'accès).http_build_query()
: crée une URL à partir d'un tableau d'arguments GET. Par défaut le séparateur est "&" (3e argument). Cette fonction échappe les séparateurs. Ex :
php -r "var_dump(http_build_query(['x' => 1, 'y' => 2]));" string(7) "x=1&y=2" php -r "var_dump(http_build_query(['x' => '1/', 'y' => '2&'], '/', '/'));" string(13) "x=1%2F/y=2%26"
Sur les nombres
pow($nombre, $exposant)
(power) : élève un nombre à la puissance de l'exposant donné.sqrt($nombre)
(square root) : racine carrée.max($nombre1, $nombre2, ...)
[12] : affiche le plus grand nombre parmi ceux en paramètres.min($nombre1, $nombre2, ...)
: affiche le plus petit nombre d'une liste.round($nombre)
: arrondit un nombre à l'entier le plus proche, ou selon une précision en deuxième paramètre s'il est renseigné[13]. Exemple :
echo round(5.49); // 5 echo round(5.50); // 6 echo round(5.555, 2); // 5.56
number_format($nombre, 2, ',', ' ')
: formate un nombre donné, où "2" représente le nombre de chiffres après la virgule, "," le séparateur décimal et " " le séparateur de milliers.
Ne jamais comparer des nombres issus du number_format()
car les séparateurs faussent les calculs. Exemple :
-1000.00 < -1000.02 // false
-1,000.00 < -1,000.02 // -1
Bufferisation de sortie
La bufferisation de sortie bloque l'envoie de données au client HTTP pour les mettre dans une mémoire tampon à la place[14].
ob_start()
: démarre l'utilisation du tampon.ob_get_contents
: affiche le contenu du tampon.ob_clean()
: efface le tampon sans l'envoyer.ob_flush()
: envoie le tampon au client.
Références
- ↑ http://php.net/manual/fr/indexes.functions.php
- ↑ http://php.net/manual/fr/reflectionclass.getdoccomment.php
- ↑ http://php.net/manual/fr/language.generators.syntax.php
- ↑ http://php.net/manual/fr/language.namespaces.rationale.php
- ↑ http://php.net/manual/fr/ref.funchand.php
- ↑ http://php.net/manual/fr/function.sprintf.php
- ↑ https://www.alsacreations.com/article/lire/254-le-point-sur-la-fonction-include-php.html
- ↑ http://php.net/manual/fr/function.str-replace.php
- ↑ https://www.keycdn.com/blog/php-performance#10-use-the-strongest-str-functions
- ↑ http://php.net/manual/fr/function.strpos.php
- ↑ https://www.php.net/manual/fr/function.strcspn.php
- ↑ http://php.net/manual/fr/function.max.php
- ↑ http://php.net/manual/fr/function.round.php
- ↑ https://www.php.net/manual/fr/ref.outcontrol.php
Tableaux
Création de tableau
Un tableau (en anglais array
) est une collection d'objet. En PHP, ces objets n'ont pas forcément le même type (cohabitation entre des entiers, des chaines…). Chaque objet est identifié par une clé appelée indice, que l'on met entre crochets (ex : $tableau[indice]
).
Il existe trois manières de déclarer un tableau vide :
$tab = []; // depuis PHP 5.4 $tab = {}; // moins permissif aux concaténations $tab = array(); // déconseillé depuis PHP 7
Pour créer un tableau non vide :
$t1 = array('champ1', 'champ2');
$t2 = ['champ1', 'champ2'];
$t3[0] = 'champ1';
$t3[1] = 'champ2';
// Affiche les trois mêmes tableaux : Array ( [0] => champ1 [1] => champ2 )
var_dump($t1);
var_dump($t2);
var_dump($t3);
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;"><code>print</code> ne fonctionne pas pour les tableaux, il faut utiliser <code>var_dump</code> ou <code>print_r</code>. Par ailleurs, pour récupérer la chaine affichée par ces fonctions, utiliser <code>print_r(MonTableau1, true)</code>.</p>
</div>
</div>
Autres exemples :
<pre>
$tab[0] = 1; // entier
$tab[1] = 2.0; // flottant
array_push($tab,'Ligne 3');
$tab[] = 'Ligne 4';
var_dump($tab);
</pre>
Il en est de même pour les tableaux à deux dimensions.
<pre>
$tab = [];
$tab[0][0] = '0-0';
$tab[0][1] = '0-1';
$tab[1][0] = '1-0';
$tab[1][1] = '1-1';
var_dump($tab);
</pre>
On distingue deux types de tableau :
* Le tableau standard, dont la clé est son indice (le numéro de ligne en partant de zéro). Pratique pour être parcouru par une variable compteur, ou pour être rempli dans un certain ordre.
* Le [[w:tableau associatif|tableau associatif]], auquel on accède par le nom d'une clé en chaine de caractères.
== Tableau itératifs ==
Les clés du tableaux sont des nombres. Ils ont l'avantage de pouvoir être parcourus par un compteur.
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple</div>
|-
|<pre>
$tab = ['val1', 'val2', 'val3']; // $tab[0] vaut val1 /-/ $tab[1] vaut val2 /-/ etc.
for($i = 0; $i<2; $i++)
echo $tab[$i];
</pre>
|}
Ce code affichera : ''val1val2''.
En PHP, on peut aussi directement affecter des indices du tableau, comme suit :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:50%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
$tab[0] = 1;
$tab[99] = 3;
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
Notez que les indices ne sont pas typés (on pourra indifféremment utiliser $tab[1] et $tab['1']).
== Tableaux associatifs ==
Ils fonctionnent de la même manière que les tableaux itératifs, sauf que l'utilisateur en choisit la clé. À chaque clé correspond une valeur (injection).
Voici un exemple de déclaration :
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple</div>
|-
|<pre>
$tab = ['cle1' => 'val1', 'cle2' => 'val2', 'cle3' => 'val3'];
print $tab['cle2']; //affichera : val2
//parcours du tableau en boucle
foreach ($tab as $key => $value)
print $key." : ".$value.". ";
</pre>
|}
Résultat : <code>cle1 : val1. cle2 : val2. cle3 : val3.</code>
Pour ne garder que les valeurs on peut utiliser <code>implode()</code>, qui convertit un tableau en chaine avec séparateur :
<pre>
print implode(". ", $tab).". ";
</pre>
Résultat : <code>val1. val2. val3.</code>
== Fonctions de lecture ==
* <code>count</code> : cette fonction renvoie le nombre d'éléments présent dans le tableau.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
$tab = [1, 2, 3, 4];
print count($tab); //affiche 4
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
* <code>key</code> : clé de l'élément courant du tableau, celui vers lequel le pointeur fait référence.
* <code>current</code> : valeur de l'élément courant.
* <code>reset</code> : valeur du premier élément.
* <code>end</code> : valeur du dernier élément.
* <code>each</code> : valeur de l'élément courant, et avance le pointeur au suivant.
* <code>prev</code> : valeur de l'élément précédent.
* <code>next</code> : valeur de l'élément suivant.
* <code>array_values($tab)</code> : renvoie un tableau contenant toutes les valeurs du tableau en paramètre. S'utilise pour reconstruire des clés consécutives sans changer les valeurs.
* <code>array_keys($botteDeFoin, $aiguille)</code> : renvoie un tableau contenant toutes les clés du tableau en paramètre. De plus, si une valeur est définie en paramètre deux, le résultat ne contient que les clés associées à celle-ci.
* <code>array_key_exists($cle, $tab)</code> : renvoie "vrai" si la clé est dans le tableau.
* <code>array_key_first($tab)</code> : renvoie la première clé d'un tableau.
* <code>array_key_last($tab)</code> : renvoie la dernière clé d'un tableau.
* <code>array_diff($t1, $t2)</code> : renvoie le tableau des différences entre ceux en paramètres (peut servir pour supprimer par valeur).
* <code>array_sum($tab)</code> : renvoie la somme des valeurs du tableau.
* <code>array_intersect($t1, $t2)</code> : intersection entre plusieurs tableaux.
=== Exemple ===
<pre>
$tab = ["mixte valeur<sub>1</sub>","mixte valeur<sub>2</sub>","...","mixte valeur<sub>n</sub>"];
echo key($tab);
echo ' : ';
echo current($tab);
</pre>
Affiche ''0 : mixte valeur1''
Les fonctions <code>key()</code> et <code>current()</code> peuvent accéder aux autres éléments du tableau après <code>each()</code> ou <code>next()</code>.
Il existe aussi différentes méthodes liées aux tableaux, des méthodes de tri, de recherche, de concaténation de tableaux, des méthodes d'ajouts et de suppressions d'éléments, etc.
=== Recherches ===
* <code>in_array($aiguille, $botteDeFoin)</code> : recherche de présence par valeur. Renvoie un booléen si l'élément est trouvé.
* <code>array_search($aiguille, $botteDeFoin)</code> : recherche de position par valeur. Renvoie la clé de l'élément trouvé, ou false sinon.
* <code>array_keys($botteDeFoin, $aiguille)</code> : recherche par clé (déjà décrit au paragraphe précédent).
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;"><code>php -r "var_dump(in_array(0, ['test']));"</code> = true</p>
</div>
</div>
=== Comparaison ===
Pour comparer deux tableaux :
<pre>
$a1 == $a2; // compare le contenu et la taille
$a1 === $a2; // compare le contenu, la taille et l'index
</pre>
=== Condition ===
Un tableau vide dans une condition vaudra "faux", alors qu'un non vide vaudra "true".
== Fonctions d'écriture ==
Pour manipuler des tableaux il est indispensable de connaitre les fonctions suivantes :
* <code>str_split($string)</code> : convertit une chaine de caractères en tableau itératif, chaque ligne étant composée d'un caractère.
* <code>mb_str_split($string)</code> : idem mais en caractères multi-octets.
* <code>explode($separateur, $tableau)</code> : convertit une chaine de caractères en tableau itératif, donc le contenu correspond aux sous-chaines situées autour d'un séparateur donné.
* <code>implode($separateur, $tableau)</code> : convertit un tableau en chaine de caractères. Le séparateur à placer dans la chaine est facultatif.
* <code>sizeof($tableau)</code> : renvoie la taille du tableau (le nombre d'objets qu'il contient). Attention : avant PHP 7.2 cette fonction pouvait aussi remplacer <code>strlen()</code>.
* <code>array_push($monTableau, $valeur)</code> : ajoute une ligne à la fin du tableau, équivaut à <code>$monTableau[]</code><ref>http://php.net/manual/fr/function.array-push.php</ref> (empile).
* <code>array_unshift($monTableau, $valeur)</code> : ajoute une ligne au début du tableau</code><ref>http://php.net/manual/fr/function.array-unshift.php</ref>.
* <code>array_pop($monTableau)</code> : retire la dernière ligne du tableau, en la renvoyant<ref>http://php.net/manual/fr/function.array-pop.php</ref> (dépile).
* <code>array_shift($monTableau)</code> : retire la première ligne du tableau, en la renvoyant<ref>http://php.net/manual/fr/function.array-shift.php</ref>.
* <code>array_merge($monTableau1, $monTableau2, $monTableau3...)</code> : fusionne plusieurs tableaux<ref>http://php.net/manual/fr/function.array-merge.php</ref>.
* <code>array_merge_recursive()</code> : idem en multidimensionnel.
* <code>array_replace($monTableau1, $monTableau2, $monTableau3...)</code> : fusionne plusieurs tableaux en replaçant les clés existantes du premier par celles des autres. Cela permet par exemple de fusionner deux tableaux en préservant leurs clés.
* <code>array_replace_recursive()</code> : idem en multidimensionnel.
* <code>array_unique($tableau)</code> : filtre les valeurs en doublon (quelles que soient leurs clés).
* <code>array_filter($tableau, fonction)</code> : filtre les lignes selon une fonction exécutée sur chaque élément. Pour injecter des variables dans la fonction, utiliser "use" (ex : <code>array_filter($tableau, function($ligne) use($variable1){</code>...).
* <code>array_column($tableau, colonne)</code> : filtre par colonne. Renvoie uniquement les valeurs d'un champ donné pour chaque élément.
* <code>array_reduce($tableau, fonction)</code> : transforme le tableau selon une fonction exécutée sur chaque élément.
* <code>array_map(fonction, $tableau)</code> exécute une fonction sur chaque valeur du tableau<ref>http://php.net/manual/fr/function.array-map.php</ref>. Exemples :
** Pour trimer chaque ligne d'un tableau : <code>array_map('trim', $tableau)</code>
** Pour créer un tableau de tableau : <syntaxhighlight lang=php>
array_map(function($ligne) {
return explode('=', $ligne);
}
, $tableau)
</pre>
* <code>array_walk($tableau, fonction)</code> : exécute une fonction sur chaque élément (clé ou valeur).
* <code>array_chunk($tableau, $taille)</code> : découpe le tableau fourni en tableaux de la taille fournie.
* <code>array_slice($tableau, $début, $taille)</code> : renvoie la partie du tableau à partir de l'élément dont le numéro est le premier paramètre, de la taille en paramètre deux.
* <code>array_flip($tableau)</code> : inverse les clés et valeurs. Attention : un tableau ne peut avoir que des types primitifs en clé, pas des objets (sinon c'est l'erreur ''Illegal offset type'').
* <code>unset($tableau[$index])</code> : supprimer la ligne.
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple</div>
|-
|<pre>
$chaine = 'MonFichier.2016.txt';
$tab = explode('.', $chaine); // au niveau des points, on explose la chaine (en trois)
var_dump($tab); /* affiche :
array(3) {
[0]=> string(10) "MonFichier"
[1]=> string(4) "2016"
[2]=> string(3) "txt"
}
*/
echo $tab[0]; // affiche : MonFichier
echo 'L\'extension du fichier est : '.$tab[sizeof($tab)-1]; // affiche : L'extension du fichier est : txt
</pre>
|}
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Comme le premier indice du tableau est zéro, le dernier est égal à sa taille moins un.</p>
</div>
</div>
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Joindre les éléments "id" d'un tableau de tableaux</div>
|-
|<pre>
echo implode(', ', array_map(function ($ligne) {
return $ligne['id'];
}, $tableau));
</pre>
|}
=== Tris ===
* <code>array_multisort($tableau, SORT_ASC)</code> permet de trier un tableau dans l'ordre croissant de ses valeurs.
* <code>sort($tableau)</code> : trie le tableau par valeurs croissantes, en recréant des clés numériques.
* <code>asort($tableau)</code> : trie le tableau par valeurs croissantes, en conservant les clés associées.
* <code>arsort, fonction)</code> : trie le tableau par valeurs décroissantes, en conservant les clés associées.
* <code>ksort($tableau)</code> : trie par clés croissantes par défaut.
* <code>krsort($tableau)</code> : trie par clés décroissantes par défaut.
* <code>usort($tableau, fonction)</code> : trie selon une fonction donnée<ref>http://php.net/manual/fr/function.usort.php</ref>.
<div class="remarque"> Les tris croissants définis par défaut sont modifiables par des flags en deuxième paramètre.</div>
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple</div>
|-
|<pre>
$array = array("name"=>"Toyota", "type"=>"Celica", "colour"=>"black", "manufactured"=>"1991");
array_multisort($array, SORT_ASC);
var_dump($array);
// array(4) { ["manufactured"]=> string(4) "1991" ["type"]=> string(6) "Celica" ["name"]=> string(6) "Toyota" ["colour"]=> string(5) "black" }
// On remarque que les majuscules sont avant les minuscules.
arsort($array);
var_dump($array);
// array(4) { ["colour"]=> string(5) "black" ["name"]=> string(6) "Toyota" ["type"]=> string(6) "Celica" ["manufactured"]=> string(4) "1991" }
asort($array);
var_dump($array);
// array(4) { ["manufactured"]=> string(4) "1991" ["type"]=> string(6) "Celica" ["name"]=> string(6) "Toyota" ["colour"]=> string(5) "black" }
sort($array);
var_dump($array);
// array(4) { [0]=> string(4) "1991" [1]=> string(6) "Celica" [2]=> string(6) "Toyota" [3]=> string(5) "black" }
ksort($array);
var_dump($array);
// array(4) { ["colour"]=> string(5) "black" ["manufactured"]=> string(4) "1991" ["name"]=> string(6) "Toyota" ["type"]=> string(6) "Celica" }
</pre>
|}
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Pour trier de l'Unicode il faut utiliser le flag "SORT_LOCALE_STRING".</p>
</div>
</div>
Exemple :
<pre>
$array = ['à', 'i', 'o', 'u', 'é'];
sort($array);
print_r($array);
setlocale(LC_COLLATE, 'fr'); // parfois 'fr_FR.UTF-8'
sort($array, SORT_LOCALE_STRING);
print_r($array);
</pre>
donne :
Array
(
[0] => i
[1] => o
[2] => u
[3] => à
[4] => é
)
Array
(
[0] => à
[1] => é
[2] => i
[3] => o
[4] => u
)
== Tableaux multi-dimensionnels ==
La clé d’un tableau peut pointer sur un second tableau créant ainsi un tableau multi-dimensionnel.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
$indiv[] = [
'nom' => 'Hubert',
'poste' => 'Gérant',
'Email' => 'hubert@example.com',
'idBureau' => 1,
];
$indiv[] = [
'nom' => 'Jean',
'poste' => 'Réceptionniste',
'Email' => 'reception@example.com',
'idBureau' => 1,
];
$indiv[] = [
'nom' => 'Amélie',
'poste' => 'Président',
'Email' => 'contact@example2.com',
'idBureau' => 2,
];
$affBureau = 1;
foreach ($indiv as $no => $data) {
if ($data['idBureau'] == $affBureau) {
echo $no .'-'. $data['nom'] .' <i>'. $data['poste'] .'</i> : '. $data['Email'] .'<br />';
}
}
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
Résultat :
: 0-Hubert ''Gérant'' : hubert@example.com
: 1-Jean ''Réceptionniste'' : reception@example.com
: '''NB :''' Dans cet exemple, une base de données serait sûrement plus adéquate.
== ArrayAccess ==
Cette classe native permet de redéfinir l'opérateur d'index (<code>[]</code>) dans les objets qui en héritent. Par exemple, pour lui faire accepter un autre tableau ou NULL comme index, ou déclencher un évènement quand on le modifie<ref>http://php.net/manual/fr/class.arrayaccess.php</ref>.
== Destructuration ==
PHP ne gère pas la destructuration avec l'opérateur égal comme le fait JavaScript ou Python (ex : <code>x, y = getXY()</code>). À la place, il propose plusieurs fonctions :
* <code>list($x, $y)</code> : traite les variables en paramètre comme un tableau<ref>http://php.net/manual/fr/function.list.php</ref>. Ex : <code>list($x, $y) = getArrayWithTwoLines();</code>
* <code>compact($x, $y)</code> : crée un tableau avec les noms des variables en paramètre comme clés, et leurs valeurs comme valeurs<ref>http://php.net/manual/fr/function.compact.php</ref>.
* <code>extract($tableau)</code> : déclare et assigne une variable pour chaque ligne du tableau en paramètre, de nom la clé de la ligne, et de valeur la valeur de la ligne<ref>http://php.net/manual/fr/function.extract.php</ref> (contraire de <code>compact()</code>).
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Structures de contrôle=
Une structure conditionnelle fonctionne de manière à ce que si la valeur de la condition est <code>true</code> alors tel schéma est appliqué, et si la valeur est <code>false</code>, un autre schéma est réalisé.
== if - else ==
La structure la plus simple se présente sous la forme d'un <code>if() {}</code> et d'un <code>else {}</code>. Le <code>if</code> teste une condition :
*Si elle est réalisée le code entre les accolades après le if sera exécuté, puis le serveur passera au code après le <code>else {}</code> ;
*Si elle n'est pas réalisée c'est ce qui est entre les accolades suivant le <code>else</code> qui sera exécuté.
<pre>
<?php
if (condition) {
instruction au cas où la condition serait réalisée;
} else {
instruction au cas où la condition ne serait pas réalisée;
}
</pre>
=== Emploi de if seul ===
Un <code>if</code> peut être employé seul, en fait le <code>else</code> étant l'alternative, le code à exécuter par défaut, on peut s'en passer pour n'exécuter un code seulement si une condition est réalisée.
<pre>
<?php
if (condition) {
instruction au cas où la condition est réalisée;
}
//si la condition n'est pas réalisée, il ne se passe rien
</pre>
=== Emploi de elseif ===
Lorsqu'une condition n'est pas réalisée, plutôt que de passer directement à l'exécution du code par défaut (déclaré par <code>else</code>), il peut être plus judicieux de tester une autre condition (déclarée par <code>elseif</code>). En clair la structure est semblable à un ordre de préférence :
{|align="center" rules="all" cellspacing="0" cellpadding="4" style="border: 1px solid #999; border-right: 2px solid #999; border-bottom: 2px solid #999; background-color: #3333ff40"
|+Emploi de <code>elseif</code>
|-style="background-color: #3333ff40"
!align="center" | Symboliquement !!align="center" | Traduction en code
|-
|<pre>
Soit une condition 1
La condition 1 est elle réalisée ?
OUI
code 1 (puis sortie de la structure hypothétique)
NON
la condition 2 est elle réalisée ?
OUI
code 2 (puis sortie de la structure hypothétique)
NON
la condition 3 est elle réalisée ?
OUI
code 3 (puis sortie de la structure hypothétique)
NON
...
(aucune des conditions annexes n'ont été vérifiées)
Par défaut exécutons le code défaut
</pre> || <pre>
<?php
if (condition 1) {
code 1;
} elseif(condition 2) {
code 2;
} elseif(condition 3) {
code 3;
} else {
code par défaut;
}
</pre>
|}
=== Imbrication ===
On peut imbriquer les <code>if</code> les uns dans les autres. Simplement lorsqu'un <code>if</code> imbriqué aura fini d'être exécuté, il retournera à l'étape logique suivante du rang hiérarchique supérieur.
<pre>
<?php
if() {
if() { }
elseif() { }
else { }
}
elseif() {}
else {}
</pre>
Notons que dans ce cas mieux vaut imaginer tous les cas de figure pour ne pas se retrouver avec une structure hypothétique vacillante en ce que la situation n'aura pas été prévue.
== switch ==
Lorsque l'on teste des conditions en nombre important sur une même valeur, l'utilisation de <code>if</code> est fastidieuse. Il existe bien heureusement une structure créée à cet usage : le <code>switch</code>. On déclare la variable à tester avec <code>switch</code> : <code>switch($surlasellette) {}</code>. Dans ce <code>switch</code> on utilise <code>case</code> pour déclarer la valeur de la variable pour laquelle une action est envisagée : <code>case "valeur":</code> (ici ne pas oublier les deux points !) une suite d'instructions s'ensuit et est généralement clôturée par <code>break;</code> pour que les autres cas ne soient pas traités à la suite. La valeur par défaut, corollaire du <code>else</code> pour <code>if</code>, est introduite par <code>default:</code>.
{|align="center" rules="all" cellspacing="0" cellpadding="4" style="border: 1px solid #999; border-right: 2px solid #999; border-bottom: 2px solid #999; background-color: #3333ff40"
|+Intérêt et emploi de <code>switch</code>
|-style="background-color: #3333ff40"
!align="center" | Avec if !!align="center" | Traduction en switch
|-
|<pre>
<?php
$valeur = "testable";
if ($valeur == "ce n'est pas cela") {
echo "ok";
} elseif ($valeur == "ce n'est pas non plus cela") {
echo "ok";
} elseif($valeur == "c'est encore moins cela" or $valeur == "ou cela") {
echo "ok";
} else {
echo "pas d'accord";
}
</pre> || <pre>
<?php
switch (variable) {
case valeur_1:
instruction(s);
break;
case valeur_2:
instruction(s);
break;
case valeur_3:
case valeur_4:
instruction(s);
break;
default:
instruction(s);
}
</pre>
|}
<code>switch</code> s'imbrique aussi comme <code>if</code>.
Le plus important étant de <span style="color:#FF0000;">ne pas oublier de mettre l'instruction <code>break</code> avant un nouveau <code>case</code></span>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Le switch compare avec "==" et pas "===".</p>
</div>
</div>
== match ==
Depuis PHP8, cette structure retourne une valeur selon plusieurs conditions<ref>https://www.php.net/manual/fr/control-structures.match.php</ref>. Ex :
<pre>
echo match ($variable) {
'clé_1' => 'valeur 1',
'clé_2' => 'valeur 2',
default => 'unknown value',
};
</pre>
== Syntaxe alternative ==
PHP propose une syntaxe alternative aux accolades pour ses structures de contrôle, qui sont le ":" puis "end_nom_de_la_structure;"<ref>https://www.php.net/manual/fr/control-structures.alternative-syntax.php</ref>. Ex :
<pre>
if ($a == $b):
echo "Test";
endif;
</pre>
C'est notamment pratique si vous utilisez php comme moteur de templating
<pre>
<?php if ($a == $b): ?>
<p>Ici un paragraphe</p>
<?php else if ($a > $b): ?>
<p>Paragraphe alternatif</p>
<?php else: ?>
<p>Paragraphe si rien ne correspond</p>
<?php endif; ?>
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Boucles=
== Concept ==
Une boucle est une instruction qui exécute un code tant qu'une condition établie est vérifiée. Si la condition est toujours vérifiée, on se trouve dans une ''boucle infinie''.
Les boucles permettent le parcours des tableaux et d'utiliser des données rendues sous la forme de tableau par une fonction de php dialoguant par exemple avec un autre langage.
Il en existe de deux types (avec des variantes) :
# <code>while()</code> : le programme se répète tant qu'une condition est vraie (ex : tant que x est inférieur à 10).
# <code>for()</code> : le programme se répète un certain nombre de fois (ex : pour x allant de 1 à 10 avec un pas de +1 à chaque itération).
== while ==
<code>while</code> est un mot anglais signifiant "tant que" en français. Le programme exécute une routine tant que la condition est vraie.
<pre>
while (condition) {
instructions(s);
}
</pre>
Deuxième syntaxe (moins courante) :
<pre>
while (condition):
instructions(s);
endwhile;
</pre>
Troisième syntaxe (assez rare), quand la condition contient une méthode qui change à chaque itération, jusqu'à renvoyer faux :
<pre>
while (condition);
</pre>
=== do while ===
Idem avec premier passage obligatoire : la condition est vérifié en fin de bloc.
<pre>
do {
instructions(s);
} while (condition);
</pre>
== for ==
</code>for</code> est un mot anglais signifiant "pour" en français. Le programme exécute une routine pour des valeurs d'une variable qui vérifient une certaine condition. Généralement cette condition est de type "intervalle", c'est-à-dire pour des valeurs plus petites qu'une borne.
<pre>
for (première expression ; condition d'arrêt ; itération) {
instruction;
}
</pre>
* Le premier élément est exécuté au début de la boucle dans tous les cas.
* Le second élément (la condition) est testé avant chaque exécution de l'instruction ou itération, s'il renvoie <code>TRUE</code> l'instruction sera exécutée, si <code>FALSE</code> est renvoyé on sort de la boucle.
* La dernière expression est exécutée après chaque itération.
Attention la structure dans la parenthèse est <code>for(<span style="color:#FF0000; font-weight:bold;"> ; ; </span>)</code>
=== foreach ===
Les boucles <code>foreach</code> apparues avec PHP 4, constituent une manière simple de parcourir des tableaux. Il existe deux syntaxes :
* La plus simple s'intéresse aux clés dans les tableaux. Le type de ces clés dépend des valeurs contenues dans le tableau. <code>foreach</code> simplifie une tache qui aurait certes été possible avec <code>for</code>, mais fastidieuse :
<pre>
-- Avec for
$array = array('valeur1', 'valeur2', 'valeur3');
for ($i = 0; $i < count($array); $i++) {
echo $array[$i]; //renvoie "valeur1valeur2valeur3"
}
-- Avec foreach
$array = array('valeur1', 'valeur2', 'valeur3');
foreach ($array as $value) {
echo $value; // renvoie "valeur1valeur2valeur3"
}
</pre>
<code>as</code> signifiant "comme", on récupère une variable contenant la valeur dans la cellule correspondante.
* La seconde se penche davantage sur les tableaux associatifs du type
<pre>
$array = array( "ville" => "Montargis", "température" => "15 degrés" );
</pre>
Ainsi on récupère le nom de la clé et la valeur du champ. En fait la structure <code>$cle => $valeur</code> est celle de la déclaration du tableau.
<pre>
foreach ($array as $cle => $valeur) {
commandes;
}
</pre>
Par ailleurs, il est possible d'itérer [[../Programmation_orientée_objet|des objets]] depuis PHP 5.
== break et continue ==
Les mots clés :
* <code>break</code> : sort de la boucle avant la fin.
* <code>continue</code> : passe immédiatement à l'itération suivante.
Pour sortir ou continuer la boucle mère :
* <code>break 2;</code>
* <code>continue 2;</code>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Dates=
== date() ==
La fonction <code>date()</code> créer une chaîne de caractère contenant la date du jour au format défini par son paramètre, selon la syntaxe suivante<ref>http://php.net/manual/fr/function.date.php</ref> :
* Y (year) : année sur quatre chiffres.
* y (year) : année sur deux chiffres.
* m (month) : mois sur deux chiffres.
* M : nom des mois (en français si <code>setlocale(LC_TIME, 'fr_FR');</code>)
* d (day) : jour du mois sur deux chiffres.
* t : dernier jour du mois
* w (week) : jour de la semaine sous forme d'un numéro :
: {|
| 0 || dimanche
|-
| 1 || lundi
|-
| 2 || mardi
|-
| 3 || mercredi
|-
| 4 || jeudi
|-
| 5 || vendredi
|-
| 6 || samedi
|}
À cela on peut rajouter les options d'horodatage les plus courantes :
* a ([[w:ante meridiem ou post meridiem|ante meridiem ou post meridiem]]) : renvoie "am" le matin et "pm" l'après-midi.
* h (hour) : heure de 0 à 12. À utiliser avec "a".
* H (Hour) : heure de 0 à 24.
* i (minute) : minute.
* s (second) : seconde.
=== Exemples ===
<pre>
echo date('Y-m-d'); // affiche 2016-07-10
echo date('Y-m-d H:i:s'); // affiche 2016-07-10 20:06:34
</pre>
== strtotime() ==
Cette fonction transforme un texte (en anglais) en date<ref>http://php.net/manual/fr/function.strtotime.php</ref>.
Exemples courants :
<pre>
$demain = date('Y-m-d', strtotime('+1 day'));
$hier = date('Y-m-d', strtotime('-1 day'));
</pre>
Pour afficher la plage des dates de la semaine précédente<ref>http://stackoverflow.com/questions/21644002/how-can-i-get-last-week-date-range-in-php</ref> :
<pre>
$previous_week = strtotime('-1 week +1 day');
$start_week = strtotime('last monday midnight', $previous_week);
$end_week = strtotime('next sunday', $start_week);
$start_week = date('Y-m-d', $start_week);
$end_week = date('Y-m-d', $end_week);
echo 'La semaine dernière était du '.$start_week.' au '.$end_week;
// Le vendredi 2016-07-29 cela affiche : La semaine dernière était du 2016-07-18 au 2016-07-24
</pre>
== checkdate() ==
Il est impératif dans un formulaire de vérifier si une date est au bon format. Pour ce faire il existe <code>checkdate()</code><ref>http://php.net/manual/fr/function.checkdate.php</ref> qui demande de séparer le mois, le jour puis l'année. Exemple :
<pre>
var_dump(checkdate(0, 0, 2000)); // false
var_dump(checkdate(1, 1, 2000)); // true
</pre>
== DateTime ==
Cette classe peut être instanciée en style POO ou en style procédural<ref>http://php.net/manual/fr/datetime.format.php</ref>. Exemple :
<pre>
$date = new DateTime('2018-01-01');
echo $date->format('Y-m-d H:i:s');
// ou
$date = date_create('2018-01-01');
echo date_format($date, 'Y-m-d H:i:s');
</pre>
Résultat : <code>2018-01-01 00:00:00</code>.
Pour générer une date relative à l'actuelle :
<pre>
echo (new DateTime('now -2 days'))->format('Y-m-d H:i:s')
</pre>
Pour date relative à une absolue :
<pre>
echo (new DateTime('2018-01-01 -2 days'))->format('Y-m-d H:i:s')
</pre>
Elle possède également des méthodes pour modifier les dates :
* Via une string : <code>$date->modify('-1 day');</code>
* Via un objet de type ''DateInterval''.
* Pour modifier les heures, par exemple pour obtenir la date du jour à minuit : <code>(new DateTime())->setTime(0, 0);</code>.
=== add ===
La méthode <code>DateTime::add()</code> (et son alias <code>date_add()</code>), permet d'ajouter deux dates<ref>https://www.php.net/manual/fr/function.date-add.php</ref>.
=== diff ===
La méthode <code>DateTime::diff()</code> (et son alias <code>date_diff()</code>), permet de soustraire deux dates<ref>https://www.php.net/manual/fr/function.date-diff.php</ref>.
Exemple :
<pre>
$date1 = new DateTime('2017-01-01');
$date2 = new DateTime('2020-11-01');
$dateInterval = $date2->diff($date1);
echo $dateInterval->format('%a jours');
</pre>
Résultat : <code>1400 jours</code>.
=== Conversion Timestamp en DateTime ===
$date = new DateTime('@1676564671');
== DateTimeImmutable ==
Idem que DateTime mais [[wikt:immutable|immutable]]<ref>http://php.net/manual/fr/class.datetimeimmutable.php</ref>.
== DateInterval ==
Cette classe gère les intervalles entre deux dates et est instanciée avec les paramètres suivants, sensibles à la casse :
* P : period (période).
* Y : year (année).
* M : month (mois).
* D : day (jour).
* H : hour (heure).
* I : minute.
* S : second (seconde).
Exemple :
<pre>
$dateInterval = new DateInterval('P1Y2M3D');
echo $dateInterval->format('%y an %m mois %d jours');
</pre>
Résultat : <code>1 an 2 mois 3 jours</code>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">La syntaxe au sein de la méthode "format" est légèrement différente puisqu'elle accepte les minuscules (pour afficher les chiffres sans préfixe "0", ex : "1" au lieu de "1")<ref>http://php.net/manual=/fr/dateinterval.format.php</ref>, et aussi :
* a : nombre de jours au total.
* N : numéro du jour de la semaine (1 = lundi, 7 = dimanche).</p>
</div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">La syntaxe pour les dates (année, mois, jour) et les temps (heure, minute, seconde) est différente : il faut ajouter un "T" pour les temps. Ex :
<pre>
var_dump(new DateInterval('P1D')); // un jour
var_dump(new DateInterval('PT1H')); // une heure
</pre>
</p>
</div>
</div>
Les objets de cette classe peuvent être placés en paramètre des méthodes de ''DateTime'' <code>add()</code> et <code>sub()</code>. Ils peuvent aussi être obtenus en résultat de <code>diff()</code> vu ci-dessus.
== DatePeriod ===
Pour calculer le nombre de récurrences d'un intervalle au sein d'une période (mais pas la date de fin à partir du début et de la durée)<ref>https://www.php.net/manual/en/class.dateperiod.php</ref>.
== Timestamps ==
=== time() ===
Cette fonction affiche la date courante au format [[w:horodatage|horodatage]] Unix, c'est-à-dire en nombre de seconde depuis le premier janvier 1970.
Ce format permet d'additionner ou soustraire deux dates très facilement, est peut être reconverti en date en étant placé en second paramètre de la fonction <code>date()</code>.
=== mktime() ===
Cette fonction (''make time'') crée une chaine de caractères contenant un horodatage Unix, c'est-à-dire un nombre de seconde représentant une date comprise en 1970 et 2038.
Exemple de calcul avec <code>strtotime()</code>, qui accepte les timestamps en second paramètre :
<pre>
$christmasTimeStamp = mktime(0, 0, 0, 12, 25, 2017); // 1514178000
$FirstDayOfNextMonthTimeStamp = strtotime('first day of next month', $christmasTimeStamp); // 1514782800
echo 'Le premier du mois après Noël 2017 est : '.date('Y-m-d', $FirstDayOfNextMonthTimeStamp); // 2018-01-01
</pre>
== date_default_timezone_set() ==
Ex :
<pre>
date_default_timezone_set('Europe/Paris');
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Sessions=
Comme vous le savez, à la fin d'un script PHP, toutes les variables créées sont détruites et il est impossible d'y accéder depuis un autre script. Alors comment faire pour passer des données entre les pages ?
La réponse : '''les sessions'''.
C'est en effet la solution la plus simple pour un webmaster afin de garder des informations sur son visiteur.
== Un cookie et une session, quelles différences ==
Une session est plus ou moins semblable aux cookies HTTP. Les données d'une session sont cependant stockées sur le serveur et non chez le client, ce qui l'empêche de pouvoir les modifier manuellement, comme il peut le faire pour un cookie.
== La durée de vie d'une session ==
Une session est, comme son nom l'indique, une session de navigation. Elle commence donc lors de l'accès à une page les utilisant et se termine à la fermeture du navigateur du client.
Une fois la session terminée, elle est détruite ainsi que toutes les données qu'elle contient. Elle doit donc ne contenir que des données temporaires.
Cependant, la session possède aussi un délai d'expiration. Celui-ci peut être modifié dans la configuration du serveur (directive ''session.gc_maxlifetime'' de php.ini), mais vaut généralement une trentaine de minutes. Une fois ce délai dépassé, la session est supprimée.
== Comment ça marche ? ==
Lors de l'accès à une page nécessitant une session, PHP va vérifier si une a déjà été ouverte et la réutiliser ou en créer une nouvelle. Il va donc lui attribuer un identifiant unique et se débrouiller pour que la prochaine page consultée puisse le connaître.
Pour cela, il exploite deux fonctionnalités. Premièrement, si l'utilisation de cookie est possible (le client ne les a pas désactivés), PHP crée un cookie contenant l'identifiant de session. Deuxièmement, si les cookies ne sont pas disponibles, il va réécrire les URL de la page pour inclure cet identifiant dans le lien.
Dans les deux cas, le script ayant besoin d'accéder aux données de la session recevra l'identifiant et sera capable de charger les données qu'elle contient.
== Est-ce que c’est sécurisé ==
Une session est toujours plus sécurisée qu'un cookie puisque le client ne peut pas la modifier manuellement. Un risque subsiste tout de même si l'identifiant de session peut être découvert.
Par exemple, les cookies transitent sur le réseau en clair. Si quelqu’un est capable d'intercepter les communications entre le client et le serveur, il est capable de voir le cookie et de découvrir l'identifiant de session. Il n'aura alors plus qu’à créer un faux cookie et PHP le prendra pour le pauvre internaute s'étant fait voler son cookie.
Pour éviter cela, on peut utiliser une connexion cryptée ou configurer le serveur de façon à ce qu’il rende la lecture de ce cookie impossible par [[JavaScript]] (''Http Only'').
== Utiliser les sessions ==
=== Initialiser une session ===
Pour pouvoir utiliser la fonctionnalité de session de PHP, il faut lancer le moteur de session en utilisant la fonction <code>session_start()</code>.
Cette fonction doit être en mesure d'envoyer des ''headers'' HTTP, aucune donnée ne doit donc avoir été transmise au navigateur. Vous pouvez soit placer ce code au tout début de votre script, soit utiliser les fonctions de bufférisation de sortie.
==== Une session portant un nom personnalisé ====
Une session porte par défaut le nom "PHPSESSID", c’est lui qui sera utilisé comme nom de cookie ou comme paramètre GET dans les liens... pas très esthétique. Il peut donc vous venir l'envie de changer ce nom.
Pour cela, la fonction <code>session_name()</code> entre en scène.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:50%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
session_name('nom_de_session');
session_start();
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
Ce code va donc charger une session avec l'identifiant provenant du cookie ou du paramètre GET portant le nom <code>nom_de_session</code>.
Noter la position de l'instruction 'session_start()'. Elle doit se trouver AVANT n’importe quel traitement de votre page '.php' .
==== Changer manuellement l'identifiant de la session ====
PHP détecte automatiquement l'identifiant à utiliser, cependant, vous pourrez avoir besoin de le changer manuellement, si vous voulez développer un système alternatif pour passer l'identifiant de session entre les pages. Vous pouvez par exemple vous baser sur le hachage (<code>md5()</code> ou <code>sha1()</code>) de l'adresse IP du client pour déterminer l'identifiant de la session. Attention aux proxys et aux internautes dont l'IP change à chaque requête.
Pour définir manuellement l'identifiant de session, la fonction <code>session_id()</code>. Elle prend un paramètre optionnel, qui est le nouvel identifiant de session. Dans tous les cas, la fonction retourne l'identifiant courant de la session.
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple</div>
|-
|<pre>
<?php
$identifiant = sha1($_SERVER['REMOTE_ADDR']);
/* Premièrement, nous récupérons l'adresse IP du client,
puis nous utilisons une fonction de hachage pour
générer un code valide. En effet, l'identifiant d'une
session ne peut contenir que des lettres (majuscules
ou minuscules) et des chiffres. */
$ancien_identifiant = session_id($identifiant);
/* Ensuite, nous modifions manuellement l'identifiant de
session en utilisant session_id() et en lui donnant
l'identifiant généré plus haut. Comme toujours, la
fonction retourne l'ancien identifiant, on le place
dans une variable, au cas où on aurait besoin de le
réutiliser. */
session_start();
/* On démarre la session, PHP va utiliser le nouvel
identifiant qu'on lui aura fournis. */
</pre>
|}
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Dans l'exemple ci-dessus, deux ordinateurs accédant au site par la même adresse IP publique, auront accès à la dernière session ouverte (donc pas forcément la leur).</p>
</div>
</div>
=== Lire et écrire des données dans la session ===
Les données de la session sont très facilement accessibles au travers d'un simple tableau PHP. Depuis PHP 4.1.0, vous pouvez utiliser le tableau super-global <code>$_SESSION</code>. Dans les versions plus anciennes, il s'appelait $HTTP_SESSION_VARS et nécessitait le mot clé <code>global</code> pour y accéder depuis une fonction.
Ces tableaux n'existent qu'une fois la session chargée. Tout ce qui est stocké à l'intérieur est sauvegardé et accessible depuis toutes les pages PHP utilisant les sessions.
==== Exercice : Une zone d'administration protégée par mot de passe ====
Dans cet exercice, nous allons fabriquer pas-à-pas une zone d'administration pour votre site web, protégée par un mot de passe. Nous nous occuperons de la partie identification uniquement.
Dans le chapitre précédent, vous avez également créé le même type de script, mais en utilisant un cookie. Nous allons donc adapter le code en utilisant des sessions à la place.
===== Le formulaire =====
Voici le code du formulaire en HTML, il va afficher une boîte de texte et un bouton "Connexion".
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:50%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<syntaxhighlight lang="html">
<form action="verification.php" method="post">
<input type="password" name="mdp" />
<input type="submit" value="Connexion" />
</form>
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
===== La page de vérification =====
Le formulaire ci-dessus pointe vers une page nommée <code>verification.php</code>. Cette page va vérifier que le mot de passe est juste et, si c’est le cas, placer un élément dans le tableau de la session pour que la page suivante puisse vérifier que vous êtes bien autorisé à voir la page.
Premièrement, nous devons initialiser la session. Nous laissons PHP choisir le nom.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:50%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
session_start();
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
L'appel à la fonction <code>session_start()</code> a fabriqué le tableau <code>$_SESSION</code>. Pour l'instant celui-ci est vide.
Il faut maintenant vérifier que le mot de passe fourni est le bon. Nous créons ensuite une entrée dans le tableau de la session contenant <code>true</code> (vrai) si le code est le bon. Nous pouvons alors rediriger le navigateur de l'internaute ou afficher un message d'erreur.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:50%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
$mdp = 'qwertz';
if ($mdp == $_POST['mdp']) {
$_SESSION['admin'] = true;
require_once('admin.php');
} else {
echo 'Le mot de passe est erroné';
}
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
Si vous ne comprenez pas la ligne <code>if ($mdp == $_POST['mdp']) {</code>, vous devriez lire (ou relire) le chapitre sur les formulaires.
Pour résumé, le code suivant suffit à l'identification du visiteur :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:50%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
session_start();
$mdp = 'qwertz';
if ($mdp == $_POST['mdp']) {
$_SESSION['admin'] = true;
require_once('admin.php');
} else {
echo 'Le mot de passe est erroné';
}
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
===== La page d'administration =====
Cette page doit se nommer <code>admin.php</code>. Si vous décidez d’utiliser un autre nom, il faudra modifier le script d'identification pour qu’il pointe sur la bonne page.
Comme dans l'autre page, nous devons commencer par initier une session.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
session_start();
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
Nous avons alors accès au tableau $_SESSION.
Si le visiteur a fourni le bon mot de passe, il existe un élément dans le tableau nommé 'admin' et valant <code>true</code>. Dans le cas contraire, cet élément n'existe pas. Il suffit donc de vérifier sa présence pour savoir si le visiteur est réellement l'administrateur du site. Pour cela, nous utilisons la fonction <code>isset()</code> qui vérifie si la variable (ou l'élément du tableau) existe.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
session_start();
if (isset($_SESSION['admin']) && $_SESSION['admin']) {
// Code à exécuter si le visiteur est administrateur.
} else {
// Code à exécuter si le visiteur n'est pas administrateur.
}
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
La suite du script n'est plus le sujet de cet exercice, le but est en effet atteint. Vérifier l'identité du visiteur pour lui permettre d'accéder à un espace privé.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">La bonne pratique en terme de sécurité, pour éviter que des attaques puissent énumérer les utilisateurs (avant de chercher leurs mots de passe), et de renvoyer la même erreur [[w:Liste des codes HTTP|403]] si l'utilisateur n'existe pas, ou s'il existe mais que son mot de passe est incorrect.</p>
</div>
</div>
=== Fermer une session ===
De la même façon que d'autre fonctionnalités de PHP, comme les connexions aux bases de données ou un pointeur de fichier, les sessions n'ont pas besoin d’être fermée.
Une fois le script PHP terminé, les données de la session sont automatiquement sauvegardées pour le prochain script.
Cependant, durant toute l'exécution du script, le fichier de la session est verrouillé et aucun autre script ne peut y toucher. Ils doivent donc attendre que le premier arrive à la fin.
Vous pouvez fermer plus rapidement une session en utilisant la fonction <code>session_write_close()</code>.
Si vous voulez également détruire la session, vous pouvez utiliser la fonction <code>session_destroy()</code> couplée à la fonction <code>session_unset()</code>.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Cookies=
== Introduction ==
[[Image:Gestionnaire SQLite 3.7.16.1 fr.PNG|thumb|500px|Base de données des cookies Firefox, lue avec [[w:SQLite|SQLite]].]]
[[Image:Editeur d'enregistrement du gestionnaire SQLite 3.7.16.1 fr.PNG|thumb|300px|Cookie ajouté à Firefox après commande PHP <code>setcookie('wiki', 'user');</code>.]]
Les [[w:Cookie (informatique)|cookies]] sont téléchargés du serveur HTTP sur le PC client, stockés dans le répertoire du navigateur de l'utilisateur courant. Par exemple dans Windows 7<ref>https://openclassrooms.com/forum/sujet/ou-sont-stockes-les-cookies-87753</ref> :
# [[Firefox|Firefox]] : <code>C:\Users\%USERNAME%\AppData\Roaming\Mozilla\Firefox\Profiles\xxxxxxxx.default\cookies.sqlite</code> (lisible par exemple avec https://addons.mozilla.org/en-US/firefox/addon/sqlite-manager/).
# [[w:Chrome|Chrome]] <code>C:\Users\%USERNAME%\AppData\Local\Google\Chrome\User Data\Safe Browsing Cookies</code>.
# [[w:Internet Explorer|Internet Explorer]] : <code>C:\Users\%USERNAME%\AppData\Roaming\Microsoft\Windows\Cookies</code>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">
* Ne pas mettre d'informations privées (mots de passe du serveur...) dans ces variables car elles sont stockées dans un fichier non protégé, sur le disque dur de l'utilisateur.
* Le cookie étant défini lors de l'affichage de l'en-tête HTTP, on ne peut pas le modifier puis le relire dans la même exécution. En effet, il est destiné à être lu après rechargement des pages.
* Ces cookies sont limités à 20 par domaine dans la configuration par défaut de PHP.
</p>
</div>
</div>
D'une manière générale, les cookies prennent la forme suivante :
Set-Cookie: nom=nouvelle_valeur; expires=date; path=/; domain=.exemple.org
* Le chemin (''<span class="lang-en" lang="en">path</span>'') permet de ne les rendre opérants que dans certaines parties d'un site.
* Le domaine fonctionne même avec d'autres serveurs (voir [[w:pixel espion|pixel espion]]).
== Syntaxe ==
<pre>
<?php
setcookie('cookie1', 'valeur1');
echo $_COOKIE['cookie1'];
</pre>
Pour définir la durée du cookie (à 30 jours) et la page du site qui lui est associée :
<pre>
<?php
setcookie('cookie2', 'valeur2', time() + 3600 * 24 * 30, '/');
</pre>
Pour supprimer un cookie, on lui confère une durée de vie négative :
<pre>
<?php
setcookie('cookie2', 'valeur2', time() - 1, '/');
</pre>
== Exemples ==
<pre>
<?php
if (isset($_COOKIE["cookie1"])) {
echo 'Authentifié';
} else {
echo 'Non authentifié';
}
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
* http://php.net/manual/fr/features.cookies.php
* http://www.php.net/manual/fr/function.setcookie.php
* http://fr2.php.net/explode/
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Cache=
La [[w:mémoire cache|mémoire cache]] stocke des données calculées afin de pouvoir y ré-accéder sans les recalculer, donc plus rapidement.
== Classification ==
Il existe plusieurs systèmes de cache en PHP pour accélérer l'exécution du code rappelé<ref>http://www.php-cache.com/en/latest/</ref> :
{| class="wikitable sortable"
! Nom !! Données stockées !! Flush
|-
| Cache d'instance || Objet PHP. Ex :<pre>
if (null === $x) {
$x = 1;
}</pre> || Relancer le script (ex : rafraichir la page Web).
|-
| Cache de session || Objet PHP<ref>https://www.php.net/manual/fr/session.security.php</ref> || Vider les cookies du navigateur.
|-
| [[w:en:List_of_PHP_accelerators#Zend_Opcache_.28ex._Zend_Optimizer.2B.29|OPcache]] || [[wikt:opcode|Opcode]]<ref>https://www.php.net/manual/fr/intro.opcache.php</ref> || opcache_reset();
|-
| [[w:Alternative PHP Cache|APCu]] || Variables utilisateurs dans la RAM<ref>https://www.php.net/manual/fr/intro.apcu.php</ref> || apcu_clear_cache();
|-
| Cache du navigateur || Rendering || CTRL + F5
|-
| [[w:Edge Side Includes|ESI]] || Partie de pages Web || Dépend du [[w:Réseau de diffusion de contenu|CDN]] ou [[w:proxy|proxy]] utilisé
|-
| Cache de framework || Configuration, traductions || Exemple de [[Programmation PHP/Symfony|Symfony]] : <code>php bin/console cache:clear</code> vide les fichiers temporaires de var/cache.
|-
| Proxy || Page web entière || Exemples, voir [[w:Varnish|Varnish]], [[w:en:HAProxy|HAProxy]]
|-
| Base de données [[w:NoSQL|NoSQL]] || Paire clé-valeur || Voir les pages [[Programmation PHP/Memcached|Memcached]] et [[Programmation PHP/Redis|Redis]] ci-après.
|-
| Cache d'[[wikt:ORM|ORM]] || Annotations, requêtes SQL ou leurs résultats || Exemple de [[Programmation PHP/Doctrine|Doctrine]] :
<pre>
php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:cache:clear-query
php bin/console doctrine:cache:clear-result
</pre>
ou :
<pre>
bin/console cache:pool:clear --all
</pre>
ou en PHP :
<pre>
$qb = $entityManager->createQuery();
$cacheDriver = $qb->getResultCacheDriver();
$cacheDriver->delete('ma_cle_de_cache');
</pre>
|-
| Chain cache || Tout || Utiliser les flushs de chaque cache inclus dans la chaine.
|}
Les dépendances des caches gérés en PHP se doivent de respecter la norme PSR6<ref>https://www.php-fig.org/psr/psr-6/</ref>, c'est-à-dire de fournir les méthodes de manipulation du cache suivantes :
* hasItem
* getItem
* deleteItem
* clear
* save
Normalement chaque item a une durée de rétention (lifetime) avant renouvellement, ce qui évite de chercher à tout invalider régulièrement. Voici les opérations sur les items :
* getKey
* get (valeur)
* isHit (est utilisable)
* set (valeur)
* expiresAt
* expiresAfter
== Installation ==
=== OPcache ===
Dans Docker :
<pre>
RUN docker-php-ext-install opcache
</pre>
=== APCu ===
==== Dans Docker ====
<pre>
RUN pecl install apcu \
&& docker-php-ext-enable apcu --ini-name 10-docker-php-ext-apcu.ini \
&& docker-php-ext-enable apc --ini-name 20-docker-php-ext-apc.ini \
&& docker-php-ext-enable apc --ini-name 20-docker-php-ext-apc.ini
</pre>
<div class="remarque"> Sur PHP < 8.0, ajouter après la première ligne : <code>&& pecl install apcu_bc \</code></div>
==== Sur machine hôte Linux ====
<pre>
sudo pecl install apcu
echo "extension=apcu.so" >> php.ini
</pre>
==== Sur machine hôte Windows ====
Télécharger la DLL sur https://pecl.php.net/package/APCu/5.1.21/windows dans le dossier local des extensions PHP (ex : C:\wamp64\bin\php\php7.4.33\ext).
Dans php.ini, ajouter :
<pre>
extension=apcu
</pre>
<pre>
[apcu]
apc.enabled=1
apc.enable_cli=1
</pre>
Si ce n'est pas pris en compte, on peut avoir l'erreur "APCu is not enabled".
De plus, on peut personnaliser la configuration par défaut de plusieurs manières. Ex<ref>https://stackoverflow.com/questions/24448261/how-to-install-apcu-in-windows</ref> :
<pre>
apc.shm_size=32M
apc.ttl=7200
apc.enable_cli=1
apc.serializer=php
</pre>
ou<ref>https://phpflow.com/php/how-to-install-apc-cache-on-wamp-and-xampp/?utm_content=cmp-true</ref>
<pre>
apc.shm_size=64M
apc.shm_segments=1
apc.max_file_size=10M
apc.stat=1
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Memcached=
[[w:Memcached|Memcached]] est un système d'usage général servant à gérer la mémoire cache distribuée. Il est souvent utilisé pour augmenter la vitesse de réponse des sites web créés à partir de bases de données. Il gère les données et les objets en RAM de façon à réduire le nombre de fois qu'une même donnée stockée dans un périphérique externe est lue. Il tourne sous [[Unix]], [[Windows]] et [[MacOS]] et est distribué selon les termes d'une licence libre dite permissive<ref>{{lien web|langue=en
|url=http://code.sixapart.com/svn/memcached/trunk/server/LICENSE |titre=License of memcached }}</ref>.
== Installation ==
[[w:Memcached|Memcached]] s'installe sur un serveur de mémoire cache distribuée, base de données de paires clé-valeur, qui est accessible par ses clients sur le port 11211, en TCP ou UDP<ref>{{lien web|url=https://www.zdnet.fr/actualites/amplification-d-attaque-ddos-memcached-fait-exploser-les-compteurs-39864804.htm|titre=Amplification d'attaque DDoS : Memcached fait exploser les compteurs|éditeur=|consulté le=19 octobre 2018}}</ref>.
Installation :
<syntaxhighlight lang=bash>
sudo apt-get install memcached
</syntaxhighlight>
=== Client ===
Sur Docker PHP :
<syntaxhighlight lang=bash>
RUN pecl install memcached \
&& docker-php-ext-enable memcache
</syntaxhighlight>
=== Test ===
<syntaxhighlight lang=bash>
telnet localhost 11211
</syntaxhighlight>
Si ça fonctionne sur le serveur mais pas depuis les autres machines, c'est certainement qu'il écoute 127.0.0.1 au lieu de son IP externe. Pour le vérifier :
<syntaxhighlight lang=bash>
netstat -an | grep ":11211"
tcp 0 0 127.0.0.1:11211 0.0.0.0:* LISTEN
</syntaxhighlight>
ou
<syntaxhighlight lang=bash>
ss -nlt | grep 11211
LISTEN 0 1024 127.0.0.1:11211
</syntaxhighlight>
Pour le résoudre :
<syntaxhighlight lang=bash>
sudo vim /etc/memcached.conf
sudo /etc/init.d/memcached restart
</syntaxhighlight>
=== Commandes ===
* Reset mémoire :
<syntaxhighlight lang=bash>
echo "flush_all" | nc -q 1 localhost 11211
</syntaxhighlight>
== Utilisation ==
Memcached propose plusieurs commandes<ref>https://github.com/memcached/memcached/wiki/Commands</ref>. Pour tester si le serveur fonctionne avant de l'utiliser en PHP, on peut donc les lancer avec <code>telnet nom_du_serveur 11211</code>.
* stats : informations sur le cache en cours.
* set : ajoute une paire clé-valeur dans le cache.
* add : ajoute une paire clé-valeur uniquement si la clé n'existe pas déjà.
* get : récupère la valeur à partir de la clé donnée en paramètre.
* delete : supprime la paire clé-valeur de la clé donnée.
* flush_all : supprime tout ce qu'il y a dans le cache.
Par exemple pour lire une clé, il faut d'abord voir les descriptions de toutes les clés :
<syntaxhighlight lang=bash>
stats items
STAT items:1:number 1
...
STAT items:2:number 1
...
STAT items:3:number 1
...
</syntaxhighlight>
Puis l'appeler par son numéro pour voir son nom (le zéro représente l'absence de limite) :
<syntaxhighlight lang=bash>
stats cachedump 1 0
</syntaxhighlight>
== Utilisation en PHP ==
=== Installation ===
Pour vérifier l'installation de la bibliothèque PHP pour Memcached :
php -i |grep memcached
* S'il est absent :
sudo pecl install memcached
=== Utilisation ===
<syntaxhighlight lang=php>
$memcached = new \Memcached();
$memcached->addServer('127.0.0.1', 11211);
$memcached->set('nom du test', 'valeur du test');
echo $memcached->get('nom du test');
</syntaxhighlight>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Redis=
== Installation ==
[[w:Redis|Redis]] est comme Memcached, un système de gestion de base de données clef-valeur scalable, très hautes performances. En 2019, il devient plus utilisé que Memcached car il possède plus de fonctionnalités<ref>https://aws.amazon.com/fr/elasticache/redis-vs-memcached/</ref>. Par exemple il permet en plus une persistance sur la mémoire morte utile pour les reprises sur panne, autoriser les groupes de paires clé-valeur, et gère mieux le parallélisme<ref>https://www.disko.fr/reflexions/technique/redis-vs-memcached/</ref>.
=== Serveur ===
Pour l'installer :
<pre>
sudo apt-get install redis-server
</pre>
Obtenir la version :
<pre>
redis-server -v
Redis server v=6.2.5 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=19d2f4c94e0b6820
</pre>
=== Client ===
<pre>
sudo apt-get install redis
</pre>
Sur [[Docker]] PHP :
<pre>
RUN pecl install redis \
&& docker-php-ext-enable redis
</pre>
Sur [[WAMP]] :
* Télécharger le .dll sur https://pecl.php.net/package/redis
* L'extraire dans le dossier correspondant à la version de PHP. Ex : bin\php\php8.2.0\ext
* L'activer dans php.ini.
* Redémarrer WAMP.
Ensuite on le voit dans phpinfo.
== Commandes ==
Pour se loguer au serveur Redis :
<pre>
telnet nom_du_serveur 6379
</pre>
Les commandes Redis les plus utiles<ref>https://redis.io/commands</ref> :
=== Lecture ===
* MONITOR : suivre l'activité du serveur en temps réel.
* KEYS * : liste des clés.
* GET : affiche la valeur de la clé en paramètre (ou <code>nil</code> si elle n'existe pas).
* MGET : affiche les valeurs des clés en paramètre.
* QUIT : quitter.
=== Écriture ===
* SET : définit une valeur associée à la clé en paramètre<ref>https://redis.io/commands/set/</ref>.
* EXPIRE : définit une durée avant expiration pour la clé en paramètre, en secondes.
* SETEX : SET + EXPIRE<ref>https://redis.io/commands/setex/</ref>.
* DEL : supprimer par le nom complet de la clé.
* FLUSHALL : vider toute la base de données.
Exemple de reset mémoire depuis le shell :
echo "FLUSHALL" | nc -q 1 localhost 6379
== redis-cli ==
Pour afficher les clés de la base en shell :
<pre>
redis-cli KEYS '*'
</pre>
Par défaut, <code>redis-cli</code> pointe sur 127.0.0.1. Pour regarder une autre machine :
<pre>
redis-cli -h redis.example.com KEYS '*'
</pre>
Supprimer des clés par leurs noms<ref>https://rdbtools.com/blog/redis-delete-keys-matching-pattern-using-scan/</ref> (exemple de celles qui ont le préfixe "users:") :
<pre>
redis-cli KEYS "users:*" | xargs redis-cli DEL
</pre>
ou :
<pre>
redis-cli --scan --pattern users:* | xargs redis-cli DEL
</pre>
== Plusieurs bases ==
Chaque instance de Redis peut accueillir jusqu'à 16 bases de données<ref>https://www.mikeperham.com/2015/09/24/storing-data-with-redis/</ref>.
Elles sont accessibles par un suffixe dans leur DSN. Par défaut, <code>redis://localhost:6379</code> pointe sur la base zéro : <code>redis://localhost/0:6379</code>.
== Utilisation en PHP ==
<syntaxhighlight lang=php>
$redis = new \Redis();
$redis->connect('localhost', 6379);
$redis->set('nom du test', 'valeur du test');
echo $redis->get('nom du test');
</syntaxhighlight>
=== predis ===
Cette bibliothèque permet d'utiliser Redis en clustering, avec des masters et slaves<ref>https://github.com/predis/predis</ref>.
=== Dans Symfony ===
Dans le framework PHP [[Programmation PHP/Symfony|Symfony]].
==== Session ====
===== SncRedisBundle =====
Avant Symfony 4.1, il fallait passer par un bundle tel que SncRedisBundle<ref>https://github.com/snc/SncRedisBundle/blob/master/Resources/doc/index.md</ref>.
Depuis :
<pre>
composer require snc/redis-bundle predis/predis
</pre>
Pour que les sessions soient stockées dans Redis au lieu de var/cache/, remplacer dans framework.yaml, <code>session.handler_id:</code> null par snc_redis.session.handler. Cela permet par exemple de les partager entre plusieurs conteneurs.
===== RedisSessionHandler =====
Depuis Symfony 4.1, le composant ''HttpFoundation'' contient une classe ''RedisSessionHandler''<ref>https://github.com/symfony/symfony/blob/4.1/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php</ref>.
Installation dans services.yaml :
<syntaxhighlight lang=yaml>
redis:
class: Redis
calls:
- connect:
- '%env(REDIS_HOST)%'
- '%env(int:REDIS_PORT)%'
Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
arguments:
- '@redis'
</syntaxhighlight>
Puis dans config/packages/framework.yaml<ref>https://symfony.com/doc/current/session/database.html</ref> :
<syntaxhighlight lang=yaml>
framework:
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
</syntaxhighlight>
Ensuite le service ''session'' utilisera automatiquement Redis.
==== Doctrine ====
Pour mettre le cache [[Programmation PHP avec Symfony/Doctrine#Cache|Doctrine]] de requête et de résultat dans Redis, on peut utiliser SncRedisBundle<ref>https://kunicmarko20.github.io/2017/07/20/Doctrine-Second-Level-Cache-with-Translations-and-Redis.html</ref><ref>https://github.com/snc/SncRedisBundle/issues/554</ref>.
Pour vider les caches :
bin/console cache:pool:clear doctrine.query_cache_pool doctrine.result_cache_pool
==== Cache chainé ====
L'inconvénient de Redis ainsi configuré est que s'il tombe en panne, les applications qui l'utilisent crashent aussi (erreur de connexion à Redis dès l'instanciation des services).
Pour se prémunir de cela, il est recommandé d'inclure Redis dans un cache chainé, qui permet de basculer automatiquement sur des caches de secours sans bloquer l'application. Exemple<ref>https://symfony.com/doc/current/components/cache/adapters/chain_adapter.html</ref> :
<syntaxhighlight lang=yaml>
# cache.yaml
framework:
cache:
prefix_seed: my_app_
app: cache.chain
pools:
cache.chain:
adapter: cache.adapter.psr6
provider: chain_adapter_provider
cache.redis:
adapter: cache.adapter.redis
cache.apcu:
adapter: cache.adapter.apcu
cache.filesystem:
adapter: cache.adapter.filesystem
</syntaxhighlight>
<syntaxhighlight lang=yaml>
# services.yaml
services:
chain_adapter_provider:
class: Symfony\Component\Cache\Adapter\ChainAdapter
arguments:
- ['@cache.redis', '@cache.apcu', '@cache.filesystem']
- '%env(int:CACHE_DEFAULT_LIFETIME)%'
</syntaxhighlight>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Formulaire=
Le PHP est un langage de traitement. Une page en php pourra analyser et effectuer des opérations suite à un formulaire. Ce formulaire devra être écrit en HTML, dans une page .html (.htm) ou .php. Pour notre exemple, nous allons créer une page avec laquelle un utilisateur pourra se connecter à une zone administrateur.
== Présentation ==
Notre formulaire (form.html) comprendra deux éléments :
* le champ du mot de passe ("password")
* un bouton pour soumettre le formulaire
La page de traitement, en PHP (traitement.php) :
* vérification si le mot de passe est correct
* envoi d'un cookie. ceci sera la preuve que l'ordinateur distant est autorisé à accéder aux pages
Une page de la zone administration (admin.php) :
* vérification si l'utilisateur est autorisé à consulter les pages
== Le formulaire ==
Le code source présenté ici est uniquement le formulaire. Pour un affichage agréable de la page il est nécessaire de l'"habiller". Voir comment créer une page en HTML. Ce script ne sera pas expliqué. Pour le comprendre vous devez avoir les bases du formulaire en HTML.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<form action="traitement.php" method="post">
<input type="password" size="20" name="mdp">
<input type="submit" value="OK">
</form>
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
En gros, ce formulaire enverra sur la page traitement.php la valeur de l'entrée "mdp".
== Le traitement ==
Pour comprendre la suite, vous devez avoir en tête la chose suivante sur les variables. Le champ dont le nom est "mdp" (name=mdp) envoie sur la page de traitement la variable '''$mdp''' avec pour valeur l'entrée renseignée.
Pour récupérer les valeurs d'un formulaire on utilise ''$valeur=$_POST["nomvariable"];''
Si vous désirez récupérer les valeurs passée via une URL, par exemple http://www.example.com/index.php?mdp=valeur, on utilise ''$valeur=$_GET["mdp"];''
Il est possible de récupérer directement la valeur d'un formulaire via le nom du champ (dans notre exemple $mdp contiendrait la valeur saisie du formulaire) mais '''il est fortement conseillé d’utiliser <code>$_POST</code> pour des raisons de sécurité et de compatibilité.'''
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php //traitement.php
$motdepasse = 'qwerty';
/* voici le mot de passe à envoyer si l’on veut être
connecté */
if (empty($_POST["mdp"]) OR $_POST["mdp"] != $motdepasse) {
/* si la valeur envoyée est vide ou différente de la valeur
demandée */
exit;
/* interruption du script (voir php/interrompre_un_script) */
}
setcookie("wiki",$_POST["mdp"],time()+3600);
/* le serveur envoie un cookie à l'utilisateur pour
permettre l'accès aux pages administration */
header("Location: admin.php");
/* la page redirige l'utilisateur vers la page de la
zone d'administration (cette fonction doit être
utilisée avant tout code HTML) */
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
== La zone administration ==
La zone administration va vérifier si l'utilisateur est autorisé à consulter ces pages. Il va comparer le mot de passe entré dans le cookie avec le mot de passe réel.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php // admin.php
$motdepasse = 'qwerty';
/* le (vrai) mot de passe */
$mdp = $_COOKIE["wiki"];
/* le mot de passe enregistré sur le cookie.
L'accès aux cookies se fait au travers du
tableau super-global $_COOKIE. Il fonctionne
comme $_POST ou $_GET. */
if ($mdp != $motdepasse) {
/* si le mot de passe n'est pas correct */
exit('Haha ! Tu voulais voir l\'admin sans mot de passe ?!');
/* interruption du script avec un joli message ^^
notez l'antislash devant l'apostrophe qui permet
de ne pas interrompre la chaine de caractères */
}
echo "affichage de admin.php";
/* la page peut s'afficher correctement. Si le script
arrive ici, c’est que le mot de passe est correct,
autrement le script aurait été arrêté (appel à exit
plus haut. */
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
== Types de champ ==
Pour des <code><input type="checkbox"></code>, on vérifie si leurs valeurs sont 'on' ou 'off'.
== Types MIME ==
Les données d'un formulaire rempli par le client sont envoyées au serveur dans une requête POST avec en en-tête un Content-type x-www-form-urlencoded ou multipart/form-data. La différence est que le premier est semblable aux urlencode() des GET mais dans le body POST, et cette conversion est légèrement plus lente qu'en multipart/form-data<ref>https://javarevisited.blogspot.com/2017/06/difference-between-applicationx-www-form-urlencoded-vs-multipart-form-data.html#axzz71dnIiWZu</ref>.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Fichiers=
L'utilisation de fichier peut être utile pour exporter des données à archiver ou accélérer un traitement par un système de cache. Cette page explique comment interagir avec un fichier.
== Dossiers ==
Voici les fonctions usuelles de navigation et manipulation du système de fichier.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Il est recommandé de ne pas les disperser dans le code, mais de les regrouper dans une classe de manipulation de fichiers, pour pouvoir utiliser un stockage cloud alternativement (qui les manipule par des appels HTTP, dans un stockage "illimité").</p>
</div>
</div>
=== Lecture ===
* <code>PHP_OS</code> : contient de système d'exploitation courant (ex : WINNT, Linux).
* <code>basename($path)</code> : extraire le nom du fichier à partir de son chemin.
* <code>dirname(__DIR__)</code> : extraire le répertoire parent du dossier en paramètre, au niveau précisé en second paramètre (le parent direct par défaut).
* <code>chdir($dossier)</code> : changer de dossier courant ;
* <code>opendir($dossier)</code> : ouvrir un dossier ;
* <code>closedir($dossier)</code> : fermer le dossier ;
* <code>is_dir($dossier)</code> : vérifier la présence d'un dossier ;
* <code>is_file($fichier)</code> : vérifier la présence d'un fichier ;
* <code>file_exists($fichier)</code> : vérifier la présence d'un fichier ou un dossier local<ref>http://php.net/manual/fr/function.file-exists.php</ref> ;
* <code>filesize($fichier)</code> : renvoie la taille du fichier en octet<ref>https://www.php.net/manual/en/function.filesize.php</ref> ;
* <code>readdir($dossier)</code> : lire le contenu du dossier ligne par ligne ;
* <code>scandir($dossier)</code> : lire le contenu du dossier en une fois ;
* <code>glob($regex)</code> : lire le contenu du dossier courant, avec filtrage regex (ex : *.txt)<ref>http://php.net/manual/fr/function.glob.php</ref>.
* <code>pathinfo($fichier)</code> : renvoie un tableau avec les caractéristiques du fichier : dossier, nom, extension.
* <code>mime_content_type($fichier)</code> : renvoie le type [[w:MIME|MIME]].
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Généralement le suffixe du type MIME et identique à l'extension du <code>pathinfo</code>, mais il y a des exceptions, car le premier se base sur le contenu du fichier alors que le second sur son nom. Ex :
<pre>
$filePath = 'test.jpg';
var_dump(pathinfo($filePath, PATHINFO_EXTENSION)); // jpg
var_dump(mime_content_type($filePath)); // image/jpeg
</pre></p>
</div>
</div>
==== Récupérer certains fichiers d'un dossier ====
Par exemple, pour trouver tous les fichiers .sql du dossier "sql" :
<pre>
chdir('sql');
$files = glob('*.sql');
var_dump($files);
</pre>
=== Écriture ===
* <code>shell_exec('pwd')</code> : exécuter n'importe quelle commande [[Programmation Bash|shell]]. Le nom du répertoire courant (pwd) dans cet exemple.
* <code>mkdir()</code> (''make directory'') : créer un dossier.
* <code>rmdir()</code> (''remove directory'') : supprime un dossier non vide.
* <code>touch()</code> : créer un fichier (ou le rafraichir s'il existe).
* <code>rename()</code> : déplacer un fichier.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Les commandes du shell_exec dépendent du système d'exploitation. Par exemple, sur Linux <code>shell_exec('ls')</code> fonctionne mais sur Windows il renvoie null et il faut utiliser <code>shell_exec('dir')</code>.
De plus, sachant que les systèmes d'exploitation n'ont pas les mêmes séparateurs de dossiers (sous Windows on utilise "\" et en unixerie c'est "/"), on peut utiliser la constante prédéfinie : <code>DIRECTORY_SEPARATOR</code> qui fonctionne pour les deux<ref>http://php.net/manual/fr/dir.constants.php</ref>.
</p>
</div>
</div>
<div class="remarque"> Pour supprimer un dossier non vide<ref>https://stackoverflow.com/questions/1653771/how-do-i-remove-a-directory-that-is-not-empty</ref> :
<pre>
array_map('unlink', glob('your/path/*.*'));
rmdir('your/path');
</pre></div>
== Droits des fichiers ==
=== Windows ===
Sous Windows il suffit de se rendre dans l'onglet sécurité des propriétés d'un fichier, pour cocher les cases autorisant le programme à le lire et/ou le modifier.
=== Unix ===
''chmod'' est le système de droit d'accès a un fichier [[Unix|Unix]]. Il s'agit d'un [[w:Permissions_Unix|nombre à trois chiffres]] que l’on attribut à un fichier (ex. : 755). Il détermine le droit d'accès au fichier en question, qui peut le modifier.
Selon sa valeur le [[:Catégorie:système d'exploitation|système d'exploitation]] autorise ou non la modification du fichier. Sous GNU/Linux, l'utilisateur 'root', (superutilisateur), a tous les droits, c'est-à-dire qu’il peut modifier tous les fichiers.
Lorsque qu'un fichier est créé manuellement, le ''chmod'' du fichier en question est '''755''', avec un tel ''chmod'' nous ne pouvons pas modifier le fichier avec un script PHP. Pour pouvoir le modifier, il suffit juste de changer le ''chmod'' du fichier, en lui donnant la valeur ''766''. Sous Windows, cette notion est masquée et il suffit d’être administrateur de la machine (utilisateur par défaut) pour pouvoir modifier un fichier.
* Pour récupérer les permissions : <code>fileperms($localFilePath) & 0777</code>
* Le propriétaire : <code>fileowner($localFilePath))</code>
== Ouvrir un fichier ==
Pour déclencher l'ouverture d'un fichier chez celui qui exécute le script, on précise son type puis son emplacement :
<pre>
header("Content-Type: application/pdf");
header("Content-Disposition: inline; filename='".$fichier."'");
</pre>
== Télécharger un fichier ==
<pre>
header("Content-Type: application/pdf");
header("Content-Disposition: attachment; filename='".$fichier."'");
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Le téléchargement ne doit pas être précédé d'affichages (ex : <code>echo</code> ou logs de warning) sinon ils apparaitront dans l'en-tête du fichier, le rendant illisible.</p>
</div>
</div>
== Zipper un fichier ==
=== Installation ===
Cette fonctionnalité est fournie nativement depuis PHP 5.2.0. Par contre sur les versions >= 7 sur Linux il faut l'installer :
RUN apt-get update && \
apt-get install -y \
libzip-dev \
&& docker-php-ext-install zip
=== Utilisation ===
Pour compresser des fichiers, il faut d'abord créer l'archive vide, puis les y ajouter un par un avec la méthode <code>addFile(Fichier source, Nom du fichier dans l'archive)</code><ref>http://php.net/manual/fr/ziparchive.addfile.php</ref> :
<pre>
$zip = new ZipArchive();
$f = '../Temp/'.$nomFichier . '.zip';
if ($zip->open($f, ZipArchive::CREATE) !== true) {
exit('Impossible de créer le .zip');
}
$zip->addFile('../Temp/' . $nomFichier . '.xls', $nomFichier . '.xls');
$zip->close();
</pre>
=== Dézipper un fichier ===
<pre>
$zip = new ZipArchive();
$f = '../Temp/'.$nomFichier . '.zip';
if ($zip->open($f) === true) {
$zip->extractTo('../Temp/');
$zip->close();
}
</pre>
Pour les .gz<ref>https://stackoverflow.com/questions/11265914/how-can-i-extract-or-uncompress-gzip-file-using-php</ref> :
<pre>
private function uncompressFile(string $target): void
{
$uncompressedFileName = str_replace('.gz', '', $target);
$file = gzopen($target, 'rb');
$outFile = fopen($uncompressedFileName, 'wb');
while (!gzeof($file)) {
fwrite($outFile, gzread($file, 4096));
}
fclose($outFile);
gzclose($file);
</pre>
== Éditer et fermer un fichier ==
Créer un fichier avec un attribut ''chmod'' de ''766''. Ensuite il faut ouvrir le fichier en question avant de lire/écrire. Pour cela la fonction ''fopen'' est là :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:60%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
$objetFichier = fopen($nomFichier, $mode);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
'''Explication :''' La fonction ''fopen'' à besoin de deux paramètres pour pouvoir s'exécuter :
* $nomFichier, il s'agit du chemin du fichier
* $mode, il s'agit du mode de l'ouverture
La fonction ''fopen'' utilise le premier paramètre, pour déterminer le chemin du fichier a ouvrir/créer.
Voici les différents modes d'ouvertures pour la fonction ''fopen'' :
{| class="wikitable"
! Mode !! Description
|-
| align="center" | r
| (read) Ouvre en lecture seule, et place le pointeur de fichier au début du fichier.
|-
|-
| align="center" | r+
|Ouvre en lecture et écriture, et place le pointeur de fichier au début du fichier.
|-
|-
| align="center" | w
| (write) Ouvre en écriture seule, et place le pointeur de fichier au début du fichier et réduit la taille du fichier à 0. Si le fichier n'existe pas, on tente de le créer.
|-
|-
| align="center" | w+
|Ouvre en lecture et écriture, et place le pointeur de fichier au début du fichier et réduit la taille du fichier à 0. Si le fichier n'existe pas, on tente de le créer.
|-
|-
| align="center" | a
| (append) Ouvre en écriture seule, et place le pointeur de fichier à la fin du fichier. Si le fichier n'existe pas, on tente de le créer.
|-
|-
| align="center" | a+
|Ouvre en lecture et écriture, et place le pointeur de fichier à la fin du fichier. Si le fichier n'existe pas, on tente de le créer.
|-
|-
| align="center" | x
| Créé et ouvre le fichier en lecture seule ; place le pointeur de fichier au début du fichier.
Si le fichier existe déjà, fopen() va échouer, en retournant FALSE et en générant une erreur de niveau E_WARNING.
Si le fichier n'existe pas, fopen() tente de le créer. Ce mode est l'équivalent des options O_EXCL{{#parsoid\0fragment:2}}O_CREAT pour l'appel système open(2) sous-jacente. Cette option est supportée à partir de PHP 4.3.2 et fonctionne uniquement avec des fichiers locaux.
|-
|-
| align="center" | x+
|Crée et ouvre le fichier en lecture et écriture ; place le pointeur de fichier au début du fichier.
Si le fichier existe déjà, fopen() va échouer, en retournant FALSE et en |générant une erreur de niveau E_WARNING.
Si le fichier n'existe pas, fopen() tente de le créer.
|-
| align="center" | c
| Ouvre le fichier en écriture seule ; place le pointeur de fichier au début du fichier.
Si le fichier existe déjà, il ne le tronque pas, sinon il le crée.
|-
|-
| align="center" | c+
| Ouvre le fichier en lecture et écriture, puis se comporte comme "c".
|}
Pour le fermer maintenant, il y a la fonction ''fclose''.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
$objetFichier = fopen($nomFichier, $mode);
fputs($objetFichier, $contenu);
fgets($objetFichier);
fclose($objetFichier);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
== Copier un fichier ==
<pre>
if (!copy($ancienFichier, $nouveauFichier)) {
echo 'La copie a échoué.';
}
</pre>
Pour les images il existe aussi <code>imagecopyresampled</code><ref>http://php.net//manual/fr/function.imagecopyresampled.php</ref>.
== Supprimer un fichier ==
<pre>
if (!unlink($fichier)) {
echo 'La suppression a échoué.';
}
</pre>
== Interagir avec le fichier ==
=== Lecture ===
Une fois ouvert, il existe plusieurs manière de lire le contenu d'un fichier : caractère par caractère, ligne par ligne, jusqu'à une certaine taille, ou tout entier.
==== Tout le fichier ====
===== file =====
Pour stocker le fichier entier dans un tableau, on peut utiliser ''file()'' qui renvoie un tableau séparant chaque ligne du fichier :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
$fichier = file($nomFichier); // ou : file("http://MonSite/" . $nomFichier);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
Ainsi, ''$fichier[0]'' correspond à la première ligne, ''$fichier[1]'' à la seconde, etc.
Si le fichier sature la mémoire, utiliser file_lines() avec les générateurs<ref>https://www.startutorial.com/articles/view/php-generator-reading-file-content</ref>.
===== file_get_contents =====
Pour stocker le fichier entier dans un scalaire, on utilise cette fonction.
===== readfile =====
Pour afficher tout le fichier dans la sortie standard<ref>https://www.php.net/manual/en/function.readfile.php</ref>.
==== Ligne par ligne ====
===== fgets =====
La méthode générale pour lire ligne par ligne avec la fonction ''fgets'', dont la définition est la suivante :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
$ligne = fgets($objetFichier);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
La variable ''$objetFichier'' doit être le résultat de l'ouverture d'un fichier avec ''fopen''. Pour lire un fichier en entier, il suffit d’utiliser ''fgets'' en boucle ; la condition de sortie de la boucle serait alors l'arrivée à la fin de fichier, évènement notifié par la fonction ''feof''.
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple de parcours d'un fichier ligne par ligne</div>
|-
|<pre>
<?php
$nomFichier = "chemin_ou_nom_de_fichier";
$objetFichier = fopen($nomFichier, "r"); //ouverture en lecture
if ($objetFichier) {
//on lit le fichier tant que la fin n'est pas atteinte
while (!feof($objetFichier)) {
$ligne = fgets($objetFichier);
echo $ligne;
}
fclose($objetFichier);
} else {
echo "Erreur : impossible d'ouvrir le fichier.";
}
</pre>
|}
===== fgetc =====
La fonction équivalente pour lire caractère par caractère se nomme ''fgetc'' :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
$caractere = fgetc($objetFichier);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
Notez que cette fonction retourne FALSE arrivée à la fin du fichier. Lors d'un parcours, il faut donc tester impérativement la valeur avant de l’utiliser, avec un test logique de la forme « $caractere !== FALSE ».
===== fgetcsv =====
Cette fonction fonctionne comme "fgets" sauf qu'en plus elle utilise le séparateur "," (qui peut être changé par le paramètre "delimiter"<ref>http://php.net/manual/fr/function.fgetcsv.php</ref>) pour créer un sous-tableau de champs par ligne.
=== Écriture ===
Une fois le fichier ouvert, l'écriture se fait via la fonction ''fwrite''.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
fwrite($objetFichier, $chaine);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
'''Explication''' :
* $objetFichier : variable pointant vers un fichier ouvert avec ''fopen''
* $chaine : la chaîne à écrire dans le fichier
* retour : le nombre de caractère écrits, ou FALSE si erreur
L'utilisation est alors la même que pour la lecture : ouvrir le fichier, écrire et fermer.
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple d'écriture</div>
|-
|<pre>
<?php
$nomFichier = "chemin_ou_nom_de_fichier";
$chaine = "Je suis une chaine écrite par PHP !\n"
$objetFichier = fopen($nomFichier, "w"); //ouverture en lecture
if ($objetFichier) {
if(fwrite($objetFichier, $chaine) === FALSE) {
echo "Erreur : impossible d'écrire dans le fichier.";
}
fclose($objetFichier);
} else {
echo "Erreur : impossible d'ouvrir le fichier.";
}
</pre>
|}
'''Attention :''' Si vous ouvrez le fichier avec l'option ''w'' ou ''w+'', le contenu du fichier sera effacé s'il existait. Pour écrire à la fin, il faut ouvrir avec les options ''a'' ou ''a+'' (voir ci-dessus). Enfin, si vous pouvez avec l'option ''r+'', le contenu sera écrasé, puisque le pointeur de fichier sera placé au début.
Par ailleurs, la fonction ''file_put_contents()'' effectue un ''fopen()'', ''fwrite()'' et ''fclose()'' successivement<ref>http://php.net/manual/fr/function.file-put-contents.php</ref>.
=== Se déplacer ===
Parfois, il peut être nécessaire de se déplacer dans le fichier, par exemple pour revenir au début. Pour cela, il faut utiliser la fonction ''fseek'' comme suit :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
fseek($objetFichier, $position);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
'''Explication :'''
* $objetFichier : variable pointant vers un fichier ouvert avec ''fopen''
* $position : la position à laquelle on veut se déplacer. Pour revenir au début, $position doit valoir zéro.
Pour aller à la fin :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
<?php
fseek($objetFichier, 0, SEEK_END);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
== Fichiers uploadés ==
Copier-coller ces lignes dans un fichier vierge ''upload.php'' pour qu’il affiche le nom des fichiers qu'on upload avec :
<pre>
<?php
echo '
<form method="POST" action="upload.php" enctype="multipart/form-data">
<input type="file" name="fichier">
<input type="submit" name="envoyer" value="Uploader">
</form>';
if (isset($_FILES['fichier'])) {
echo $_FILES['fichier']['name'];
}
</pre>
Il existe la fonction is_uploaded_file() pour vérifier si un fichier est bien issu d'un upload<ref>https://www.php.net/manual/en/function.is-uploaded-file.php</ref>, et move_uploaded_file() pour le déplacer.
== Fichiers temporaires ==
Pour créer des fichiers qui seront automatiquement détruits en fin de connexion, le dossier temporaire est accessible avec : <code>sys_get_temp_dir()</code>.
La fonction <code>tempnam()</code> quant-à elle nomme automatiquement un nouveau fichier temporaire avec un nom unique :
<pre>
$fileName = tempnam(sys_get_temp_dir(), 'MonFichier1');
</pre>
== Fichiers distants ==
=== HTTP ===
Pour lire un fichier en HTTP (par exemple cette page web) :
<pre>
<?php
$page = file_get_contents("http://fr.wikibooks.org/wiki/PHP/Fichiers");
echo $page;
</pre>
ou<ref>[http://www.developpez.net/forums/d66310/php/langage/syntaxe/lire-contenu-page-web-grace-script-php/ developpez.net]</ref>
<pre>
<?php
$url = 'http://fr.wikibooks.org/wiki/PHP/Fichiers';
echo htmlspecialchars(implode('', file($url)));
</pre>
Pour tester si un fichier distant existe, utiliser <code>get_headers()</code><ref>http://php.net/manual/fr/function.get-headers.php</ref>.
<div class="remarque"> fonctionne aussi bien avec HTTPS.</div>
=== FTP ===
Installation : dans php.ini, décommenter "extension=ftp" (anciennement "extension=php_ftp.dll").
Déposer un fichier (sans vérifier s'il en écrase un)<ref>http://php.net/manual/fr/book.ftp.php</ref> :
<pre>
<?php
copy('fichier_local.txt', 'ftp://login:password@server/repertoire/fichier_distant.txt');
</pre>
Souvent le répertoire porte le nom de l'utilisateur, et on écrase le fichier s'il existe :
<pre>
<?php
$serveur = 'serveur1';
$login = 'utilisateur1';
$password = 'mdp1';
$fichier = 'fichier1';
copy($fichier, 'ftp://'.$login.':'.$password.'@'.$serveur.'/'.$login.'/'.$fichier, stream_context_create(array('ftp' => array('overwrite'=>True))));
</pre>
Fonctions propres au FTP, pour lire un serveur distant, y télécharger et déposer des fichiers<ref>http://php.net/manual/fr/function.ftp-nlist.php</ref> :
<pre>
$cnx = ftp_connect($serveur);
$loginResult = ftp_login($cnx, $login, $password);
// Liste des noms des dossiers et fichiers du dossier distant (dans le désordre)
$dossierDistant = ftp_nlist($cnx, ".");
var_dump($dossierDistant);
// Liste des éléments du dossier distant avec leurs inodes
$dossierDistant2 = ftp_rawlist($cnx, ".");
var_dump($dossierDistant2);
// Change de répertoire :
var_dump(ftp_pwd($cnx));
ftp_chdir($cnx, 'tmp');
var_dump(ftp_pwd($cnx));
// Téléchargement du dernier fichier distant
sort($dossierDistant);
$distant = $dossierDistant[sizeof($dossierDistant)-1];
$local = 'FichierLocal.txt';
if (!ftp_get($cnx, $local, $distant, FTP_BINARY)) {
echo "Erreur ftp_get\n";
} else {
ftp_delete($cnx, $distant);
}
// Téléversement d'un fichier local
$local = 'FichierLocal2.txt';
if (!ftp_put($cnx, $distant, $local, FTP_ASCII)) {
echo "Erreur ftp_put\n";
}
</pre>
Pour définir le timeout, voir <code>ftp_set_option()</code><ref>http://php.net/manual/fr/function.ftp-set-option.php</ref>.
=== SFTP ===
Prérequis : libssh2-1-dev && libssl-dev. Activer ssh2 dans phpinfo.
On utiliser les trois fonctions suivantes pour construire l'URL ouverte par <code>fopen</code><ref>http://php.net/manual/fr/function.ssh2-sftp.php</ref> :
<pre>
$connection = ssh2_connect('tools-login.wmflabs.org', 22);
ssh2_auth_password($connection, 'monLogin', 'MonMotDePasse');
$sftp = ssh2_sftp($connection);
$stream = fopen("ssh2.sftp://$sftp/monFichier", 'r');
ssh2_disconnect($sftp);
ssh2_disconnect($connection);
</pre>
=== php:// ===
Ce protocole donne accès aux flux (entrant et sortant) de PHP<ref>http://php.net/manual/fr/wrappers.php.php</ref>.
* php://fd : ''<span class="lang-en" lang="en">file descriptor</span>''.
* php://filter
* php://input : lecture de ce qui est posté.
* php://memory
* php://output : écriture.
* php://stderr
* php://stdin
* php://stdout
* php://temp
== Fichiers structurés ==
Les fichiers structurés comme les PDF, XML, DOCX et XLSX peuvent facilement être manipulés par des bibliothèques et frameworks existant, qui seront abordés dans les chapitres suivants.
== Références ==
<references/>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Objets COM=
== Installation ==
Le système d'exploitation Windows fournit des objets COM ([[w:Component Object Model|Component Object Model]]) pour manipuler des fichiers dans divers langages de programmation dont PHP.
Le serveur Web IIS charge déjà ce composant par défaut, pour Apache par contre il faut l'ajouter au php.ini : ''extension=php_com_dotnet.dll''.
Pour être sûr qu'il soit activé ensuite, on peut utiliser<ref>http://php.net/manual/fr/com.configuration.php</ref> :
<pre>
ini_set('com.allow_dcom','1');
</pre>
== Exemple ==
Création d'un .xls :
<pre>
$excel = new COM('excel.application');
$classeur = $excel->Workbooks->Add();
$feuille = $classeur->Worksheets(1);
$cellule = $feuille->Cells(1,1);
$cellule->Value = 'Hello World!';
$classeur->SaveAs('Monclasseur.xls');
$classeur->Close();
$excel->Quit();
</pre>
On peut aussi créer des .doc.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Images=
== Introduction ==
PHP peut créer et modifier dynamiquement des images, par exemple avec la bibliothèque [[w:GD (bibliothèque)|GD]], inclue depuis PHP 3.0.
La création d'une nouvelle image se déroule généralement ainsi :
# Chargement en mémoire d'une image, nouvelle ou existante.
# Chargement éventuel des couleurs à y apporter.
# Modifications éventuelles de ses composants (création de lignes, points, remplissages, ajout de textes...).
# Restitution de l'image après avoir posté son type dans l'en-tête.
# Libération mémoire.
== Créer une nouvelle image ==
Pour créer une image ex-nihilo, on utilise la fonction :
<pre>imagecreatetruecolor($hauteur, $largeur)</pre>
qui crée en mémoire une nouvelle image de hauteur et largeur définies en pixel, et restitue une référence à l'image crée.
Il existe aussi une autre fonction pour cela, mais elle n'est pas recommandée car son amplitude de couleurs est moindre<ref>http://php.net/manual/fr/function.imagecreate.php</ref> :
<pre>imagecreate($largeur, $hauteur)</pre>
Pour charger en mémoire une image sauvegardée sur le disque :
<pre>imagecreatefrom<type>($chemin)</pre>
Exemple :
<pre>$img = imagecreatefrompng('image.png');</pre>
Autre fonction :
<pre>imagecreatefromstring($texte)</pre>
qui crée une image à partir de son format texte, spécifié en paramètre.
S'il survient une erreur, ces fonctions renvoient <code>false</code>.
== Travailler avec les couleurs ==
Pour allouer une couleur il faut en définir les paramètres [[w:Rouge vert bleu|RVB]] :
<pre>$couleur = imagecolorallocate($image, $r, $v, $b)</pre>
Pour définir une transparence dans un PNG :
<pre>imagecolortransparent($image, $couleur)</pre>
où <code>$couleur</code> est le résultat de <code>imagecolorallocate</code>.
Il est également possible de déterminer la transparence, comprise entre 0 et 127 (qui représente la transparence totale) à l'aide de la fonction :
<pre>imagecolorallocatealpha($image, $r, $v, $b, $transparence)</pre>
<div class="remarque"> la première couleur allouée définit le fond de toute l'image.</div>
Sinon, un fond transparent peut également être assuré par les deux fonctions ci-dessous :
<pre>
imageAlphaBlending($image, false);
imageSaveAlpha($image, true);
</pre>
Une fois l'image créée et colorisée, on peut y appliquer les opérations :
* Dessiner des pixels (ex : créer des lignes).
* Travailler sur les pixels existants en désignant des zones.
== Dessiner des formes ==
Pour dessiner un pixel, on utilise ses coordonnées (x, y ci-dessous) :
<pre>imagesetpixel(image, x, y, couleur)</pre>
Pour tracer une ligne entre deux points :
<pre>imageline(image, x1, y1, x2, y2, couleur)</pre>
Pour créer un rectangle par sa diagonale :
<pre>imagerectangle(image, x1, y1, x2, y2, couleur)</pre>
Pour représenter une ellipse par son centre, sa hauteur et sa largeur :
<pre>imageellipse(image, x, y, h, l, couleur)</pre>
ou en précisant son arc par ses angles en gradient (numérotés dans le sens horaire) :
<pre>imagearc(image, x, y, h, l, angle1, angle2, couleur)</pre>
== Retravailler les pixels existants ==
Une des fonctions les plus utilisées pour retravailler des images comme des photos, est certainement <code>imagecopyresized</code>, qui permet de copier une zone rectangulaire pour la coller dans une autre image<ref>http://php.net/manual/fr/function.imagecopyresized.php</ref>. Exemple :
<pre>imagecopyresized(dst_image, src_image, dst_x, dst_y, src_x, src_y, dst_w, dst_h, src_w, src_h);</pre>
où :
*<code>src_image</code> est l'image source ;
*<code>dst_image</code> est l'image de destination ;
*<code>dst_x, dst_y</code> sont les coordonnées de <code>dst_image</code> ;
*<code>src_x, src_y</code> sont les coordonnées de <code>src_image</code> en partant d'en haut à gauche ;
*<code>dst_w, dst_h, src_w, src_h</code> sont les largeurs et hauteurs des rectangles de source et destination.
On peut ensuite comprendre que si <code>dst_w</code> est égal à <code>src_w</code>, et <code>dst_h</code> à <code>src_h</code>, la portion rectangulaire de l'image restera de la même taille. Dans le cas contraire on allonge ou on élargit.
La fonction <code>imagecopyresampled</code> reçoit les mêmes paramètres que <code>imagecopyresized</code>, mais avec la différence que, en cas de redimensionnement, la qualité est améliorée.
Puis il existe la fonction <code>imagefilter</code>, qui permet de nombreux effets tels que les niveaux de gris, le relief, ou la recoloration<ref>http://fr.php.net/manual/fr/function.imagefilter.php</ref>.
== Imprimer l'output ==
Le format de l'image obtenue ("png", "jpeg" ou "gif") peut être spécifié par la fonction <code>header</code>, via le ''content-type'' (par défaut ''text/html'') ainsi :
<pre>header("Content-type: image/<type>");</pre>
Pour visualiser l'image ensuite, la placer en paramètre dans une fonction dépendant de son type : <code>imagepng</code>, <code>imagejpeg</code> ou <code>imagegif</code>.
Puis, libérer la mémoire avec <code>imagedestroy($image)</code>. Cette étape facultative est particulièrement recommandée pour les images volumineuses.
== Exemple ==
Le code suivant affiche dans le navigateur, un carré rouge de 50 pixels dans un noir de 100.
<pre>
$image = imagecreatetruecolor(100, 100);
$couleur = imagecolorallocate($image, 255, 0, 0);
imagefilledrectangle($image,0,0,50,50,$couleur);
header("Content-type: image/png");
imagepng($image);
imagedestroy($image);
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Mails=
== <code>Mail()</code> ==
Le code suivant se connecte à localhost:25 pour envoyer un mail<ref>http://php.net/manual/fr/function.mail.php</ref> :
<pre>
<?php
$to = 'Destinataire@gmail.com';
$subject = 'Sujet du mail';
$message = 'Contenu du message';
$headers = 'From: Expediteur@gmail.com' . "\r\n" .
'Reply-To: Expediteur@gmail.com' . "\r\n" .
'X-Mailer: PHP/' . phpversion();
mail($to, $subject, $message, $headers);
</pre>
Si la machine hébergeant le script n'est pas pourvue d'un serveur SMTP, le freeware portable ''Simple Mail Server''<ref>https://sourceforge.net/projects/simplemailsvr/</ref> peut jouer ce rôle rapidement sans installation.
== Utilisation de Mail() plus complexe... ==
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple</div>
|-
|<pre>
<?php
$mail = 'test@test.fr'; // Déclaration de l'adresse de destination.
if (!preg_match("#^[a-z0-9._-]+@(hotmail|live|msn).[a-z]{2,4}$#", $mail)) { // On filtre les serveurs qui rencontrent des bogues.
$passage_ligne = "\r\n";
} else {
$passage_ligne = "\n";
}
//=====Déclaration des messages au format texte et au format HTML.
$message_txt = "Bonjour, voici un e-mail envoyé par un script PHP.";
$message_html = "<html><head></head><body><b>Bonjour</b>, voici un e-mail envoyé par un <i>script PHP</i>.</body></html>";
//==========
//=====Création de la boundary
$boundary = "-----=".md5(rand());
//==========
//=====Définition du sujet.
$sujet = "Hey mon ami !";
//=========
//=====Création du header de l'e-mail.
$header = "From: \"WeaponsB\"<test@test.fr>".$passage_ligne;
$header.= "Reply-to: \"WeaponsB\" <test@test.fr>".$passage_ligne;
$header.= "MIME-Version: 1.0".$passage_ligne;
$header.= "Content-Type: multipart/alternative;".$passage_ligne." boundary=\"$boundary\"".$passage_ligne;
//==========
//=====Création du message.
$message = $passage_ligne."--".$boundary.$passage_ligne;
//=====Ajout du message au format texte.
$message.= "Content-Type: text/plain; charset=\"ISO-8859-1\"".$passage_ligne;
$message.= "Content-Transfer-Encoding: 8bit".$passage_ligne;
$message.= $passage_ligne.$message_txt.$passage_ligne;
//==========
$message.= $passage_ligne."--".$boundary.$passage_ligne;
//=====Ajout du message au format HTML
$message.= "Content-Type: text/html; charset=\"ISO-8859-1\"".$passage_ligne;
$message.= "Content-Transfer-Encoding: 8bit".$passage_ligne;
$message.= $passage_ligne.$message_html.$passage_ligne;
//==========
$message.= $passage_ligne."--".$boundary."--".$passage_ligne;
$message.= $passage_ligne."--".$boundary."--".$passage_ligne;
//==========
//=====Envoi de l'e-mail.
mail($mail,$sujet,$message,$header);
//==========
</pre>
|}
Explication:
*Ouverture boundary.(sert à séparer les différentes parties de notre e-mail)
*Déclaration de type (exemple texte, par défaut les clients mail tentent de convertir l'HTML en texte).
*Texte.
*Ouverture boundary.
*Déclaration de type (exemple HTML,par défaut les clients mail tentent de convertir l'HTML en texte)).
*HTML.
*Fermeture boundary.
*Fermeture boundary.
== Avec la mise en place de pièce jointe ==
{|border=1
|
<div style="background:#9999ff80; border:solid 1px #55555580;">
Exemple</div>
|-
|<pre>
<?php
$mail = 'test@test.fr'; // Déclaration de l'adresse de destination.
if (!preg_match("#^[a-z0-9._-]+@(hotmail|live|msn).[a-z]{2,4}$#", $mail)) { // On filtre les serveurs qui présentent des bogues.
$passage_ligne = "\r\n";
} else {
$passage_ligne = "\n";
}
//=====Déclaration des messages au format texte et au format HTML.
$message_txt = "Bonjour, voici un e-mail envoyé par un script PHP.";
$message_html = "<html><head></head><body><b>Bonjour</b>, voici un e-mail envoyé par un <i>script PHP</i>.</body></html>";
//==========
//=====Lecture et mise en forme de la pièce jointe.
$fichier = fopen("background.jpg", "r");
$attachement = fread($fichier, filesize("background.jpg"));
$attachement = chunk_split(base64_encode($attachement));
fclose($fichier);
//==========
//=====Création de la boundary.
$boundary = "-----=".md5(rand());
$boundary_alt = "-----=".md5(rand());
//==========
//=====Définition du sujet.
$sujet = "Fichier important";
//=========
//=====Création du header de l'e-mail.
$header = "From: \"WeaponsB\"<test@test.fr>".$passage_ligne;
$header.= "Reply-to: \"WeaponsB\" <weaponsb@mail.fr>".$passage_ligne;
$header.= "MIME-Version: 1.0".$passage_ligne;
$header.= "Content-Type: multipart/mixed;".$passage_ligne." boundary=\"$boundary\"".$passage_ligne;
//==========
//=====Création du message.
$message = $passage_ligne."--".$boundary.$passage_ligne;
$message.= "Content-Type: multipart/alternative;".$passage_ligne." boundary=\"$boundary_alt\"".$passage_ligne;
$message.= $passage_ligne."--".$boundary_alt.$passage_ligne;
//=====Ajout du message au format texte.
$message.= "Content-Type: text/plain; charset=\"ISO-8859-1\"".$passage_ligne;
$message.= "Content-Transfer-Encoding: 8bit".$passage_ligne;
$message.= $passage_ligne.$message_txt.$passage_ligne;
//==========
$message.= $passage_ligne."--".$boundary_alt.$passage_ligne;
//=====Ajout du message au format HTML.
$message.= "Content-Type: text/html; charset=\"ISO-8859-1\"".$passage_ligne;
$message.= "Content-Transfer-Encoding: 8bit".$passage_ligne;
$message.= $passage_ligne.$message_html.$passage_ligne;
//==========
//=====On ferme la boundary alternative.
$message.= $passage_ligne."--".$boundary_alt."--".$passage_ligne;
//==========
$message.= $passage_ligne."--".$boundary.$passage_ligne;
//=====Ajout de la pièce jointe.
$message.= "Content-Type: image/jpeg; name=\"background.jpg\"".$passage_ligne;
$message.= "Content-Transfer-Encoding: base64".$passage_ligne;
$message.= "Content-Disposition: attachment; filename=\"background.jpg\"".$passage_ligne;
$message.= $passage_ligne.$attachement.$passage_ligne.$passage_ligne;
$message.= $passage_ligne."--".$boundary."--".$passage_ligne;
//==========
//=====Envoi de l'e-mail.
mail($mail,$sujet,$message,$header);
//==========
</pre>
|}
Explication:
*Ouverture boundary.
*Déclaration du nouveau content-type et de la seconde boundary.
*Ouverture boundary_2.
*Déclaration de type (exemple texte).
Texte.
*Ouverture boundary_2.
*Déclaration de type (exemple HTML).
HTML.
*Fermeture boundary_2.
*Ouverture boundary.
*Déclaration de la pièce jointe 1.
*Ouverture boundary.
*Déclaration de la pièce jointe 2.
*Ouverture boundary.
*Déclaration de la pièce jointe [...].
*Ouverture boundary.
*Déclaration de la pièce jointe n.
*Fermeture boundary.
== Bibliothèques ==
Ce livre abordera dans un chapitre ultérieur, des bibliothèques contenant des fonctions d'envoi d'email permettant plus d'options dans une syntaxe nettement plus concise :
* [[w:en:PHPMailer|PHPMailer]]<ref>http://sourceforge.net/projects/phpmailer/</ref>
* ''PEAR Mail''<ref>http://pear.php.net/package/Mail/download</ref>, ''Net SMTP''<ref>http://pear.php.net/package/Net_SMTP/download</ref> et ''Net Socket''<ref>http://pear.php.net/package/Net_Socket/download</ref>, et ''Mail Mime''<ref>http://pear.php.net/package/Mail_Mime/download</ref> pour ajouter un fichier joint.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Sécurité=
== Sécuriser les en-têtes HTTP ==
Pour commencer, il est préférable d'utiliser HTTPS à HTTP pour complexifier l'[[w:attaque de l'homme du milieu|attaque de l'homme du milieu]], en garantissant l'intégrité et la confidentialité du flux réseau.
Ceci peut être forcé par le mécanisme [[w:HSTS|HSTS]], qui utilise l'en-tête HTTP ''Strict-Transport-Security''.
=== Masquer les versions ===
Pour se prémunir des exploitations des failles de sécurité liées à sa version de PHP ou de serveur Web, on peut chercher régulièrement si certaines ont été détectées et ont une mise à jour sur https://www.exploit-db.com/.
Mais pour éviter d'être la cible de celles qui fonctionneraient, il est préférable de masquer ces versions dans les en-têtes HTTP.
Ceci se fait généralement au niveau des fichiers de configuration du serveur, mais peut aussi être réalisé en PHP ainsi :
<pre>
ini_set('expose_php', 'Off');
header('X-Powered-By: UnknownWebServer');
</pre>
=== Restreindre les connexions ===
De plus, il existe d'autres sécurisations des en-têtes HTTP pour se prémunir des attaques de type [[w:Cross-site request forgery|Cross-site request forgery]] (CSRF).
Exemple de couche de protection utilisant le [[w:Content Security Policy|Content Security Policy]] (CSP) :
<pre>
ini_set('register_globals', 'Off');
header('Content-Security-Policy "default-src \'self\'; style-src \'self\' \'unsafe-inline\'; script-src \'self\' \'unsafe-inline\'; img-src \'self\' data:"');
header('X-Frame-Options "SAMEORIGIN" always');
header('X-Content-Type-Options nosniff');
header('Referrer-Policy: origin');
header('Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()"');
</pre>
Voir aussi [[w:Cross Origin Ressource Sharing|Cross Origin Ressource Sharing]] (CORS) avec ''Access-Control-Allow-Origin''.
=== OpenSSL ===
PHP propose des fonctions des bibliothèques [[OpenSSL|OpenSSL]], pour le chiffrement et déchiffrement symétrique et asymétrique<ref>https://www.php.net/manual/fr/ref.openssl.php</ref>.
Par exemple, pour signer un en-tête HTTP (et vérifier ensuite) :
<pre>
$myHeaderStringToSign = '...';
$privateKey = openssl_pkey_get_private(
file_get_contents($this->myPrivateKeyPath),
$this->myPassphrase
);
openssl_sign($myHeaderStringToSign, $signature, $privateKey, OPENSSL_ALGO_SHA512);
var_dump($signature);
$publicKey = file_get_contents($this->myPublicKeyPath);
$isValid = openssl_verify($myHeaderStringToSign, $signature, $publicKey, OPENSSL_ALGO_SHA512);
var_dump($isValid);
</pre>
== Protéger l'accès à l'application ==
Pour éviter que des robots spammeurs polluent l'application via ses formulaires et ses API, ou la saturent par une [[w:attaque par déni de service|attaque par déni de service]] (''DoS attack''), il y a plusieurs solutions complémentaires :
* Filtrer l'accès par un whitelistage d'IP (ex : via [[Le système d'exploitation GNU-Linux/Protection avec iptables|iptables]]).
* Configurer un ''throttle'' dans le serveur Web, qui va empêcher un trop grand nombre de connexions de la même provenance (via iptables aussi).
* Chiffrer la connexion par un [[w:VPN|VPN]] (impossible les utilisateurs prévus sont inconnus et proviennent des moteurs de recherche).
* Échapper les caractères interprétés lors des attaques de type [[w:injection SQL|injection SQL]] (généralement les apostrophes), et [[w:cross-site scripting|cross-site scripting]] (alias XSS, qui injecte du JavaScript entre balises HTML "script"), ou [[w:clickjacking|clickjacking]] (balises "iframe"). En échappant les chevrons cela suffit)<ref>https://github.com/WebGoat/WebGoat</ref>.
* Imposer aux utilisateurs des mots de passe complexes (c'est-à-dire impossible à deviner par robots avant plusieurs années de tentatives infructueuses).
* Installer un [[w:captcha|captcha]] sur les formulaires accessibles aux utilisateurs non connectés. Par exemple [[w:reCAPTCHA|reCAPTCHA]], ou sur [[MediaWiki]] ''[[mw:Extension:ConfirmEdit|ConfirmEdit]]''.
* Filtrer les messages postés sur l'application en les comparant aux listes partagées de spams connus, comme l'API [[WordPress/Plugins/Akismet|Akismet]]<ref>https://symfony.com/doc/current/the-fast-track/fr/16-spam.html</ref>, ou sur MediaWiki ''[[mw:Extension:TitleBlacklist|TitleBlacklist]]''.
* Utiliser PHP >= 8 qui protège par défaut contre les attaques [[w:en:XML external entity attack|XML external entity attack]] (XXE)<ref>https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#php</ref>.
== Protéger les données sensibles ==
Le fait de stocker les mots de passe en clair, dans un fichier ou une base de connées constitue une faille de sécurité.
Il est donc obligatoire de les hasher. Pour réaliser cela, il existe plusieurs fonctions PHP:
* hash($algo, $data)<ref>https://www.php.net/manual/fr/function.hash.php</ref> : avec un algorithme de la liste <code>hash_algos()</code><ref>https://www.php.net/manual/fr/function.hash-algos.php</ref>. Ex : <code>hash('sha256', $maChaine)</code>. Déconseillé car pas adapté pour les mots de passe.
* crypt()<ref>http://php.net/manual/fr/function.crypt.php</ref> est une [[w:fonction de hachage|fonction de hachage]] à sens unique d'une chaine, qui accepte en deuxième paramètre optionnel un [[w:Salage (cryptographie)|salage]]. Déconseillé car vulnérable à certaines attaques.
* [[w:SHA-1|sha1]]() : déconseillé car facile à casser<ref>https://www.php.net/manual/fr/function.sha1.php</ref>.
* [[w:MD5|md5]]() : déconseillé car facile à casser<ref>https://www.php.net/manual/fr/function.md5.php</ref>.
La méthode actuellement recommandée pour hasher un mot de passe est l'utilisation de la fonction <code>password_hash()</code> conjointement avec <code>password_verify()</code> pour vérifier si un mot de passe saisi par l'utilisateur est correct.<ref>https://www.php.net/manual/fr/function.password-hash.php</ref><ref>https://www.php.net/manual/fr/function.password-verify.php</ref>
== Scanners ==
Certaines sociétés proposent des pentests payant (en boite noire ou blanche).
Mais un bon outil gratuit permet déjà de se faire une idée : https://observatory.mozilla.org.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Programmation orientée objet=
== Introduction ==
Une classe est un format de variable non scalaire, comprenant trois types de composants :
# des constantes, accessibles par réflexion avec <code>$maClasse::getConstants()</code>.
# des variables appelées "propriétés", accessibles avec <code>$maClasse::getProperties()</code>.
# des fonctions appelées "méthodes", accessibles avec <code>$maClasse::getMethods()</code>.
La [[programmation orientée objet]] s’effectue en deux étapes : la définition des classes, puis leur utilisation. Une fois la classe définie, il est en effet possible de créer des objets, appelés "instances", au format de la classe définie. Toutefois, les composants déclarés avec le mot <code>static</code> sont persistants, et accessibles sans instanciation préalable.
=== Opérateur objet ===
Pour accéder aux propriétés et méthodes d'un objet, on utilise l'[[wikt:opérateur object|opérateur object]] : <code>-></code>.
=== Opérateur de résolution de portée ===
Pour accéder aux constantes, propriétés et méthodes statiques d'une classe, on utilise l'[[wikt:opérateur de résolution de portée|opérateur de résolution de portée]] : <code>::</code>.
Cet opérateur peut également être précédé de noms de classes ou des mots réservés suivants<ref>http://php.net/manual/fr/language.oop5.paamayim-nekudotayim.php</ref> :
* <code>$this</code> : l'objet courant.
* <code>parent</code> : la classe parente.
* <code>static</code> : la classe courante.
* <code>self</code> : la classe parente puis la courante s'il n'y a rien<ref>https://www.php.net/manual/fr/language.oop5.static.php#104823</ref>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Le mot-réservé <code>static</code> a donc deux sens : un pour les déclarations et un pour les appels.</p>
</div>
</div>
== Inclusion ==
À l'instar d'une bibliothèque de fonctions, une classe est généralement stockée dans un fichier dédié, qui peut porter son nom.
Elle s'inclut donc dans un programme de la même manière qu'une bibliothèque :
<pre>
include('ma_classe.php');
// ou
include_once('ma_classe.php');
// ou
require('ma_classe.php');
// ou
require_once('ma_classe.php');
</pre>
Mais la syntaxe à privilégier est celle par espace de nom :
<pre>
use mon_namespace/ma_classe;
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">En PHP, l'inclusion doit précéder les appels du code qui y figure.</p>
</div>
</div>
<div class="remarque"> Les classes et fonctions globales peuvent être appelées directement dans le code, ou avec le préfixe "\" (signifiant "namespace global"). Mais il existe aussi <code>use function ma_fonction</code> pour déclarer l'utilisation d'une fonction.</div>
<div class="remarque"> Avant PHP7.4, on pouvait mettre une URL dans ces fonctions, si la configuration allow_url_include=1<ref>https://www.php.net/manual/fr/filesystem.configuration.php</ref>. Ex :
require_once("http://example.com/");</div>
== Instanciation ==
Une fois la classe incluse, on peut l'appeler.
* Directement pour une classe statique.
* Après instanciation sinon. Elle est réalisée par le mot-clé "new".
Par défaut, PHP fournit déjà la classe suivante pour créer des objets anonymes :
<pre>
$c = new stdClass();
var_dump($c);
</pre>
== Définition des classes ==
Définir une nouvelle classe adopte la syntaxe suivante :
<pre>
class nomObjet
{
var $variable1;
var $variable2;
...
function maFonction1()
{
...code
}
function maFonction2()
{
}
}
</pre>
Il est possible d’attribuer une valeur par défaut. Le code dans la classe est alors <code>var $variable1 = valeur;</code>. Cette syntaxe est économe puisqu'elle évite d'initialiser la variable à chaque appel des méthodes qui l'utilisent.
La définition de méthodes de classe est identique à celle de n’importe quelle fonction à la différence que lorsqu’elle fait référence à une variable de sa classe, <code>'''$'''variable</code> doit être :
* <code>'''$'''this->variable</code> pour cibler l'objet instancié (et <code>'''$'''this::constante</code>, <code>'''$'''this->méthode()</code>).
* <code>self::variable</code> pour cibler la classe statique.
De même pour exécuter une autre méthode de sa classe. ex :
<pre>
class client
{
var $aDitBonjour = false;
function direBonjour()
{
$this->message("Bonjour");
}
function message($message)
{
echo $message;
$this->aDitBonjour = true;
}
}
</pre>
Pour utiliser une variable qui n'est pas dans la classe ou exécuter les méthodes d'une autre classe, il faut les redéclarer avec <code>global</code> :
<pre>
class client
{
function message($message)
{
global $InstanceAutreClasse;
$InstanceAutreClasse->aDitBonjour = true;
}
}
</pre>
== Utilisation d’un objet ==
Attention : la classe est la définition d’un format de variable personnalisable. Le code n’est pas exécuté et il est impensable d’introduire le code suivant qui n’aurait aucun sens :
<pre>
class client
{
for ($i=0; $i<5; $i++)
echo "$i\n";
}
</pre>
Une fois la classe définie, il va falloir créer des variables objet du format de la classe définie. On crée un objet par le code suivant :
<pre>
$objet = new client();
</pre>
Il faut bien entendu avoir préalablement défini la classe client. La variable <code>$objet</code> contient donc un objet. Pour accéder à une variable pour lui faire subir des modifications, il suffit d’entrer le code suivant :
<pre>
$objet->variable1 = "Hello world";
</pre>
Il est possible de lui faire subir les mêmes opérations qu’à une variable normale. De même pour exécuter une fonction :
<pre>
$objet->maFonction();
</pre>
Autant les méthodes une fois définies ne peuvent pas être modifiées, autant il est possible d’ajouter ou de supprimer des variables dans l’objet :
<pre>
$objet->variable = "valeur"; // définition de variable
unset($objet->variable); // suppressions
</pre>
L’objet est unique, de sorte que s’il est enregistré dans une autre variable et qu’une modification lui est faite, elle sera visible pour les deux variables :
<pre>
//Le code reprend l'ancien script
$objet = new client();
$objet2 = $objet;
$objet2->direBonjour();
echo $objet->aDitBonjour;
//affiche true
</pre>
Pour dupliquer une variable de type objet, il faut donc entrer le code suivant :
<pre>
$objet2 = clone $objet;
</pre>
La nouvelle variable sera différente de l’ancienne mais aura les mêmes valeurs.
Il est également possible d'exécuter la méthode d'un objet sans avoir créé de variable auparavant :
<pre>
class Message
{
function direBonjour()
{
echo "salut";
}
}
/* Exécute la méthode */
Message::direBonjour();
</pre>
== Héritage ==
PHP était initialement un langage à héritage simple<ref>{{ouvrage|url=https://books.google.fr/books?id=IfEft8zQgQcC&pg=PT362&dq=php+%22h%C3%A9ritage+simple%22&hl=fr&sa=X&ved=0ahUKEwjFpuyt76PPAhVCqxoKHdiyCwkQ6AEILDAB#v=onepage&q=%22PHP%20autorise%20l%27h%C3%A9ritage%20simple%20mais%20pas%20l%27h%C3%A9ritage%20multiple%22&f=false|titre=Algorithmique : Techniques fondamentales de programmation (avec des exemples en PHP)|prénom1=Sébastien|nom1=Rohaut|éditeur=Editions ENI|année=2009}}</ref>, c'est-à-dire qu'une classe ne peut hériter que d'au plus une seule autre classe.
L'héritage consiste à transmettre les propriétés et méthodes d’une classe mère à une classe fille, en déclarant cette dernière avec <code>extends</code>. Ex :
<pre>
class parent1
{
var $varParent;
function méthodeParente()
{
print 'Je connais méthodeParente' . PHP_EOL;
}
}
class enfant extends parent1
{
var $varEnfant;
function méthodeEnfant()
{
print 'Je connais méthodeEnfant' . PHP_EOL;
}
}
$Enfant1 = new enfant();
$Enfant1->méthodeParente();
</pre>
L'héritage permet le [[w:Polymorphisme (informatique)|polymorphisme]], qui consiste à utiliser des variables ou méthodes dans des classes de plusieurs types, grâce à l'héritage.
Les classes filles bénéficieront automatiquement de toutes les propriétés et des méthodes de leur classe mère (qui n'a pas de limite dans le nombre de ses filles<ref>http://php.net/manual/fr/language.oop5.basic.php</ref>).
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Les interfaces peuvent par contre bénéficier d'un héritage multiple.</p>
</div>
</div>
On peut aussi invoquer les méthodes parentes depuis la classe enfant :
<pre>
class enfant2 extends parent1
{
var $varEnfant;
function méthodeEnfant()
{
parent::méthodeParente();
print 'Je connais méthodeEnfant2' . PHP_EOL;
}
}
$Enfant2 = new enfant2();
$Enfant2->méthodeEnfant();
</pre>
=== Traits ===
Depuis PHP 5.4.0, une structure de données appelée "[[wikt:trait|trait]]" permet l'héritage multiple. Exemple d'utilisation :
<pre>
<?php
trait MonTrait1
{
function Hello()
{
print 'Hello';
}
}
trait MonTrait2
{
function World()
{
print 'World';
}
}
class MaClasse1
{
use MonTrait1;
use MonTrait2;
function __construct()
{
$this->Hello();
$this->World();
}
}
$Test = new MaClasse1;
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Les traits sont limités par rapport aux classes :
* Un trait ne peut pas contenir de constante.
* Un trait ne peut pas hériter d'une classe, il doit utiliser un autre trait à la place.
</p>
</div>
</div>
De plus, ce type d'injection de dépendance est contraire au principe [[w:SOLID (informatique)|SOLID]] d'[[w:inversion des dépendances|inversion des dépendances]].
=== Final ===
Pour empêcher une classe ou une méthode d'être étendue (et en faire donc une [[wikt:classe finale|classe finale]] ou une [[wikt:méthode finale|méthode finale]]), on peut la déclarer avec le mot-clé <code>final</code>. Ex :
<pre>
final class MaClasseFinale
{
...
}
</pre>
== Classes abstraites ==
La classe abstraite ne peut pas être instanciée, mais elle peut être appelée en statique. Comme pour l'héritage classiques, ses classes filles accèdent à ses attributs et méthodes publics et protégés.
Voici un exemple de [[w:classe abstraite|classe abstraite]] :
<pre>
abstract class MaClasseAbstraite
{
public $var="Bonjour";
abstract protected function MaMethode($var1, $var2);
protected function MaMethode2($var1, $var2)
{
return 'TODO';
}
}
</pre>
Dans cet exemple, on voit que les méthodes d'une classe abstraite peuvent contenir du code, mais les méthodes abstraites non (elles ne définissent que les arguments<ref>http://php.net/manual/fr/language.oop5.abstract.php</ref>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Les méthodes abstraites sont obligatoirement à implémenter par les classes filles.</p>
</div>
</div>
== Closures ==
Apparues avec PHP 5.3<ref>http://php.net/manual/fr/class.closure.php</ref>, les closures sont des classes avec des méthodes gérant les [[wikt:fonction anonyme|fonctions anonymes]].
== Classes anonymes ==
Apparues avec PHP 7<ref>https://secure.php.net/manual/fr/language.oop5.anonymous.php</ref>, les [[wikt:classe anonyme|classes anonymes]] sont des classes sans nom, déclarées lors de l'exécution.
== Interfaces ==
Voici un exemple d'[[w:Interface (programmation orientée objet)|interface]] :
<pre>
interface MonInterface
{
public function setName($name);
public function getName();
}
</pre>
Et son utilisation : la classe doit reprendre les méthodes de l'interface sous peine d'erreur.
<pre>
class MaClasse implements MonInterface
{
private $myName;
public function setName($name)
{
print 'Définition de '.$name;
$myName = $name;
}
public function getName()
{
print 'Récupération de '.$myName;
}
}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">
* Les méthodes d'une interface ne peuvent pas contenir de code.
* Une classe ou une interface ne peut implémenter qu'une ou plusieurs interfaces (donc pas d'implémentation de classe).
* Une interface ne peut hériter que d'une autre interface<ref>https://www.safaribooksonline.com/library/view/php-5-power/013147149X/013147149X_ch03lev1sec14.html</ref>.
* Toutes les méthodes d'une interface doivent être publiques.
* Si un objet hérite et implémente, toujours le déclarer en plaçant le <code>extends</code> avant le <code>implements</code>.
</p>
</div>
</div>
== Namespaces ==
Exemple d'[[w:Espace_de_noms_(informatique)|espace de noms]] :
<pre>
namespace MonEspace\Nom;
class MaClasse {}
function MaMethode() {}
const MYCONST = 1;
$a = new MaClasse;
$c = new \MonEspace\Nom\MaClasse;
$d = new \ClasseGlobale;
</pre>
Pour utiliser un namespace, "use" conserve son nom mais on peut le changer avec "as" :
<pre>
use MonEspace\Nom;
use SonEspace\Nom as NomExterne;
</pre>
Depuis PHP7 on peut même importer plusieurs classes, fonctions ou constantes sur la même ligne :
<pre>
use MonEspace\{MaClasseA, MaClasseB as B};
</pre>
== Portée des variables ==
Il est possible depuis PHP5 de préciser l'accès à certaines variables ou méthodes, en les déclarant à la place de <code>var</code> avec :
* <code>public</code> : visible dans tout le programme.
* <code>protected</code> : visible uniquement dans les instances de la classe et de ses sous-classes.
* <code>private</code> : visible uniquement dans les instances de la classe.
Exemple :
<pre>
class CompteEnBanque
{
private $argent = 0;
private function ajouterArgent($valeur)
{
$this->argent += $valeur;
}
function gagnerArgent($valeur)
{
$this->ajouterArgent($valeur);
}
}
$compte = new CompteEnBanque()
//les actions suivantes sont impossibles :
$compte->argent = 3000;
$compte->ajouterArgent(3000);
//l'action suivante est possible
$compte->gagnerArgent(3000);
</pre>
En effet, il faut gagner de l’argent avant d’en ajouter à la banque (quoique...).
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Ce code retournera un message d’erreur s'il est exécuté sous PHP5 ou une version ultérieure.</p>
</div>
</div>
== Les méthodes prédéfinies ==
Il existe quelques méthodes prédéfinies qui s’exécutent automatiquement à des périodes de la vie de l’objet. Elles sont appelées ''méthodes magiques''<ref>http://php.net/manual/fr/language.oop5.magic.php</ref>, et leurs noms commencent toujours par deux underscores :
# __call() : à chaque appel d'une méthode de la classe.
# __callStatic() : à chaque appel statique d'une méthode de la classe.
# __clone() : lors du clonage de l'objet (via la fonction "clone").
# __construct() : à l'instanciation de la classe.
# __debugInfo() : modifie les résultats des <code>var_dump()</code>.
# __destruct() : à la suppression de l'objet instancié.
# __get() : à la lecture de propriétés inexistantes ou interdites.
# __invoke() : à l'appel de l'objet comme une fonction (ex : <code>echo $object(1)</code>).
# __isset() : à l'appel de <code>isset()</code> (ou <code>empty()</code>) sur des propriétés inexistantes ou interdites.
# __serialize() : à l'appel de <code>serialize()</code>.
# __set() : à l'écriture de propriétés inexistantes ou interdites.
# __set_state() : modifie les résultats des <code>var_export()</code>.
# __sleep() : à l'appel de <code>serialize()</code>, pour en modifier le résultat.
# __toString() : à l'appel de l'objet comme une chaine de caractères (ex : <code>echo $object</code>).
# __unserialize() : à l'appel de <code>serialize()</code>.
# __unset() : à l'appel de <code>unset()</code> sur des propriétés inexistantes ou interdites.
# __wakeup() : à l'appel de <code>unserialize()</code>, pour en modifier le résultat.
=== Constructeur et destructeur ===
; <code>__construct()</code> : Cette méthode s’exécute lors de la création de l’objet. On entre alors les attributs potentiels de la fonction lors de sa création. Cette méthode est appelée "le constructeur"
; <code>__destruct()</code> : Cette méthode s’exécute au contraire au moment de la destruction de la variable. Elle est appelée "le destructeur".
Voici un exemple utilisant les méthodes :
<pre>
//Définition de la classe
class Humain
{
public $homme = false;
public $femme = false;
function __construct($type)
{
if ($type=="homme")
$this->homme=true;
if ($type=="femme")
$this->femme=true;
}
function extremeOnction()
{
echo 'Amen';
}
function __destruct()
{
$this->extremeOnction();
}
}
//C'est un garçon !
$homme = new Humain("homme");
if ($homme->homme) {
echo "C'est un homme";
} elseif ($homme->femme) {
echo "C'est une femme";
}
//mort de l'homme
unset($homme);
/*
La sortie sera
C'est un homme
Amen
*/
</pre>
Sous php4, le constructeur avait pour nom celui de la classe. Sous php5, si la fonction <code>__construct()</code> n’est pas trouvée, l’interpréteur cherchera une méthode du même nom que la classe.
=== Copie en profondeur ===
Il existe une méthode qui s’exécute lors d’une duplication de l’objet. Son nom est <code>__clone()</code>.
En effet, elle est utile car par défaut si $x = $y, $x n'est qu'une référence à $y et changer $x changera $y.
=== __get, __set, __call ===
Ces méthodes permettent de rendre dynamique l'utilisation de la classe, et permettent la surcharge magique</code><ref>http://php.net/manual/fr/language.oop5.overloading.php</ref>.
==== __call() ====
La méthode <code>__call()</code> s'exécute quand une méthode appelée est inaccessible ou inexistante. Exemple :
<pre>
function __call($method,$arguments)
{
echo "On a appelé $method sans succès avec les paramètres :<br/>";
var_dump($arguments);
}
</pre>
==== _get() ====
Cette méthode s'exécute quand une variable appelée est inaccessible ou inexistante. L'exemple ci-dessous lui permet de retourner une donnée dépendant du contenu de la variable <code>$nom</code>. Important : le contenu de la variable <code>$nom</code> ne sera pas prioritaire sur le nom d'une variable interne à la classe.
<pre>
class test
{
public $a;
private $b;
function __construct($a,$b)
{
$this->a=$a;
$this->b=$b;
}
function __get($nom)
{
echo "On a appelé __get(\$$nom)";
}
}
// Utilisation
$var=new test(5,10);
echo $var->a; // affiche : "5"
echo '<br/>';
echo $var->b; // affiche : "On a appelé __get($b)". En effet, b est privée et ne peut donc pas être accédée.
echo '<br/>';
echo $var->__get('a'); // affiche : "On a appelé __get($a)"
echo '<br/>';
echo $var->c; // affiche : "On a appelé __get($c)"
</pre>
On voit ici que PHP va chercher en priorité à retourner une variable interne, mais si elle est privée ou inexistante, il prendra le résultat du <code>__get</code>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Ne jamais accéder à une variable de classe privée dans son <code>__get()</code> sous peine de boucle infinie.</p>
</div>
</div>
Exemple de réécriture de la méthode <code>__get</code> ci-dessus pour accéder à la variable privée :
<pre>
function __get($nom)
{
if ($nom == 'b') {
echo $this->b;
}
}
</pre>
==== __set() ====
Cette méthode s'exécute quand on modifie une variable inaccessible ou inexistante. Exemple :
<pre>
class test2
{
public $a;
private $b;
function __construct($a,$b)
{
$this->a=$a;
$this->b=$b;
}
function __get($nom)
{
echo 'get '.$nom;echo '<br/>';
}
function __set($nom,$value)
{
echo 'set '.$nom.' '.$value;echo '<br/>';
}
}
$var=new test2(5,10);
$var->a=6;
echo $var->a; // affiche 6
echo '<br/>';
$var->b=11; // appelle __set('b',11)
echo $var->b; // appelle __get('b')
</pre>
=== __sleep() et __wakeup() ===
Ces méthodes ne fonctionnent plus avec l'interface ''Serializable'' depuis PHP 8.1, au profit de serialize et unserialize<ref>https://www.php.net/manual/en/class.serializable.php</ref>.
Elles permettent respectivement de sauvegarder et restaurer l'état d'un objet, pour qu'il soit fonctionnel après une sérialisation / désérialisation. C'est utile par exemple pour se reconnecter à une base de données.
=== __invoke() ===
Cette méthode rend la classe invocable, c'est-à-dire qu'après instanciation, elle s'exécute si on l'appelle comme une méthode. Ex :
<pre>
$maClasse = new MaClasse();
return $maClasse();
</pre>
== Quelques fonctions intégrées ==
Voici quelques fonctions en relation avec la programmation orientée objet qui peuvent vous être utiles.
=== self() ===
Une classe peut s'instancier elle-même avec <code>new self();</code>.
=== class_exists() ===
Vérifie qu’une classe existe. Renvoie une valeur booléenne. ex :
<pre>
if (class_exists('maClasse'))
$var = new maClasse();
</pre>
=== get_class_methods() ===
Retourne toutes les méthodes d’une classe sous forme de tableau. Ex :
<pre>
$maClasse = new Classe();
$methodes = get_class_methods($maClasse);
print_r($methodes);
</pre>
=== get_class_vars() ===
Retourne tous les attributs d'une classe (dont la portée est accessible, donc généralement les publiques), ainsi que leurs valeurs par défaut sous forme de tableau. Ex :
<pre>
$attributs = get_class_vars('Classe');
print_r($attributs);
// Fonctionne aussi avec les instances :
$maClasse = new Classe();
$attributs = get_class_vars(get_class($maClasse));
print_r($attributs);
</pre>
Peut donc servir pour un "foreach" propriétés de la classe.
Pour récupérer ou filtrer les attributs privés, utiliser <code>\ReflectionClass::getProperties</code><ref>https://www.php.net/manual/fr/reflectionclass.getproperties.php</ref> :
<pre>
$reflecttion = new \ReflectionClass('Classe');
print_r($reflection->getProperties());
</pre>
=== get_object_vars() ===
Idem avec les valeurs courantes de l'objet instance de classe.
=== method_exists($classe, $méthode) ===
Teste sur une méthode existe dans une classe.
=== serialize() et unserialize() ===
Assurent la transformation du flux de données, en précisant les types des variables et index des tableaux. Exemple :
<pre>
$Hello = 'Hello World';
var_dump($Hello); // string(11) "Hello World"
$Hello = serialize($Hello);
print $Hello; // s:11:"Hello World";
$Hello = array('Hello', 'World');
var_dump($Hello); // array(2) { [0]=> string(5) "Hello" [1]=> string(5) "World" }
$Hello = serialize($Hello);
print $Hello; // a:2:{i:0;s:5:"Hello";i:1;s:5:"World";}
</pre>
Le préfixe "a:2" signifie "array of 2 lines", et il est obligatoire pour désérialiser.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Bases de données=
== Introduction ==
Soit la base de données ''BDDNAME'' contenant la table ''NOMTABLE'' qui sera utilisée pour la suite du livre. Voici la table :
:*ID : id
:*NOM : chaine de caractères
:*PRENOM : chaine de caractères
:*ADRESSE1 : chaine de caractères
:*ADRESSE2 : chaine de caractères
:*TEL1 : entier long
:*TEL2 : entier long
Cette base de données contient les deux enregistrements suivants :
{| class="wikitable"
|align="center"|0||align="center"|"DUPOND"||align="center"|"LOUIS"||align="center"|"1,Petite rue"||align="center"|"2,Petite rue"||align="center"|0543454654||align="center"|0543454352
|-
|align="center"|1||align="center"|"DUSS"||align="center"|"Jean-Claude"||align="center"|"1,Grande rue"||align="center"|"2, Grande rue"||align="center"|null||align="center"|null
|}
Constantes propres à la base utilisées par la suite :
:'''Nom de la BDD''' : "BDDNAME"
:'''Adresse de la BDD''' : "BDDADRESSE"
:'''Login d'accès à la BDD''' : "BDDUSER"
:'''Mot de passe pour accéder à la BDD''' : "BDDPASS"
== SQL imbriqué en PHP ==
À l'instar du HTML, on peut trouver du code SQL imbriqué dans du code PHP. Dans ce cas il faut que les limitateurs de chaines soient bien échappés. Ex :
<pre>
$sql = sprintf('
SELECT *
FROM ma_table
WHERE (mon_champ1 = "%1$s" AND mon_champ2 != "") OR mon_champ2 = "%1$s"
', $maChaine);
</pre>
NB : <code>\'%1$s\'</code> serait équivalent à <code>"%1$s"</code>.
== [[Oracle]] ==
Documentation sur http://php.net/manual/fr/book.oci8.php.
== [[PostgreSQL]] ==
Documentation sur http://php.net/manual/fr/book.pgsql.php.
== Voir aussi ==
* [[../PEAR#DB|Bibliothèque DB du framework PEAR]]
* http://php.net/manual/fr/book.mysqli.php
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Microsoft SQL Server=
== Installation ==
On distingue plusieurs pilotes PHP pour MS-SQL Server :
* mssql (désuet en PHP7).
* sqlsrv
* [[w:PHP Data Objects|PDO]]
** pdo_sqlsrv
** pdo_dblib ([[w:Sybase|Sybase]])
** pdo_odbc ([[w:ODBC|ODBC]]).
=== Windows ===
Pour se connecter au serveur MS-SQL à partir d'un tout-en-un comme [[EasyPHP|EasyPHP]], il suffit de télécharger les pilotes .dll<ref>http://www.microsoft.com/en-us/download/details.aspx?id=20098</ref> correspondant à sa version de PHP, puis d'indiquer leurs chemins dans le PHP.ini :
* Sous PHP 4, copier le fichier <code>php_mssql.dll</code> dans les extensions.
* Pour PHP 5.4 :
*# Télécharger les .dll SQL30
*# Les copier dans <code>C:\PROGRA~2\EasyPHP\binaries\php\php_runningversion\ext</code>
*# Les ajouter dans <code>C:\PROGRA~2\EasyPHP\binaries\php\php_runningversion\php.ini</code> via les lignes suivantes<ref>http://www.php.net/manual/fr/ref.pdo-sqlsrv.php</ref> :
*#:<code>extension=php_sqlsrv_54_ts.dll</code>
*#:<code>extension=php_pdo_sqlsrv_54_ts.dll</code>
* Dans PHP 5.5 on obtient toujours <code>Fatal error: Call to undefined function sqlsrv_connect()</code>, donc upgrader ou downgrader PHP.
* Dans PHP 7 : cela fonctionne.
Pour vérifier l'installation, redémarrer le serveur Web, puis vérifier que la ligne <code>pdo_sqlsrv</code> s’est bien ajoutée dans la configuration (ex : <code>http://127.0.0.1/home/index.php?page=php-page&display=extensions</code>).
=== Linux ===
Pour se connecter au serveur MS-SQL, il suffit de télécharger les pilotes .so<ref>https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server</ref> correspondant à sa version de PHP, puis d'indiquer leurs chemins dans le PHP.ini<ref>https://www.barryodonovan.com/2016/10/31/linux-ubuntu-16-04-php-and-ms-sql</ref> :
<syntaxhighlight lang=apache>
extension=php_pdo_sqlsrv_7_nts.so
extension=php_sqlsrv_7_nts.so
</syntaxhighlight>
Pour vérifier l'installation, redémarrer le serveur Web, puis vérifier que la ligne <code>pdo_sqlsrv</code> s’est bien ajoutée dans la configuration (ex : <code>php -r "phpinfo();" |grep sql</code>).
== Connexion ==
=== Pilote 1.1 SQL Server pour PHP ===
Ce pilote fournit des fonctions d’interaction avec [[Microsoft SQL Server]]. Il peut s'utiliser avec les version de SQL Server postérieure à 2005<ref>http://msdn.microsoft.com/fr-fr/library/cc296152%28v=sql.90%29.aspx</ref>.
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
$serverName = "(local)";
$connectionInfo = array("login" => $uid, "password" => $pwd, "Database"=>"my_db");
$connect = sqlsrv_connect($serverName, $connectionInfo);
// ... votre script
sqlsrv_close($connect);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
=== Alternative désuète ===
Les fonctions suivantes sont supprimées depuis PHP 7.0<ref>http://php.net/manual/fr/function.mssql-connect.php</ref> :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<ref>http://www.php.net/manual/fr/function.mssql-select-db.php</ref>
<pre>
$serverName = "(local)";
$connect = mssql_connect($serverName, "login", "password");
mssql_select_db("my_db", $connect)
// ... votre script
mssql_close($connect);
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
=== Alternative Doctrine ===
Les bibliothèques Doctrine utilisent cette syntaxe<ref>http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html</ref> :
<div style="display:none;">Début d’un principe</div>
<div class="emphase" style="text-align:left; clear:left;">
<div class="plainlinks mw-trackedTemplate" style="margin: 5px 1% 5px 0; padding: 0.5em; width:70%; display:inline-block; box-shadow: 0 0 10px #4f4f4f40; border: 1px solid rgb(153, 153, 153); border-radius: 0.5em; background-color: rgba(187,187,187,0.25); background-image: -moz-linear-gradient(center top , rgba(119,119,119,0.25), rgba(187,187,187,0.25)); text-align: left;">
<div style="margin-left:1%; font-size:14pt; padding-bottom:0px; valign:center;">[[Fichier:Tango atom.svg|20px]] Principe</div>
<div style="margin-top:2px; background-color: rgba(255,255,255,0.25); width:auto; border-radius: 0.5em; padding:1em;">
<pre>
$config = new \Doctrine\DBAL\Configuration();
$connectionParams = array(
'driver' => 'sqlsrv', // ou pdo_sqlsrv selon le pilote
'host' => $this->serverName,
'dbname' => $this->databaseName,
'user' => $this->userName,
'password' => $this->password,
);
$this->connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
$this->connection->query($query);
$this->connection->close();
</pre>
</div>
</div>
</div>
<div style="display:none;">Fin du principe</div>
== Requête SQL ==
<pre>
// ... connexion
$sql = "DELETE FROM ma_table WHERE colonne = '1' LIMIT 0,30";
if (!sqlsrv_query($connect, $sql)) {
die (sqlsrv_errors());
}
// ... déconnexion
</pre>
=== Récupération des données ===
<pre>
// ... connexion
$sql = "SELECT ville, pays, code_postal FROM table_clients WHERE code_postal = '78000' AND client_id = '44' LIMIT 1";
if (!$result = sqlsrv_query($connect, $sql)) {
die (sqlsrv_errors());
}
$client_data = sqlsrv_fetch_array($result);
extract($client_data);
echo $ville;
echo $pays;
echo $code_postal;
// ... déconnexion
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Par défaut, <code>sqlsrv_fetch_array()</code> renvoie deux tableaux imbriqués : le tableau itératif et l'associatif. Pour n'en sélectionner qu'un, il faut remplir son deuxième paramètre avec<ref>http://php.net/manual/fr/function.sqlsrv-fetch-array.php</ref> :
* <code>SQLSRV_FETCH_ASSOC</code> : pour le tableau associatif.
* <code>SQLSRV_FETCH_NUMERIC</code> : pour le tableau itératif.
* <code>SQLSRV_FETCH_BOTH</code> : pour les deux (déjà par défaut).
</p>
</div>
</div>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=MySQL=
PHP offre plusieurs fonctions d’interaction avec [[MySQL]].
== mysql_connect() ==
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">La fonction <code>mysql_connect()</code> est obsolète en PHP7, et remplacée par la classe <code>mysqli</code>.</p>
</div>
</div>
=== Connexion ===
<div class="cadre-code" style="padding: 0.2em; margin: 0.5em 0; width: 100%; background-color: #80ff801c; border: solid 1px #AAAAAA;">
<div class="cadre-code-titre" style="background-color: #83838340; border-bottom: dotted 1px #AAAAAA; padding: 0.2em 1em;">Exemple d'accès à une base de données MySQL</div>
<div style="font-family: monospace"><pre>
$user="BDDUSER";
$pass="BDDPASS";
$db="BDDNAME";
$table="demande_intervention";
$link=mysql_connect("localhost", $user, $pass); // Préproduction locale de BDDADRESSE
if (!$link) {
die("Impossible de se connecter à mysql");
}
mysql_select_db($db, $link) or die ("Impossible d'ouvrir $db :".mysql_error());
$query="insert into $table values($znom,$ztel,$znomach,zlieu,zdate)";
mysql_query($query, $link) or die("Impossible d'ajouter des nouvelles données".mysql_error());
mysql_close($link);
</pre></div>
</div>
=== Exécution d'une requête ===
Maintenant que nous sommes connectés à notre base de données, il est possible d’exécuter des requêtes dessus. En voici un exemple
<div class="cadre-code" style="padding: 0.2em; margin: 0.5em 0; width: 100%; background-color: #80ff801c; border: solid 1px #AAAAAA;">
<div class="cadre-code-titre" style="background-color: #83838340; border-bottom: dotted 1px #AAAAAA; padding: 0.2em 1em;">Exploiter une requête de type SELECT (avec <code>mysql_fetch_assoc</code>)</div>
<div style="font-family: monospace"><pre>
$requete = "SELECT * FROM NOMTABLE";
$res = mysql_query($requete);
//On obtient alors tous les enregistrements présents dans la table nom table, et pour exploiter les enregistrements, on peut boucler de la manière suivante :
while ($enregistrement = mysql_fetch_assoc($res)) {
$nom = $enregistrement['NOM'];
$prenom = $enregistrement['PRENOM'];
$adresse1 = $enregistrement['ADRESSE1'];
$adresse2= $enregistrement['ADRESSE2'];
$tel1 = $enregistrement['TEL1'];
$tel2 = $enregistrement['TEL2'];
}
</pre></div>
</div>
De cette manière on récupère un tableau associatif sous la forme ''Clé''->''Valeur'' pour chacun des enregistrements retournés par la requête.
Il existe d'autres fonctions pour cela :
*<code>mysql_num_rows()</code> : retourne le nombre de lignes données par la requête.
*<code>mysql_fetch_row()</code> : identique à <code>mysql_fetch_assoc</code> mais retourne un tableau simple ''indice''->''valeur''.
*<code>mysql_fetch_object()</code> : identique à <code>mysql_fetch_assoc</code> mais retourne un objet.
*<code>mysql_fetch_array()</code> : retourne les tableaux <code>mysql_fetch_assoc()</code> et <code>mysql_fetch_row()</code>.
Exemple :
<div class="cadre-code" style="padding: 0.2em; margin: 0.5em 0; width: 100%; background-color: #80ff801c; border: solid 1px #AAAAAA;">
<div class="cadre-code-titre" style="background-color: #83838340; border-bottom: dotted 1px #AAAAAA; padding: 0.2em 1em;">Exploiter une requête de type SELECT (avec <code>mysql_fetch_object</code>)</div>
<div style="font-family: monospace"><pre>
$requete = "SELECT * FROM NOMTABLE";
$res = mysql_query($requete);
while ($enregistrement = mysql_fetch_object($res)) {
$nom = $enregistrement->NOM;
$prenom = $enregistrement->PRENOM;
$adresse1 = $enregistrement->ADRESSE1;
$adresse2= $enregistrement->ADRESSE2;
$tel1 = $enregistrement->TEL1;
$tel2 = $enregistrement->TEL2;
}
</pre></div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Par défaut, <code>mysql_fetch_array()</code> renvoie deux tableaux imbriqués : le tableau itératif et l'associatif. Pour n'en sélectionner qu'un, il faut remplir son deuxième paramètre avec<ref>http://php.net/manual/fr/function.mysql-fetch-array.php</ref> :
* <code>MYSQL_ASSOC</code> : pour le tableau associatif.
* <code>MYSQL_NUM</code> : pour le tableau itératif.
* <code>MYSQL_BOTH</code> : pour les deux (déjà par défaut).
</p>
</div>
</div>
=== Fermeture d'une connexion ===
<pre>
//Ferme la connexion MySQL
mysql_close ($ressource);
</pre>
== mysqli_connect() ==
Cette fonction fonctionne un peu comme <code>mysql_connect()</code><ref>http://www.w3schools.com/php/func_mysqli_connect_errno.asp</ref> :
<pre>
$link = mysqli_connect("BDDADRESSE","BDDUSER","BDDPASS","BDDNAME");
if (!$link) { die(mysqli_connect_errno()); }
mysqli_query($link, $query);
mysqli_close($link);
</pre>
Les fonctions de manipulation de données ont pour préfixe "mysqli" avec les mêmes suffixes que celles du paragraphe précédent :
*<code>mysql_num_rows()</code>
*<code>mysql_fetch_assoc()</code>
*<code>mysql_fetch_row()</code>
*<code>mysql_fetch_object()</code>
== Classe mysqli ==
La classe <code>mysqli</code> permet les mêmes opérations que les fonctions précédentes, avec l'avantage de la programmation objet en plus (ex : héritage, introspection...) :
<pre>
$query = 'SELECT * FROM NOMTABLE';
$link = new mysqli("BDDADRESSE","BDDUSER","BDDPASS","BDDNAME");
if ($link->connect_error) {
die(mysqli_connect_errno());
}
$result = $link->query($query);
while ($row = $result->fetch_assoc()) {
var_dump($row);
}
$result->close();
$link->close();
</pre>
<code>query</code> accepte les valeurs suivantes en second paramètre :
* <code>MYSQLI_STORE_RESULT</code> : récupère tous les résultats du serveur (valeur par défaut).
* <code>MYSQLI_USE_RESULT</code> : récupère ligne par ligne.
* <code>MYSQLI_ASYNC</code> : requête SQL asynchrone (non bloquante).
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=PDO=
== Introduction ==
'''PDO''' ([[w:PHP Data Objects|PHP Data Objects]]) est une extension définissant l'interface pour accéder à plusieurs types de base de données, fournie automatiquement depuis PHP 5.1. Avec ce système, pour utiliser plusieurs SGBD il n'est pas nécessaire de changer les fonctions de communication dans tout le code, mais seulement les arguments envoyés au constructeur.
Les pilotes suivants sont disponibles<ref>http://php.net/manual/fr/pdo.drivers.php</ref> :
# [[w:4e Dimension (langage)|4D]]
# [[w:en:CUBRID|CUBRID]]
# [[w:Firebird (base de données)|Firebird]]
# [[w:IBM DB2|IBM DB2]]
# [[w:Informix|Informix]]
# [[Microsoft SQL Server]]
# [[MySQL]]
# [[Oracle Database]]
# [[PostgreSQL]]
# [[w:SQLite|SQLite]]
Et aussi via [[w:ODBC|ODBC]].
== Installation ==
=== Machine hôte ===
<div class="remarque"> Pour PHP 5.0, l'extension est disponible en tant qu'extension [[w:PECL|PECL]], et doit être activée<ref>http://fr2.php.net/manual/fr/pdo.installation.php</ref> en ajoutant ou décommentant la ligne de "php_pdo.dll" dans php.ini.</div>
Pour activer les différents SGBD qui doivent communiquer avec PDO, il faut ajouter ou décommenter les lignes <code>extension=php_pdo_''[SGBD utilisé]''.dll</code> dans php.ini, que ce soit sur Apache ou IIS. Exemple :
extension=php_pdo_firebird.dll
extension=php_pdo_mysql.dll
extension=php_pdo_oci.dll
extension=php_pdo_odbc.dll
extension=php_pdo_pgsql.dll
extension=php_pdo_sqlite.dll
extension=php_pdo_sqlsrv_54_ts.dll
Les .dll de Microsoft SQL Server sont téléchargeables sur le site officiel<ref>https://www.microsoft.com/en-us/download/details.aspx?id=20098</ref>.
=== Docker ===
MySQL :
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
PostgreSQL :
RUN apt-get update && apt-get install -y libpq-dev
RUN docker-php-ext-install pdo_pgsql && docker-php-ext-enable pdo_pgsql
== Les classes de l'extension PDO ==
L'extension PDO comporte trois classes<ref>http://php.net/manual/fr/class.pdo.php</ref> :
* La classe <code>PDO</code> correspond à une connexion à la base de données.
* La classe <code>PDOStatement</code> représente d'une part une requête préparée et d'autre part le jeu de résultats de la requête une fois qu'elle est exécutée.
Cette classe offre des méthodes de parcours, de comptage, d'informations.
* La classe <code>PDOException</code> représente une erreur émise par PDO.
== Accès à la base de données avec PDO ==
L'accès à la base de données se fait en instanciant un objet PDO. Les paramètres à indiquer au constructeur sont :
* la source de la base de données ;
* optionnellement, le nom d'utilisateur et le mot de passe.
Par exemple, pour accéder à une base de données de nom ''ma_bdd'' accessible sur le port ''mon_port'' du serveur ''mon_serveur'' avec l'utilisateur ''mon_id'' associé au mot de passe ''mon_mot_de_passe'', le code sera le suivant :
* Pour MySQL<ref>http://php.net/manual/fr/ref.pdo-mysql.php</ref> :
<pre>
$connexion = new PDO('mysql:host=mon_serveur;dbname=ma_bdd;port=3306','mon_id','mon_mot_de_passe');
</pre>
* Pour MS-SQL<ref>http://php.net/manual/fr/ref.pdo-sqlsrv.connection.php</ref> les noms des paramètres diffèrent :
<pre>
$connexion = new PDO('sqlsrv:server=mon_serveur:mon_port;Database=ma_bdd','mon_id','mon_mot_de_passe');
</pre>
* MS-SQL via une source de données ODBC (à créer dans <u>C:\Windows\SysWOW64\odbcad32.exe</u>) :
<pre>
$connexion = new PDO('odbc:nom_de_la_source','mon_id','mon_mot_de_passe');
</pre>
* Pour PostgreSQL<ref>https://www.php.net/manual/fr/ref.pdo-pgsql.php</ref> :
<pre>
$connexion = new PDO('pgsql:host=mon_serveur;dbname=ma_bdd;port=5432;user=mon_id;password=mon_mot_de_passe');
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Par rapport à mysqli, PDO indique le nom réseau de la machine qui l'utilise en suffixe du compte MySQL, ce qui change le nom du compte et donc les permissions sur les bases. Par exemple si 'mon_id@%' est autorisé, ce n'est pas forcément le cas de 'mon_id@192.168.1.10', ce qui provoque l'erreur ''[[../Exceptions#SQLSTATE.5B28000.5D_.5B1045.5D_Access_denied_for_user|Access denied for user]]''.</p>
</div>
</div>
== Passer des requêtes en PDO ==
Des méthodes permettent de passer des requêtes à l'objet récupéré lors de la connexion.
* <code>PDO::exec(string $statement)</code>
La méthode <code>exec()</code> permet de passer et exécuter une requête SQL de type INSERT, UPDATE, DELETE.<br />Elle retourne le nombre de lignes affectées par la requête.
<pre>
$requete="DELETE * FROM matable WHERE champ1='mavaleur'";
$resultat=$connexion->exec($requete);
echo $resultat.' suppressions effectuées';
</pre>
* <code>PDO::query(string $statement)</code>
La méthode <code>query()</code> permet de passer et exécuter une requête SQL de type SELECT.<br />Elle retourne le jeu de résultats (s'il y en a) sous forme d'objet PDOStatement.
<pre>
$requete="SELECT champ1, champ2 FROM matable";
$resultat=$connexion->query($requete);
</pre>
== Quelques méthodes de PDOStatement ==
* <code>PDOStatement::fetch()</code> récupère la ligne suivante d'un jeu de résultats PDO.
* <code>PDOStatement::fetchAll()</code> retourne un tableau contenant toutes les lignes du jeu d'enregistrements PDO.
* <code>PDOStatement::fetchObject()</code> récupère la ligne suivante et la retourne en tant qu'objet.
* <code>PDOStatement::fetchColumn()</code> retourne une colonne depuis la ligne suivante d'un jeu de résultats PDO.
* <code>PDOStatement::rowCount()</code> retourne le nombre de lignes affectées par le dernier appel à la fonction.
* <code>PDOStatement::closeCursor()</code> libère la connexion au serveur, permettant ainsi à d'autres requêtes SQL d'être exécutées. La requête reste dans un état lui permettant d'être de nouveau exécutée. Cette fonction retourne TRUE en cas de succès ou FALSE si une erreur survient.
* <code>PDOStatement::execute()</code> exécute une requête préparée.
== Les paramètres de la méthode <code>fetch</code> ==
La méthode <code>fetch()</code> peut avoir en paramètre le type de retour du résultat. Par défaut, sans paramètre, le paramètre implicite est <code>PDO::FETCH_BOTH</code>.
* <code>PDO::FETCH_ASSOC</code> retourne le jeu de résultats sous forme d'un [[w:tableau associatif|tableau associatif]], dont la clé est le nom de colonnes.
Exemple d'utilisation :
<pre>
while ($ligne = $resultat ->fetch(PDO::FETCH_ASSOC)) {
echo $ligne['champ3'].' '.$ligne['champ1'].'<br/>';
}
</pre>
* <code>PDO::FETCH_NUM</code> retourne le jeu de résultats sous forme d'un tableau indexé par numéro commençant à 0.
Exemple d'utilisation :
<pre>
while ($ligne = $resultat ->fetch(PDO::FETCH_NUM)) {
echo $ligne[2].' '.$ligne[0].'<br/>';
}
</pre>
* <code>PDO::FETCH_BOTH</code> retourne un tableau indexé et un tableau associatif.
* <code>PDO::FETCH_OBJ</code> retourne le jeu de résultats sous forme d'un objet dont les noms de propriétés correspondent aux noms des colonnes.
== Faire une requête préparée avec PDO ==
<code>PDO::prepare()</code> permet de préparer une requête que l'on exécutera ensuite avec la méthode <code>PDOStatement::execute()</code>.
Les variables (parties de la requête spécifiques au moment de l'exécution) seront passées à la requête préparée grâce à cette méthode <code>execute()</code>. Ce dispositif protège l'application d'attaque de type [[w:injection SQL|injection SQL]].
Exemple :
<pre>
$pdoStatement = PDO::prepare('SELECT * FROM people WHERE name = :name');
$pdoStatement->bindParam(':name', $name, PDO::PARAM_STR);
$pdoStatement->execute();
return $pdoStatement->fetchAll(PDO::FETCH_ASSOC);
</pre>
== Notes et références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=SQLite=
[[w:SQLite|SQLite]] est le moteur de base de données intégré à PHP5.
=== Connexion ===
<pre>
// On se connecte à la base
// CHEMIN_BDD constitue de chemin physique de la base de données
$db = new SQLiteDatabase(CHEMIN_BDD);
</pre>
=== Exécution d'une requête ===
Maintenant que nous sommes connectés à notre base de données, il est possible d’exécuter des requêtes dessus. En voici un exemple <pre>
$requete = "SELECT * FROM NOMTABLE";
$res = $db->arrayQuery ($requete, SQLITE_ASSOC);
</pre>
On obtient alors tous les enregistrements présents dans la table nom table, et pour exploiter les enregistrements, on peut boucler de la manière suivante :
<pre>
foreach ($res as $enregistrement) {
$nom = $enregistrement['NOM'];
$prenom = $enregistrement['PRENOM'];
$adresse1 = $enregistrement['ADRESSE1'];
$adresse2= $enregistrement['ADRESSE2'];
$tel1 = $enregistrement['TEL1'];
$tel2 = $enregistrement['TEL2'];
}
</pre>
De cette manière on récupère un tableau associatif sous la forme ''Clé''->''Valeur'' pour chacun des enregistrements retournés par la requête. C'est la constante '''SQLITE_ASSOC''' qui permet cela. La constante '''SQLITE_NUM''' permet de retourner un tableau indexé numériquement.
Il existe d'autres méthodes...
:*<code>''numRows ()''</code> : retourne le nombre de lignes données par la requête.
=== Fermeture d'une connexion ===
<pre>
$db->close();
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Expressions rationnelles=
En informatique, une [[w:Expression régulière|'''expression régulière''']] ou '''expression rationnelle''' ou '''expression normale''' ou '''motif''', est une chaîne de caractères, qui décrit, selon une syntaxe précise, un ensemble de chaînes de caractères possibles.
Les expressions régulières sont également appelées '''''regex''''' (de l'anglais ''regular expression'').
Elles sont issues des théories mathématiques des [[w:langage formel|langages formels]].
Les expressions régulières sont aujourd’hui utilisées pour la lecture, le contrôle, la modification, et l'analyse de textes ainsi que la manipulation des langues formelles que sont les [[w:Langage informatique|langages informatiques]].
L'exemple d'expression régulière suivant permet de valider qu'une chaîne de caractère correspond à la syntaxe d'un nombre entier non signé, c'est à dire une suite non vide de chiffres :
[0-9]+
En détails :
* Les crochets spécifient l'ensemble des caractères auquel doit appartenir le caractère courant de la chaîne. Dans cet exemple, l'ensemble est celui des chiffres de 0 à 9 inclus.
* Le caractère plus indique de répéter le motif précédent au moins une fois (suite non vide).
En PHP, la validation d'une chaîne de caractères peut se faire en utilisant la fonction <code>preg_match</code> :
<pre>
<?php
$chaine = '12345'; // ou '12ABC'
if (preg_match('`[0-9]+`', $chaine)) {
print('Le texte est un entier positif');
} else {
print('Le texte n\'est pas un entier positif');
}
</pre>
== Syntaxe ==
PHP utilise la norme [[wikt:PCRE|PCRE]].
Les expressions rationnelles peuvent être analysées et testées via un débogueur en ligne comme https://regex101.com/.
{| class="wikitable altlines1"
|+ Expressions rationnelles courantes
! Caractère
! Type
! Explication
|-
| style="text-align:center;" | <code>'''.'''</code>
| Point
| N'importe quel caractère
|-
| style="text-align:center;" | <code>'''['''...''']'''</code>
| crochets
| [[wikt:classe de caractères|classe de caractères]] : tous les caractères énumérés dans la classe, avec possibilité de plages dont les bornes sont séparées par "-". Ex : <code>[0-9a-z]</code> pour tout l'alphanumérique en minuscule, ou <code>[0-Z]</code> pour tous les caractères de la table Unicode entre "0" et "Z", c'est-à-dire l'alphanumérique majuscule plus ":;<=>?@"<ref>https://unicode-table.com/fr/</ref>.
|-
| style="text-align:center;" | <code>'''[^'''...''']'''</code>
| crochets et circonflexe
| [[wikt:classe complémentée|classe complémentée]] : tous les caractères sauf ceux énumérés.
|- style="display:none;"
| style="text-align:center;" | <code>'''['''...'''['''...''']]'''</code>
| union
| Union des deux ensembles
|- style="display:none;"
| style="text-align:center;" | <code>'''['''...'''&&['''...''']]'''</code>
| intersection
| Intersection des deux ensembles
|-
| style="text-align:center;" | <code>'''^'''</code>
| circonflexe
| Marque le début de la chaîne ou de la ligne.
|-
| style="text-align:center;" | <code>'''$'''</code>
| dollar
| Marque la fin de la chaîne ou de la ligne.
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:3}}'''</code>
| barre verticale
| Alternative - ou reconnaît l'un ou l'autre
|-
| style="text-align:center;" | <code>(...)</code>
| parenthèses
| [[wikt:groupe de capture|groupe de capture]] : utilisé pour limiter la portée d'un masque ou de l'alternative, grouper un motif répété ou capturer une séquence
|-
| style="text-align:center;" | <code>'''\'''''n''</code>
| référence
| Même séquence que celle capturée précédemment par le ''n''<sup><small>ème</small></sup> [[wikt:groupe de capture|groupe de capture]]
|- style=""
| style="text-align:center;" | <code>'''\g{'''''n'''''}'''</code>
| référence
| Même séquence que celle capturée précédemment par le ''n''<sup><small>ème</small></sup> [[wikt:groupe de capture|groupe de capture]]
|-
| style="text-align:center;" | <code>'''(?P<'''''nom'''''>'''''pattern''''')'''</code>
| Sous-motif nommé
| Nomme le résultat d'un groupe de capture par un nom.
|- style=""
| style="text-align:center;" | <code>'''\g{'''''nom'''''}'''</code>
| référence
| Même séquence que celle capturée précédemment par le [[wikt:groupe de capture|groupe de capture]] nommé ''nom''.
|- style="display:none;"
| style="text-align:center;" | <code>'''\k<'''''nom'''''>'''</code>
| référence
| Même séquence que celle capturée précédemment par le [[wikt:groupe de capture|groupe de capture]] nommé ''nom''.
|}
Par défaut, les caractères et groupes ne sont pas répétés.
Les quantificateurs permettent de spécifier le nombre de répétitions et sont spécifiés immédiatement après le caractère ou groupe concerné.
{| class="wikitable altlines1"
|+ Quantificateurs
! Caractère
! Type
! Explication
|-
| style="text-align:center;" | <code>'''*'''</code>
| astérisque
| 0, 1 ou plusieurs occurrences
|-
| style="text-align:center;" | <code>'''+'''</code>
| plus
| 1 ou plusieurs occurrences
|-
| style="text-align:center;" | <code>'''?'''</code>
| interrogation
| 0 ou 1 occurrence
|-
| style="text-align:center;" | <code>'''{'''...'''}'''</code>
| accolades
| nombre de répétitions : spécifie le nombre de répétitions du motif précédent (minimum et maximum). Avec la présence de la virgule, quand le minimum est absent la valeur par défaut est zéro, quand le maximum est absent la valeur pas défaut est l'infini. Sans virgule (un seul nombre) il s'agit du nombre exact (minimum et maximum ont la même valeur). Exemples :
* <code>a{2}</code> deux occurrences de "a",
* <code>a{1,10}</code> (sans espace) entre une et dix,
* <code>a{,10}</code> jusqu'à 10 fois (de 0 à 10),
* <code>a{3,}</code> au moins 3 fois (de 3 à l'infini).
|}
Par défaut les quantificateurs ne recherchent pas forcément la plus longue séquence de répétition possible.
Il est possible de les suffixer avec un caractère pour modifier leur comportement.
{| class="wikitable altlines1"
|+ Modificateurs de quantificateurs
! Caractère
! Type
! Explication
|-
| style="text-align:center;" | <code>'''?'''</code>
| réticent
| Le quantificateur qui précède recherchera la plus petite séquence possible.
|-
| style="text-align:center;" | <code>'''+'''</code>
| possessif
| Le quantificateur qui précède recherchera la plus grande séquence possible.
|}
'''Remarques :'''
* Les caractères de début et fin de chaîne (<code>^</code> et <code>$</code>) ne fonctionnent pas dans <code>[]</code> où ils ont un autre rôle.
* Les opérateurs <code>*</code> et <code>+</code> sont toujours [[wikt:avide|avide]]s, pour qu'ils laissent la priorité il faut leur apposer un <code>?</code> à leur suite<ref>https://docstore.mik.ua/orelly/webprog/pcook/ch13_05.htm</ref>.
{| class="wikitable altlines1"
|+ Classes de caractères [[w:POSIX|POSIX]]<ref>https://www.regular-expressions.info/posixbrackets.html</ref>
! Classe
! Signification
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:4}}'''</code>
|n'importe quelle lettre
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:5}}'''</code>
|n'importe quel chiffre
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:6}}'''</code>
|caractères hexadécimaux
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:7}}'''</code>
|n'importe quelle lettre ou chiffre
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:8}}'''</code>
|n'importe quel espace blanc
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:9}}'''</code>
|n'importe quel signe de ponctuation
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:10}}'''</code>
|n'importe quelle lettre en minuscule
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:11}}'''</code>
|n'importe quelle lettre capitale
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:12}}'''</code>
|espace ou tabulation
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:13}}'''</code>
|caractères affichables et imprimables
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:14}}'''</code>
|caractères d'échappement
|-
| style="text-align:center;" | <code>'''{{#parsoid\0fragment:15}}'''</code>
|caractères imprimables exceptés ceux de contrôle
|}
{| class="wikitable altlines1" border="1"
|+ Expressions rationnelles Unicode<ref>https://www.regular-expressions.info/unicode.html</ref>
! Expression !! Signification
|-
| style="text-align:center;" | <code>'''\\'''</code> || Antislash
|-
| style="text-align:center;" | <code>'''\'''''C''</code> || Caractère spécial ''C'' non interprété : <code>[ ] { } ( ) ? * . : \ & - ^ $</code>
|-
| style="text-align:center;" | <code>'''\Q'''...'''\E'''</code> || Séquence littérale non interprétée
|-
| style="text-align:center;" | <code>'''\0'''''xxx''</code> || Caractère Unicode (1 à 3 chiffres octaux)
|-
| style="text-align:center;" | <code>'''\a'''</code> || Alarme (ASCII 07)
|-
| style="text-align:center;" | <code>'''\A'''</code> || Début de chaîne
|-
| style="text-align:center;" | <code>'''\b'''</code> || Caractère de début ou fin de mot
|-
| style="text-align:center;" | <code>'''\B'''</code> || Caractère qui n'est pas début ou fin de mot
|-
| style="text-align:center;" | <code>'''\c'''''X''</code> || Caractère de contrôle ASCII (X étant une lettre)
|-
| style="text-align:center;" | <code>'''\d'''</code> || Chiffre
|-
| style="text-align:center;" | <code>'''\D'''</code> || Non chiffre
|-
| style="text-align:center;" | <code>'''\e'''</code> || ''Escape'' (ASCII 1B)
|-
| style="text-align:center;" | <code>'''\f'''</code> || ''Form-feed'' (ASCII 0C)
|-
| style="text-align:center;" | <code>'''\G'''</code> || Fin de la correspondance précédente
|-
| style="text-align:center;" | <code>'''\h'''</code> || Espace blanc horizontal <code>[ \t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]</code>
|-
| style="text-align:center;" | <code>'''\H'''</code> || Non espace blanc horizontal <code>[^\h]</code>
|-
| style="text-align:center;" | <code>'''\n'''</code> || Fin de ligne
|-
| style="text-align:center;" | <code>'''\pL'''</code>, <code>'''\p{L}'''</code>, <code>'''\p{Letter}'''</code> || Lettre (dans tout langage)
|-
| style="text-align:center;" | <code>'''\r'''</code> || Retour charriot
|-
| style="text-align:center;" | <code>'''\R'''</code> || Retour à la ligne, équivaut à <code>{{#parsoid\0fragment:16}}</code>
|-
| style="text-align:center;" | <code>'''\s'''</code> || Caractères espace <code>[ \t\n\x0B\f\r]</code>
|-
| style="text-align:center;" | <code>'''\S'''</code> || Non caractères espace <code>[^\s]</code>
|-
| style="text-align:center;" | <code>'''\t'''</code> || Tabulation
|- style="display:none;"
| style="text-align:center;" | <code>'''\u'''''xxxx''</code> || Caractère Unicode (4 chiffres hexadécimaux)
|-
| style="text-align:center;" | <code>'''\v'''</code> || Espace blanc vertical <code>[\n\x0B\f\r\x85\u2028\u2029]</code>
|-
| style="text-align:center;" | <code>'''\V'''</code> || Non espace blanc vertical <code>[^\v]</code>
|-
| style="text-align:center;" | <code>'''\w'''</code> || Caractère alphanumérique : lettre, chiffre ou underscore
|-
| style="text-align:center;" | <code>'''\W'''</code> || Caractère qui n'est pas lettre, chiffre ou underscore
|-
| style="text-align:center;" | <code>'''\x'''''xx''</code> || Caractère Unicode (2 chiffres hexadécimaux)
|-
| style="text-align:center;" | <code>'''\x{'''''xx...x'''''}'''</code> || Caractère Unicode (chiffres hexadécimaux)
|- style=""
| style="text-align:center;" | <code>'''\X'''</code> || Caractère Unicode du groupe de graphèmes étendu
|-
| style="text-align:center;" | <code>'''\z'''</code> || Fin de chaîne
|}
'''Constructeurs spéciaux :'''
Ces fonctions précèdent l'expression à laquelle elles s'appliquent, et le tout doit être placé entre parenthèses.
* <code>?:</code> : [[wikt:groupe de capture|groupe non capturant]]. Ignorer le groupe de capture lors de la numérotation des [[wikt:backreference|backreference]]s. Exemple : <code>((?:sous-chaine_non_renvoyée|autre).*)</code>.
*:La présence d'un groupe capturant peut engendrer une allocation mémoire supplémentaire. Si une expression régulière particulièrement complexe provoque une erreur de mémoire, essayez de remplacer les groupes capturant non référencés et inutilisés par des groupes non-capturant en ajoutant <code>?:</code> juste après la parenthèse ouvrante, et en décalant les numéros des groupes référencés.
* <code>?></code> : groupe non capturant indépendant.
* <code>?<=</code> : [[wikt:positive lookbehind|positive lookbehind]], vérifier (sans consommer) que ce qui précède correspond au motif spécifié. Exemple :
*: Chercher une lettre u précédée d'une lettre q : <code>(?<=q)u</code>
* <code>?<!</code> : [[wikt:negative lookbehind|negative lookbehind]], vérifier (sans consommer) que ce qui précède ne correspond pas au motif spécifié.
* <code>?=</code> : [[wikt:positive lookahead|positive lookahead]], vérifier (sans consommer) que ce qui suit correspond au motif spécifié.
* <code>?!</code> : [[wikt:negative lookahead|negative lookahead]], vérifier (sans consommer) que ce qui suit ne correspond pas au motif spécifié. Exemples :
*: Chercher une lettre q non suivie d'une lettre u : <code>q(?!u)</code>
*: <code>((?!sous-chaine_exclue).)</code>
*: <code><(?!body).*></code> : pour avoir toutes les balises HTML sauf "body".
*: <code>début((?!mot_exclu).)*fin</code><ref>https://www.regextester.com/15</ref> : pour rechercher tout ce qui ne contient pas un mot entre deux autres.
*: <code>(?!000|666)</code> : pour exclure 000 et 666<ref>{{ouvrage|url=https://books.google.fr/books?id=yY_EsWFMj-QC&pg=PA256&lpg=PA256&dq=python+regex+negative+lookahead+two+strings&source=bl&ots=dFQ7mV91WE&sig=ACfU3U0i-oRwa1H6Paxnq1z8WeTkLk7HyQ&hl=fr&sa=X&ved=2ahUKEwjh4fOBsKXqAhXbCWMBHfYODjY4FBDoATACegQIChAB#v=onepage&q=negative%20lookahead&f=false|titre=Regular Expressions Cookbook|auteurs=Jan Goyvaerts, Steven Levithan|éditeur=O'Reilly Media, Inc.|date=22 mai 2009}}</ref>.
'''Options :'''
Les options d'interprétation sont en général spécifiées à part.
Mais certaines API ne permettent pas de les spécifier.
Il est possible d'insérer ces options dans l'expression régulière<ref>Les options sont appelées modificateurs (''modifiers'' en anglais), voir https://www.regular-expressions.info/modifiers.html</ref>.
:<code>'''(?'''''optionsactivées'''''-'''''optionsdésactivées''''')'''</code>
Exemples :
* Chercher un mot composé de voyelles sans tenir compte de la casse :
*:<code>(?i)[AEIOUY]+</code>
* Chercher un mot composé de voyelles en tenant compte de la casse, ici en majuscules :
*:<code>(?-i)[AEIOUY]+</code>
Les options s'appliquent à toute l'expression quelle que soit leur position dans l'expression.
[[Catégorie:Expressions rationnelles|PHP]]
* <code>$1</code> : résultat du premier groupe de capture dans les remplacements ($2 correspond au deuxième, etc.).
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">
En PHP, pour chercher un dollar, "\$" ne fonctionne pas car c'est le format de certaines variables. Il faut donc utiliser des apostrophes au lieu des guillemets : '\$'.
</p>
</div>
</div>
En PHP, les motifs d'expression régulière doivent toujours être entourés d'un symbole délimiteur.
On utilise généralement l'accent grave (<code>`</code>), mais on trouve aussi souvent <code>/</code> et <code>#</code>.
Ceci sous peine de ne pas fonctionner avec ''Warning: no ending delimiter found''.
De plus, on peut ajouter des options après ces délimiteurs<ref>http://php.net/manual/fr/reference.pcre.pattern.modifiers.php</ref> :
{| class="wikitable"
| i || insensibilité à la casse
|-
| s || le "." inclut les retours à la ligne
|-
| m || les symboles ^ et $ deviennent valables une fois par ligne (au lieu d'une fois pour la chaine)
|-
| x || ignorer les espaces
|-
| o || lors d'un remplacement, ne traite que la première correspondance (pas disponible en PHP7.2)
|-
| u || compte les caractères Unicode (en multi-octet)
|}
== Recherche ==
La fonction <code>ereg()</code> qui permettait de rechercher en regex a été remplacée par <code>preg_match()</code> depuis PHP 5.3.
=== preg_match() ===
La fonction <code>preg_match()</code><ref>http://php.net/manual/en/function.preg-match.php</ref> est la principale fonction de recherche<ref>http://php.net/manual/fr/ref.pcre.php</ref>. Elle renvoie un booléen et demande deux paramètres obligatoires : l'expression rationnelle et la chaine à scanner. Le troisième paramètre correspond à la variable dans laquelle stocker le tableau des résultats. Enfin, le quatrième accepte un flag PHP, modifiant le comportement de base de la recherche.
* Exemple minimal :
<pre>
<?php
$chaine = 'Test regex PHP pour Wikibooks francophone.';
if (preg_match('`.*Wikibooks.*`', $chaine)) {
print('Le texte parle de Wikibooks');
} else {
print('Le texte ne parle pas de Wikibooks');
}
</pre>
* Exemple avancé :
<pre>
<?php
$chaine = 'Test regex PHP pour Wikibooks francophone.';
if (preg_match('`.*Wikibooks.*`', $chaine, $resultats, $flag)) {
var_dump($resultats);
} else {
print('Le texte ne parle pas de Wikibooks');
}
</pre>
Exemples de flags<ref>http://php.net/manual/fr/pcre.constants.php</ref> :
* PREG_OFFSET_CAPTURE : affiche la position de la sous-chaine recherchée dans la chaine.
* PREG_GREP_INVERT : affiche l'inverse dans <code>preg_grep()</code>.
==== Exemples élaborés ====
* Recherche des balises images HTML sans attribut "alt"<ref>https://stackoverflow.com/questions/4031948/using-regular-expressions-to-find-img-tags-without-an-alt-attribute</ref> : <code>/(<img(?!.*?alt=(['"]).*?\2)[^>]*)(>)/</code>
* Savoir si la chaine est une année (à quatre chiffres) : <code>'/^[0-9]{4}$/'</code>
* Comparer une URL et un host sans tenir compte du protocole ou du slash de fin : <code>'`^https?://'.$hostUrl.'/?$`'</code>
* Caractères ASCII et [[w:ASCII étendu|ASCII étendu]] (ex : "e" ou "E" ou "é") : <code>'/^[a-z\x7f-\xff]+$/i*'</code>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;"><code>preg_match()</code> peut ne pas marcher sur un résultat de <code>file_get_contents()</code> (avec plusieurs lignes), car il n'a pas de flag global (/g)<ref>https://stackoverflow.com/questions/3578671/unknown-modifier-g-in-when-using-preg-match-in-php</ref>. Il faut alors utiliser <code>preg_match_all()</code> (voir ci-dessous).</p>
</div>
</div>
=== preg_grep() ===
Cette fonction recherche dans les tableaux<ref>http://php.net/manual/fr/function.preg-grep.php</ref>.
=== preg_match_all() ===
Pour obtenir tous les résultats dans un tableau, remplacer ''preg_match'' par ''preg_match_all''<ref>http://www.expreg.com/pregmatchall.php</ref>, et ''print'' par ''print_r''.
Pour filtrer le contenu d'un fichier, par exemple récupérer tout ce qui se trouve entre parenthèses dans un tableau :
<pre>
$regex = "/\(([^)]*)\)/";
preg_match_all($regex, file_get_contents($nomFichier), $matches);
print_r($matches);
</pre>
== Remplacement ==
=== preg_replace() ===
La fonction ''preg_replace'' comprend trois paramètres : remplacé, remplaçant, chaine à traiter.
<pre>
<?php
// Remplace tous les espaces par des underscores
$chaine="Test regex PHP pour Wikibooks francophone.";
$chaineTriee=preg_replace('`( )`','_',$chaine);
echo $chaineTriee;
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Seul le dernier groupe de capture sera pris en compte.</p>
</div>
</div>
=== preg_filter() ===
Idem que <code>preg_replace()</code> mais son résultat ne contient que ce qui est effectivement remplacé.
=== preg_split() ===
Décompose une chaine de caractères.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Concevoir du code de haute qualité=
== Introduction ==
La programmation est accessible à tout le monde. En revanche, créer du code de qualité demande une rigueur et une organisation qui n'est pas toujours au rendez-vous. Voici donc un tutoriel pour apprendre à programmer du code de haute qualité. Les exemples seront pris au [[Programmation PHP|PHP]] mais leur application est toujours valable quel que soit le langage de programmation.
== Pourquoi bien programmer ? ==
Il est important d'aborder cette question dès le début pour légitimer ce cours, la motivation étant un facteur non négligeable de l'apprentissage.
Le bien-programmer est tout d'abord indispensable pour travailler en équipe. Lorsque vous travaillez dans un projet avec d'autres développeurs, il est important de coordonner votre action, d'éditer du code lisible... ce qui augmentera considérablement votre rendement de travail.
Vous vous apercevrez rapidement qu'un code bien programmé simplifie la vie :
Lorsque vous avez créé un script une année auparavant et que vous devez vous replonger dessus, vous serez heureux de gagner du temps en retrouvant plus facilement le sens du code grâce à sa lisibilité. De plus, du bon code est plus compatible et générera moins d'erreurs ...
La quantité toujours grandissante de développeurs risque d'entraîner une saturation du marché. Vous serez jugés sur votre qualité de programmation. Un code aux normes est un sceau garantissant votre compétence de développeur.
== Les critères de qualité ==
La programmation doit posséder des qualités qui ne sont pas arbitraires. Chaque critère sera accompagné d'une rapide description de son utilité.
=== La portabilité ===
La portabilité du code est son degré d'indépendance à son environnement.
Cette qualité est surtout requise pour des scripts destinés à être utilisés par le grand public. Par exemple, lorsqu'en PHP, vous utilisez des balises ''<?'', vous réduisez la portabilité du code. En effet, l'usage des balises de ce type nécessite l'activation de la fonction SHORT_TAGS, donc dépend de l'environnement du script. Par définition, cela réduit la portabilité du code.
Il vaut mieux utiliser la balise ''<?php'' qui marche quel que soit l'environnement.
=== La lisibilité ===
Il n'est pas besoin de chercher loin pour trouver les avantages que confèrent un code lisible : il permet aux autres développeurs de modifier plus facilement votre script, mais également à vous-même de comprendre plus rapidement un de vos anciens programmes.
La lisibilité du code permet également d'éviter les erreurs de logique qui apparaissent plus distinctement.
La lisibilité est très liée à la définition de normes.
La lisibilité du code inclut un bon usage de la ''commentarisation''. Les commentaires sont faits pour augmenter la rapidité de compréhension du script par le développeur, mais il ne doit pas en être fait un usage excessif, lequel serait néfaste pour la lisibilité. Voici un exemple de mauvaise utilisation des commentaires :
<pre>
/* Code qui va afficher "salut" par un echo. Ce programme est fait en PHP compatible php3*/
echo "salut"; //affiche "salut"
#fin du programme
</pre>
Il devient difficile de distinguer le code parmi les commentaires. Cet exemple était volontairement exagéré, mais n'est pas si loin de quelques scripts que l'on peut trouver sur le Net.
=== La définition de normes ===
Les normes sont des décisions le plus souvent arbitraires sur des méthodes de programmation. L'important n'est pas ce que définissent les normes, mais que des normes soient définies.
La définition de normes confère une continuité logique au code. Par exemple, en PHP, il est possible de nommer des variables de deux manières différentes :
<pre>
$maSuperVariable // Écriture en "CamelCase"
$ma_super_variable // Écriture avec des underscores
</pre>
Vous serez rapidement désorienté si tantôt vous utilisez une méthode, tantôt une autre, et il se peut que vous perdiez des heures à chercher la cause d'une erreur d'écriture de variable.
De plus, lorsque vous travaillez en commun avec d'autres développeurs, si vous n'avez pas défini le nom du fichier de connexion à la base de données, lors de la fusion des scripts, vous devrez tout remodifier et perdre ainsi un temps précieux.
=== L'unicité du code (DRY) ===
L'unicité du code (ou DRY pour "Don't repeat yourself", Ne vous répétez pas) est le principe selon lequel aucun code ne doit être double dans le script.
Ce critère a un enjeu pratique et vise à augmenter la rapidité des modifications d'un script. Prenons un exemple :
Vous faites un programme de comptabilité d'entreprise et vous vous connectez à une base de données. Si dans chaque page où vous vous connectez, vous entrez le code suivant :
<pre>
<?php
$connect = mysql_connect('host','account','password');
//actions sur la BDD
</pre>
Le jour où vous voudrez changer le mot de passe de la base de données, vous devrez modifier chaque script, tâche qui s'avère laborieuse. Si en revanche lorsque vous vous connectez à la base de données vous entrez le code suivant :
<pre>
include 'connect.php';
</pre>
Et que dans le fichier ''connect.php'' vous entrez les informations de votre base de données, vous n'aurez qu'à modifier ce seul fichier lors d'un changement de mot de passe.
La [[w:duplication de code|duplication de code]] est donc considérée comme un anti-patron de programmation.
==== Exemple concret ====
Créez un fichier conf/config.php dans lequel vous mettez vos informations de connexion à la base de données :
<pre>
define('HOST', 'localhost');
define('USER', 'moi');
define('PASS', 'mon_mdp');
define('DB', 'mon_site');
define('PREFIX', 'mon_site_'); // Préfixe des tables SQL
</pre>
Lorsque vous ferez une requête, vous la ferez de la manière suivante :
<pre>
mysql_connect(HOST, USER, PASS);
mysql_select_db(DB);
$temp = mysql_query('SELECT * FROM '. PREFIX .'ma_table');
</pre>
=== La gestion des erreurs ===
Votre programme ne doit pas afficher au client de message d'erreur. Cela ne signifie pas non plus qu'il faille les étouffer. Il faut, par du code de qualité, réduire au maximum les erreurs possibles, puis traiter les autres en les enregistrant par exemple dans un fichier.
Voir le chapitre suivant pour la mise en œuvre : ''[[../Exceptions/]]''.
== Les conflits de critères ==
Réaliser un programme rassemblant toutes les qualités présentées précédemment est extrêmement difficile. La plupart du temps, certains critères entrent en conflit, par exemple entre la compatibilité et la lisibilité. Il va alors falloir établir une hiérarchie.
Il n'existe pas de hiérarchie absolue. Elle dépend du type de projet que vous menez. Voici quelques exemples de hiérarchisation de critères en fonction du projet :
=== Projet destiné au grand public ===
Par exemple, vous décidez un jour de concevoir un [[w:CMS|CMS]]. Les qualités requises seront :
* '''La portabilité du code''' (car le CMS doit fonctionner sous le plus grand nombre de serveurs, donc doit dépendre le moins possible de sa configuration)
* '''La gestion des erreurs''' (sachant que les personnes qui vont utiliser le script ne l'ont pas fait, il ne doit pas retourner de message d'erreur car ils ne pourraient pas l'arranger)
* '''La définition de normes''' (vous le programmerez certainement en équipe, donc il faut coordonner votre action)
* '''La lisibilité du code''' (toujours pour des raisons de coordination)
=== Script pour un particulier ===
Si on vous demande de programmer un système de restriction d'accès pour un site particulier, il va falloir que le code possède les critères suivants :
* '''La lisibilité du code''' (un autre développeur doit pouvoir facilement modifier votre script si le client le lui demande)
== Erreurs à éviter ==
Attention à ne pas vous laisser avoir par des idées fausses d'autant plus dangereuses qu'elles sembleraient logiques. En voici quelques exemples :
=== Le code optimisé ===
Parfois le code le plus court n'est pas le plus rapide d'exécution<ref>http://www.phpbench.com/</ref>. En voici un exemple :
<pre>
//Code le plus court
for ($i = 0; $i < count($array); $i++) {
echo $array[$i];
}
//Code le plus rapide
$count = count($array);
for ($i = 0; $i < $count; $i++) {
echo $array[$i];
}
</pre>
En effet, avec le premier code, à chaque itération, la taille du tableau est recalculée, ce qui ralentit le script. Le code le plus court n'est donc pas le plus rapide d'exécution.
=== Guillemets simples pourquoi ? ===
Toutes les chaînes peuvent être écrites autant avec des guillemets simples ('foo') qu'avec des guillemets doubles ("bar"). Cependant, on conseille généralement d'employer les guillemets simples parce qu'ils sont plus rapides<ref>https://www.keycdn.com/blog/php-performance#11-stick-with-single-quotes</ref>. En voici la raison : à l'intérieur des guillemets doubles, les variables sont interprétées et substituées correctement.
'''Exemple :'''
<pre>
echo "Votre nom est $nom et vous êtes $etatUser. Il vous reste $pointAction point(s) d'action.";
//Fait ce qu'on s'attend qu'il fasse. Alors que :
echo 'Votre nom est $nom et vous êtes $etatUser. Il vous reste $pointAction point(s) d\'action.';
//écrira bêtement la chaine avec les nom des variables. Il faudra additionner les chaines ensembles :
echo 'Votre nom est '. $nom .' et vous êtes '. $etatUser .'. Il vous reste '. $pointAction .' point(s) d\'action.';
</pre>
Si à première vue ça semble être un avantage pour les guillemets doubles et qu'il est vrai que dans certaines conditions particulières, ça puisse augmenter la lisibilité du code, ça a aussi son désavantage : chacune des chaînes écrites avec des guillemets doubles doit d'abord être traversée par PHP pour y chercher de telles variables. En y regardant bien, le plus souvent c'est à des endroits où il n'y a aucune chance que se retrouvent de telles variables. Cette opération est en soi extrêmement rapide, presque qu'imperceptible, mais les chaînes se retrouvent souvent dans des endroits clés : des boucles, des fonctions exécutées des milliers des fois. Par ailleurs, même si on doit ajouter des variables dans une chaîne, ce sont les guillemets simples qui seront les plus rapides (Sauf cas extrême : <code>echo "$a - $b,$c + $d $e $sigma $epsilon";)</code>. C'est pourquoi on conseille de s'habituer et d'employer les guillemets simples le plus souvent possible.
Par ailleurs, lorsqu'on avance dans le niveau de programmation avec les objets et les tableaux (qui ne sont pas interprétés correctement dans les guillemets doubles), les occasions de se servir de ceux-ci vont en s'amenuisant considérablement.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">En cas de migration des guillemets vers les apostrophes, il faut remplacer tous les retours à la ligne \n qui ne sont plus interprétés, par des {{#parsoid\0fragment:17}}.</p>
</div>
</div>
== Conventions de codage ==
=== [[w:Condition Yoda|Condition Yoda]] ===
Lors d'une comparaison entre une variable et un littéral, on place ce dernier en premier. Ex : <code>if (1 == $x)</code>. Cette technique sert à éviter de confondre les assignations avec les égalités, et les déréférencements de pointeurs null.
=== PSR ===
La programmation objet en PHP est régie par des recommandations nommées PSR (pour ''PHP Standards Recommendations'') publiées sur http://www.php-fig.org/psr/.
En 2022, 13 de ces recommandations ont été acceptées officiellement :
==== PSR-1 : conventions de codages basiques ====
Les voici résumées ici<ref>http://www.php-fig.org/psr/psr-1/</ref> :
* Un fichier .php doit être encodé en UTF8 sans BOM.
* Les noms de classe doivent être rédigés en [[wikt:StudlyCaps|StudlyCaps]] (commencer par une majuscule).
* Les noms des variables et méthodes de classe doivent être écris en [[wikt:camelCase|camelCase]] (en commençant par une minuscule).
* Les noms des constantes doivent être en lettres capitales et [[wikt:snake_case|snake_case]] (en séparant les mots par des underscores). Comme par exemple la native <code>DIRECTORY_SEPARATOR</code>.
==== PSR-3 : interface du logger ====
Définit une méthode par niveau de criticité du log :
{| class="wikitable sortable"
! Numéro Graylog !! Niveau
|-
| 0 || emergency
|-
| 1 || alert
|-
| 2 || critical
|-
| 3 || error
|-
| 4 || warning
|-
| 5 || notice
|-
| 6 || informational
|-
| 7 || debug
|}
<div class="remarque"> Généralement la production n'affiche pas les debug.</div>
==== PSR-4 : Autoloading ====
Régit le fait que les séparateurs dans les namespaces, et les underscores dans les noms de classe, représentent des séparateurs de dossier du système de fichier.
==== PSR-6 : interface du cache ====
==== PSR-7 : interface des messages HTTP ====
==== PSR-11 : interface des conteneurs ====
==== PSR-12 : guide de style étendu ====
Cette norme remplace la PSR-2 et prolonge le PSR-1 avec :
* Les alinéas doivent faire quatre espaces. Presser la touche "tabulation" peut le faire automatiquement en réglant les IDE.
* Les lignes ne doivent pas dépasser 120 caractères (les IDE peuvent dessiner une ligne verticale à ce niveau).
Les normes suivantes proposent des implémentations d'architectures logicielles (voir [[w:en:PHP Standard Recommendation|PHP Standard Recommendation]] sur Wikipédia (en anglais) [[Image:Wikipedia-logo.svg|20px|Article sur Wikipédia|link=w:en:PHP Standard Recommendation]][[Catégorie:Pages liées à Wikipédia (en anglais)]]).
==== PSR-13 : liens hypermédia ====
==== PSR-14 : Event Dispatcher ====
==== PSR-15 : HTTP Handlers ====
==== PSR-16 : interface des éléments du cache ====
==== PSR-17 : interface des fabriques de requêtes HTTP ====
==== PSR-18 : interface des clients HTTP ====
=== [[w:SOLID (informatique)|SOLID]] ===
Les principes de programmation objet SOLID permettent des codes avec une bonne couverture en tests unitaires et peu de conflits de commits entre les branches du SGV.
=== Une fonction ne doit faire qu'une seule chose ===
Afin de comprendre tout ce que fait une fonction par son nom sans avoir à la relire, et de réaliser facilement ses tests unitaires, il convient de lui confier un seul rôle, et de ne pas lui injecter plus de deux arguments (en les remplaçant par une classe de configuration à plusieurs attributs<ref>https://github.com/jupeter/clean-code-php</ref>, ou un tableau).
=== Séparer le code SQL dans un dossier "repository" ===
À l'instar de Doctrine, il convient de ranger les classes contenant du DQL ou de l'SQL dans un dossier séparé (qui sera le seul à évoluer en cas de changement de SGBD). Ceci est conforme au patron de conception MVC.
== Optimisation des performances ==
PHP permet d'aboutir à un même résultat de plusieurs manières différentes, il s'agit donc de privilégier les plus performantes. Voici donc les manières de coder préconisées :
=== Profilage ===
Plusieurs outils de profilage permettent de classer les codes par temps d'exécution<ref>https://www.novaway.fr/blog/tech/les-outils-de-profiling-php-open-source-en-2017</ref>.
==== Xdebug's Profiler ====
Le profilage Xdebug a été décrit dans le chapitre ''[[Programmation PHP/Xdebug]]''.
==== XHProf ====
[[Image:Grafana Wikidata construction of DataValue objects.png|Données XHProf lues par Grafana|vignette|upright=3]]
XHProf est une extension PHP dédiée au profilage du code<ref>https://www.php.net/manual/fr/book.xhprof.php</ref>, développée par Facebook et open source depuis mars 2009.
===== Installation =====
====== Linux ======
pecl install -f xhprof
Dans php.ini :
extension=xhprof.so
====== Docker ======
RUN pecl install -f xhprof \
&& docker-php-ext-enable xhprof
RUN echo 'xhprof.output_dir = "/usr/src/app/traces"' > /usr/local/etc/php/conf.d/xhprof.ini
===== Lancement =====
Modifier le .php à analyser, en précisant les flags à analyser parmi les trois disponibles :
xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY + XHPROF_FLAGS_NO_BUILTINS);
// Code à profiler
$data = xhprof_disable();
Ensuite les data peuvent être lues via un fichier :
file_put_contents('trace.xhprof', serialize($data));
===== Affichage du résultat =====
Plusieurs solutions parmi lesquelles [[w:Grafana|Grafana]], [[w:Graphviz|Graphviz]] ou des conteneurs Docker avec leurs propres URLs.
Le résultat est présenté sous la forme d'un tableau de chaque fonction avec leur consommation de CPU et RAM en valeurs et pourcents. On peut aussi voir le graphe séquentiel de leurs appels avec les plus consommatrices de ressources mises en évidence.
==== Blackfire ====
Outil payant développé par le concepteur du framework Symfony<ref>https://blackfire.io/docs/up-and-running/installation</ref> qui fournit des organigrammes des exécutions.
=== Choisir la condition sans négation ===
Cela permet de gagner une opération NOT dans le processeur, et cela simplifie également la lecture du code en simplifiant la condition.
Avant :
<pre>
if (x !== 1) {
y = 1;
} else {
y = 0;
}
</pre>
Après :
<pre>
if (x === 1) {
y = 0;
} else {
y = 1;
}
</pre>
=== Return early ===
L'évitement des else (et else if) par des return dans les conditions, permet de gagner en performances, en lisibilité et en taille de lignes<ref>https://pear.php.net/manual/en/standards.bestpractices.php</ref>.
Cela peut permettre également d'éviter trop d'imbrications de blocs de code, et la grande indentation que cela entraîne.
Mais le plus important est aussi de ne pas lancer d'instructions inutiles. Exemple si on va chercher des enfants par un appel à une base de données ou une API :
* Pas bien :
<pre>
$parent = $this->getParent();
$children = $this->getChildren($parent);
if (empty($parent) || empty($children)) {
return 404;
}
</pre>
* Bien :
<pre>
$parent = $this->getParent();
if (empty($parent)) {
return 404;
}
$children = $this->getChildren($parent);
if (empty($children)) {
return 404;
}
</pre>
=== Passage par référence des tableaux ===
Utiliser les références dans les arguments tableaux volumineux (<code>function fonction(&$tableau)</code>), pour éviter sa duplication en mémoire.
=== Tester les invariants avant les boucles ===
Une condition est testée dans une boucle comme dans l'exemple ci-dessous.
<pre>
for ($i=0; $i<$count; $i++) {
if ($mode === 'display') {
echo $array[$i];
}
}
</pre>
Cependant, la boucle n'a pas d'influence sur la valeur de la condition.
La condition peut donc être testée avant la boucle pour éviter de la retester plusieurs fois.
<pre>
if ($mode === 'display') {
for ($i=0; $i<$count; $i++) {
echo $array[$i];
}
}
</pre>
=== Tests de charge ===
Plusieurs outils existent :
* [[w:Gatling (logiciel)|Gatling]] (en Java)
* [[w:Postman (logiciel)|Postman]] (en JavaScript)
* Locust (en Python)<ref>https://github.com/locustio/locust</ref>
== Autres bonnes pratiques ==
* Pour nommer une variable, éviter $data car trop générique : choisir un nom le plus descriptif possible.
* Dans les <code>sprintf()</code>, numéroter les paramètres (ex : remplacer "%s" par "%9$s).
* Arrondir les float pour éviter les erreurs d'imprécisions dues à la virgule flottante.
* En POO, ne pas appeler les variables superglobales directement dans les classes, mais les injecter dans le constructeur comme les autres dépendances.
* Dans le cas d'un projet à plusieurs, la revue par les pairs permet d'éviter les écueils les plus évidents. Si les spécifications métier sont simples, on peut même étendre cette pratique par un test par les pairs (sinon il faut les faire par un [[wikt:PO|PO]]).
* Ne pas rediriger vers une URL avec un trailing slash quand elle n'en n'a pas, ou le contraire sous peine de provoquer des redirections 302 pouvant doubler le temps de chargement.
* Surveiller les requêtes SQL en base de données pour :
** ne pas lancer de <code>SELECT *</code> en SQL car son résultat peut être récupéré en PHP par indice numérique et donc être perturbé par des modifications de schéma en base.
** traquer le [[wikt:problème N+1|problème N+1]] : ne pas lancer de SELECT d'un élément dans une boucle si on peut la remplacer par un seul SELECT de tous les éléments (à résoudre généralement avec un left join).
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Analyse statique de programmes=
PHP offre plusieurs outils d'[[w:analyse statique de programmes|analyse statique de programmes]] (ou ''analyse de code statique'').
== microtime() et memory_get_usage() ==
Pour mesurer un temps d'exécution dans le code, on peut utiliser des fonctions natives :
<pre>
$startTime = microtime(true);
$startMemory = memory_get_usage(true);
maFonctionMesurée();
$endTime = microtime(true);
$endMemory = memory_get_usage(true);
echo sprintf(
'L\'exécution a pris %1$f secondes et %2$f mémoire',
number_format($endTime - $startTime),
number_format($endMemory - $startMemory)
);
</pre>
== phpcs ==
PHP_CodeSniffer liste ou corrige les violations des normes de codage<ref>https://github.com/squizlabs/PHP_CodeSniffer</ref><ref>http://pear.php.net/package/PHP_CodeSniffer/redirected</ref>.
=== Installation ===
composer require squizlabs/php_codesniffer --dev
=== Lancement ===
vendor/bin/phpcs --standard=PSR12 src
NB : le fait de préciser l'utilisation de la PSR12 évite les erreurs inutiles sur les espaces autour des points de concaténation.
=== Configuration ===
* phpcs : liste les mauvaises pratiques.
* phpcbf : corrige celles qui le sont automatiquement.
* ruleset.xml : liste des vérifications à vérifier ou à exclure (ce qui évite de tout préciser en argument de la commande).
Pour ignorer une erreur, ajouter un commentaire :
* Sur un fichier : <code>// phpcs:ignoreFile</code>
* Sur une ligne : <code>// phpcs:ignore</code>
=== Configuration ===
Voici un exemple de fichier phpcs.xml.dist :
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<arg name="basepath" value="."/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<rule ref="PSR12">
<exclude name="PSR12.Operators.OperatorSpacing" />
</rule>
<file>bin/</file>
<file>config/</file>
<file>public/</file>
<file>src/</file>
<file>tests/</file>
</ruleset>
</pre>
== phpinsights ==
Ajoute <code>declare(strict_types=1);</code> dans tous les fichiers, et formate code code et le style.
=== Installation ===
composer require nunomaduro/phpinsights --dev
=== Lancement ===
Pour la vérification :
vendor/bin/phpinsights analyse src -n
Pour réparer automatiquement, ajouter "--fix".
=== Configuration ===
Voici un exemple de fichier phpinsights.php :
<pre>
<?php
declare(strict_types=1);
return [
'preset' => 'symfony',
'ide' => 'phpstorm',
'exclude' => [
],
'add' => [
],
'remove' => [
\PHP_CodeSniffer\Standards\Generic\Sniffs\ControlStructures\DisallowYodaConditionsSniff::class,
\PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\SpaceAfterNotSniff::class,
\SlevomatCodingStandard\Sniffs\ControlStructures\DisallowYodaComparisonSniff::class,
\SlevomatCodingStandard\Sniffs\ControlStructures\RequireYodaComparisonSniff::class,
],
'config' => [
],
'requirements' => [
'min-quality' => 50,
'min-complexity' => 50,
'min-architecture' => 50,
'min-style' => 50,
],
'threads' => null,
];
</pre>
== phpmd ==
''PHP Mess Detector'' recense les mauvaises pratiques du type "code mort", paramètre inutilisé, mauvais nommage camelCase, présence d'un <code>exit</code>, etc.<ref>https://github.com/phpmd/phpmd</ref>.
=== Installation ===
composer require phpmd/phpmd --dev
=== Lancement ===
vendor/bin/phpmd src ansi rulesets.xml
=== Configuration ===
Comme CodeSniffer, il utilise un fichier XML pour lister les vérifications à inclure ou exclure.
Exemple de fichier rulesets.xml :
<pre>
<?xml version="1.0"?>
<ruleset name="Custom ruleset"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="
http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>
My custom rule set that checks my code...
</description>
<rule ref="rulesets/codesize.xml">
<exclude name="CyclomaticComplexity" />
<exclude name="NPathComplexity" />
<exclude name="ExcessiveMethodLength" />
<exclude name="ExcessiveParameterList" />
</rule>
<rule ref="rulesets/codesize.xml/CyclomaticComplexity">
<properties>
<property name="reportLevel" value="20" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/NPathComplexity">
<properties>
<property name="minimum" value="600" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/ExcessiveMethodLength">
<properties>
<property name="minimum" value="110" />
<property name="ignore-whitespace" value="true" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/ExcessiveParameterList">
<properties>
<property name="minimum" value="14" />
</properties>
</rule>
<rule ref="rulesets/cleancode.xml">
<exclude name="ElseExpression" />
<exclude name="IfStatementAssignment" />
<exclude name="StaticAccess" />
<exclude name="BooleanArgumentFlag" />
</rule>
<rule ref="rulesets/controversial.xml" />
<rule ref="rulesets/design.xml">
<exclude name="CouplingBetweenObjects" />
</rule>
<rule ref="rulesets/design.xml/CouplingBetweenObjects">
<properties>
<property name="maximum" value="20" />
</properties>
</rule>
<rule ref="rulesets/naming.xml">
<exclude name="ShortVariable" />
<exclude name="LongVariable" />
</rule>
<rule ref="rulesets/unusedcode.xml" />
<exclude-pattern>src/DataFixtures</exclude-pattern>
<exclude-pattern>src/Migrations</exclude-pattern>
</ruleset>
</pre>
De plus, pour exclure une classe d'une analyse, on peut placer le nom de l'analyse dans une annotation de la classe. Ex :
@SuppressWarnings(PHPMD.CyclomaticComplexity)
Ou tout ignorer :
@SuppressWarnings(PHPMD)
== phpmetrics ==
Outil d'analyse fournissant un rapport graphique.
=== Installation ===
composer require phpmetrics/phpmetrics --dev
=== Lancement ===
vendor/bin/phpmetrics --report-html=reports/phpmetrics-$$(date '+%Y%m%d_%H%M%S').html src
=== Configuration ===
Éviter de versionner les rapports avec l'application. Par exemple ajouter "reports" au .gitignore.
== phpstan ==
PHP Static Analysis détecte des erreurs potentielles à l'exécution (ex : mauvais type) sans réellement exécuter le code<ref>https://github.com/phpstan/phpstan</ref>.
=== Installation ===
composer require phpstan/phpstan --dev
Voir plus pour vérifier l'utilisation de certaines dépendances :
composer require phpstan/phpstan-phpunit --dev
composer require phpstan/phpstan-symfony --dev
composer require phpstan/phpstan-doctrine --dev
=== Lancement ===
vendor/bin/phpstan analyse --no-progress --memory-limit=256M src
=== Configuration ===
Pour ignorer une erreur sur un élément, il y a trois solutions :
* utiliser une annotation sur la ligne précédente :
/** @phpstan-ignore-next-line */
* ou sur la même ligne :
/** @phpstan-ignore-line */
* ou préciser dans le fichier de configuration (phpstan.neon), le dossier à exclure de l'analyse ou le message d'erreur à ignorer.
Exemple de fichier phpstan.neon :
<pre>
includes:
- 'vendor/phpstan/phpstan-symfony/extension.neon'
- 'vendor/phpstan/phpstan-phpunit/extension.neon'
- 'vendor/phpstan/phpstan-doctrine/extension.neon'
parameters:
level: 7
paths:
- src
excludePaths:
- src/Migrations/*
reportUnmatchedIgnoredErrors: false
inferPrivatePropertyTypeFromConstructor: true
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
- '#Cannot assign offset [^ ]+ to [^\.]+.#'
- '#Call to an undefined method [a-zA-Z0-9\\_]+::getItem\(\)#'
- '#Call to an undefined method Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::add\(\)#'
</pre>
Le champ "level" va de 0 (violations les plus graves) à 9 (bonnes pratiques idéales). Donc plus il est haut et plus il y a de vérifications<ref>https://phpstan.org/user-guide/rule-levels</ref>.
== psalm ==
Psalm cherche à détecter des bugs, par exemple des erreurs de type<ref>https://psalm.dev/docs/</ref>.
=== Installation ===
composer require --dev vimeo/psalm
vendor/bin/psalm --init
=== Lancement ===
vendor/bin/psalm
=== Configuration ===
Pour ignorer une erreur, ajouter l'annotation de la fonction concernée :
/** @psalm-suppress UndefinedConstant */
En effet, les métriques telles que les taux de duplication ou de couverture du code (par les tests automatiques) sont révélatrices de la qualité du projet. Pour les mesurer il existe plusieurs outils :
== php-scrutinizer ==
Comprend des analyses de sécurité et de performances<ref>https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/</ref>.
== GrumPHP ==
Idem<ref>https://github.com/phpro/grumphp</ref>.
composer require --dev phpro/grumphp
== [[w:SonarQube|SonarQube]] ==
[[File:SonarQube Project page.png|vignette|upright=2]]
Il s'agit d'un logiciel de qualimétrie dont la version gratuite supporte PHP.
Il nécessite trois composants :
* le serveur, qui stocke les analyses et permet de les visualiser sur un dashboard. Il est téléchargeable gratuitement, ou utilisable dans sa version payante SonarCloud pour ne pas avoir à héberger le serveur soi-même. Identifiant par défaut : admin / admin.
* le scan, client qui envoie les analyses au serveur. A l'instar du serveur, il est disponible au téléchargement en multi-plateforme (via Java) ou sur Docker.
* un plugin PhpStorm (SonarLint) pour suggérer des améliorations en cours de frappe.
<div class="remarque"> Afin de ne pas versionner le cache du scan, il convient d'ajouter ceci au .gitignore du projet :
/.scannerwork/
</div>
=== Exemples ===
Pour lancer le scan sur un projet Symfony installé sur Windows (dans un .cmd) :
<pre>
REM Copy this file in the projet to scan, or add: -D"sonar.projectBaseDir=C:\myProjectPath" ^
C:\sonar-scanner\bin\sonar-scanner.bat ^
-D"sonar.projectKey=MY_PROJECT_KEY" ^
-D"sonar.login=MY_PROJECT_LOGIN" ^
-D"sonar.host.url=http://localhost:9000" ^
-D"sonar.sourceEncoding=UTF-8" ^
-D"sonar.language=php" ^
-D"sonar.sources=src" ^
-D"sonar.tests=tests"
pause
</pre>
Sur Docker :
<pre>
docker run --rm \
-v "/$(pwd)/":/var/www/symfony \
-w "/var/www/symfony" \
-e SONAR_HOST_URL="http://sonar-server.localhost:9000" \
-e SONAR_LOGIN="MY_PROJECT_LOGIN" \
sonarsource/sonar-scanner-cli \
-Dsonar.projectKey="MY_PROJECT_KEY" \
-Dsonar.sourceEncoding=UTF-8 \
-Dsonar.language=php \
-Dsonar.sources=src \
-Dsonar.tests=tests
</pre>
Pour remplir le taux de couverture de code, il faut ajouter le lien vers un rapport PhpUnit :
-Dsonar.php.tests.reportPath=reports/phpunit.logfile.xml \
-Dsonar.php.coverage.reportPaths=reports/phpunit.coverage.xml
== Makefile d'analyse ==
Pour éviter de retenir toutes ces commandes, on peut les inclure dans le composer.json à la rubrique "scripts", ou dans un Makefile.
Exemple sur Symfony :
<pre>
all: lint phpcs-check phpinsights-check phpmd phpstan-check psalm
lint:
bin/console lint:container
bin/console lint:twig templates
bin/console lint:yaml config
phpcs-fix:
vendor/bin/phpcbf --standard=PSR12 --ignore=Migrations src/
phpcs-check:
vendor/bin/phpcs --standard=PSR12 --ignore=Migrations src/
phpinsights-check:
vendor/bin/phpinsights analyse src -n
phpmd:
vendor/bin/phpmd src ansi rulesets.xml
phpmd-report:
vendor/bin/phpmd src html rulesets.xml --reportfile reports/phpmd-$$(date '+%Y%m%d_%H%M%S').html
phpmetrics:
vendor/bin/phpmetrics --report-html=reports/phpmetrics-$$(date '+%Y%m%d_%H%M%S').html src
phpstan:
vendor/bin/phpstan analyse src/
phpstan-check:
vendor/bin/phpstan analyse --no-progress --memory-limit=256M src/
psalm:
vendor/bin/psalm --no-cache
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exceptions=
== Tester l'existence ==
Pour éviter les warnings de variables inexistantes il vaut mieux les initialiser au début. Si toutefois cela s'avère impossible, on recourt à des tests sur les fonctions suivantes pour le faire :
<pre>
if (!isset($maVariable)) { $maVariable = ''; }
if (!defined('MA_CONSTANTE')) { define('MA_CONSTANTE', ''); }
if (!function_exists('maFonction')) { function maFonction() {} }
</pre>
== empty() ==
Permet de savoir si une variable est définie et si contient une valeur. Son comportement s'adapte au type de la variable. Cela équivaut à :
* String : <code>isset($v) and $v == ""</code>.
ou
* Booléen : <code>isset($v) and $v == false</code>.
ou
* Tableau : <code>isset($v) and sizeof($v) == 0</code>.
== try... catch ==
Tout comme en [[Programmation Java/Exceptions|Java]], la [[w:Système de gestion d'exceptions|levée d'exception]] est assurée par un bloc <code>try... catch</code>. Cela permet à un script de poursuivre son exécution malgré les erreurs (ex : panne réseau), et ainsi de ne pas bloquer l'utilisateur sur une page blanche ou en anglais destinée au développeur.
Ces interruptions sont représentées par des classes qui héritent toutes du type <code>Throwable</code>, qui a deux classes filles : <code>Error</code> et <code>Exception</code><ref>https://www.php.net/manual/fr/language.errors.php7.php</ref>.
=== Exemples ===
<pre>
try {
echo '1 / 2 = ' 1/2;
echo '3 / 0 = ' 3/0; // instruction qui déclenchera l'exception
echo '2 / 1 = ' 2/1; // cette instruction ne sera pas exécutée à cause de la précédente
} catch (Exception $e) {
echo $e->getMessage(); // afficher le message lié à l'exception
}
</pre>
Il n'est donc pas nécessaire de prévoir ce qui peut interrompre le programme pour s'en prémunir et poursuivre l'exécution en fonction.
La classe de gestion des erreurs nommée ''Exception'' est gracieusement mise à votre disposition par l’interpréteur dans les versions ultérieures à PHP 5.0.
Autre exemple d’utilisation :
<pre>
class Humain
{
var $age;
function __construct($combien)
{
$this->age = $combien;
try {
if ($this->age < 0) {
throw new Exception('Un peu jeune');
}
if ($this->age > 200) {
throw new Exception('Un peu vieux');
}
} catch (Exception $e) {
echo $e->getMessage();
return;
}
}
}
//Retournera un message d'erreur
$humain = new Humain(700);
$humain = new Humain(-3);
//Sans erreur
$humain = new Humain(16);
</pre>
<div class="remarque"> Le bloc <code>finally</code> ajouté après les catch sera exécuté après les instructions du try et des catch.</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">La bonne pratique est de ne jamais envoyer <code>\Exception</code> directement mais d'utiliser ses sous-classes, qui ont de plus généralement le bon code HTTP (au lieu de erreur 500). Par exemple sur Symfony, les erreurs inhérente à l'utilisateur (400) sont accessibles dans le namespace <code>Symfony\Component\HttpKernel\Exception</code>.</p>
</div>
</div>
Par ailleurs, il est possible de créer ses propres classes d'exceptions, et de modifier les exceptions natives PHP avec <code>set_exception_handler()</code><ref>http://php.net/manual/fr/function.set-exception-handler.php</ref>.
=== Recréer une exception ou une erreur ===
Les ''Throwable'' PHP sont immutables, pour les modifier il faut donc les recréer. Pour faciliter cela, ils possèdent depuis PHP7 une méthode <code>getPrevious()</code>, dont le résultat injecté en troisième argument du constructeur d'un autre, permet de cloner l'objet. Ex :
<pre>
try {
crash();
} catch (\Exception $e) {
throw new \Exception('Nouveau message + ancien : '.$e->getMessage(), $e->getCode(), $e->getPrevious());
}
</pre>
NB : la classe ''Throwable'' n'est pas instanciable directement.
== trigger_error() ==
Cette fonction lance une exception du type placé en second paramètre, dont certains peuvent stopper l'exécution du programme<ref>http://php.net/manual/fr/function.trigger-error.php</ref>. Par exemple :
<pre>
trigger_error('Message d'erreur', E_USER_ERROR);
</pre>
Affiche : ''ErrorException. User Error: Message d'erreur''.
La liste des types d'erreur est disponible sur http://php.net/manual/fr/errorfunc.constants.php.
== Logs ==
=== Affichage d'une trace lisible ===
Afin de clarifier la trace de l'exécution des scripts (il peut être utile de formater celle-ci avec la balise ''{{#parsoid\0fragment:18}}'') :
<pre>
if ($debogage) {
var_dump(scandir('.'));
}
</pre>
NB : [[../Installer_PHP#Installer_PHP|on rappelle]] que les commandes <code>ini_set('display_errors', 1);</code> et <code>error_reporting(E_ALL);</code> permet d'afficher à l'écran toutes les erreurs et avertissements.
Voici une manière de n'afficher les erreurs que lors du débogage :
<pre>
if ($debogage) {
error_reporting(E_ALL);
} else {
error_reporting(0);
}
</pre>
Pour créer une bonne gestion des erreurs, partez du principe que l'utilisateur est imprévisible, et qu'il va justement faire ce qu'il ne faut pas ([[w:Loi de Murphy|Loi de Murphy]]). Envisagez toutes les possibilités et trouvez-y une solution, en testant les exceptions (ex : caractères d'échappements ou symboles non ASCII comme des sinogrammes pour voir s'ils s'affichent bien).
==== debug_backtrace et debug_print_backtrace ====
Ces fonction affichent automatiquement la trace de l'exécution menant à elles.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Sur un framework cela peut être trop long à exécuter, il faut donc ne faire afficher que les noms des fichiers exécutés avec l'argument suivant :</p>
</div>
</div>
<pre>
var_dump(debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
</pre>
<pre>
$logFile = fopen('debug.log', 'a+');
fwrite($logFile, json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)).PHP_EOL);
fclose($logFile);
</pre>
==== throw ====
Si l'affichage de la trace doit être suivi d'une interruption du programme, utiliser :
<pre>
throw new \Exception('Trace');
</pre>
==== Exception::getTraceAsString() ====
Si l'exception ne surgit pas avec un throw, on peut afficher la trace depuis avec sa méthode :
<pre>
echo $e->getTraceAsString();
</pre>
=== Fichier de log maison ===
Bien souvent on ne peut pas afficher les logs dans le navigateur ou rapidement via le logeur existant, donc il faut recourir à la création d'un nouveau fichier de log temporaire :
<pre>
$logFile = fopen('debug.log', 'a+');
fwrite($logFile, $maVariable.PHP_EOL);
fclose($logFile);
</pre>
=== Symfony ===
Depuis une commande Symfony, on peut afficher des logs en console :
<pre>
$output->writeln('Update complete');
</pre>
Mais le mieux est d'utiliser la bibliothèque Monolog<ref>https://symfony.com/doc/current/logging.html</ref> (compatible PSR-3<ref>https://www.php-fig.org/psr/psr-3/</ref>), pour que les logs soient horodatés et s'enregistrent dans var/log/ (puisque les commandes peuvent être lancées par cron et donc sans affichage en console). De plus, Monolog est configurable pour afficher en plus ces logs en console<ref>https://symfony.com/doc/current/logging/monolog_console.html</ref> (ce qui est fait par défaut sur la v3.3 dans <u>config/packages/dev/monolog.yaml</u>).
composer require symfony/monolog-bundle
Pour booster ses performances, on peut le régler ainsi dans monolog.yaml :
<pre>
monolog:
use_microseconds: false
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Quand l'application tourne sur plusieurs serveurs frontaux, il vaut mieux centraliser les logs dans un agrégateur de logs comme [[w:Kibana|Kibana]] ou Graylog. Or, dedans il est possible que tous les logs ne soient pas visibles. Par exemple en cas de caractères spéciaux dans les clés des tableaux, tels que ">" ou "$", ou en cas de valeur NULL :
<pre>
$a = 'HelloWorld!';
$this->logger->info('Test d\'affichage.', [
'$a' => $a, // absent de Graylog
'a' => $a, // visible
'b' => null, // absent de Graylog
]);
</pre>
</p>
</div>
</div>
<div class="remarque"> C'est une raison pour ne pas inclure de variable dans le message et de ne les mettre que dans le tableau, en plus de pouvoir retrouver tous les logs similaires plus facilement.</div>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Xdebug=
[[Image:Xdebug Logo.svg|vignette]]
[[w:Xdebug|Xdebug]] est un système qui permet de calculer le taux de [[w:couverture de code|couverture de code]], de le [[w:Profilage de code|profiler]], ou de l'exécuter pas à pas.
Cela montre donc l'exécution plus précisément qu'en relecture seule, et élimine donc le risque de sauvegarder des "echo", des "print" ou des "var_dump" en production.
== Installation ==
=== Linux ===
Installation sur Linux<ref>{{lien web|url=https://www.youtube.com/watch?v=QcfGyn_jmm8|titre=PHPStorm : Xdebug}}</ref><ref>{{lien web|url=https://www.youtube.com/watch?v=8HidJasG_ds|titre=Tutoriel PHP : Xdebug, l'exécution pas à pas}}</ref> :
<pre>
sudo apt-get install php8.4-xdebug
</pre>
ou
<pre>
pecl install xdebug
</pre>
Puis dans php.ini :
<pre>
xdebug.remote_enable = On
</pre>
ou sur PHP via Apache :
<pre>
sudo vim /etc/php/8.4/apache2/conf.d/20-xdebug.ini
</pre>
Ajouter :
<pre>
xdebug.remote_enable=1
</pre>
=== Configuration complète ===
Exemple en V3<ref>https://xdebug.org/docs/upgrade_guide</ref> :
<pre>
sudo apt-get install php8.4-xdebug
&& echo "xdebug.discover_client_host=0" >> /usr/local/etc/php/php.ini \
&& echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/php.ini \
&& echo "xdebug.mode=debug,coverage,profile" >> /usr/local/etc/php/php.ini
</pre>
<div class="NavFrame" style="clear:left; "><div class="NavHead " align="center" style="cursor:default;clear:left; text-align:center; background-color: #80808020; " title="Cliquez pour afficher/masquer">
Exemple en V2
</div>
<div class="NavContent" align="left" style="clear:left; column-count: 1; -webkit-column-count: 1; -moz-column-count: 1; ">
On peut aussi forcer d'autres paramètres<ref>https://xdebug.org/docs/all_settings</ref> :
<pre>
sudo apt-get install php7.4-xdebug
&& echo "xdebug.remote_handler=dbgp" >> /usr/local/etc/php/php.ini \
&& echo "xdebug.remote_connect_back=0" >> /usr/local/etc/php/php.ini \
&& echo "xdebug.remote_port=9000" >> /usr/local/etc/php/php.ini \
&& echo "xdebug.remote_enable=1" >> /usr/local/etc/php/php.ini \
&& echo "xdebug.remote_autostart=1" >> /usr/local/etc/php/php.ini
</pre>
</div>
<div class="NavEnd" style="clear:left;"> </div>
</div>
<div class="remarque"> Le client_port par défaut est passé de 9000 à 9003 entre Xdebug 2 et 3<ref>https://www.jetbrains.com/help/phpstorm/2023.2/configuring-xdebug.html#5a0181d2</ref>.</div>
=== Docker ===
Avec Docker, il faut aussi spécifier le "client_host"<ref>https://blog.eleven-labs.com/fr/debug-run-phpunit-tests-using-docker-remote-interpreters-with-phpstorm/</ref>.
Exemple d'installation et activation :
<pre>
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug
RUN echo "xdebug.client_host = 172.170.0.1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.discover_client_host=0" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.mode=debug,coverage,profile" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
</pre>
=== Windows ===
Wamp fournit Xdebug en mode "develop". Pour activer le débogage pas à pas, il faut donc éditer le php.ini :
<pre>
xdebug.mode=debug
xdebug.client_host=127.0.0.1
xdebug.start_with_request=trigger
</pre>
NB : on peut aussi le lancer tout le temps mais cela ralentit l'application et si l'IDE n'écoute plus, rajouter des logs "Xdebug: [Step Debug] Could not connect to debugging client".
<pre>
xdebug.start_with_request=yes
</pre>
== Lancement ==
=== Dans un navigateur ===
Il faut installer un module sur son navigateur (ex : Xdebug-ext sur Firefox<ref>https://addons.mozilla.org/fr/firefox/addon/xdebug-ext-quantum/?src=search</ref>) pour pouvoir activer ou désactiver le débogage d'une page. Ce module s'interface avec les principaux IDE, par exemple [[Programmation/PhpStorm|PhpStorm]]<ref>https://www.jetbrains.com/help/phpstorm/configuring-xdebug.html</ref>, pour leur faire lancer le débogage lors du chargement d'une page.
=== En ligne de commande ===
On peut ajouter un argument à PHP<ref>https://stackoverflow.com/questions/2288612/how-to-trigger-xdebug-profiler-for-a-command-line-php-script</ref>. Ex :
<pre>
php -d xdebug.mode=debug,profile bin/console MaCommande.php
</pre>
Pour [[Programmation PHP/PhpStorm|PhpStorm]] et [[Docker]], il faut ajouter l'interpréteur dans les paramètres<ref>https://www.jetbrains.com/help/phpstorm/configuring-remote-interpreters.html#remote-interpreter-docker</ref>.
=== Débogage pas à pas ===
Lors du débogage, PhpStorm fera apparaitre un menu "Debug" avec trois sous-menus dont :
# la liste des variables du script et leurs valeurs (modifiables à la volée)
# les warnings PHP
# la sortie HTML.
Quand on clique dans la marge, un [[wikt:point d’arrêt|point d’arrêt]] est créé et représenté par une ligne rouge.
Raccourcis clavier (voir le menu "Run" en haut) :
* F7 (step into) : mode pas à pas détaillé.
* F8 (step over) : mode pas à pas sans sauter dans les dépendances.
* F9 (resume) : poursuivre l'exécution du programme sans s'arrêter.
* Alt + F9 : poursuivre jusqu'au prochain point d'arrêt.
* Shift + F7 : pas à pas intelligent.
=== Profilage ===
Ajouter le mode au précédent<ref>https://xdebug.org/docs/profiler</ref> :
xdebug.mode=debug,profile
Un ensemble de vidéos explicatives existent en anglais<ref>https://xdebug.org/docs/profiler#related_content</ref>.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Mots réservés=
== Mots du langage ==
Les [[wikt:mot réservé|mots]] qui suivent ont un sens spécial en PHP. Si vous les utilisez hors de leur contexte, des problèmes de confusions peuvent arriver.
{|align="center" rules="all" cellspacing="0" cellpadding="4" style="border: 1px solid #999; border-right: 2px solid #999; border-bottom: 2px solid #999; background: #ccccff"
|+ style="font-weight: bold; font-size: 1.1em; margin-bottom: 0.5em"|Liste des mots spécifiques en PHP
|- style="background: #9999ff"
! !! !! !! !! Uniquement sous PHP 5
|-
| and || or || xor || __FILE__ || exception
|-
| __LINE__ || array || as || break || final
|-
| case || class || const || continue || php_user_filter
|-
| declare || default || die || do || public
|-
| echo || else || elseif || empty || private
|-
| enddeclare || endfor || endforeach || endif || catch
|-
| endswitch || endwhile || eval || exit || try
|-
| extends || for || foreach || function || clone
|-
| global || if || include || include_once || implements
|-
| isset || list || new || print || interface
|-
| require || require_once || return || static || throw
|-
| switch || unset || use || var || protected
|-
| while || __FUNCTION__ || __CLASS__ || __METHOD__ || abstract
|-
| extends || cfunction* || old_function* || || yield<ref>http://php.net/manual/fr/language.generators.syntax.php</ref>
|-
| colspan="5" align="center"| <small>* : depuis PHP4 seulement</small>
|}
Liste des 72 mots réservés par ordre alphabétique<ref>http://www.php.net/manual/fr/reserved.keywords.php</ref> :
<pre>
__CLASS__
__DIR__
__FILE__
__FUNCTION__
__LINE__
__METHOD__
__NAMESPACE__
abstract
and
array()
as
break
case
catch
cfunction ''(PHP 4)''
class
clone
const
continue
declare
default
die()
do
echo()
else
elseif
empty()
enddeclare
endfor
endforeach
endif
endswitch
endwhile
eval()
exit()
explode()
extends
final
for
foreach
function
global
goto
if
implements
include_once()
include()
instanceof
interface
isset()
list()
namespace
new
old_function ''(PHP 4)''
or
print()
private
protected
public
require_once()
require()
return()
split() ''(PHP < 5.3)''
static
switch
throw
try
unset()
use
var
while
xor
</pre>
== Nouveautés PHP 7 ==
Plusieurs opérateurs composés permettent de réduire la syntaxe d'opérations courantes :
* <code>??</code> : [[wikt:opérateur de fusion null|opérateur de fusion null]]. Équivalent d'un opérateur ternaire avec un <code>is_null()</code>.
* <code><=></code> : [[wikt:opérateur vaisseau spatial|opérateur vaisseau spatial]]. Équivalent d'un <code>switch</code> à trois cas : inférieur, égal et supérieur.
* <code>intdiv()</code> : division entière. Équivalent de <code>/</code> + <code>intval()</code>.
De plus, on peut maintenant utiliser :
* plusieurs classes dans le même <code>use</code>.
* <code>define()</code> pour définir un tableau de constantes.
Depuis PHP 7.4, les propriétés typées. Ex :
public int $id;
== Extensions ==
Liste des 48 bibliothèques natives PHP 5.5.0 avec EasyPHP<ref>https://www.php.net/manual/fr/extensions.alphabetical.php</ref> :
Core
[[w:PHP Data Objects|PDO]]
[[w:phar (informatique)|Phar]]
Reflection
SPL
[[w:SimpleXML|SimpleXML]]
apache2handler
bcmath
bz2
calendar
ctype
[[w:cURL|curl]]
date
dom
ereg
filter
ftp
[[w:GD (bibliothèque)|gd]]
hash
iconv
json
libxml
mbstring
mcrypt
mhash
[[MySQL|mysql]]
[[w:MySQLi|mysqli]]
mysqlnd
[[w:ODBC|odbc]]
[[OpenSSL|openssl]]
[[w:PCRE|pcre]]
pdo_mysql
pdo_sqlite
pdo_sqlsrv
[[w:Session (informatique)|session]]
[[w:socket|socket]]s
[[w:SQLite|sqlite3]]
sqlsrv
standard
tokenizer
wddx
[[w:xdebug|xdebug]]
[[XML|xml]]
xmlreader
xmlwriter
xsl
[[w:ZIP (format de fichier)|zip]]
[[w:zlib|zlib]]
Voir aussi [[w:en:List of PHP extensions|List of PHP extensions]] sur Wikipédia (en anglais) [[Image:Wikipedia-logo.svg|20px|Article sur Wikipédia|link=w:en:List of PHP extensions]][[Catégorie:Pages liées à Wikipédia (en anglais)]].
==Références==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/Variables=
== Utilisation de variables ==
=== Un exemple de programme ===
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Variables en PHP !</title>
</head>
<body>
<?php
for($i = 1; $i <= 10; $i++)
echo ' <p>Ligne numéro '.$i.'</p>'."\n";
?>
</body>
</html>
</pre>
=== Explications ===
*Une variable en php commence par le symbole $. Ici nous utilisons une variable d'identificateur $i.
*Il n'y a pas de déclaration ni de typage fixe : une variable peut changer dynamiquement de type, ce qui est parfois vu comme un atout, parfois comme une faiblesse !
* Ce programme comporte une boucle for qui a sa sémantique habituelle. La variable $i va donc prendre successivement les valeurs 1,2,... jusqu'à 10.
* Dans cet exemple les chaînes de caractères sont entre apostrophes.
* La concaténation des chaînes de caractères s'effectue grâce à l'opérateur '''.'''.
* '''Remarque : ''' si on veut qu'une chaîne de caractères contienne une apostrophe droite il faut écrire \' à l'intérieur de la chaîne.
=== Exécution du programme ===
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Variables en php</title>
</head>
<body>
<p>Ligne numéro 1</p>
<p>Ligne numéro 2</p>
<p>Ligne numéro 3</p>
<p>Ligne numéro 4</p>
<p>Ligne numéro 5</p>
<p>Ligne numéro 6</p>
<p>Ligne numéro 7</p>
<p>Ligne numéro 8</p>
<p>Ligne numéro 9</p>
<p>Ligne numéro 10</p>
</body>
</html>
</pre>
=== Les guillemets ===
Une chaîne de caractère entre guillemet est assez particulière : si elle contient $a alors $a est remplacé par la valeur de la variable $a. Il y a automatiquement substitution. Si on écrit \$ alors il n'y a plus substitution. De la même manière, pour afficher le caractère guillemet on écrit \".
=== Exemple 2 : guillemets et variables ===
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Variables en PHP !</title>
</head>
<body>
<?php
$a=67+33;
echo "la variable \$a vaut $a";
?>
</body>
</html>
</pre>
=== Explications ===
Dans ce programme la variable $a vaut 67+33 donc vaut 100. Dans la chaîne de caractères \$a affichera $a et le deuxième $a sera remplacé par la valeur 100. Il s'affichera donc :<br/>
''la variable $a vaut 100''
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/Sommaire=
== Un sommaire simple ==
Imaginons un site Web composé de 4 pages entre lesquelles on peut naviguer grâce à un sommaire. Notre sommaire est par exemple à gauche de l'écran et contient 4 liens hypertextes : page 1, page 2, page 3 et page 4. À droite de l'écran, le contenu principal de la page change en fonction de la page affichée. Par contre notre sommaire apparaît lui sur chacune des pages.
=== Le programme en php ===
'''Fichier index.php '''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Sommaire en PHP !</title>
<style type="text/css">
#sommaire
{
position:absolute;
background-color:cyan;
left:10px;
width:100px;
}
#page
{
position:absolute;
background-color:#AAAAAA;
left : 200px;
width:500px;
height:500px;
}
</style>
</head>
<body>
<div id="sommaire">
<h3>Sommaire</h3>
<?php for($i = 1; $i <= 4; $i++) :?>
<a href="index.php?page=<?php echo $i ?>">Page <?php echo $i ?></a><br/>
<?php endfor ?>
</div>
<div id="page">
<?php
if (isset($_GET['page'])) $numero=$_GET['page']; else $numero='1';
require 'page'.$numero.'.html';
?>
</div>
</body>
</html>
</pre>
'''fichier page1.html'''
<pre>
<h1>Page 1</h1>
bla bla bla
</pre>
'''fichier page2.html'''
<pre>
<h1>Page 2</h1>
ble ble ble
</pre>
'''fichier page3.html'''
<pre>
<h1>Page 3</h1>
bli bli bli
</pre>
'''fichier page4.html'''
<pre>
<h1>Page 4</h1>
blo blo blo
</pre>
=== Explications ===
* Dans ce programme, la page est découpée en 2 parties : à gauche une partie ayant comme id sommaire et à droite une page ayant comme id page.
*La partie « sommaire » utilise le style #sommaire de la feuille de style et la partie « page », le style #page.
* Le sommaire est constitué de 4 liens hypertextes appelant respectivement ''index.php?page=1'', ''index.php?page=2'', ''index.php?page=3'' et ''index.php?page=4''. Lorsqu'on met un point d'interrogation après l'URL d'une page, cela signifie qu'on donne une valeur à un paramètre et qu'on envoie cette information au serveur par la [[w:Hypertext_Transfer_Protocol#M.C3.A9thodes|méthode GET]]. Lorsqu'on clique sur l'un des 4 liens hypertextes, on appelle à chaque fois la même page index.php mais à chaque fois la valeur du paramètre nommé <code>page</code> change : il vaut 1, 2, 3 ou 4 selon le lien cliqué.
* Dans la partie « page » de index.php, la valeur du paramètre <code>page</code> est récupérée de l'url cliquée en écrivant <code>$_GET['page']</code>. Ce paramètre peut très bien ne pas exister : ceci a lieu notamment la première fois qu'on appelle notre page index.php. Dans ce cas, la fonction isset() permet de savoir si une variable existe. La ligne
::<code>if (isset($_GET['page']))$numero=$_GET['page']; else $numero='1';</code>
:récupère la valeur du paramètre page et la place dans la variable $numero. Si ce paramètre n'existe pas $numero vaut 1.
* La fonction <code>require'nom_du_fichier';</code> permet d'insérer un fichier à cet endroit dans le code. Le server insère donc page1.html ou page2.html ou page3.html ou page4.html en fonction de la valeur du paramètre page (et de la variable numero).
* Notre sommaire est terminé.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/Formulaire=
== Interaction avec un formulaire ==
=== Les principaux concepts ===
L'interaction entre une application en PHP et un utilisateur peut s'effectuer par des liens hypertextes ou par l'envoi d'un formulaire. C'est ce cas dernier que nous allons étudier ici.
Le formulaire comporte une balise '''form''' qui précise que la méthode utilisée pour envoyer le contenu du formulaire au programme en PHP est la [[w:Hypertext_Transfer_Protocol#M.C3.A9thodes|méthode POST]]. Elle précise également l'action du formulaire, c'est-à-dire à quelle adresse envoyer le contenu du formulaire pour son traitement. Dans notre exemple, après un clic sur le bouton d'envoi, le formulaire déclenchera l'exécution du programme go.php.
Le formulaire est composé de 3 éléments graphiques : 2 champs de type texte nommés respectivement nom et prénom et un bouton sur lequel il est écrit '''envoyer le formulaire'''. Le formulaire invite donc l'utilisateur à entrer un nom et un prénom et à cliquer sur le bouton « envoyer le formulaire ».
Le programme go.php doit récupérer les valeurs contenues dans le formulaire : pour récupérer la valeur du champ nom, il faut écrire '''<code>$_POST['nom']</code>'''. De la même manière, pour récupérer la valeur du champ prénom, il faut écrire '''<code>$_POST['prenom']</code>'''. Après un clic sur le bouton « envoyer le formulaire », une autre page s'affiche. Elle contient un message contenant "Bienvenue à" suivi du prénom et du nom définis dans le formulaire rempli. Le programme a bien récupéré la valeur des différents champs du formulaire.
=== Le programme en php ===
'''Le fichier index.html : '''
<pre>
<!DOCTYPE html PUBLIC "-//DTD XHTML 2.0 Transitional//EN"
"http://www.org/TR/xhtml/xhtml-transitional.dtd">
<form action="go.php" method="post">
<p>Votre nom : <input type="text" name="nom" /></p>
<p>Votre prénom : <input type="text" name="prenom" /></p>
<p><input type="submit" value="envoyer le formulaire" /></p>
</form>
</pre>
'''Le fichier go.php : '''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 1.0 Transitional//EN"
"http://www.org/xhtml/html-transitional.dtd">
<?php
$nom = $_POST['nom'];
$prenom = $_POST['prenom'];
echo "<h3>Bienvenue à ".$prenom.' '.$nom,"</h3>";
echo "<p><a href='index.html'>Retour au formulaire</a></p>";
?>
</pre>
=== Captures d'écran ===
[[Image:Formulaire-1.jpg|Le formulaire initial]]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/BD 1=
== Afficher le résultat d'une requête ==
=== Présentation ===
Dans cet exemple, on va créer une application qui extrait des données à partir d'une base de données et qui affiche le résultat à l'écran. Notre programme va afficher une liste d'employés d'une entreprise imaginaire : chaque employé est défini par un nom, un prénom et un salaire. La base de données utilisée sera une base '''mysql'''. Les données seront dans une table nommée ''employe''. La table employé possède des champs NOM, PRENOM et SALAIRE. La table employé possède de plus un identifiant nommé ID qui est un entier (BIGINT) autoincrémenté et qui est une clé primaire de la table.
Nous étudierons une première version du programme et dans un second temps nous améliorerons le programme en utilisant la méthode GET lors d'une deuxième version.
=== Descriptif du site ===
*La première page affiche 3 liens hypertextes :
** un lien pour afficher la liste complète des employés,
** un lien pour afficher la liste dans l'ordre alphabétique,
** un lien pour afficher par salaire décroissant.
* Sur chacune des pages, on affiche la liste et il y a un lien pour revenir à la première page.
=== Création de la base ===
Les requêtes suivantes permettent la création et l'initialisation des données dans notre table :
'''Fichier BD.sql : '''
<pre>
CREATE TABLE `employe` (
`ID` BIGINT NOT NULL AUTO_INCREMENT ,
`NOM` VARCHAR( 20 ) ,
`PRENOM` VARCHAR( 20 ) ,
`SALAIRE` DOUBLE DEFAULT '0',
PRIMARY KEY ( `ID` )
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Dupond', 'Marcel', '8000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Martin', 'Xavier', '4000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Gogol', 'Henri', '3000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Hugo', 'Victor', '2000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Gali', 'Daniel', '6000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Martin', 'Georges', '9000'
);
</pre>
La table créée par ces requêtes est la suivante :
<table border="1">
<tr> <td width="25%" ><b>ID</b></td><td width="25%"><b>NOM</b></td><td width="25%"><b>PRENOM</b></td><td width="25%"><b>SALAIRE</b></td></tr>
<tr><td>1</td><td>Dupond</td><td>Marcel</td><td>8000</td></tr>
<tr><td>2</td><td>Martin</td><td>Xavier</td><td>4000</td></tr>
<tr><td>3</td><td>Gogol</td><td>Henri</td><td>3000</td></tr>
<tr><td>4</td><td>Hugo</td><td>Victor</td><td>2000</td></tr>
<tr><td>5</td><td>Gali</td><td>Daniel</td><td>6000</td></tr>
<tr><td>6</td><td>Martin</td><td>Georges</td><td>9000</td></tr>
</table>
=== Configuration de la base de données ===
La table ''employe'' appartient à la base de données ''toto''. L'administrateur de cette base est root. Il n'a pas de mot de passe. Attention ceci est une configuration type utilisée par défaut pour la plateforme de développement [[Apache/Installation|EasyPHP]] : il est vivement recommandé de mettre un vrai mot de passe pour une plateforme en exploitation.
=== Les requêtes utilisées ===
*Pour afficher la liste des employés, on utilisera la requête :
''SELECT * FROM employe''
*Pour afficher la liste des employés par ordre alphabétique, on utilisera la requête :
''SELECT * FROM employe ORDER BY NOM, PRENOM''
*Pour afficher la liste des employés par salaire décroissant, on utilisera la requête :
''SELECT * FROM employe ORDER BY SALAIRE DESC''
=== Comment accéder en php à une base mysql ===
Les différentes étapes à respecter seront les suivantes :
# récupérer les informations suivantes :
## le nom de la machine qui héberge le serveur de la base de données (ici localhost)
## le nom de la base de données (ici toto)
## le nom de l'utilisateur de la base de données (ici root)
## le mot de passe de cet utilisateur (ici le mot de passe est vide).
# se connecter au serveur de base de données
# sélectionner la base de données (ici toto) sur ce serveur.
# créer la requête dans une chaîne de caractères
# envoyer la requête à la base de données et récupérer le résultat
# fermer la connexion avec le serveur de base de données
# transformer le résultat de la requête en HTML.
==== Étape 1 : les paramètres spécifiques au site ====
Pour réaliser la première étape nous allons créer un fichier '''params.php''' qui contient 4 variables $host (le nom de la machine qui héberge la base de données, $user (le nom de l'utilisateur de la base de données), $password (le mot de passe de cet utilisateur) et $base le nom de la base de données. Ces paramètres changent en fonction de la plateforme que vous utilisez. Il est intéressant d'isoler ces paramètres dans un fichier séparé, que ce soit au niveau de la sécurité ou pour porter l'application sur une autre plateforme. Il faut se renseigner au niveau de l'hébergeur sur les caractéristiques de la plateforme utilisée. Les paramètres suivants sont ceux par défaut si on utilise EasyPHP. On a juste créé une base de données appelée ''toto''.
<pre>
<?php
$host='localhost';
$user='root';
$password='';
$base='toto';
?>
</pre>
Pour récupérer, ces paramètres il suffit d'inclure ce fichier en utilisant le mot clé require.
On écrira ''require'params.php' ''. Le fichier sera alors inclus dans le programme en php et le programmeur utilisera les 4 variables.
==== Étape 2 : connexion à la base de données ====
La fonction mysql_connect($host,$user,$password) permet de se connecter au serveur de base de données mysql situé sur la machine nommée ''$host'' en utilisant le nom d'utilisateur ''$user'' ayant comme mot de passe ''$password''. Cette fonction renvoie le booléen true si la connexion est établie et false sinon.
==== Étape 3 : sélectionner la base de données ====
Un serveur de base de données peut héberger plusieurs bases de données (une base de données est un ensemble de tables). La fonction mysql_select_db($base) permet de sélectionner la base de données à laquelle on va envoyer les requêtes. Cette fonction renvoie true si tout s'est bien passé et false sinon.
==== Étape 4 : création de la requête ====
Il faut ensuite créer la requête dans une chaîne de caractères. Ici la requête va être simple, par exemple ''$query='SELECT * FROM employe';'' : on met la requête dans la variable $query. Parfois, il faudra concaténer des chaînes de caractères pour construire la requête--.
==== Étape 5 : envoyer la requête et récupérer le résultat ====
Pour envoyer la requête au serveur, il faut utiliser la fonction ''$r=mysql_query($query);'' qui envoie la requête ''$query'' et récupère le résultat dans ''$r''.
==== Étape 6 : fermer la connexion avec le serveur de base de données ====
Pour fermer la connexion avec le serveur de base de données, il suffit d'appeler la fonction ''mysql_close();''.
==== Étape 7 : générer du HTML à partir du résultat de la requête ====
Supposons que le résultat d'une requête soit contenu dans la variable $r. Il est souvent intéressant de parcourir un à un tous les enregistrements contenus dans $r. L'appel de fonction ''$a=mysql_fetch_object($r)'' permet de récupérer un à un tous les enregistrements dans la variable $a. Lors du premier appel, on récupère le premier enregistrement dans $a, lors du second appel, on récupère le second enregistrement et ainsi de suite jusqu'au dernier enregistrement. Si on appelle alors une nouvelle fois cette fonction, elle va renvoyer le booléen false. Ceci permet donc de traiter un à un tous les enregistrements en écrivant
<pre>
while ($a=mysql_fetch_object($r)) {
...
}
</pre>
Lors de l'exécution de ce while $a va prendre toutes les valeurs des différents enregistrements contenus dans $r, du premier au dernier. À partir de l'objet $a, on pourra récupérer le champs NOM de cet enregistrement en écrivant ''$a->NOM''. On peut ainsi récupérer la valeur de chaque champ d'un enregistrement.
Il suffit donc maintennant de parcourir chaque enregistrement et d'afficher du HTML en utilisant la commande <code>echo</code> à partir des différents champs des enregistrements.
=== Les pages du site (version 1) ===
'''Fichier index.html'''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher le résultat d'une requête</title>
</head>
<body>
<h1> Choisissez dans le menu ci-dessous</h1>
<a href="liste1.php">Afficher la liste des employés</a><br/>
<a href="liste2.php">Afficher la liste des employés par ordre alphabétique</a><br/>
<a href="liste3.php">Afficher la liste des employés par salaire décroissant</a><br/>
</body>
</html>
</pre>
'''Fichier params.php'''
<pre>
<?php
$host='localhost';
$user='root';
$password='';
$base='toto';
?>
</pre>
'''Fichier liste1.php'''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher la liste des employés</title>
</head>
<body>
<h1>Afficher la liste des employés</h1>
<?php
require 'params.php';
mysql_connect($host,$user,$password) or die('Erreur de connexion au SGBD.');
mysql_select_db($base) or die('La base de données n\'existe pas');
$query='SELECT * FROM employe';
$r=mysql_query($query);
mysql_close();
echo'<table><tr><td>NOM</td><td>PRENOM</td><td>SALAIRE</td></tr>';
while ($a=mysql_fetch_object($r)) {
$nom=$a->NOM;
$prenom=$a->PRENOM;
$salaire=$a->SALAIRE;
echo"<tr><td>$nom</td><td>$prenom</td><td>$salaire</td></tr>";
}
echo '</table>';
?>
<br/>
<br/>
<a href="index.html">Retour à la page d'accueil</a>
</body>
</html>
</pre>
'''Fichier liste2.php'''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher la liste des employés par ordre alphabétique</title>
</head>
<body>
<h1>Afficher la liste des employés par ordre alphabétique</h1>
<?php
require 'params.php';
mysql_connect($host,$user,$password) or die('Erreur le connexion au SGBD.');
mysql_select_db($base) or die('La base de données n\'existe pas');
$query='SELECT * FROM employe ORDER BY NOM, PRENOM';
$r=mysql_query($query);
mysql_close();
echo'<table><tr><td>NOM</td><td>PRENOM</td><td>SALAIRE</td></tr>';
while ($a=mysql_fetch_object($r)) {
$nom=$a->NOM;
$prenom=$a->PRENOM;
$salaire=$a->SALAIRE;
echo"<tr><td>$nom</td><td>$prenom</td><td>$salaire</td></tr>";
}
echo '</table>';
?>
<br/>
<br/>
<a href="index.html">Retour à la page d'accueil</a>
</body>
</html>
</pre>
'''Fichier liste3.php'''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher la liste des employés par salaire décroissant</title>
</head>
<body>
<h1>Afficher la liste des employés par ordre alphabétique</h1>
<?php
require 'params.php';
mysql_connect($host,$user,$password) or die('Erreur le connexion au SGBD.');
mysql_select_db($base) or die('La base de données n\'existe pas');
$query='SELECT * FROM employe ORDER BY SALAIRE DESC';
$r=mysql_query($query);
mysql_close();
echo'<table><tr><td>NOM</td><td>PRENOM</td><td>SALAIRE</td></tr>';
while ($a=mysql_fetch_object($r)) {
$nom=$a->NOM;
$prenom=$a->PRENOM;
$salaire=$a->SALAIRE;
echo"<tr><td>$nom</td><td>$prenom</td><td>$salaire</td></tr>";
}
echo '</table>';
?>
<br/>
<br/>
<a href="index.html">Retour à la page d'accueil</a>
</body>
</html>
</pre>
=== Les pages du site (version 2) ===
Lorsqu'on étudie les fichiers liste1.php, liste2.php et liste3.php, on s'aperçoit qu'ils sont extrêmement proches : on va donc réécrire l'application en écrivant un seul fichier liste.php et en le paramétrant grâce à la méthode GET.
Le nouveau '''fichier index.html''' devient donc :
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher le résultat d'un requête</title>
</head>
<body>
<h1> Choisissez dans le menu ci-dessous</h1>
<a href="liste.php?l=1">Afficher la liste des employés</a><br/>
<a href="liste.php?l=2">Afficher la liste des employés par ordre alphabétique</a><br/>
<a href="liste.php?l=3">Afficher la liste des employés par salaire décroissant</a><br/>
</body>
</html>
</pre>
On constate donc que les liens hypertextes, par exemple ''liste.php?l=1'' appelle le même fichier liste.php en lui donnant un paramètre par la méthode GET, ici l=1, ce qui permet de paramétrer le fichier.
'''Remarque importante : '''lorsqu'on utilise ce genre de liens hypertextes, il faut toujours se poser la question : que se passe-t-il si l'utilisateur accède à la page liste.php sans passer de paramètres ou alors en écrivant ''liste.php?l=4'' ? Le programmeur doit garantir que dans un tel cas, le site reste cohérent et que ceci n'engendre pas de failles de sécurité.
Le '''fichier liste.php''' devient :
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher la liste des employés</title>
</head>
<body>
<h1>Afficher la liste des employés</h1>
<?php
require 'params.php';
mysql_connect($host,$user,$password) or die('Erreur le connexion au SGBD.');
mysql_select_db($base) or die('La base de données n\'existe pas');
if(isset($_GET['l']))
$l=$_GET['l'];
else $l=1;
if($l==1)$query='SELECT * FROM employe';
else if($l==2)$query='SELECT * FROM employe ORDER BY NOM, PRENOM';
else $query='SELECT * FROM employe ORDER BY SALAIRE DESC';
$r=mysql_query($query);
mysql_close();
echo'<table><tr><td>NOM</td><td>PRENOM</td><td>SALAIRE</td></tr>';
while ($a=mysql_fetch_object($r)) {
$nom=$a->NOM;
$prenom=$a->PRENOM;
$salaire=$a->SALAIRE;
echo"<tr><td>$nom</td><td>$prenom</td><td>$salaire</td></tr>";
}
echo '</table>';
?>
<br/>
<br/>
<a href="index.html">Retour à la page d'accueil</a>
</body>
</html>
</pre>
Le programmeur teste en utilisant ''isset($_GET['l'])'' pour savoir si la variable l a été passée par la méthode GET et il récupère sa valeur dans la variable $l. Si ce paramètre n'existe pas, $l est fixé à la valeur 1. On construit ensuite la requête $query en fonction de la valeur de $l. Cette version du programme est plus simple mais îil faut toutefois faire très attention à la sécurité dans un tel cas.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/BD 2=
== Modifier une table ==
=== Les fichiers de l'application ===
==== Le fichier BD.sql ====
<pre>
CREATE TABLE `employe` (
`ID` BIGINT NOT NULL AUTO_INCREMENT ,
`NOM` VARCHAR( 20 ) ,
`PRENOM` VARCHAR( 20 ) ,
`SALAIRE` DOUBLE DEFAULT '0',
PRIMARY KEY ( `ID` )
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Dupond', 'Marcel', '8000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Martin', 'Xavier', '4000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Gogol', 'Henri', '3000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Hugo', 'Victor', '2000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Gali', 'Daniel', '6000'
);
INSERT INTO `employe` ( `ID` , `NOM` , `PRENOM` , `SALAIRE` )
VALUES (
'', 'Martin', 'Georges', '9000'
);
</pre>
==== Le fichier index.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher le résultat d'un requête</title>
</head>
<body>
<h1> Choisissez dans le menu ci-dessous</h1>
<a href="liste.php?l=1">Afficher la liste des employés</a><br/>
<a href="liste.php?l=2">Afficher la liste des employés par ordre alphabétique</a><br/>
<a href="liste.php?l=3">Afficher la liste des employés par salaire décroissant</a><br/>
<a href="add.php">Ajouter un employé</a><br/>
<a href="delete.php">Supprimer un employé</a><br/>
<?php
if(is_admin())echo '<a href="disconnect.php">Se déconnecter</a><br/>';
?>
</body>
</html>
</pre>
==== Le fichier liste.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Afficher la liste des employés</title>
</head>
<body>
<h1>Afficher la liste des employés</h1>
<?php
if(isset($_GET['l'])) {
$l=$_GET['l'];
} else {
$l=1;
}
liste($l,false);
?>
<br/>
<br/>
<a href="index.php">Retour à la page d'accueil</a>
</body>
</html>
</pre>
==== Le fichier add.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
$_SESSION['add_NOM']='';
$_SESSION['add_PRENOM']='';
$_SESSION['add_SALAIRE']='';
$_SESSION['add_ERROR']=0;
if (is_admin()) {
require 'Appli/addForm.php';
} else {
$_SESSION['connect_target']='Appli/addForm.php';
$_SESSION['connect_error']=0;
$_SESSION['connect_login']='';
require 'Appli/connectForm.php';
}
?>
</pre>
==== Le fichier addAction.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
if (is_admin()) {
if(isset($_POST['add'])) {
$nom=$_POST['nom'];
$prenom=$_POST['prenom'];
$salaire=$_POST['salaire'];
$r=add_liste($nom,$prenom,$salaire);
if ($r==0) {
echo '<meta http-equiv="Refresh" content="0;URL=index.php">';
} else {
$_SESSION['add_NOM']=$nom;
$_SESSION['add_PRENOM']=$prenom;
$_SESSION['add_SALAIRE']=$salaire;
$_SESSION['add_ERROR']=$r;
require 'Appli/addForm.php';
}
} else {
echo '<meta http-equiv="Refresh" content="0;URL=index.php">';
}
}
?>
</pre>
==== Le fichier connect.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
if (isset($_POST['connect'])) {
if (isset($_POST['login'])) {
$login = $_POST['login'];
} else {
$login = '';
}
if (isset($_POST['password'])) {
$password = $_POST['password'];
} else {
$password = '';
}
}
if (connect($login, $password)) {
$target=$_SESSION['connect_target'];
} else {
$target = 'Appli/connectForm.php';
$_SESSION['connect_login'] = $login;
$_SESSION['connect_error'] = 1;
} else {
$target='index.php';
}
require $target;
?>
</pre>
==== Le fichier disconnect.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
disconnect();
?>
<meta http-equiv="Refresh" content="0;URL=index.php">
</pre>
==== Le fichier delete.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
if (is_admin()) {
require 'Appli/deleteForm.php';
} else {
$_SESSION['connect_target']='Appli/deleteForm.php';
$_SESSION['connect_error']=0;
$_SESSION['connect_login']='';
require 'Appli/connectForm.php';
}
?>
</pre>
==== Le fichier deleteAction.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
if (is_admin()) {
$ID = $_GET['ID'];
delete($ID);
require 'Appli/deleteForm.php';
} else {
echo '<meta http-equiv="Refresh" content="0;URL=index.php">';
}
?>
</pre>
==== Le fichier Appli/.htaccess ====
<pre>
deny from all
</pre>
==== Le fichier Appli/params.php ====
<pre>
<?php
$host = 'localhost';
$user = 'root';
$password = '';
$base = 'toto';
$admin_login = 'admin';
$admin_password = 'aligator';
?>
</pre>
==== Le fichier Appli/appli.php ====
<pre>
<?php
function is_admin()
{
return isset($_SESSION['admin']) and ($_SESSION['admin']==true);
}
function connect($login,$user_password)
{
$r=false;
require 'params.php';
if ($login==$admin_login and $user_password==$admin_password) {
$r=true;$_SESSION['admin']=true;}
return $r;
}
function disconnect()
{
$_SESSION['admin'] = false;
}
function liste($l,$editable)
{
require 'params.php';
mysql_connect($host,$user,$password) or die('Erreur le connexion au SGBD.');
mysql_select_db($base) or die('La base de données n\'existe pas');
if ($l==1) {
$query='SELECT * FROM employe';
} else if ($l==2) {
$query = 'SELECT * FROM employe ORDER BY NOM, PRENOM';
} else {
$query = 'SELECT * FROM employe ORDER BY SALAIRE DESC';
}
$r=mysql_query($query);
mysql_close();
if (editable==false) {
echo'<table><tr><td>NOM</td><td>PRENOM</td><td>SALAIRE</td></tr>';
} else {
echo'<table><tr><td>NOM</td><td>PRENOM</td><td>SALAIRE</td><td> </td></tr>';
}
while ($a=mysql_fetch_object($r)) {
$nom=$a->NOM;
$prenom=$a->PRENOM;
$salaire=$a->SALAIRE;
$ID=$a->ID;
if ($editable==false) {
echo"<tr><td>$nom</td><td>$prenom</td><td>$salaire</td></tr>";
} else {
echo"<tr><td>$nom</td><td>$prenom</td><td>$salaire</td><td><a href=\"deleteAction.php?ID=$ID\">SUPPRIMER</a></td></tr>";
}
echo '</table>';
}
function add_liste($nom,$prenom,$salaire)
{
$r = 0;
if ($nom == '') {
$r = 1;
} else if ($prenom == '') {
$r = 2;
} else if ($salaire == '') {
$r = 3;
} else {
require 'params.php';
mysql_connect($host,$user,$password) or die('Erreur le connexion au SGBD.');
mysql_select_db($base) or die('La base de données n\'existe pas');
$query="INSERT INTO employe (NOM, PRENOM, SALAIRE) VALUES ('$nom', '$prenom', '$salaire')";
mysql_query($query);
mysql_close();
}
return $r;
}
function delete($ID)
{
require 'params.php';
mysql_connect($host,$user,$password) or die('Erreur le connexion au SGBD.');
mysql_select_db($base) or die('La base de données n\'existe pas');
$query="DELETE FROM employe WHERE ID=$ID";
mysql_query($query);
mysql_close();
}
?>
</pre>
==== Le fichier Appli/connectForm.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Connexion</title>
</head>
<body>
<h1> Connexion</h1>
<form method="post" action="connect.php">
<table>
<?php
$value=$_SESSION['connect_login'];
echo "<tr><td><b>LOGIN</b></td> <td><input type=\"text\" name=\"login\" value=\"$value\"/></td></tr>";
?>
<tr><td><b>MOT DE PASSE</b></td> <td><input type="password" name="password"/></td></tr>
<tr><td colspan="2"><input type="submit" value="Se connecter" name="connect"><input type="submit" value="Annuler" name="cancel"></td></tr>
</table>
</form>
<br/>
<?php
$error= $_SESSION['connect_error'];
if($error==1)echo'Erreur de connexion';
$_SESSION['connect_error']=0;
?>
<p/>
<a href="index.php">Retour</a>
</body>
</html>
</pre>
==== Le fichier Appli/addForm.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Ajouter</title>
</head>
<body>
<h1>Ajouter un employé</h1>
<form method="post" action="addAction.php">
<table>
<?php
$nom=$_SESSION['add_NOM'];
$prenom=$_SESSION['add_PRENOM'];
$salaire=$_SESSION['add_SALAIRE'];
echo '<tr><td><b>NOM</b></td> <td><input type="text" name="nom" value="'.$nom.'"/></td></tr>';
echo '<tr><td><b>PRENOM</b></td> <td><input type="text" name="prenom" value="'.$prenom.'"/></td></tr>';
echo '<tr><td><b>SALAIRE</b></td> <td><input type="text" name="salaire" value="'.$salaire.'"/></td></tr>';
?>
<tr><td colspan="2"><input type="submit" value="Ajouter" name="add"><input type="submit" value="Annuler" name="cancel"></td></tr>
</table>
</form>
<br/>
<?php
$error=$_SESSION['add_ERROR'];
if ($error==1) echo'ERREUR : le nom est vide';
if ($error==2) echo'ERREUR : le prénom est vide';
if ($error==3) echo'ERREUR : le salaire est vide';
?>
<p/>
<a href="index.php">Retour</a>
</body>
</html>
</pre>
==== Le fichier Appli/deleteForm.php ====
<pre>
<?php
session_start();
require_once'Appli/appli.php';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Suppression</title>
</head>
<body>
<h1> Suppression</h1>
<?php
liste(2,true);
?>
<p/>
<a href="index.php">Retour</a>
</body>
</html>
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/Livre=
== Créer le livre ==
Tout d'abord, il faut créer le livre d'or, c'est-à-dire une table où seront stockés les messages.
Voici un exemple de [[Programmation_SQL/D%C3%A9finitions#Les_tables|table]], avec un identifiant auto-incrémenté qui sera la clé (pour pouvoir identifier de manière unique chaque message), puis un champ pour le nom, courriel, date, ... et bien sûr le message.
<pre>
CREATE TABLE `livre` (
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
`nom` VARCHAR( 128 ),
`email` VARCHAR( 128 ),
`date` DATE,
`message` TEXT NOT NULL,
PRIMARY KEY( `id` )
);
</pre>
== Ajouter un message ==
Voici la requête [[Programmation SQL|SQL]] qui permet d'insérer les données dans la table:
<pre>
INSERT INTO `livre` ( `nom` , `email` , `date` , `message` )
VALUES ( `UnNom` , `Une@` , `xx/xx/xxxx` , `blalblablabla...` );
</pre>
=== Saisie des données ===
Voici un exemple de formulaire servant à récupérer les données saisies par l'utilisateur pour les faire suivre vers 'ajout_message.php' qui les traitera. La méthode utilisée est POST, bien que GET marche aussi, mais POST possède l'avantage de ne pas rendre visible dans l'URL les paramètres passés, ce qui rend cette méthode plus « propre » et plus claire.
'''fichier formulaire.html'''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Formulaire</title>
</head>
<body>
<form name="formulaire" method="post" action="ajout_message.php">
<label for="nom">Nom :</label><input type="text" id="nom" name="nom" /><br />
<label for="email">Courriel :</label><input type="text" id="email" name="email" /><br />
<label for="message">Votre message :</label><br />
<textarea id="message" name="message" cols="50" rows="10">Message par défaut</textarea><br />
<input type="submit" />
</form>
</body>
</html>
</pre>
=== Traitement des données ===
Le fichier « ajout_message.php » va récupérer et traiter les données saisies par l'utilisateur puis effectuer une insertion dans la base (c'est-à-dire ajouter le message dans le livre d'or). Ces données portent le nom qui leur a été donné dans le formulaire, précédé d'un '$' (représente une [[Programmation PHP/Variables|variable]] en PHP).
Il est fortement conseillé d'effectuer des tests de saisie à ce niveau, pour vérifier que l'utilisateur a rempli correctement les champs (champs non vides, adresse email correcte,...).
'''fichier params.php'''
<pre>
<?php
$host='localhost';
$user='root';
$password='';
$base='toto';
?>
</pre>
'''fichier ajout_message.php'''
<pre>
<?php
isset($_POST['nom']) or die('Pas de paramètres en entrée');
//récupération des données entrées par l'utilisateur
$nom = $_POST['nom'];
$message = $_POST['message'];
$email= $_POST['email'];
//vérification des données entrées par l'utilisateur
if (($nom == "")||($message == ""))
die("Paramètres incorrects.
<a href='formulaire.html'>Cliquez ici pour revenir au formulaire</a>".mysql_error());
//connexion à la base
require 'params.php';
$link=mysql_connect($host,$user,$password) or die('Erreur de connexion au SGBD.');
mysql_select_db($base,$link) or die('La base de données n\'existe pas');
//insertion du message
$date=date("Y-m-d"); // on récupère la date
$message = htmlspecialchars($message,ENT_QUOTES); //convertit les caractères spéciaux
$sql="INSERT INTO `livre` ( `id` , `nom` , `email` , `date` , `message` ) VALUES ( NULL , '$nom', '$email', '$date', '$message' );";
$res=mysql_query($sql,$link);
mysql_close($link);
if ($res==NULL) die('Erreur lors de l\'écriture du message'.mysql_error());
print "Message correctement ajouté.<br />Merci d'avoir pris le temps de remplir ce livre d'or";
?>
</pre>
La fonction <code>htmlspecialchar()</code> permet de convertir les caractères spéciaux qui pourrait être interprétés comme du code HTML par un navigateur.
Il existe d'autres fonctions, comme <code>htmlentities()</code>, qui permet en plus de spécifier les caractères de remplacement, ou <code>n12br()</code> qui permet de remplacer les retours chariots par le caractère HTML de retour à la ligne (<code>br</code>).
== Récupérer les messages et les faire afficher ==
Cette partie est beaucoup plus simple à réaliser que la partie précédente.
Voici la requête SQL qui permet de récupérer toutes les informations de la table dans la base de donées:
<pre>
SELECT * FROM `livre` ORDER BY `id`
</pre>
Les messages seront alors triés en fonction du champ `id`, et, par défaut, par ordre croissant, donc plus du plus ancien au plus récent.
Pour les classer dans l'ordre décroissant, il faut rajouter l'instruction DESC, ce qui donne la requête suivante :
<pre>
SELECT * FROM `livre` ORDER BY `id` DESC
</pre>
Après, il suffit d'afficher les informations obtenues grâce à la requête.
'''fichier affiche_message.php'''
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Livre d'or</title>
</head>
<body>
<?php
//connexion à la base
require 'params.php';
$link=mysql_connect($host,$user,$password) or die('Erreur de connexion au SGBD.');
mysql_select_db($base,$link) or die('La base de données n\'existe pas');
//récupération des messages
$sql="SELECT * FROM `livre` ORDER BY `id` DESC";
$rep=mysql_query($sql,$link);
mysql_close($link);
while ( $ligne = mysql_fetch_array($rep))
{
//on récupère les valeurs des diffèrent champs pour chaque message
$nom=$ligne['nom'];
$email=$ligne['email'];
$date=$ligne['date'];
$message=$ligne['message'];
/* on remplace les retours chariots '\n'
par le symbole de retour à la ligne en HTML : <br /> */
$message=nl2br($message);
//on affiche ses valeurs dans un tableau
print "<table>
<tr><th>Par :<a href=mailto:$email>$nom</a>/th>
<th>Le : $date</th></tr>
<tr><td colspan='2'>$message</td></tr>
</table>";
}
?>
</body>
</html>
</pre>
Ceci n'est que le strict minimum pour faire un livre d'or. D'autres fonctionnalités, comme le nombre d'affichage par page, peuvent être ajoutées.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniForum=
== Faire un mini forum de discussion ==
Bon nombre de forums sur internet ont repris des opensources et les ont customisé. Dans cet article, nous verrons comment simplement opposer de meilleures solutions, plus souples et personnelles.
Ce chapitre traitera du programme à fournir pour cette solution sans proposer de sécurisation.
== Introduction ==
Voici un forum modulaire, universel, illimité, fonctionnel et très réactif mais à sécuriser avec MySQL en ''backend'' et HTML en ''frontend''.
== Architecture ==
À bâtir en MVC, le forum est segmenté comme :
<pre>
./index.php
./config.inc.php
// STATICAL MODULES
./Objects/
./Objects/frameset.inc.php // main frameset
./Objects/mainPage.inc.php // main pages content
// FUNCTIONNAL MODULES
./Libraries/
</pre>
== Déploiement ==
La méthodologie [[w:Processus unifié|processus unifié]] fonctionne par itérations successives et augmentation, amélioration du code. On obtient donc pour chaque module différentes itérations représentatives de l'état d'avancement de l'application. L'état 0 ou <u>incrément 0</u> étant l'ébauche et <u>incrément 1</u>, la <b>première itération</b> de l'applicatif.
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : config.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* */
define('OBJ', './Objects/');
define('LIB', './Libraries/');
$cnx = array('host'=>'localhost','user'=>'root','db'=>'miniForum','pass'=>'');
?>
</pre>
</div>
</div>
<b>increment 0 : index.php </b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : index.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* Mini Forum - main entry
This is a mini forum
version : 1.0
date : 2009 07 28
author : zulul
*/
require_once "./config.inc.php";
require_once OBJ . "frameset.inc.php";
echo $_mainSet;
?>
</pre>
</div>
</div>
<b>increment 1 : index.php </b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : index.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* Mini Forum - main entry
This is a mini forum
version : 1.0
date : 2009 07 28
author : zulul
*/
@session_start();
require_once "./config.inc.php";
require_once OBJ . "frameset.inc.php";
// lib
require_once LIB . "Connectivity.cla.php";
require_once LIB . "Request.cla.php";
require_once LIB . "utils.fnc.php";
// autho
if(chk_usr($cnx))
$_SESSION['autho'] = 1;
echo $_mainSet;
?>
</pre>
</div>
</div>
<b>increment 2 : index.php </b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : index.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* Mini Forum - main entry
This is a mini forum
version : 1.0
date : 2009 07 28
author : zulul
*/
@session_start();
require_once "./config.inc.php";
// lib
require_once LIB . "Connectivity.cla.php";
require_once LIB . "Request.cla.php";
require_once LIB . "utils.fnc.php";
// page listener
require_once OBJ . "listener.inc.php";
// autho
if(chk_usr($cnx))
{
$_SESSION['autho'] = 1;
}
// controling application
require_once OBJ . "controler.inc.php";
// variing frameset call
require_once OBJ . "frameset.inc.php";
# OUTPUT
echo $_mainSet;
?>
</pre>
</div>
</div>
=== Objects ===
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : frameset.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* main page
*/
require_once OBJ."mainPage.inc.php";
$_mainSet = <<<EOPAGE
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//FR" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html lang="fr" xml:lang="fr">
<head>
{$_content['title']}
{$_content['meta']}
{$_content['style']}
</head>
<body>
{$_content['main']}
</body>
</html>
EOPAGE;
?>
</pre>
</div>
</div>
<b>increment 3 : frameset.inc.php</b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : frameset.inc.php</div>
<div style="font-family: monospace">
<pre>
<head>
{$_content['title']}
{$_content['meta']}
{$_content['script']}
{$_content['style']}
</head>
</pre>
</div>
</div>
<b>increment 0 : mainPage.inc.php</b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : mainPage.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* Main page content
*/
$title = ':: TITLE ::';
$meta = '';
$style = '';
// head content
$_content['title'] = '<title>'.$title.'</title>';
$_content['meta'] = '<meta>'.$meta.'</meta>';
$_content['style'] = '<style>
body
{
margin:0 0 0 0;
font-family:tahoma;
font-size:10px;
}
table td
{
border:1px solid;
padding:5px;
margin:5px;
}
.grey
{
background-color:#eee;
}
.creeper
{
border:1px solid;
height:200px;
}
</style>';
// content contents
$_content['authorize'] = '
<div id="cAuthorize">
<table align="center" width="100%">
<tr><td class="grey">pseudo <input type="text" value=""/></td></tr>
<tr><td class="grey">passwd <input type="text" value=""/></td></tr>
<tr><td class="grey"><input type="button" value="submit()"/></td></tr>
</table>
</div>
';
// content contents
$_content['skyCreeper'] = '
<div>
<div class="creeper">
<br/>this is the content
<br/>this is thec content
<br/>this is the content
</div>
</div>';
$_content['pager'] = <<<EOPAGE
<div id="cAuthorize">
<table width="100%">
<tr valign="top"><td width="50%" height="250" class="grey">:: 1 ::</td><td class="grey">:: 2 ::</td></tr>
<tr valign="top"><td width="50%" height="250" class="grey">:: 3 ::</td><td class="grey">:: 4 ::</td></tr>
</table>
</div>
EOPAGE;
// body contents
$_content['main'] = <<<EOPAGE
<table width="99%" height="600">
<!-- its still better to stay with table instead of divs -->
<tr>
<td height="18" colspan="2" width="99%" align="center" class="grey">
:: THIS IS TOP CONTENT ::
</td>
</tr>
<tr valign="top">
<td width="20%">
{$_content['authorize']}
{$_content['skyCreeper']}
</td>
<td width="79%">
{$_content['pager']}
</td>
</tr>
<tr>
<td height="18" colspan="2" width="99%" align="center" class="grey">:: THIS IS BOTTOM CONTENT ::</td>
</tr>
</table>
EOPAGE;
?>
</pre>
</div>
</div>
<b>increment 1 : listener.inc.php</b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : listener.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* application listener
*/
/* flag cases
*/
switch(true)
{
case(@$_REQUEST['aut']=='0'):
//action disconnect
unset($_SESSION['autho'],$_SESSION['userMod']);
break;
/*
case ():
break;
*/
}
?>
</pre>
</div>
</div>
<b>increment 1 : controler.inc.php</b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : controler.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* application controler
*/
# INIT
/* admin and mod status control
*/
if(@$_REQUEST['aut']!='0')
{
// moderating user
mod();
}
# PREPARE
/* cases
*/
if(@$_SESSION['userMod'])
{//
if(preg_match('/[0]/',$_SESSION['userMod']['adm']))
$_mod[0] = 'superAdmin';
if(preg_match('/[1]/',$_SESSION['userMod']['adm']))
$_mod[0] = 'Admin';
if(preg_match('/[0]/',$_SESSION['userMod']['mod']))
$_mod[1] = 'superModerator';
if(preg_match('/[1]/',$_SESSION['userMod']['mod']))
$_mod[1] = 'Moderator';
}
?>
</pre>
</div>
</div>
<b>increment 1 : mainPage.inc.php</b>
:À cette itération, l'authentification du user est fonctionnelle.
:Le snippet content['authorize'] change de mode.
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : mainPage.inc.php</div>
<div style="font-family: monospace">
<pre>
...
// content contents
$_content['authorize'] = (!@$_SESSION['autho'])?'
<div id="cAuthorize">
<form action="" method="post" name="form">
<table align="center" width="100%">
<tr><td colspan="2" class="grey">pseudo <input name="login" type="text" value=""/></td></tr>
<tr><td colspan="2" class="grey">passwd <input name="pass" type="text" value=""/></td></tr>
<tr><td class="grey" align="left"><input type="submit" value="submit" /> </td>
<td align="right"><a href="">forgot password</a><br/><a href="">new registration</a></td></tr>
</table>
</form>
</div>
':'logged';
...
</pre>
</div>
</div>
<b>increment 2 : mainPage.inc.php</b>
:À cette itération, l'attribution des privilèges du user est fonctionnelle.
:Le snippet content['authorize'] affiche le statut user mode.
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : mainPage.inc.php</div>
<div style="font-family: monospace">
<pre>
// content contents
$_content['authorize'] = (!@$_SESSION['autho'])?'
<div id="cAuthorize">
<form action="'.$_SERVER['PHP_SELF'].'" method="post" name="form">
<table align="center" width="100%">
<tr><td colspan="2" class="grey">pseudo <input name="login" type="text" value=""/></td></tr>
<tr><td colspan="2" class="grey">passwd <input name="pass" type="text" value=""/></td></tr>
<tr><td class="grey" align="left"><input type="submit" value="submit" /> </td>
<td align="right"><a href="">forgot password</a><br/><a href="">new registration</a></td></tr>
</table>
</form>
</div>
':'<div id="cAuthorize"><table align="center" width="100%"><tr><td>logged |
<a href="'.$_SERVER['PHP_SELF'].'?aut=0">disconnect</a><br/><br/>Status : '.@$_mod[0] . " " . @$_mod[1] .'</td></tr></table></div>';
// content contents
$_content['containers'] = '
<table width="100%"><tr><td>:: PARTNER 1 ::</td></tr></table>
<table width="100%"><tr><td>:: PARTNER 2 ::</td></tr></table>
';
</pre>
</div>
</div>
<b>increment 3 : jsScript.inc.php</b>
:intégration des javascripts
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : mainPage.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
$script = '
/* javascript scripts
*/
function showHide(obj)
{// >(element)
if(obj.style.display!="none")
obj.style.display="none";
else
obj.style.display="";
}
';
?>
</pre>
</div>
</div>
<b>increment 3 : pages.inc.php</b>
:contenus des pages
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : pages.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* pager contents
*/
// mod 1
if(@$_REQUEST['mod'] == '1' && @$_SESSION['autho']) //
{
// list rubriq
$_res=Request::SQL($cnx,'SELECT * FROM topics ORDER BY id');
foreach($_res as $_tmp)
{
@$str .= '<br/><form method="post" action="' . $_SERVER['PHP_SELF'] . '?mod=1" name="f3"><input name="iDel" type="hidden" value="' . $_tmp['id'] . '" /><input type="submit" value="[-]"/> <a href="">' . $_tmp['title'] . '</a></form>';
}
$_content['pager'] = '
<div class="title">Rubriques existantes | <span class="toAct" onclick="showHide(getElementById(\'cRubriq\'));">Ajouter une rubrique</span><br/>
<!-- Rubriq adder -->
<table width="100%">
<tr valign="top">
<td>
<div id="cRubriq">Ajouter une nouvelle rubrique
<form name="f2" method="post" action="' . $_SERVER['PHP_SELF'] . '?mod=1">
<p><input name="iRubriq" type="text" size="50" /><input type="submit" value="add"/></p>
</form>
</div>
</td>
</tr>
<tr valign="top" height="500"><td><p>' . @$str . '</p></td></tr>
</table>
</div>
';
}
// rub 1
if(@$_REQUEST['rub'] == '1') //
{
if(!@$_REQUEST['tId'])
{
// list rubriq
$_res=Request::SQL($cnx,'SELECT * FROM topics ORDER BY id');
foreach($_res as $_tmp)
{
@$str .= '<br/> <a href="'.$_SERVER['PHP_SELF'].'?rub=1&tId=' . $_tmp['id'] . '">' . $_tmp['title'] . '</a>';
}
$_content['pager'] = '
<div class="title">Rubriques existantes
<table width="100%">
<tr valign="top" height="500"><td><p>' . @$str . '</p></td></tr>
</table>
</div>
';
} else {
// list rubriq
$_res=Request::SQL($cnx,'SELECT * FROM contents WHERE topicsId=' . @$_REQUEST['tId'] . ' ORDER BY id');
foreach($_res as $_tmp)
{
@$str .= '<br/> <a href="' . $_SERVER['PHP_SELF'] . '?rub=1&tId=' . $_tmp['topicsId'] . '">' . $_tmp['subject'] . '</a>';
}
$_content['pager'] = '
<div class="title">Topics existants
<table width="100%">
<!-- Topics adder -->
<tr valign="top">
<td>
<div id="cTopics">Ajouter un nouveau sujet
<form name="f4" method="post" action="' . $_SERVER['PHP_SELF'] . '?rub=1">
<p><input name="iTopics" type="text" size="50" /><input type="submit" value="add" /></p>
</form>
</div>
</td>
</tr>
<tr valign="top" height="500"><td><p>' . @$str . '</p></td></tr>
</table>
</div>
';
}
}
?>
</pre>
</div>
</div>
<b>increment 4 : pages.inc.php</b>
:contenus des pages complétés
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : pages.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* pager contents
*/
// mod 1
if(@$_REQUEST['mod'] == '1' && @$_SESSION['autho']) //
{
// list rubriq
$_res=Request::SQL($cnx,'SELECT * FROM topics ORDER BY id');
foreach($_res as $_tmp)
{
@$str .= '<br/><form method="post" action="' . $_SERVER['PHP_SELF'] . '?mod=1" name="f3"><input name="iDel" type="hidden" value="' . $_tmp['id'] . '" /><input type="submit" value="[-]"/> <a href="">' . $_tmp['title'] . '</a></form>';
}
$_content['pager'] = '
<div class="title">Rubriques existantes | <span class="toAct" onclick="showHide(getElementById(\'cRubriq\'));">Ajouter une rubrique</span><br/>
<!-- Rubriq adder -->
<table width="100%">
<tr valign="top">
<td>
<div id="cRubriq">Ajouter une nouvelle rubrique
<form name="f2" method="post" action="' . $_SERVER['PHP_SELF'] . '?mod=1">
<p><input name="iRubriq" type="text" size="50" /><input type="submit" value="add"/></p>
</form>
</div>
</td>
</tr>
<tr valign="top" height="500"><td><p>' . @$str . '</p></td></tr>
</table>
</div>
';
}
// rub 1
if(@$_REQUEST['rub'] == '1') //
{
if(!@$_REQUEST['tId'])
{
// list rubriq
$_res=Request::SQL($cnx,'SELECT * FROM topics ORDER BY id');
foreach($_res as $_tmp)
{
@$str .= '<br/> <a href="'.$_SERVER['PHP_SELF'].'?rub=1&tId=' . $_tmp['id'] . '">' . $_tmp['title'] . '</a>';
}
$_content['pager'] = '
<div class="title">Rubriques existantes
<table width="100%">
<tr valign="top" height="500"><td><p>' . @$str . '</p></td></tr>
</table>
</div>
';
} else {
// list rubriq
$_res=Request::SQL($cnx,'SELECT * FROM contents WHERE topicsId="' . @$_REQUEST['tId'] . '" ORDER by id');
foreach($_res as $_tmp)
{
@$str .= '<br/> <a href="' . $_SERVER['PHP_SELF'] . '?aId='.$_tmp['id'].'&art=1&tId=' . $_tmp['topicsId'] . '">' . $_tmp['subject'] . '</a>';
}
$_content['pager'] = '
<div class="title">Topics existants
<table width="100%">
<!-- Topics adder -->
<tr valign="top">
<td>
<div id="cTopics">Ajouter un nouveau sujet
<form name="f4" method="post" action="' . $_SERVER['PHP_SELF'] . '?rub=1">
<p>
<input name="tId" type="hidden" value="'.$_REQUEST['tId'].'" />
<input name="iTopics" type="text" size="50" /><input type="submit" value="add" /></p>
</form>
</div>
</td>
</tr>
<tr valign="top" height="500"><td><p>' . @$str . '</p></td></tr>
</table>
</div>
';
}
}
if(@$_REQUEST['art']=='1' && @$_REQUEST['tId'])
{
// list article
$_res=Request::SQL($cnx,'SELECT * FROM contents WHERE topicsId= ' .@$_REQUEST['tId'] . ' ORDER BY id');
foreach($_res as $_tmp)
{
@$str .= '<table width="100%">
<tr><td width="10%" rowspan="2">:: IMAGE ::<br/>:: usrId ' . $_tmp['usrId'] . ' ::</td>
<td width="90%"><a href="'.$_SERVER['PHP_SELF'].'?aId='.$_tmp['id'].'&art=1&tId=' . $_tmp['topicsId'] . '">' . $_tmp['subject'] . '</a></td>
</tr>
<tr><td width="90%">' . $_tmp['content'] . '</td></tr>
';
}
$rec = getRecordNfo($cnx,'contents', @$_REQUEST['aId']);
$_content['pager'] = @$str . '
<div class="article">
<table width="100%">
<tr valign="top">
<td>
<div id="cArticle"><b>Introduisez votre commentaire : </b><br/><br/>
<form name="f5" method="post" action="' . @$_SERVER['PHP_SELF'] . '?art=1&tId=' . @$_REQUEST['tId'] . '>
<p>
<input name="tId" type="hidden" value="' . @$_REQUEST['tId'] . '" />
<input name="aId" type="hidden" value="' . @$_REQUEST['aId'] . '" />
<input name="iSubject" type="text" value="' . (@$rec['subject']) . '" />
<textarea name="iContent" rows="5" style="width:95%">' . (@$res['content']) . '</textarea>
<input type="submit" value="add" />
</p>
</form>
</div>
</td>
</tr>
</table>
</div>
';
}
?>
</pre>
</div>
</div>
<b>increment 4 : controler.inc.php</b>
:controler complété
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : controler.inc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* application controler
*/
# INIT
/* admin and mod status control
*/
if(@$_REQUEST['aut']!='0')
{
// moderating user
mod();
}
# PREPARE
/* cases
*/
if(@$_SESSION['userMod'])
{//
if(preg_match('/[0]/',$_SESSION['userMod']['adm']))
$_mod[0] = 'superAdmin';
if(preg_match('/[1]/',$_SESSION['userMod']['adm']))
$_mod[0] = 'Admin';
if(preg_match('/[0]/',$_SESSION['userMod']['mod']))
$_mod[1] = 'superModerator';
if(preg_match('/[1]/',$_SESSION['userMod']['mod']))
$_mod[1] = 'Moderator';
}
if(@$_REQUEST['mod']=='1')
{// add rubriq
if(@$_REQUEST['iRubriq'])
Request::SQL($cnx,'INSERT INTO topics (title) values ("' . $_POST['iRubriq'] . '");');
if(@$_REQUEST['iDel'])
{
Request::SQL($cnx,'DELETE FROM topics WHERE id='.$_REQUEST['iDel'].';');
}
}
if(@$_REQUEST['rub']=='1' && @$_REQUEST['tId'])
{
if(@$_REQUEST['iTopics'])
{
Request::SQL($cnx,'INSERT INTO contents (topicsId,subject,usrId) values (' . $_REQUEST['tId'] . ', "' . $_REQUEST['iTopics'] . '",' . $_SESSION['userId'] . ')');
}
}
if(@$_REQUEST['art']=='1')
{
if(@$_REQUEST['iContent'])
{
Request::SQL($cnx,'UPDATE contents SET content="' . $_REQUEST['iContent'] . '" WHERE id=' . $_REQUEST['aId']);
}
}
?>
</pre>
</div>
</div>
:<b>À ce stade on a une ébauche de layout pour notre forum :</b>
[[Image:MiniForum002.GIF]]
[[Image:MiniForum003.GIF]]
==== Constatations ====
Après le login, on a :
:[1] admin
:[2] mod
:[3] user
[2] & [3] sont similaires, quelques indicateurs à basculer dans la base.
Il reste donc deux cas.
L'admin aura sa propre IHM avec fonctions élevées sur le forum.
==== [[RUP]] - Rational Unified Process (ou comment coder ?) ====
La granularité de la solution s'effectuera par l'affinage des modules.
Il n'est pas intelligent de coder en procédurale ou suivant un développement linéaire dans ce cas.
Chaque module ou ''addon'' s'interfacera par adjonction de classes et ''snippets''.
Ceux-ci étant bien définis quand aux (in/out)put, avec le type précis.
De cette manière l'applicatif sera modulaire et évolutif.
==== Module administratif ====
Le forum doit être dédié aux légumes. L'admin doit avoir le choix de créer ces différents sujets principaux.
Par exemple les sections :
* coups de cœur,
* espace détente,
* nouveauté & trouvailles,
* légumes.
=== Bibliothèques ===
Nous aurons d'emblée besoin des deux utilitaires :
[classes]
:Les classes usuelles
*Connectivity
*Request
[functions]
:Les fonctionnalités usuelles
<b>[classes]</b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : Connectivity.cla.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* Connectivity class
version : 1.0
author : zulul
date : 2009 07 28
*/
class Connectivity
{//
var $ident;
var $sql = array('host'=>'','user'=>'','db'=>'','pass'=>'');
//INIT
function DB ($cnx) {
$this->sql = $cnx;
$this->connect();
}
function connect () {
// Connect to MySQL
$this->ident = mysql_connect($this->sql['host'], $this->sql['user'], $this->sql['pass']);
// Select assigned DB
$this->sql['db']?mysql_select_db($this->sql['db']):0;
}
function disconnect () {
// Close the connection
if (!mysql_close($this->ident)) {
die("Could not close Database");
}
}
}
?>
</pre>
</div>
</div>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : Request.cla.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* Request class
*/
class Request
{//
var $res, $Connectivity;
function SQL($cnx,$sql) {
$DB=new Connectivity($cnx);
$res = mysql_fetch_all(mysql_query($sql,$DB->ident));
$DB->disconnect();
return($res);
}
}
?>
</pre>
</div>
</div>
<b>[functions]</b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : utils.fnc.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* utilities functions
*/
/* fetch all db results
*/
function mysql_fetch_all($query, $kind = 'assoc')
{
$result = array();
$kind = $kind === 'assoc' ? $kind : 'row';
eval('while(@$r = mysql_fetch_'.$kind.'($query)) array_push($result, $r);');
return $result;
}
/* check if user is on db
*/
function chk_usr($cnx)
{
if(!isset($_SESSION['autho']))
{
if(@$_REQUEST['login']&&@$_REQUEST['pass'])
{
return count(Request::SQL($cnx,'SELECT * FROM users WHERE login="'.$_POST['login'].'" and pass="'.$_POST['pass'].'"'))?true:false;
}
}else{
return true;
}
}
?>
</pre>
</div>
</div>
<b>increment 2 : utils.fnc.php</b>
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : utils.fnc.php</div>
<div style="font-family: monospace">
<pre>
/* dump array 2 html
*/
function print_r_html($data,$return_data=false)
{
$data = print_r($data,true);
$data = str_replace( " "," ", $data);
$data = str_replace( "\r\n","<br/>\r\n", $data);
$data = str_replace( "\r","<br/>\r", $data);
$data = str_replace( "\n","<br/>\n", $data);
if (!$return_data)
echo $data;
else
return $data;
}
/* adm and mod status checker
*/
function mod()
{
if(@$_SESSION['userMod'])
{
foreach($_SESSION['userMod'] as $_tmp)
{
$_stat = explode("/",$_tmp);
@$_SESSION['userMod'][$_stat[0]]=$_stat[1];
}
}
}
</pre>
</div>
</div>
<b>increment 4 : utils.fnc.php</b>
:add function
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : utils.inc.ohp</div>
<div style="font-family: monospace">
<pre>
/* get record by id
*/
function getRecordNfo($cnx, $table, $id)
{
return count($res=Request::SQL($cnx,"SELECT * FROM $table WHERE id=$id"))?$res[0]:false;
}
</pre>
</div>
</div>
=== Modélisation ===
La modélisation est fonction du métier et des affinités.
En voici une fonctionnelle pour notre exemple :
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : install.sql</div>
<div style="font-family: monospace">
<pre>
-- phpMyAdmin SQL Dump
-- version 3.2.0.1
-- http://www.phpmyadmin.net
--
-- Serveur: localhost
-- Généré le : Mar 28 Juillet 2009 à 19:11
-- Version du serveur: 5.1.36
-- Version de PHP: 5.3.0
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
--
-- Base de données: `miniForum`
--
-- --------------------------------------------------------
--
-- Structure de la table `contents`
--
CREATE TABLE IF NOT EXISTS `contents` (
`usrId` int(11) NOT NULL,
`topicsId` int(11) NOT NULL,
`content` text NOT NULL,
`comment` text NOT NULL,
`nfo` text NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- Contenu de la table `contents`
--
-- --------------------------------------------------------
--
-- Structure de la table `topics`
--
CREATE TABLE IF NOT EXISTS `topics` (
`title` varchar(255) NOT NULL,
`content` varchar(255) NOT NULL,
`nfo` varchar(255) NOT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
--
-- Contenu de la table `topics`
--
-- --------------------------------------------------------
--
-- Structure de la table `users`
--
CREATE TABLE IF NOT EXISTS `users` (
`login` varchar(11) NOT NULL,
`pass` varchar(11) NOT NULL,
`nfo` varchar(255) NOT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;
--
-- Contenu de la table `user`
--
</pre>
</div>
</div>
un script d'installation pourrait être :
<div class="cadre-fichier" style="padding: 0.2em; width: 100%; background-color: #ffcbcb40; border: solid 1px #ffbfbf;">
<div class="cadre-fichier-titre" style="background-color: #ff000040; border: solid 1px #9b0d0d"> Fichier : _install.php</div>
<div style="font-family: monospace">
<pre>
<?php
/* install tables
*/
require_once "./config.inc.php";
require_once LIB."Request.cla.php";
require_once LIB."Connectivity.cla.php";
if(!(new Connectivity($cnx)))
{
// Créer la base de données
Request::SQL($cnx,'CREATE DATABASE IF NOT EXISTS miniForum;');
// table de contenu
Request::SQL($cnx,'CREATE TABLE IF NOT EXISTS `contents` (
`usrId` int(11) NOT NULL,
`topicsId` int(11) NOT NULL,
`content` text NOT NULL,
`comment` text NOT NULL,
`nfo` text NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;');
// table de sujets
Request::SQL($cnx,'CREATE TABLE IF NOT EXISTS `topics` (
`title` varchar(255) NOT NULL,
`content` varchar(255) NOT NULL,
`nfo` varchar(255) NOT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;');
// table des utilisateurs
Request::SQL($cnx,'CREATE TABLE IF NOT EXISTS `users` (
`login` varchar(11) NOT NULL,
`pass` varchar(11) NOT NULL,
`nfo` varchar(255) NOT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;');
}
?>
</pre>
</div>
</div>
== Conclusion ==
Cette façon de coder n'est pas la plus élégante et ne vise pas l'excellence.
Bien que l'approche modulaire accroisse la quantité de code, elle a cependant l'avantage d'être lisible pour les longs développements.
L'approche itérative et incrémentale de la méthodologie RUP tire fortement parti du développement modulaire.
<div class="noprint bandeau noprint
bandeaujaune
" style="min-height:80px;clear:;">
<div class="bandeautitre" style="margin:0.5em;">
À faire...<span style="display:inline-block;float:right;margin:0.5em 1em;">[[Image:Nuvola apps korganizer.svg|40px|link={{{link}}}]]</span></div>
<div class="bandeautexte" style="padding: 0.5em; background-color: white;">
*une vue
*un frameset variable
*des sujets illimités
*des catégories illimités
*un système de classement
*un système d'administration
*un système d'authentification
*un espace myAccount
NICE TO HAVE :
* tout automatiser (censure / délation / ..)
* mailer
* BO admin
</div>
</div>[[Catégorie:À faire|Programmation PHP/Version imprimable]]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/DomXml=
Les documents au format XML ont des imbrications parfois complexes et il n'est pas rare de devoir avoir recours à plusieurs fonctions pour faire le travail de décapsulation du contenu.
À travers cet exemple pratique nous écrirons une fonction pour convertir tout document XML en tableau suivant l'approche du web2 qui tient en deux tags très galvaudés [meta] et [data].
== Objectif ==
* Élaborer une fonction permettant de convertir en tableaux tout XML bien formé.
* Créer une classe utilitaire domxml pour la recevoir avec ses petites sœurs.
== Écriture d'un XML complexe ==
* Écriture d'un document XML valide à imbrications multiples d'éléments hétéroclites comportant des attributs ou non...
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<root>
<tag1 m1="attTag1" m2="att2">
<sous-tag1>texte sous-tag1
<sous-tag2></sous-tag2>
texte tag1
</sous-tag1>
</tag1>
<tag1>
<sous-tag1>texte sous-tag1
<sous-tag2 att1="att2" att3="att3"></sous-tag2>
</sous-tag1>
<tag3>
<sous-tag1>texte sous-tag1
<sous-tag2 att1="attribut1"></sous-tag2>
</sous-tag1>
<etEncoreUnTagSuperflu>
<sous-tag1>texte sous-tag1
<sous-tag2>test</sous-tag2>
</sous-tag1>
<tag1>
<p><b><sous-tag1>texte sous-tag1
<sous-tag2 att1="attribut1" /></b>
</sous-tag1>
ceci</p>est du body texte à extraire
</tag1>
</etEncoreUnTagSuperflu>
text de début ou de fin
</tag3>
</tag1>
<tagNfo id="1" description="description">texteDescription</tagNfo>
</root>
</pre>
* Sauvegarde de ce ''document.xml'' bien formé dans le même répertoire.
== Création de la fonction ==
On doit maintenant écrire une fonction, la plus optimale possible, pour charger ''document.xml'' dans un tableau...
Cette fonction doit :
* recevoir en entrée un flux xml/rss valide
* doit migrer les attributs et le contenu dans un tableau
On écrit la fonction récursive qui décapsulera chaque tag en deux sous-tableaux par tag ([meta] ou attributs ) et ([data] ou nœud texte)
Cette fonction doit :
* tester le type de nœud (texte ou tag)
* ? si tag >extraire tous ses attributs dans >[meta]
* ? si texte >extraire le texte dans >[data]
* comme la structure est imbriquée et non listée :
** les tags de débuts et de fins ne se suivent pas...
** la fonction sera donc récursive et s'appellera elle-même pour un output [[lifo]]. Elle devra donc se passer son propre résultat en paramètre
** par soucis du détail technique on fera une fonction <code>getAttribute()</code> pour optimiser le code
<pre>
function getAttribute($node)
{// >((dom)node) ((array)tab)>
$tab=array();
foreach($node->attributes() as $k1->$v1) {
$tab[$k1->{''}->name]=$k1->{''}->value;
}
return $tab;
}
</pre>
Description :
*Pour chaque attribut, on place le contenu à une clé du tableau <code>tab</code> à retourner.
On s'attaque ensuite au plus gros du travail de notre convertisseur à savoir <code>domxml2array()</code> :
<pre>
function domxml2array($node,&$tab,&$i)
{// >((dom)node, (array)tab, (int)i) ((array)tab)>
if($next=$node->first_child()) #1
{
do
{
switch($next->node_type()) #2
{
case 3:
$tab['data']=$next->node_value();
break;
case 1:
$tab[$next->node_name()."#".++$i]['meta'] = $this->getAttribute($next);
$this->domxml2array($next,$tab[$next->node_name()."#".$i],$i);
break;
}
}while($next=$next->next_sibling()); #3
}
return $tab;
}
</pre>
Description :
# si le premier enfant existe,
# on test le type de nœud,
# on passe au nœud suivant.
La fonction utilitaire print_r_html disponible sur php.net permettra de déposer le contenu à l'écran :
<pre>
function print_r_html($data,$return_data=false)
{
$data = print_r($data,true);
$data = str_replace( " "," ", $data);
$data = str_replace( "\r\n","<br/>\r\n", $data);
$data = str_replace( "\r","<br/>\r", $data);
$data = str_replace( "\n","<br/>\n", $data);
if (!$return_data)
echo $data;
else
return $data;
}
</pre>
== Création de la classe ==
On élabore une classe utilitaire pour php4 à implémenter au fur et à mesure :
* On la baptise ''DomTree''.
* On y implémente les fonctions créées...
* On sauvegarde la classe dans <code>DomTree.Class.php</code>.
<pre>
<?php
Class DomTree
{
#init
var $tab = array();
var $domNode;
#constructor
function DomTree($xml,$type)
{// >((string)xml,(int)type) ((dom)node)>
if(!$type)
{
$this->domNode = domxml_open_file($xml);
} else {
$this->domNode = domxml_open_mem($xml);
}
}
#get
function getTag($tag)
{// >((string)tag) ((dom)node)>
return $this->domNode->get_elements_by_tagname($tag);
}//
function getAttribute($node)
{// >((dom)node) ((dom)node)>
$tab=array();
foreach($node->attributes() as $k1->$v1)
{
$tab[$k1->{''}->name]=$k1->{''}->value;
}
return $tab;
}//
#set
#spec
function domxml2array($node,&$tab,&$i)
{// >((dom)node, (array)tab, (int)i) ((array)tab)>
if($next=$node->first_child())
{
do
{
switch($next->node_type())
{
case 3:
$tab['data']=$next->node_value();
break;
case 1:
$tab[$next->node_name()."#".++$i]['meta'] = $this->getAttribute($next);
$this->domxml2array($next,$tab[$next->node_name()."#".$i],$i);
break;
}
}while($next=$next->next_sibling());
}
return $this->tab=$tab;
}//
}
?>
</pre>
== Application ==
Dans un fichier test.php on instancie la classe et on l'exécute:
<pre>
<?php
header("Cache-Control: no-cache, must-revalidate");
header("Content-Type: text/html");
// appel de la classe
require_once"DomTree.class.php";
// création de l'objet
$doc = new DomTree('document.xml');
// sélection du nœud
$root = $doc->getTag('root');
// conversion du nœud root en tableau
$tab = $doc->domxml2array($root[0]);
// affichage du tableau
print_r_html($tab);
?>
</pre>
== Aperçu ==
On obtient un arbre structuré easy2use pour le web2
<pre>
Array
(
[tag1#1] => Array
(
[meta] => Array
(
[m1] => attTag1
[m2] => att2
)
[sous-tag1#2] => Array
(
[meta] => Array
(
)
[data] => texte tag1
[sous-tag2#3] => Array
(
[meta] => Array
(
)
)
)
[data] =>
)
[data] =>
[tag1#4] => Array
(
[meta] => Array
(
)
[sous-tag1#5] => Array
(
[meta] => Array
(
)
[data] => texte sous-tag1
[sous-tag2#6] => Array
(
[meta] => Array
(
[att1] => att2
[att3] => att3
)
)
)
[data] =>
[tag3#7] => Array
(
[meta] => Array
(
)
[sous-tag1#8] => Array
(
[meta] => Array
(
)
[data] => texte sous-tag1
[sous-tag2#9] => Array
(
[meta] => Array
(
)
)
)
[data] =>
text de début ou de fin
[etEncoreUnTagSuperflu#10] => Array
(
[meta] => Array
(
)
[sous-tag1#11] => Array
(
[meta] => Array
(
)
[data] => texte sous-tag1
[sous-tag2#12] => Array
(
[meta] => Array
(
)
[data] => test
)
)
[data] =>
[tag1#13] => Array
(
[meta] => Array
(
)
[p#14] => Array
(
[meta] => Array
(
)
[b#15] => Array
(
[meta] => Array
(
)
[sous-tag1#16] => Array
(
[meta] => Array
(
)
[data] => texte sous-tag1
[sous-tag2#17] => Array
(
[meta] => Array
(
[att1] => attribut1
)
)
)
)
[data] =>
ceci
)
[data] => est du body texte à extraire
)
)
)
)
[tagNfo#18] => Array
(
[meta] => Array
(
[id] => 1
[description] => description
)
[data] => texteDescription
)
)
</pre>
== En bref ==
On a une fonction fort utile à porter sur php5 ou à optimiser histoire de ne plus avoir d'incréments dans les données du tableau.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MVC=
Historiquement, PHP est un langage ''glue'', il peut être intégré avec le langage de balisage [[Le langage HTML|HTML]]. L'avantage est cette simplicité de mise-en-œuvre mais l'inconvénient est le mélange entre le traitement et l'affichage. Pour produire une application web claire et facile à entretenir, on peut séparer les différentes parties de l'application selon l'architecture [[w:Modèle-Vue-Contrôleur|Modèle-Vue-Contrôleur]] (ou MVC).
#Modélisation (Modèle : Partie métier spécifique à l'application)
#Visualisation (Vue : Partie visuelle de l'application)
#Contrôles (Contrôleur : Partie de gestion des événements de l'application)
De cette façon on peut implémenter son application en sous composantes ce qui augmente légèrement l'analyse de l'application mais fera gagner beaucoup de temps de développement par la suite. Cette architecture est déjà couramment employée dans les applications locales et les applications web l'utilise de plus en plus. On remarquera notamment que beaucoup de [[w:Cadriciel|Cadriciel]] web sont basés sur ce principe.
== Développement ==
*Objectif : Faire un mini système de validation de données saisies.
Pour ce faire on a besoin :
# d'un formulaire HTML
# d'un module de validation
# d'un module de gestion homme-machine
*Pré-requis conseillés :
#html4
#php5
=== Création de la vue ===
La vue comprendra un formulaire HTML pour la saisie des données utilisateur :
<pre>
<html>
<head>
</head>
<body>
<form id="fForm" name="fForm" action="" method="POST">
<!-- données utilisateur -->
<input type="text" id="iNom" name="iNom" /><br/>
<input type="text" id="iPrenom" name="iPrenom" /><br/>
<input type="text" id="iVisa" name="iVisa" /><p/>
<input type="submit" value="envoyer" />
</form>
</body>
</html>
</pre>
==== Spécialisation des composantes ====
Il est mieux de découper son application en sous composante.
La vue par exemple pourrait contenir :
# le frameset de la page
# les containers à afficher
1. Le frameset (vueFrame.inc.php) :
<pre>
<?php
$page['container']['frameSet'] = '
<html>
<head>'
.$page['css']
.$page['script'].
'</head>
<body>'
.$page['container']['header']
.$page['container']['main']
.$page['container']['footer'].
'</body>
</html>';
?>
</pre>
2. Les formulaires deviendraient (vueFormulaire.inc.php) :
<pre>
<?php
$page['container']['visa']='
<div id="cVisa">
<form id="fForm" name="fForm" action="" method="POST">
<!-- données utilisateur -->
<input type="text" id="iNom" name="iNom" value="'.$value['user']['nom'].'"/>
<span id="msgNom">'
// communication nom
.$msg['error']['nom'].'</span><br/>
<input type="text" id="iPrenom" name="iPrenom" value="'.$value['user']['prenom'].'"/>
<span id="msgPrenom">'
// communication prenom
.$msg['error']['prenom'].'</span><br/>
<input type="text" id="iVisa" name="iVisa" value="'.$value['user']['visa'].'"/>
<span id="msgVisa">'
// communication visa
.$msg['error']['visa'].'</span><p/>
<input type="submit" value="envoyer" />
</form>
</div>';
$page['container']['form1']='
<div id="">
<!-- autre formulaire -->
<form>
</form>
</div>';
?>
</pre>
=== Création du modèle ===
Cette bibliothèque utilitaire reprendra les traitements de validation
On a besoin des fonctions suivantes :
# check des string alphabétiques
# check d'un numéro VISA (4 groupes de 4 chiffres avec le premier groupe commençant par 4 pour simplifier)
<pre>
<?php
/* modeleValidation
SYNOPSIS : Cette bibliothèque reprend toutes les fonctions de validation de l'application
*/
/* isAlpha : contrôle de chaîne alphabétique
@author : nom
@date : 04.11.2007
@version : 1
*/
function isAlpha($str)
{// >((string)str)-(bool)>
return preg_match('/^([a-zA-Z]*)$/',$str);
}//
/* isVisa : contrôle de numéro VISA
@author : nom
@date : 04.11.2007
@version : 2
*/
function isVisa($str)
{// >((string)str)-(bool)>
return preg_match('/^(4)([0-9]{15})$/',$str);
}//
/* DEPRECIEE - isVisa : contrôle de numéro VISA
@author : nom
@date : 07.05.2006
@version : 1
function isVisa($str)
{// >((string)str)-(bool)>
return preg_match('/^([0-9]{16})$/',$str);
}//
*/
?>
</pre>
! Ce modèle modeleValidation.inc.php par exemple comporte une nouvelle fonction isVisa remplaçant celle du 07.05.2006.
== Création du contrôleur ==
Le contrôleur regroupe les traitements de gestion de l'application, c'est à dire ici les événements déclenchés par l'utilisateur ou par le système. On parle souvent d'IHM, voilà un exemple.
On a besoin des contrôles suivants :
#validation de saisie
#affichage en fonction du statut de la saisie
<pre>
<?php
/* controlAffichage
SYNOPSIS : Cette page reprend tous les contrôles pour gérer l'affichage
*/
# INIT
require_once "./Modele/modeleValidation.inc.php";
// var de contrôle de données saisies
$checkSum=3;
# CONTROL
if($_POST)
{
if(!isAlpha($_POST['iNom']))
{
$value['user']['nom'] = $_POST['iNom'];
$msg['error']['nom']='nom invalide !';
@$checkSum--;
}
if(!isAlpha($_POST['iPrenom']))
{
$value['user']['prenom'] = $_POST['iPrenom'];
$msg['error']['prenom']='prenom invalide !';
@$checkSum--;
}
if(!isVisa($_POST['iVisa']))
{
$value['user']['visa'] = $_POST['iVisa'];
$msg['error']['visa']='visa invalide !';
@$checkSum--;
}
}
# CHOIX DE L'OUTPUT
//if(!$checkSum)
if($checkSum==3)
{// formulaire validé
//$page['container']['main'] = 'Formulaire valide';
$msg['error']['alert'] = 'Formulaire valide';
}
else
{// formulaire invalide
//$page['container']['main'] = $page['container']['visa'].'<p> Formulaire invalide !</p>';*/
$msg['error']['alert'] = $page['container']['visa']
.'<p> Formulaire invalide !</p>';
}
?>
</pre>
! utilisation de $page['container']['main'] ne sert a rien ici car sera écrasé dans l'appel de la page principale.
== Gestion des containers ==
Chaque container ou div est ici considéré comme un flux à gérer. Ce qui permettra par la suite d'évoluer simplement vers l'ajax.
Pour bien construire sa page sans mauvaise surprise, il vaut mieux :
#charger les containers du plus petit enfant au plus grand parent
#ne faire l'output que du frameset
Par ex. :
<pre>
<?php
// page principale
# INIT
@session_start();
# PREPARE PAGE
$page['container']['header']='ceci est le header<p/>';
$page['container']['footer']='ceci est le footer<p/>';
require_once "./Control/controlAffichage.inc.php";
require_once './Vue/vueFormulaire.inc.php';
// preparation du container principal
$page['container']['main']='
<div>Veuillez introduire vos données utilisateur :</div>'
.$page['container']['visa'];
require_once './Vue/vueFrame.inc.php';
# OUTPUT final
echo $page['container']['frameSet'];
?>
</pre>
! d'abord $page['container']['main'] et en dernier $page['container']['frameSet']
! $page['container']['main'] de Control/controlAffichage.inc.php sera jamais affiché car il se fait écraser systématiquement avant ./Vue/vueFrame.inc.php' qui passe à l'affichage à proprement parler. Que faire?
== Ajout de nouvelles fonctionnalités ==
On souhaiterait pouvoir gérer les communications dans plusieurs langues.
Pour ce faire :
#on rajoute un modele messageList.inc.php
#on modifie un peu le contrôleur
#on crée ou modifie les composantes de la vue
1. messageList.inc.php
<pre>
<?php
/* messageList
SYNOPSIS : Cette liste reprend toutes les communications usuelles
NOTA : Il est préférable d'en faire une table dans une DB
pour décharger la RAM du serveur et de créer la fonction d'appel du message
*/
// par convention 1=fr et 2=uk
# ERRORS
$msg['error']['alpha'][1] = 'champ alpha invalide';
$msg['error']['alpha'][2] = 'invalid alpha field';
$msg['error']['num'][1] = 'champ numérique invalide';
$msg['error']['num'][2] = 'invalid numeric field';
$msg['error']['invalid'][1] = 'formulaire invalide !';
$msg['error']['invalid'][2] = 'invalid form !';
# SUCCESS
$msg['success']['valid'][1] = 'le formulaire a été validé';
$msg['success']['valid'][2] = 'the form has been validated';
# MESSAGE
$msg['communication']['form'][1] = 'veuillez introduire vos données utilisateur';
$msg['communication']['form'][2] = 'please introduce your user data';
?>
</pre>
2. controlAffichage.inc.php devient
<pre>
<?php
/* controlAffichage
SYNOPSIS : Cette page reprend tous les contrôles pour gérer l'affichage
*/
# INIT
require_once "./Modele/modeleValidation.inc.php";
require_once "./Modele/messageList.inc.php";
// var de controle de données saisies
$_SESSION['checkSum']=3;
# CONTROL
if($_POST)
{
if(!isAlpha($_POST['iNom']))
{
$value['user']['nom'] = $_POST['iNom'];
$msg['error']['nom']= $msg['error']['alpha'][$lang]; // <--
@$_SESSION['checkSum']--;
}
if(!isAlpha($_POST['iPrenom']))
{
$value['user']['prenom'] = $_POST['iPrenom'];
$msg['error']['prenom']= $msg['error']['alpha'][$lang]; // <--
@$_SESSION['checkSum']--;
}
if(!isVisa($_POST['iVisa']))
{
$value['user']['visa'] = $_POST['iVisa'];
$msg['error']['visa']= $msg['error']['num'][$lang]; // <--
@$_SESSION['checkSum']--;
}
}
if($_SESSION['checkSum']==3)
{// formulaire validé
$page['container']['main'] = $msg['success']['valid'][$lang]; // <--
}
else
{// formulaire invalide
$page['container']['main'] .= $page['container']['visa'] // <--
.'<p>'.$msg['error']['invalid'][$lang].'</p>'; // <--
}
?>
</pre>
3. ici rien est à faire
== Application ==
L'application se construit ensuite par inclusion des composantes
<pre>
<?php
// Main page
# INIT
@session_start();
// preparation de la langue choisie (fr par defaut)
$lang = $_GET['lang']?$_GET['lang']:1;
# PREPARE PAGE
$page['container']['header']='ceci est le header<p/>';
$page['container']['footer']='ceci est le footer<p/>';
// appel de controlAffichage
// page['container']['main'] va etre remplace si le formulaire est valide
require_once "./Control/controlAffichage.inc.php";
require_once './Vue/vueFormulaire.inc.php';
#region[1] mainPage
// preparation du container principal
$page['container']['main']='
<div><a href="'.$_SERVER['PHP_SELF'].'?lang=1">fr</a> | <a href="'.$_SERVER['PHP_SELF'].'?lang=2">uk</a></div><p/>
<div>'.$msg['communication']['form'][$lang].'</div>'
.$page['container']['visa'];
require_once './Vue/vueFrame.inc.php';
#endRegion[1]
# OUTPUT final
echo $page['container']['frameSet'];
?>
</pre>
! Il y a encore moyen d'optimiser la region[1] pour ne pas avoir à initialiser le container main avant l'inclusion et de faire de controlAffichage.inc.php un contrôleur ne comprenant aucun autre traitement que l'appel pour affichage.
== En bref ==
Cette façon de programmer permet de construire des applications plus souples et évolutives et ne demande pas plus de méthode que celle de choisir correctement ses variables lors de l'analyse. L'utilisation de classes permet d'optimiser plus encore le développement et faciliter l'ergonomie.
Il existe de nombreuses implémentations de MVC dans des cadriciels web écrit en PHP.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/Webservice=
== Introduction ==
Le langage XML se propage peu à peu dans le système d'information. Il est devenu nécessaire de connaître ce standard. Il permet de développer des applications sous plateforme JEE, .Net ou PHP et de s'affranchir des problèmes de portabilité. Les webservices sont basés sur XML, permettant de créer des composants logiciels distribués, de les utiliser indépendamment du langage d'implémentation. SOAP, WSDL, UDDI et WS-Inspection sont les technologies standard qui rendent possibles la construction et la publication de ces services.
Dans nos exemples, nous aborderons l'utilisation de SOAP. Zend propose dans son framework quelques utilitaires de la technique [[w:SOAP|SOAP]] et [[w:REST|REST]].
=== Qu'est-ce que SOAP (''Simple Object Access Protocol'') ? ===
Il s'agit d'un protocole d'échange permettant d'invoquer des applications sur différents types de réseaux, en faisant appel, à distance, à des méthodes. Il utilise différents protocoles de transport tel que HTTP mais aussi le protocole POP ou SMTP pour transmettre des données sous forme de messages.
SOAP repose sur une approche RPC (''Remote Procedure Call''), basée donc sur des messages dont le contenu est structuré en XML.
== Webservice PHP4 ==
=== Utilisation de la bibliothèque NuSOAP ===
Pour mettre en place un service web utilisant le protocole SOAP sous technologie PHP, il vous faut récupérer la bibliothèque NUSOAP sous licence GNU en PHP4.
La bibliothèque a été développée par NuSphere et Dietrich Ayala. Elle permet de créer des services web basés sur SOAP 1.1, WSDL 1.1 et HTTP 1.0/1.1.
Vous pouvez la télécharger sur le site suivant: http://sourceforge.net/projects/nusoap/
L'utilisation de cette bibliothèque ne nécessite pas la mise en place d'extensions PHP spécifiques ce qui est un avantage pour la mise en place de ce système.
=== Mise en place du webservice ===
Une fois la bibliothèque téléchargée et placée dans un sous répertoire où va se trouver votre fichier webservice, nous allons pouvoir commencer à voir comment créer votre webservice.
Vous devez créer un fichier pour votre webservice, nous allons le nommer par exemple webservice.php.
<pre>
<?php
// On inclut la bibliothèque nécessaire pour mettre en place le webservice
require_once("lib/nusoap.php");
// On initialise un nouvel objet serveur
$server = new soap_server();
// On configure en donnant un nom et un Namespace
$server -> configureWSDL('nomDuWebservice','Namespace');
// On spécifie l'emplacement du namespace
$server -> wsdl->schemaTargetNamespace = 'http://emplacementDuNamespace';
?>
</pre>
Votre webservice est créé, il vous faut maintenant ajouter des méthodes et le faire communiquer avec les différents clients.
=== Création des méthodes ===
Nous allons voir ici comment ajouter des méthodes dans votre webservice en prenant un exemple simple. Nous allons créer une méthode qui prend en argument une chaîne de caractères et qui la renvoie.
Dans votre fichier webservice.php, à la suite du code déjà écrit, nous allons rajouter les lignes suivantes :
<pre>
<?php
//on enregistre la méthode grâce à register()
$server->register('ReturnChaine',array('ChaineString'=>'xsd:string'),
array('return'=>'xsd:string'),'Namespace');
//nous créons ici la fonction ReturnChaine() qui correspond à la méthode créée
function ReturnChaine($ChaineString) {
return new soapval('return','string',$ChaineString);
}
$HTTP_RAW_POST_DATA = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : '';
$server->service($HTTP_RAW_POST_DATA);
?>
</pre>
Nous avons vu dans cet exemple comment retourner une chaîne de caractère, un exemple assez simple. Il est aussi possible de renvoyer des tableaux grâce aux méthodes lorsqu'on souhaite extraire des éléments d'une base de données.
== Webservice PHP5 ==
On utilise ici la bibliothèque SOAP<ref>http://php.net/manual/fr/book.soap.php</ref>.
=== WSDL ===
<pre>
<?xml version="1.0"?>
<!-- partie 1 : Definitions -->
<definitions name="HelloYou"
targetNamespace="urn:HelloYou"
xmlns:typens="urn:HelloYou"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<!-- partie 2 : Types-->
<types>
<xsd:schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:HelloYou">
</xsd:schema>
</types>
<!-- partie 3 : Message -->
<message name="getHelloRequest">
<part name="prenom" type="xsd:string"/>
<part name="nom" type="xsd:string"/>
</message>
<message name="getHelloResponse">
<part name="return" type="xsd:string"/>
</message>
<!-- partie 4 : Port Type -->
<portType name="HelloYouPort">
<!-- partie 5 : Operation -->
<operation name="getHello">
<input message="typens:getHelloRequest"/>
<output message="typens:getHelloResponse"/>
</operation>
</portType>
<!-- partie 6 : Binding -->
<binding name="HelloYouBinding" type="typens:HelloYouPort">
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="getHello">
<soap:operation soapAction="HelloYouAction"/>
<input name="getHelloRequest">
<soap:body use="encoded"
namespace="urn:HelloYou"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
</input>
<output name="getHelloResponse">
<soap:body use="encoded"
namespace="urn:HelloYou"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
</output>
</operation>
</binding>
<!-- partie 7 : Service -->
<service name="HelloYouService">
<documentation>Retourne une phrase simple </documentation>
<!-- partie 8 : Port -->
<port name="HelloYouPort" binding="typens:HelloYouBinding">
<soap:address location="http://monDns:monPort/monChemin/server.php"/> <!-- modifier ce chemin vers server.php -->
</port>
</service>
</definitions>
</pre>
=== SERVER ===
Pour que soap soit actif, il faut décommenter extension=php_soap.dll dans php.ini
<pre>
<?php
// première étape : désactiver le cache lors de la phase de test
ini_set("soap.wsdl_cache_enabled", "0");
// on indique au serveur à quel fichier de description il est lié
$serveurSOAP = new SoapServer('HelloYou.wsdl');
// ajouter la fonction getHello au serveur
$serveurSOAP->addFunction('getHello');
// lancer le serveur
if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
$serveurSOAP->handle();
}
else
{
echo 'désolé, je ne comprends pas les requêtes GET, veuillez seulement utiliser POST';
}
function getHello($prenom, $nom)
{
return 'Hello ' . $prenom . ' ' . $nom;
}
?>
</pre>
=== Client ===
<pre>
<?php
// première étape : désactiver le cache lors de la phase de test
ini_set("soap.wsdl_cache_enabled", "0");
// lier le client au fichier WSDL
$clientSOAP = new SoapClient('HelloYou.wsdl');
// exécuter la méthode getHello
echo $clientSOAP->getHello('Marc','Assin');
?>
</pre>
== Conclusion ==
Nous avons pu voir dans cet article comment développer un webservice en PHP.
Comme pour les autres technologies dans lesquelles sont développés les webservices, il est possible de construire des méthodes plus complexes,avec accès aux bases de données et un véritable traitement de l'information.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/Vérification RIO=
Le [[w:fr:Relevé d'identité opérateur|relevé d'identité opérateur]] (en abrégé RIO) est un identifiant unique attribué à chaque contrat de téléphonie mobile en France.
Le fragment de code PHP ci-dessous permet de vérifier si celui-ci est correct.
===explications===
* le champ <code>coderio</code> doit être renseigné avec le code RIO sans blanc.
* le champ <code>mobile</code> doit être renseigné avec un numéro de téléphone mobile, sans blanc.
===code===
<pre>
$rio = isset($_POST["coderio"]) ? strtoupper(trim($_POST["coderio"])) : null ;
$mobile = isset($_POST["mobile"]) ? $_POST["mobile"] : null ;
if ($rio === null OR $mobile === null) {
echo "Un des champs est vide";
} else if(strlen($rio) !=12) {
echo "Le code RIO doit contenir 12 caractères exactement";
} else {
$operateur=substr($rio,0,2);
$typecontrat=substr($rio,2,1);
$refclient=substr($rio,3,6);
if($typecontrat != "P" && $typecontrat != "E")
{
echo "Le code RIO est erroné, l'identification du contrat est faux";
exit;
}
$ordre="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+";
$res = array(0,0,0);
$tmp=$operateur.$typecontrat.$refclient.$mobile;
for($n=0;$n<19;$n++) {
$pos=strpos($ordre,substr($tmp,$n, 1));
$res[0]=($res[0]+$pos)%37;
$res[1]=((2*$res[1])+$pos)%37;
$res[2]=((4*$res[2])+$pos)%37;
}
$clecalculee = substr($ordre,$res[0],1).substr($ordre,$res[1],1).substr($ordre,$res[2],1);
if(substr($rio,9) != $clecalculee) {
echo "Le code RIO est erroné";
} else {
echo "<em>!!! Le code RIO est BON !!!</em>";
}
}
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Ajax/Sommaire=
== Ajax : comment créer un sommaire ==
=== Intérêt de l'utilisation d'Ajax ===
Lorsqu'on écrit un sommaire en PHP classique, à chaque fois qu'on clique sur un lien la totalité de la page est affichée. Sur un sommaire, cela crée un effet de clignotement indésirable et d'autant plus important que la page est lourde. De plus, comme il faut régénérer toute la page, la tendance est à surcharger le serveur avec des requêtes inutiles.
Avec la technologie [[w:Ajax (informatique)|AJAX]], seule la partie qui est modifiée dans la page est rechargée. On diminue ainsi à la fois la charge du serveur, celle du réseau et l'effet de clignotement.
* Sans la technologie AJAX, on observe un effet de clignotement.
* Avec Ajax, plus de clignotement et un site plus rapide.
'''Remarque : '''
* ici on voit très peu la différence (quasiment pas d'ailleurs) car le site est super simple. Si le site était plus complexe notamment avec des accès à une base de données, la différence serait beaucoup plus nette !
* Regardez aussi la différence au niveau de l'utilisation des retours en arrière.
=== Les fichiers ===
==== Le fichier index.html ====
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Sommaire en PHP !</title>
<style type="text/css">
#sommaire
{
position:absolute;
background-color:cyan;
left:10px;
width:100px;
}
#page
{
position:absolute;
background-color:#AAAAAA;
left : 200px;
width:500px;
height:500px;
}
</style>
<script type='text/JavaScript'>
var xhr = null;
function getXhr()
{
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if(window.ActiveXObject) {
try {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
} else {
alert("Votre navigateur ne supporte pas les objets XMLHTTPRequest...");
xhr = false;
}
}
function ShowPage(page)
{
getXhr();
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById('page').innerHTML=xhr.responseText;
}
}
xhr.open("GET","ajax.php?page="+page,true);
xhr.send(null);
}
</script>
</head>
<body onLoad="ShowPage(1)">
<div id="sommaire">
<h3>Sommaire</h3>
<a href="#" onClick="ShowPage(1)">Page 1</a><br/>
<a href="#" onClick="ShowPage(2)">Page 2</a><br/>
<a href="#" onClick="ShowPage(3)">Page 3</a><br/>
<a href="#" onClick="ShowPage(4)">Page 4</a><br/>
</div>
<div id="page">
</div>
</body>
</html>
</pre>
==== Le fichier ajax.php ====
<pre>
<?php
$page=$_GET['page'];
if($page==1)require 'page1.html';
else if($page==2)require 'page2.html';
else if($page==3)require 'page3.html';
else require 'page4.html';
?>
</pre>
==== Le fichier page1.html ====
<pre>
<h1>Page 1</h1>
bla bibib blan
</pre>
==== Le fichier page2.html ====
<pre>
<h1>Page 2</h1>
bonjour
</pre>
==== Le fichier page3.html ====
<pre>
<h1>Page 3</h1>
bli bli bli
</pre>
==== Le fichier page4.html ====
<pre>
<form method="get" action="http://www.google.com/search"><fieldset style="border: 1px solid black;"><legend style="font-family:verdana;font-weight:bold;font-size:1em;color:orange;">Recherche Google</legend><TABLE><tr><td align="center"><div style="text-align: center;">
<A HREF="http://www.google.fr">
<IMG SRC="http://www.google.com/logos/Logo_40wht.gif" border="0"
ALT="Google" align="middle"></A></div></td></tr>
<tr><td align="center"><div style="text-align: center;"><INPUT TYPE=text name=q size=20 value="">
<INPUT TYPE=hidden name=hl value=fr></div></td></tr>
<tr><td align="center" colspan="2"><div style="text-align: center;"><INPUT style="border: 2px outset purple;color:white;background-color:purple;font-weight:bold;font-family:verdana;" type=submit name=btnG VALUE="Recherche"></div>
</td></tr></TABLE>
</FORM></fieldset>
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Ajax/Date=
== Démonstration ==
Le résultat est visible sur http://xavier.merrheim.free.fr/date.
== Les fichiers ==
=== Le fichier index.html ===
<pre>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Sommaire en PHP !</title>
<style type="text/css">
#page1
{
position:absolute;
background-color:#AAAAAA;
left : 200px;
width:500px;
top:10px;
height:200px;
}
#page2
{
position:absolute;
background-color:cyan;
left:200px;
width:500px;
height:200px;
top:250px;
}
</style>
<script type='text/JavaScript'>
var xhr = null;
var n=0;
function getXhr()
{
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if(window.ActiveXObject) {
try {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
} else {
alert("Votre navigateur ne supporte pas les objets XMLHTTPRequest...");
xhr = false;
}
}
function init()
{
loop();
}
function loop()
{
setTimeout('loop();',3000);
ShowPage();
}
function ShowPage()
{
getXhr();
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById('page1').innerHTML=xhr.responseText;
}
}
xhr.open("GET","ajax.php",true);
xhr.send(null);
}
</script>
</head>
<body onLoad="init()">
<div id="page1">
</div>
<div id="page2">
Ce cadre n'est pas mis à jour.<br/>
<H1>Bienvenue chers amis</h1>
</div>
</body>
</html>
</pre>
=== Le fichier ajax.php ===
Ce fichier peut contenir tout script php dont le résultat doit être mis à jour régulièrement.
Pour cet exemple :
<pre>
<?php
echo "(Cadre mise à jour le ".date("r").")";
?>
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS=
Les CMS ou Content Management Systems ([[:Catégorie:système de gestion de contenu|système de gestion de contenu]]) sont répandus sur les toiles intra/extra(nets). Leur objectif étant d'assister la publication de contenu.
L'article décrit comment en concevoir un, ajaxifié côté client et modulaire côté serveur (''backend'').
Définissons quelques fonctionnalités et appelons le package "myTinyCMS".
[[Programmation_PHP/Exemples/MiniCMS/Faire_un_miniCMS|Aller au livre]]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Faire un miniCMS=
== Faire un miniCMS ==
Les CMS ou Content Management Systems sont répandus sur les toiles intra/extra(nets). Leur objectif étant d'assister la publication de contenu.
L'article décrit comment en concevoir un, ajaxifié côté client et modulaire côté serveur (''backend''). Définissons quelques fonctionnalités et appelons le package "myTinyCMS".
=== Les modules développés ===
Ne seront développés et intégrés que les modules nécessaires à la démonstration.
Ces modules peuvent s'organiser à desseins.
: <b>Backend(s)manager</b>
:: Users
:: Contents
:: Customizations
: <b>FrontEnds Manager</b>
:: Templates
:: Controlers
: <b>Front-Ends</b>
:: Templates
=== Analyse ===
Les schémas suivants décrivent le déploiement des composants.
<pre>
./index.php
<Utilities>_____<Backend>
|
[Functions] | [ContentModule]
[Classes] | [TemplatesModule]
[Objects] | [UsersModule]
|
|__<Frontend>
[Outputer]
[AjaxLayer]
</pre>
On a ici trois couches à distribuer en MVC.
=== Déploiements ===
Les parties "clientes" et "administratives" sont séparées par simplicité. On prend un déploiement admins vs customers sur deux répertoires.
<pre>
./Root
main.php
config.inc.php
/Cms
/Functions
/common.inc.php
/Classes
/contentPane.class.php
/dataManager.class.php
/templateControler.class.php
/utilities.class.php
/snippetActer.class.php
/Images
/Objects
/Backend
/authorize.inc.php
/services.inc.php
/webservices.inc.php
/responder.inc.php
/Frontend
/entry.inc.php
/admin.inc.php
/Owners
<AdminAccount>
/root
/Contents
/page.xml
/user.xml
/...
/Templates
/admin.xml
/default.xml
/Users
<Customers>
/customer1
/Contents
/Templates
/Users
/customer2
/...
</pre>
Les composantes back&front ends sont dans cette version des packages ou sous composantes. Les données étant distinctement séparées du backOffice.
=== Technologies ===
*Les données : Xmlisation
*Le code : Php5 Object
*Dumping & archiving : mysql en soutien pour la recherche indexée et le harvesting
=== Méthodologie ===
*RUP et patterns
*pas de frameworks (zend ou autre...) ou rapatriement total des snippets
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Développement=
== Développement ==
*Les couches choisies
*Code de service vs Code métier
*Modélisation
*Implémentation
*Déploiement de la vue
*Ajaxification
*Intégration des gestionnaires dans un premier noyau
*Fournir les données
*Interface Homme - Machine
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Les couches choisies=
*Couche de services
:: Fonctionnalités
:: Patterns
*Couche métier
:: Orientations
:: Modularités
*Couche d'accès aux données
:: Connectiques
*Couche de présentation
:: Modèles (''Templates'')
Il y aura peu de métier et beaucoup de service dans cette version, l'essentiel du travail reposant sur la modélisation, la sérialisation et la modularisation à faible granularité.
Avec l'expérience, en développant de manière modulaire, l'écriture du code se fait sans difficulté, hormis les problèmes liés aux limitations de la solution. Il faut s'efforcer de rester le moins limitatif possible (sauf développements business).
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Code de service vs Code métier=
Le choix de l'orientation est déterminé par le fait de coder pour soi-même ou pour un client. À moins de devoir développer un code polyvalent au client, il est déconseillé de délivrer du code de service pour la simple et bonne raison, qu'il peut s'approprier le code et le faire devenir propriétaire.
En effet, le code de service, libéré des contraintes business, doit être "déposé et protégé", en particulier quand il propose un service considérable. Au mieux il doit être sous licence d'utilisation.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Modélisation=
La modélisation se veut simple et polyvalente.
==== Champs de données ====
<b>[champs contenus]</b>
Sous customer1/contents, les champs ou "espaces de données" sont organisés en paquets d'enregistrements. D'autres modèles sont possibles.
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<!-- content dataChunk model -->
<crop id="" number="" scope="" links="">
<topic/>
<assert>
<!-- HERE COMES FRONT END CODE -->
</assert>
<!-- Content records -->
<fields>
<field id="" number="" subject="">
<topic/>
<assert/>
<content></content>
</field>
</fields>
</crop>
</pre>
Distribués ou non, les paquets s'accèdent par du service pour ne pas dépendre d'un développement purement métier.
On peut opérer la distinction service / métier, en disant que le service est moins spécialisé.
La méthode d'accession aux données sera par exemple un :
*getCropById ou byScope (prendreChampParId ou parEtendue)
*getFieldNearSubject (prendreDonnéePrèsDuSujet)
L'id permettant une extraction directe de contenu.
<b>[champs utilisateurs]</b>
Même logique de service :
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<!-- users dataChunk model -->
<users id="" number="" links="">
<!-- u:usr / m:mod / a:adm -->
<user login="" pass="" lastname="" forname="" rights="u|m|a" />
</users>
</pre>
Mêmes accès que pour le modèle précédent.
<b>[Champs page.xml]</b>
L'indexation des pages pointeront sur des données data ou layout
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<pages count="4">
<page number="1" id="standard" entry="data:1|1|rootEntry"/>
<page number="2" id="vaisseau" entry="data:1|2|information_2"/>
<page number="3" id="espace" entry="data:1|3|doorEntry"/>
<page number="4" id="admin" entry="data:1|1|Error"/>
<page number="6" id="potiron" entry="data:1|1|content" num="5"/>
<page number="7" id="er" entry="e"/>
<page number="8" id="terere" entry="re"/>
</pages>
</pre>
<b>[Champs templates]</b>
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">:: standard ::</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<!-- templateChunk model -->
<template id="standard" number="" topic="" links="">
<!-- HEADER CONTENT -->
<header>
<assert>
<![CDATA[
:: HERE COMES SERVER/CLIENT SIDE SNIPPETS ::
]]>
</assert>
<!-- -->
<content>
<![CDATA[
<html><head/>
]]>
</content>
</header>
<!-- HEADER CONTENT -->
<main>
<assert>
<![CDATA[
:: HERE COMES SERVER/CLIENT SIDE SNIPPETS ::
]]>
</assert>
<!-- -->
<content>
<![CDATA[
<body>
...
]]>
</content>
</main>
<!-- FOOTER CONTENT -->
<footer>
<assert>
<![CDATA[
:: HERE COMES SNIPPET ::
]]>
</assert>
<content>
<![CDATA[
</body></html>
]]>
</content>
</footer>
</template>
</pre>
</div>
</div>
La version suivante a une complétion par points d'ancrages. Cette version va orienter le projet.
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">:: nested ::</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<!-- templateChunk model -->
<template id="nested" number="" topic="" links="">
<!-- HEADER CONTENT -->
<header>
<assert>
<![CDATA[
:: HERE COMES SERVER/CLIENT SIDE SNIPPETS ::
]]>
</assert>
<!-- -->
<content>
<styles>
<style id="" />
<style id="christmas">
body
{
background-color:red;
font-family:verdana;
padding:10px;
}
.style_01
{
/* some style in */
}
</style>
</styles>
<scripts>
<script id="scr_1" number="1"/>
<script id="scr_2" number="2"/>
...
</scripts>
</content>
</header>
<!-- HEADER CONTENT -->
<main>
<assert>
<![CDATA[
:: HERE COMES SERVER/CLIENT SIDE SNIPPETS ::
]]>
</assert>
<!-- -->
<content>
<page id="standard">
<![CDATA[
<table width="100%" border="0">
<tr><td>
{{inc:page[header]}}
</td></tr>
<tr><td>
{{inc:page[main]}}
</td></tr>
</table>
]]>
</page>
<page id="lay_01"/>
<page id="lay_02"/>
<page id="lay_03"/>
</content>
</main>
<!-- PAGE CONTENT -->
<frameset>
<assert>
<code type="text/javascript" id="scr_01">
<![CDATA[
function lastScript()
{
return "this is last script";
}
]]>
</code>
<code type="action/php" id="act_01">
<![CDATA[
ContentCompleter::setContent(array("frameset.content[text/head]","header.content[*]"));
ContentCompleter::setContent(array("frameset.content[text/body]","main.content[*]"));
]]>
</code>
<code type="action/php" id="act_02">
<![CDATA[
ContentCompleter::setContent(array("frameset.assert.code[id:scr_01]","footer.content[text/id:inc_03]"));
]]>
</code>
<code type="text/php" id="toStringClass">
<![CDATA[
class toString
{//
public $self = array();
public function __construct( /* ... */ $init )
{
}
}
$tmp = new toString(1);
]]>
</code>
</assert>
<content><!-- -->
<![CDATA[
<?xml version="1.0" encoding="UTF-8" ?>
{{inc:token[strict]}}
<html>
<head>
<style>{{inc:style[christmas]}}</style>
<script>{{inc:script[topScript]}}</script>
</head>
<body onload="feedContent()">
{{inc:page[standard]}}
</body>
<script>
{{inc:script[endScript]}}
</script>
</html>
]]>
</content>
</frameset>
<!-- FOOTER CONTENT -->
<footer>
<assert>
<![CDATA[
:: HERE COMES SNIPPET ::
]]>
</assert>
<content>
<![CDATA[
{{inc:scripts[last]}}
<script>
action.load();
</script>
<script id="inc_03">
</script>
]]>
</content>
</footer>
<tokens>
<token id="HTML4">
<![CDATA[
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
]]>
</token>
<token id="xhtml_xmlns">
<![CDATA[
http://www.w3.org/1999/xhtml
]]>
</token>
</tokens>
</template>
</pre>
</div>
</div>
L'autorisation de snippets php exécutables rendra possible l'intégration côté client (''customer'').
L'adjonction du contenu à l'output se faisant par injection aux points d'ancrage {{inc:nomDeCle[idDeCle]}} par la méthode servant à compléter le flux avant sa sortie, comme :
*getFeedByTagName ou getPartByName ...
*incomes(header/content) à l'injection
Le contenu peut être sorti côté php ou ajaxifié comme ici par le javascript de fin de flux (frameset/content) feedContent().
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Implémentation=
Nous avons besoin des fonctionnalités :
<b>TODO</b>
:[1] Complétion des données
:: (none) Create
:: (done) Read
:: (done) Update
:: (done) Delete
:[2] Gestion des users
:[3] Complétion du frameset via templates
:: (done) injection point replacer
:[4] Un webService pour les inputs&query distants
:: (none) common web services
:[5] Parser de snippet
:: (do ) php snippet acter
::: (none) class writer & executer
::: (do ) single actions executer
:[6] Vue xhtml fonctionnelle ajaxifiée
:: (done) xhtml template "nested"
<b>NICE TO HAVE</b>
:[1.1] Versionning sur données
:: (todo) new crop creating passing current to old
Le but étant de fournir un maximum de code de service pour les héritages futur le métier n'est approché qu'en vue de personnaliser le CMS client.
<b>Concernant l'approche modulaire :</b>
La segmentation en composantes (classes ou ''scriptlets'') permets un découpage net et précis des différentes parties du projets développable séparément les unes des autres. L'association au sein d'un IHM ou couche de présentation via contrôleur, venant :
*avant (pour orienter le développement)
*pendant (pour stabiliser le développement)
*après (pour finaliser le développement)
L'interface utilisatrice devant tirer parti des composantes et non l'inverse. La richesse des fonctionnalités est dégagée par le service et enclavé par un code orienté solution client. Il va donc de soi de privilégier proprement, pour enrichir ses propres librairies, de chercher le plus possible à développer ses classes et composantes plutôt que de perdre son temps en métier.
Concevoir du code de haute qualité signifie souvent d'optimiser le code vers une solution à logique 'customer' càd spécialisée, ce qui n'est pas recherché lorsque l'on doit se développer de l'expérience sur le terrain. Au possible, il faut adapté sa modularité et péricliter le service en business pour le client.
Concevoir du code modulaire permets de développer concrètement sans trop d'efforts d'analyse pour les développement XP et sur un grand délais. <b>Il est utile d'avoir conscience de ces deux approches sur le terrain ou lorsque l'on décide d'ouvrir son code au monde. La majorité des développeur ne faisant trop souvent qu'intégrer pour l'argent ou la renommée chez le client le fruit du travail des autres</b>.
<b>'Classes & Fermes de fonctions' contre 'scriptlets'</b>
Les premières sont :
* plus simple à coder,
* plus simple à utiliser,
* plus simple à entretenir,
* plus simple à spécialiser,
* plus facilement réutilisable,
* plus orienté objet,
* plus susceptible d'être récupéré.
Les secondes sont :
* plus cryptiques,
* plus optimisée pour le client,
* aisément traductible en classes et fermes,
* plus fonctionnelle,
* plus procédurales,
* plus chaotiques<ref>produire un code obscure voir chaotique présente de nombreux avantages. D'autres analystes et collègues y perdront leur temps d'une part, les piping inhérents au code permettant l'implémentation, le hacking, le reroutage et la divergence fonctionnelle de script. Ce qui n'est pas sans avantages.</ref>.
<references/>
==== [1] Complétion des données ====
Pour disposer des fonctionnalités de gestion de données.
* On a besoin de Getter/Setter de données :
: - depuis la vue
: - depuis le webservice
: - depuis l'URL
* Pour les fichiers xml
Les fonctionnalités d'appels sont regroupées ci-dessous
<pre>
<?php
class dataManager
{//
private $pattern;
protected $stack, $domDoc;
//
public static
$crop
= array(
"std"=> '<?xml version="1.0" encoding="UTF-8"?>
<crop><topic /><fields /></crop>'
,""=>''
),
$data
= array(
"root"=>"/miniCMS/Owners/"
);
# TODO GETTER/SETTER
public static function getContentByData($pData)
/**
* get the content from data
*/
{}
public static function setContentByData($pData)
/**
* set the content to data
*/
{}
public function setCropForCustomer($pPath, $pCrop)
/**
* set crop to customer directory
*/
{
file_put_contents($pPath, $pCrop);
}
# STATE
public function dataManager()
{
$this->initialize();
}
public function initialize()
/**
* init
*/
{
$this->domDoc = new DOMDocument();
if(@$this->data['path'])
{
$this->domDoc->load( $this->data['path'] );
}
return $this->domDoc;
}
public function saveDocument()
/**
* save document
*/
{
$this->domDoc->save($this->data['path']) ;
}
}
?>
</pre>
On implémente ces fonctions utilitaires à utilities.class.php
<pre>
public static function searchNodesByContent($pDocument, $pQueries)
/**
* return node matching request
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
/* ceci est un exemple de mauvais code de service,
il ne fonctionne que pour un nombre limité de structure de crop
c'est donc un code métier à surcharger. */
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
foreach ( $u->childNodes as $v )
{
if( $v->nodeName == $pQueries['node']
&& ! utilities::isContent ( $pQueries['value']
, $v->nodeValue ) )
$_fields->item(0)->removeChild($u) ;
}
}
/* OBSOLÈTE - Malheureusement, ce code ne marche pas une fois appelée par getNodes
for($i=0; $i<count($pNodeSet); $i++)
{
$_content = $pNodeSet[$i]->childNodes ;
// cette fonction ne descend qu'à un niveau
foreach($_content as $v)
{
if( $v->nodeName == $pQueries['node']
&& ! utilities::isContent ( $pQueries['value']
, $v->nodeValue ) )
{
$pNodeSet[$i]->parentNode->removeChild($pNodeSet[$i]) ;
}
}
}
*/
return $pDocument ;
}
public static function searchNodesByAttribute($pDocument, $pQueries)
/**
* return node matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
if( !($u->getAttribute($pQueries['attribute'])
== $pQueries['value']) ) // 1:1 match
$_fields->item(0)->removeChild($u) ;
}
return $pDocument ;
}
</pre>
On implémente la méthode getContentByData du dataManager
<pre>
public static function getContentByData($pDocument, $pData)
/**
* get the content from data
*/
{
/* DEPRECIEE - code pour l'appel à getNodes déprécié pour searchNodes
if($pData['node'])
{
if($pData['node'] != "*")
{
$expr = '//'.$pData['node'].'/..' ;
} else {
return $pDocument ;
}
} else {
return false ;
}
*/
return utilities::searchNodes($pDocument, $pData) ;
}
# Qui devient ici capable de distinguer
# recherche de contenu d'avec les attributs de nœuds
public static function getContentByData($pDocument, $pData)
/**
* get the content from data
*/
{
if(@$pData['attribute'])
{
return utilities::searchNodesByAttribute($pDocument, $pData) ;
}
else
{
return utilities::searchNodesByContent($pDocument, $pData) ;
}
}
</pre>
Un test du getContentByData nous rend nominal pour cette phase dans cette modélisation métier.
Le ''refactoring'' du business en services serait apprécié...
<pre>
<?php
require_once "./Cms/Classes/dataManager.class.php";
require_once "./Cms/Classes/utilities.class.php";
//echo '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
$_manager = new dataManager();
//
$_manager->data['path'] = $_manager->data['root'] . "Customer1/Contents/crop1.xml"; //
// init manager
$_document = $_manager->initialize();
# QUERY TESTS
// setting de valeur à récupérer
$_manager->data['query_1'] = array("node"=>"subject","value"=>"potager");
$_manager->data['query_2'] = array("node"=>"content","value"=>"molle2");
// queries test
$res = dataManager::getContentByData(
dataManager::getContentByData($_document, $_manager->data['query_1'])
,$_manager->data['query_22']);
# OUTPUT
echo $res->saveXML();
?>
</pre>
: -> Sur l'input crop1.xml
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<crop>
<topic>Cultiver son potager</topic>
<scope>légumes potager soupe</scope>
<fields>
<field id="k_01">
<subject>les légumes du potager</subject>
<content>Le potager contient une terre molle.
Parsemé ça et là, on trouve carottes,
petits poids et potirons
</content>
</field>
<field id="k_02">
<subject>les légumes du test</subject>
<content>Le potager contient une terre molle.
Parsemé ça et là, on Test carottes,
petits poids et potirons</content>
</field>
<field id="k_03">
<subject>les légumes du potager</subject>
<content>Le potager contient une terre molle2.
Parsemé ça et là, on trouve carottes,
petits poids et potirons</content>
</field>
</fields>
</crop>
</pre>
: -> On a pour résultat :
<pre>
<?xml version="1.0" encoding="UTF-8" ?>
<crop>
<topic>Cultiver son potager</topic>
<scope>légumes potager soupe</scope>
<fields>
<field id="k_03">
<subject>les légumes du potager</subject>
<content>Le potager contient une terre molle2.
Parsemé ça et là, on trouve carottes, petits poids et potirons
</content>
</field>
</fields>
</crop>
</pre>
Le deuxième test de get par l'id est également concluant
<pre>
# QUERY TESTS
// setting de valeur à récupérer
$_manager->data['query_1'] = array("node"=>"subject", "value"=>"potager");
$_manager->data['query_2'] = array("attribute"=>"id", "value"=>"k_03");
// query test
$res = dataManager::getContentByData(
dataManager::getContentByData($_document, $_manager->data['query_1'])
,$_manager->data['query_2']);
# OUTPUT
echo $res->saveXML();
</pre>
Son résultat étant :
<pre>
<?xml version="1.0" encoding="UTF-8" ?>
<crop>
<topic>Cultiver son potager</topic>
<scope>légumes potager soupe</scope>
<fields>
<field id="k_03">
<subject>les légumes du potager</subject>
<content>Le potager contient une terre molle2.
Parsemé ça et là, on trouve carottes,
petits poids et potirons
</content>
</field>
</fields>
</crop>
</pre>
On implémente à utilities.class.php les méthodes :
<pre>
public static function writeContentByAttribute($pDocument, $pQueries)
/**
* write node content matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
$_flg = false ;
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
if( $u->getAttribute($pQueries['attribute'])
== $pQueries['value'] ) // 1:1 match
{
$v = $u->getElementsByTagName($pQueries['node']) ;
$v->item(0)->nodeValue = $pQueries['replacement'] ;
$_flg = true ;
}
}
}
return $_flg ;
}
public static function deleteContentByContent($pDocument, $pQueries)
/**
* delete node matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
$_flg = false ;
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
foreach ( $u->childNodes as $v )
{
if( $v->nodeName == $pQueries['node']
&& utilities::isContent ( $pQueries['value'] ,
$v->nodeValue ) )
{
$_fields->item(0)->removeChild($u) ;
$_flg = true ;
}
}
}
}
return $_flg ;
}
public static function deleteContentByAttribute($pDocument, $pQueries)
/**
* delete node matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
$_flg = false ;
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
if( $u->getAttribute($pQueries['attribute'])
== $pQueries['value'] ) // 1:1 match
{
$_fields->item(0)->removeChild($u) ;
$_flg = true ;
}
}
return $_flg ;
}
</pre>
et à la classe dataManager.class.php
<pre>
public function setContentByData($pDocument,$pData)
/**
* set the content to data
*/
{
if(@$pData['attribute'])
{
utilities::writeContentByAttribute($pDocument, $pData) ;
}
else
{
utilities::writeContentByContent($pDocument, $pData) ;
}
$this->saveDocument();
}
</pre>
Le test suivant permet de valider la phase d'écriture :
<pre>
# QUERY TESTS
// setting de valeur à récupérer
$_manager->data['query_1'] = array("attribute"=>"id", "value"=>"k_03","node"=>"subject",
"replacement"=>"this is the new subject");
// query test
$_manager->setContentByData($_document, $_manager->data['query_1']);
</pre>
avec le resultat
<pre>
<field id="k_03">
<subject>this is the new subject</subject>
<content>Le potager contient une terre molle2.
Parsemé ça et là, on trouve carottes,
petits poids et potirons
</content>
</field>
</pre>
===== Liste des défauts =====
* searchContents / getContentByData est du code métier
: -> le transcrire en code de service
* pour la suppression, delete reste à implémenter correctement
: -> à dataManager
==== [2] Gestion des utilisateurs ====
La gestion des utilisateurs suivant le modèle ne demande que l'extraction de la ligne utilisateur pour recueillir ses infos.
Dans la classe utilities.class.php on rajoute ces deux méthodes :
<pre>
public static function getAttributesContents($pNode)
/**
* retourne les attributs dans un tableau
*/
{
foreach ($pNode->attributes as $attrName => $attrNode)
{
$_tab[$attrName] = $attrNode->value ;
}
return $_tab ;
}
public static function getLineByAttribute($pDocument, $pQueries)
/**
* return node matching request by attribute
*/
{
$_users = $pDocument->getElementsByTagName('user');
foreach ($_users as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
$_flg = true ;
foreach($pQueries as $_qa )
{
if( !($u->getAttribute($_qa['attribute'])
== $_qa['value']) || !$_flg )
{ // 1:1 match
$_flg = false ;
}
}
if($_flg)
return utilities::getAttributesContents($u) ;
}
}
return false ;
}
</pre>
Un test concluant de ces méthodes nous rend opérationnel pour cette phase
<pre>
<?php
require_once "./Cms/Classes/dataManager.class.php";
require_once "./Cms/Classes/utilities.class.php";
$_manager = new dataManager();
//
$_manager->data['path'] = $_manager->data['root'] . "Customer1/Contents/user1.xml"; //
# QUERY TESTS
$_manager->data['query_1'] = array(
array("attribute"=>"login", "value"=>"user2")
, array("attribute"=>"pass", "value"=>"pass2"));
// query test
$_res = utilities::getLineByAttribute($_manager->initialize(), $_manager->data['query_1']) ;
# OUTPUT
print_r_html($_res);
?>
</pre>
Résultat :
<pre>
Array
(
[login] => user2
[pass] => pass2
[lastname] => name2
[forname] => forName2
[rights] => u
[customer] => customer2
)
</pre>
===== Liste des défauts =====
Cette gestion des utilisateurs ne prend actuellement que les attributs de nœuds...
:-> il serait utile de pourvoir des données supplémentaires dans le nœud <code>user</code>.
==== [3] Complétion du frameset ====
* Pour commencer nous déclarons une classe utilities.class.php qui reprendra toutes les fonctionnalités utiles pour notre solution
<pre>
<?php
function print_r_html($data,$return_data=false)
{
$data = print_r($data,true);
$data = str_replace( " "," ", $data);
$data = str_replace( "\r\n","<br/>\r\n", $data);
$data = str_replace( "\r","<br/>\r", $data);
$data = str_replace( "\n","<br/>\n", $data);
if (!$return_data)
echo $data;
else
return $data;
}
class utilities
{
public static function getNodes($pDomDoc, $pXpathString)
/**
* get the node from DomDoc with XpathString
*/
{
$xp = new DOMXPath($pDomDoc);
$xp->registerNamespace('x', 'http://www.w3.org/1999/xhtml');
$xp->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
$xp->registerNamespace('i18n', 'http://apache.org/cocoon/i18n/2.1');
$ret = array();
$nodes = $xp->query($pXpathString);
foreach ($nodes as $node)
{
array_push($ret, $node);
}
}
public static function isContent($pPattern, $pFeed)
/**
* check is pattern in feed
*/
{
return preg_match('/('.$pPattern.')/',$pFeed)?true:false;
}
}
?>
</pre>
* La classe templateControler regroupe les fonctions sur les templates. Le contenu à publier est segmenté en flux.
* Son objectif est de :
: - reformater,
: - assembler,
: - publier.
<pre>
<?php
class templateControler
{
/**
* Control the templates
*
* @var (mixed) ($domDoc, $data, $stack)
*/
//
private $pattern
= array(
'inc'=>'/{{inc:[a-zA-Z]*[\[\]\*]*}}/' // inclusion patterns
,''=>''
);
protected $domDoc, $stack;
//
public $data;
/*
public function __construct()
{//
$this->domDoc = new DOMDocument();
$this->initialize();
}
*/
/**
* Enter description here...
*
* @param unknown_type $pVar
* @return templateControler
*/
public function templateControler($pVar)
{
$this->domDoc = new DOMDocument();
$this->setter($pVar);
$this->initialize();
}
/**
* Enter description here...
*
* @param unknown_type $pVar
*/
public function setter($pVar)
{//
$this->data = $pVar;
}
// TODO GETTER
public function initialize()
{//
$this->domDoc->load( $this->data['path'] );
}
/**
* Enter description here...
*
*/
public function getNode($pXPath) //"//html/body/*[@class='text']";
{//
return $this->data['feed'] = utilities::getNodes($this->domDoc, $pXPath);
}
public function getAnchors($pFeed)
{//
preg_match_all($this->pattern['inc'], $pFeed, $this->data['result']);
return $this->data['result']; // on prefere les données centralisées dans la classe
}
public function publicTemplate()
{//
return $this->domDoc->saveXML();
}
}
</pre>
Le test suivant permet de voir si on prend bien les ancres d'injection, par exemple, pour le nœud frameset/content
<pre>
//
$tmp = new templateControler(array("path"=>"object.xml"));
$tst = $tmp->getNode("//template/frameset/content");
print_r_html($tmp->getAnchors($tst[0]->nodeValue));
</pre>
<pre>
Array
(
[0] => Array
(
[0] => {{inc:styles[*]}}
[1] => {{inc:scripts[*]}}
[2] => {{inc:bodies[*]}}
)
)
</pre>
On implémente ces deux nouvelles fonctions à templateControler
<pre>
/**
* getContent retourne le contenu du/des nœuds demandés
*
* > (nodeName, nodeId) - (string) >
*/
public function getContent($nodeName,$nodeId)
{//
// set expression
$_expr = "//" . $nodeName . ($nodeId&&$nodeId!='*'?"[id='$nodeId']":"");
$set = $this->getNode($_expr);
$content=null;
foreach($set as $k=>$v)
{
$content .= "<" . $nodeName . ">" . trim($v->nodeValue) . "</" . $nodeName . ">";
}
return $content?$content:"";
}
/**
* replace anchor by content
*/
public function anchorContentReplacer()
{//
$i=0;
foreach($this->data['result'][0] as $k=>$v)
{
// formatting anchor's syllabes
$v = str_replace(array('{','[','}',']'),array('',' ','',''),$v);
$tmp = explode(":",$v);$_ = explode(' ',$tmp[1]);
// replacing
$this->data['result'][0][$i++]
= array("node"=>$_[0],"value"=>$this->getContent($_[0], $_[1]));
}
}
// TODO : getNodeById pour disposer d'un getElementById moins spécialisé et fonctionnel ici.
</pre>
Le test suivant permet de voir si on remplace bien l'ancre par son contenu :
<pre>
require_once "templateControler.class.php";
//
$tmp = new templateControler(array("path"=>"template.xml"));
$tmp->getNode("//template/frameset/content");
$tmp->getAnchors(
$tmp
->data['feed'][0]
->nodeValue);
$tmp->anchorContentReplacer();
print_r_html(
$tmp->data['result']
);
</pre>
<pre>
Array
(
[0] => Array
(
[0] => Array
(
[node] => styles
[value] =>
body
{
background-color:red;
font-family:verdana;
padding:10px;
}
.style_01
{
/* some style in */
}
)
[1] => Array
(
[node] => scripts
[value] =>
)
[2] => Array
(
[node] => bodies
[value] =>
)
)
)
</pre>
Le contenu est bien localisé dans le template, par conséquent l'objectif de cette phase est atteint. On est opérationnel pour l'injection.
* La classe snippetActer regroupe les fonctions de computation des ''snippets''.
<pre>
class SnippetActer
{//
protected $_type,$_content,$_stack;
# GET/SET
public function setType()
{//
list($_1,$_2)=explode('/',$this->_type);
$this->actCode(array($_1,$_2));
}
public function setContent($pContent)
{//
$this->_content = $pContent;
}
# STATE
/*
* act statements
*/
public function actCode($pType)
{//
switch(true)
{
case $pType[0]=="text"&&$pType[0]=="php":
//
$this->write();
break;
case $pType[0]=="text"&&$pType[0]=="javascript":
$this->inject();
//
break;
case $pType[0]=="action"&&$pType[0]=="php":
$this->unstack();$this->act();
break;
}
}
/*
* stack statements
*/
public function unstack()
{//
$_this->_stack = explode(";",$this->_content);
}
/*
* execute statements
*/
public function act()
{//
foreach($this->_stack as $v)
eval($v);
}
}
</pre>
===== Liste des défauts =====
* Les ''snippets'' actifs à la volée peuvent ralentir l'output
: -> l'ajaxification peut fournir le contenu en deux temps (données avant/après traitements)
* Cette classe reste à faire...
==== [4] Un webservice ====
Le [[../../webService|webService]] propose par défaut une synergie avec d'autres solutions, sites, applications. Des fonctionnalités distantes permettent l'interfaçage logicielle avec le serveur de données, option qui deviendra basique pour les systèmes orientés données.
<pre>
<?php
/*
webservice servker
*/
# no chache in testing
ini_set("soap.wsdl_cache_enabled", "0");
# wsdl
$serveurSOAP = new SoapServer('service.wsdl');
# provided services
/* inqueries services
in :
out :
*/
$serveurSOAP->addFunction('request');
// ...
/* high level services
in :
out :
*/
$serveurSOAP->addFunction('harvest');
// ...
/* low level services
in :
out :
*/
$serveurSOAP->addFunction('deploy');
$serveurSOAP->addFunction('f_01');
$serveurSOAP->addFunction('f_02');
// ...
# server start
if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
$serveurSOAP->handle();
}
# services integration
require_once "./Services/request.srv.php";
require_once "./Services/service_01.srv.php";
require_once "./Services/service_02.srv.php";
# online status checker
function listen($pIn)
{
return 'Copy : ' . $pIn;
}
?>
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Déploiement de la vue=
=== Le frontOffice ===
Le front office est orienté ajax.
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<!-- templateChunk model -->
<template id="nested" number="" topic="" links="">
<!-- HEADER CONTENT -->
<header>
<assert>
<![CDATA[
:: HERE COMES SERVER/CLIENT SIDE SNIPPETS ::
]]>
</assert>
<!-- -->
<content>
<style id="" />
<style id="standard">
body
{
/* background-color:red; */
font-family:verdana;
font-size:10px;
margin:0px 0px 0px 0px;
padding:0px;
}
.click
{
cursor:pointer;
}
.style_01
{
/* some style in */
}
.encoder
{
font-size:11px;
font-family:verdana;
width:95%;
}
.title td
{
font-weight:bold;
}
.specButton
{
font-weight:bold;
cursor:pointer;
font-size:10px;
font-family:verdana;
}
div
{
/* border: grey 1px solid ; */
}
.dirPath
{
background-color:yellow;
}
.red
{
color:red;
}
.green
{
color:green;
}
.dark
{
padding:2px;
background:#bbb;
/* font-weight:bold; */
color:white;
}
.panel
{
position: fixed;
background:#eee;
top: 1em;
right: 2%;
border: 1px solid #000000;
padding: 1em;
z-index: 10;
width: 200;
}
.adminMenu
{
position: fixed;
background:#eee;
top: 1em;
left: 2%;
border: 1px solid #000000;
padding: 1em;
z-index: 10;
width: 200;
}
.bottomTool
{
position: fixed;
background:#eee;
bottom: 1em;
left: 2%;
border: 1px solid #000000;
padding: 1em;
z-index: 10;
/* width: 75%; */
overflow:auto;
}
/* */
input
{
width:80%;
}
</style>
<script id="prototype" number="" >
<![CDATA[
<script src="./Cms/Objects/prototype.js"></script>
]]>
</script>
<script id="editor" number="">
<![CDATA[
// inject special tags in the textPad as bold, strike...
// input target place, start position, end position
// output nonde
function txtInject(trg,repdeb, repfin)
{//
document.all('txt#content').focus();
/* pour l'Explorer Internet */
if(typeof document.selection != 'undefined')
{
/* Insertion du code de formatage */
var range = document.selection.createRange();
var insText = range.text;
range.text = repdeb + insText + repfin;
/* Ajustement de la position du curseur */
range = document.selection.createRange();
if (insText.length == 0)
{
range.move('character', -repfin.length);
} else {
range.moveStart('character', repdeb.length + insText.length + repfin.length);
}
range.select();
}
/* pour navigateurs plus récents basés sur Gecko*/
else if(typeof input.selectionStart != 'undefined')
{
/* Insertion du code de formatage */
var start = input.selectionStart;
var end = input.selectionEnd;
var insText = input.value.substring(start, end);
input.value = input.value.substr(0, start) + repdeb + insText + repfin + input.value.substr(end);
/* Ajustement de la position du curseur */
var pos;
if (insText.length == 0)
{
pos = start + repdeb.length;
} else {
pos = start + repdeb.length + insText.length + repfin.length;
}
input.selectionStart = pos;
input.selectionEnd = pos;
} else {
/* requête de la position d'insertion */
var pos;
var re = new RegExp('^[0-9]{0,3}$');
while(!re.test(pos))
{
pos = prompt("Insertion à la position (0.." + input.value.length + "):", "0");
}
if(pos > input.value.length)
{
pos = input.value.length;
}
/* Insertion du code de formatage */
var insText = prompt("Veuillez entrer le texte à formater:");
input.value = input.value.substr(0, pos) + repdeb + insText + repfin + input.value.substr(pos);
}
}//
// sort a text pad in the main gui if double clicked on the content zone
// input content string, text container id
// output reformated string
function tinyTextPad(str,txt_id)
{// text editor
// set the utilities images on the content zone of the container
str= "<div id='txtPad' align='left'><img src='./Cms/Images/bold.gif' style='cursor:pointer;' onclick='trg=parentNode.nextSibling;txtInject(trg,\"<b>\",\"</b>\");' /><img src='./Cms/Images/italic.gif' onclick='trg=parentNode.nextSibling;txtInject(trg,\"<i>\",\"</i>\");' /><img src='./Cms/Images/underline.gif' style='cursor:pointer;' onclick='trg=parentNode.nextSibling;txtInject(trg,\"<u>\",\"</u>\");' /><img src='./Cms/Images/stroke.gif' style='cursor:pointer;' onclick='trg=parentNode.nextSibling;txtInject(trg,\"<s>\",\"</s>\");' /><img src='./Cms/Images/p.gif' style='cursor:pointer;' onclick='trg=parentNode.nextSibling;txtInject(trg,\"<p>\",\"</p>\");' /> | <img src='./Cms/Images/ul.gif' style='cursor:pointer;' onclick='trg=parentNode.nextSibling;txtInject(trg,\"<ul>\",\"</ul>\");' /><img src='./Cms/Images/li.gif' style='cursor:pointer;' onclick='trg=parentNode.nextSibling;txtInject(trg,\"<li>\",\"</li>\");' /></div><textarea id='"+txt_id+"' rows='15'>"+str+"</textarea>";
return str;
}//
]]>
</script>
<script id="topScript" number="">
<![CDATA[
function reformat( pContent )
{
return pContent.replace(/=/gi,'``i').replace(/\"/gi,'``o').replace(/\?/g,'``q').replace(/&/g,'``e').replace(/%/g,'``u').replace(/#/g,'``a');
}
function trim( content )
{
return content.replace(/^\s+|\s+$/g,"");
}
function collapse(pDom)
{
switch(pDom.innerHTML)
{
case "[_]":
pDom.innerHTML = "[+]";
break;
case "[+]":
pDom.innerHTML = "[_]";
break;
}
}
function showHide(pDom,pFlg)
{
if(pDom.style.display=='none')
pDom.style.display='' ;
else
pDom.style.display='none' ;
}
function doHtml( content )
{
content = content.replace(/</g, "<").replace(/>/g,">");
return content ;
}
function feedContainer( pDom )
/*
*/
{
var ctc = pDom.innerHTML ;
cpt = document.getElementById('iCounter') ;
var nbr = Number((cpt.value)) ;
if(trim(ctc).substring(0,4) != "<tex" && trim(ctc).substring(0,4) != "<TEX" )
{
cpt.value = nbr +1 ;
pDom.innerHTML = "<textarea id='t_"+nbr+"' style='width:99%' rows='10' cols='20'>" + trim(ctc) + "</textarea>" ;
}
else
{
cpt.value = nbr -1 ;
placeContent( pDom.firstChild.value, pDom.id ) ;
}
}
var _ = function() {} ;
// object access
_.prototype.oa = {
tab:new Array(),
getNodesById:function(pDom, pId)
/**
getNodes by id function
*/
{
if(pDom.getElementsByTagName)
{
all = pDom.getElementsByTagName("*") ;
for(g=0; g<all.length; g++)
{
if( all[g].getAttribute && all[g].getAttribute("id") == pId )
{
this.tab.push(all[g]) ;
}
}
return this.tab ;
}
},
getNodesWithId:function(pDom, pId)
/**
getNodes by id function
*/
{
if(pDom.getElementsByTagName)
{
var reg=new RegExp("("+pId+")","g") ;
all = pDom.getElementsByTagName("*") ;
for(g=0; g<all.length; g++)
{
if( all[g].getAttribute && reg.test(all[g].getAttribute("id")) )
{
this.tab.push(all[g]) ;
}
}
return this.tab ? this.tab : false ;
}
},
each:function() // pAction
/**
*/
{
for(var i=0;i<this.tab.length;i++)
{
//
}
}
}
_.prototype.Remote = {
getConnector: function()
{
var connectionObject = null ;
if (window.XMLHttpRequest)
{
connectionObject = new XMLHttpRequest() ;
}
else if (window.ActiveXObject)
{
connectionObject = new ActiveXObject('Microsoft.XMLHTTP') ;
}
return connectionObject ;
},
configureConnector: function(connector, callback)
{
connector.onreadystatechange = function()
{
if (connector.readyState == 4)
{
if (connector.status == 200)
{
callback.call(connector, {
text: connector.responseText,
xml: connector.responseXML
});
}
}
}
},
load: function(request)
{
var url = request.url || "" ;
var callback = request.callback || function() {} ;
var connector = this.getConnector() ;
if (connector)
{
this.configureConnector(connector, callback) ;
connector.open("GET", url, true) ;
connector.send("") ;
}
},
save: function(request)
{
var url = request.url || "" ;
var callback = request.callback || function() {} ;
var data = request.data || "" ;
var connector = this.getConnector() ;
if (connector)
{
this.configureConnector(connector, callback);
connector.
open("POST", url, true);
connector.
setRequestHeader("Content-type", "application/x-www-form-urlencoded") ;
connector.
setRequestHeader("Content-length", data.length) ;
connector.
setRequestHeader("Connection", "close") ;
connector.
send(data);
}
}
}
function placeContent ( pContent, pIdTarget )
/**
set the content to targetted container
*/
{
_.Remote.save(
{
url: "./main.php",
data: "qry=2&id=" + pIdTarget + "&content=" + reformat(pContent),
callback: function(response)
{
var nfo = segment( response.text ) ;
var node = document.getElementById( nfo[0] ) ;
node.innerHTML = nfo[2] ;
if($('contentKey').value==1)
{
setNodes(node);
}
_.oa.tab = new Array() ;
feedContent() ;
}
}
);
}
]]>
</script>
<script id="endScript" number="">
<![CDATA[
_ = new _() ;
function segment(pStr)
/**
*/
{
var reg=new RegExp("~~", "g") ;
return pStr.split(reg) ;
}
function isToFeed()
/**
check is still to feed
*/
{
return (set = _.oa.getNodesWithId(document.body,"lay") )?set:false ;
}
function feedLayout()
/**
feed the layout
*/
{
var ctc = _.oa.getNodesWithId(document.body,"lay") ;
if(ctc[0])
_.Remote.save({
url: "./main.php",
data: "qry=1&id="+ctc[0].id+"&owner=" + document.getElementById("customerName").value,
callback: function(response)
{
var nfo = segment( response.text ) ;
var node = document.getElementById( nfo[0] ) ;
node.innerHTML = nfo[2] ;
node.id = nfo[1] ;
_.oa.tab = new Array() ;
feedContent() ;
}
});
}
function feedData()
/**
feed the data
*/
{
var ctc = _.oa.getNodesWithId(document.body,"data") ;
if(ctc[0])
_.Remote.save({
url: "./main.php",
data: "qry=1&id="+ctc[0].id+"&request=1&owner=" + document.getElementById("customerName").value,
callback: function(response)
{
var nfo = segment( response.text ) ;
var node = document.getElementById( nfo[0] ) ;
node.innerHTML = nfo[2] ;
node.id = nfo[1] ;
_.oa.tab = new Array() ;
feedContent() ;
}
});
}
function feedContent()
/**
feed the content of the page by tag expr and Id
*/
{
feedLayout() ;
feedData() ;
if($('contentKey').value!=0)
{
if($('mainContent'))
{
$('mainContent').ondblclick = function (e) {feedContainer(this);};
}
}
}
]]>
</script>
<script id="pagerScript" number="">
<![CDATA[
function cropPage()
/*
get the crop page
*/
{
_.Remote.save(
{
url: "./main.php",
data: "qry=7",
callback: function(response)
{
var nfo = segment( response.text ) ;
document.getElementById("mainContent").innerHTML = nfo[2];
}
});
}
function itemPage()
/*
get the item page
*/
{
_.Remote.save(
{
url: "./main.php",
data: "qry=8",
callback: function(response)
{
var nfo = segment( response.text ) ;
document.getElementById("mainContent").innerHTML = nfo[2];
}
});
}
function pagePage()
/*
get the page page
*/
{
_.Remote.save(
{
url: "./main.php",
data: "qry=9",
callback: function(response)
{
var nfo = segment( response.text ) ;
document.getElementById("mainContent").innerHTML = nfo[2];
}
});
}
]]>
</script>
<script id="adminScript" number="">
<![CDATA[
// $('mainContent').onDblClick = "feedContainer(this);";
]]>
</script>
</content>
</header>
<!-- HEADER CONTENT -->
<main>
<assert>
<![CDATA[
:: HERE COMES SERVER/CLIENT SIDE SNIPPETS ::
]]>
</assert>
<!-- -->
<content>
<page id="standard">
<![CDATA[
<input type="hidden" id="iCounter" name="nCounter" value="0" />
<div id="lay:frameContent" class="toFeed" >> mainEntry container</div>
]]>
</page>
<page id="frameContent">
<![CDATA[
<div id="lay:topContent"></div>
<div id="lay:mainContent"></div>
<div id="lay:bottomContent"></div>
]]>
</page>
<page id="topContent">
<![CDATA[
<div> > My Tiny CMS | TOP CONTENT</div>
]]>
</page>
<page id="loginPane">
<![CDATA[
<div>
<input id="iUsr" type="text" value=" " style="width:96%;" /><br/>
<input id="iButton" type="button" value="log on" style="width:100%;" />
</div>
]]>
</page>
<page id="menuPane">
<![CDATA[
<div class="panel">
<span onclick="showHide($('cMenu'),1);collapse(this);">[+]</span>
<span id="cMenu"> !.crop | !.item | !.page
<p />
<span onclick="showHide($('loginPane'),2);">:: Login Pane ::</span>
<span><div id="lay:loginPane"></div></span>
</span>
</div>
]]>
</page>
<page id="mainContent">
<![CDATA[
<div id="mainContent">
<div id="data:1|1|rootEntry"></div>
</div>
]]>
</page>
<!--
The bottom tool may contain all the moderator and admin utilities
-->
<page id="bottomTool">
<![CDATA[
<!--
<div class="bottomTool">
<span onclick="showHide($('cBottom'),1);collapse(this);">[+]</span>
<span id="cBottom">:: Bottom Tool ::
<div id="cBottomPane"></div>
</span>
</div>
-->
]]>
</page>
<page id="bottomContent">
<![CDATA[
<div> myTinyCS @ | version 1.0 </div>
]]>
</page>
<page id="lay_01" />
<page id="lay_02" />
<page id="lay_03" />
<!--
!.crop | !.item | !.page
-->
</content>
</main>
<!-- PAGE CONTENT -->
<frameset>
<assert>
<code type="text/javascript" id="scr_01">
<![CDATA[
function lastScript()
{
return "this is last script";
}
]]>
</code>
</assert>
<content><!-- -->
<![CDATA[
<?xml version="1.0" encoding="UTF-8" ?>
{{inc:token[xhtmlTransitional]}}
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<style>{{inc:style[standard]}}</style>
<script>{{inc:script[topScript]}}</script>
<script>{{inc:script[pagerScript]}}</script>
{{inc:script[prototype]}}
</head>
<body onload="feedContent()">
{{inc:customer}}
{{inc:menuPane}}
{{inc:bottomPane}}
<!-- Page entry -->
<div id='mainContent'>
{{inc:page[standard]}}
</div>
</body>
<script>
{{inc:script[endScript]}}
</script>
</html>
]]>
</content>
</frameset>
<tokens>
<token id="xhtmlTransitional">
<![CDATA[
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
]]>
</token>
<token id="HTML4">
<![CDATA[
<!DOCTYPE HTML PUBLIC
"-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
]]>
</token>
<token id="xhtmlstrict">
<![CDATA[
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
]]>
</token>
<token id="xhtml_xmlns">
<![CDATA[
http://www.w3.org/1999/xhtml
]]>
</token>
</tokens>
</template>
</pre>
=== Le backOffice ===
*Config.inc.php
<pre>
<?php
/**
* configure my tiny CMS
*/
define("ROOT","") ;
define("CMS" ,ROOT."/Cms") ;
define("OWN" ,ROOT."/Owners") ;
define("CLA" ,CMS . "/Classes") ;
define("FUN" ,CMS . "/Functions") ;
//
define("OBJ" ,CMS . "/Objects") ;
define("BCK" ,OBJ . "/Backend") ;
define("FNT" ,OBJ . "/Frontend") ;
?>
</pre>
*main.php
:point d'entrée back&front end
:un switch distingue les appels entrants avec et sans paramètres
<pre>
<?php
require_once "./config.inc.php" ;
if( !@$_REQUEST['qry'] )
/*
* output type switch for ajax layer
*/
{
include_once FNT."/entry.inc.php" ;
}
else
{
include_once BCK."/responder.inc.php" ;
}
?>
</pre>
*responder.inc.php
:appels entrant avec interrogation
<pre>
<?php
# TESTING 1.0
echo $_REQUEST["id"] . "~@~" . $_REQUEST['id'] . " / " . $_REQUEST["name"] . " " . $_REQUEST["surname"] ;
?>
</pre>
*entry.inc.php
:appels entrant sans interrogation
<pre>
<?php
//
//require_once CLA . "/dataManager.class.php" ;
require_once CLA . "/utilities.class.php" ;
require_once CLA . "/templateControler.class.php" ;
//
$tmp = new templateControler(array("path"=>"./Owners/Root/Templates/default.xml"));
# STATE
$tmp->initFrame(
$tmp
->getNode("//template/frameset/content")
) ;
$tmp->getAnchors( $tmp->data['feed'][0]->nodeValue ) ;
$tmp->anchorContentReplacer() ;
$tmp->setFrame() ;
# OUTPUT
echo $tmp->data['frameset'] ;
?>
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Ajaxification=
L'ajaxification à implémenter côté serveur et client permet une plus grande souplesse d'interaction entre la vue et le backend. Le séquencement du contenu en flux offre une plus grande réactivité.
<b>Pour ce faire :</b>
Dans le template des owners, on implémente la couche AJAX suivante, "topScript" nourrit le début de document et "bottomScript" l'exécution de fin de document.
<pre>
<script id="topScript" number="">
<![CDATA[
var $ = function() {};
// object access
$.prototype.oa = {
tab:new Array(),
getNodesById:function(pDom, pId)
/**
getNodes by id function
*/
{
if(pDom.getElementsByTagName)
{
all = pDom.getElementsByTagName("*");
for(g=0; g<all.length; g++)
{
if( all[g].getAttribute
&& all[g].getAttribute("id")
== pId )
{
this.tab.push(all[g]);
}
}
return this.tab;
}
},
getNodesWithId:function(pDom, pId)
/**
getNodes by id function
*/
{
if(pDom.getElementsByTagName)
{
var reg=new RegExp("("+pId+")","g");
all = pDom.getElementsByTagName("*");
for(g=0; g<all.length; g++)
{
if( all[g].getAttribute &&
reg.test(all[g].getAttribute("id")) )
{
this.tab.push(all[g]);
}
}
return this.tab;
}
},
each:function() // pAction
/**
TODO
*/
{
for(var i=0; i<this.tab.length; i++)
{
//
}
}
}
$.prototype.Remote = {
getConnector: function()
{
var connectionObject = null;
if (window.XMLHttpRequest)
{
connectionObject = new XMLHttpRequest();
} else if (window.ActiveXObject) {
connectionObject = new ActiveXObject('Microsoft.XMLHTTP');
}
return connectionObject;
},
configureConnector: function(connector, callback)
{
connector.onreadystatechange = function()
{
if (connector.readyState == 4)
{
if (connector.status == 200)
{
callback.call(connector, {
text: connector.responseText,
xml: connector.responseXML
});
}
}
}
},
load: function(request)
{
var url = request.url || "";
var callback = request.callback || function() {};
var connector = this.getConnector();
if (connector) {
this.configureConnector(connector, callback);
connector.open("GET", url, true);
connector.send("");
}
},
save: function(request) {
var url = request.url || "";
var callback = request.callback || function() {};
var data = request.data || "";
var connector = this.getConnector();
if (connector)
{
this.configureConnector(connector, callback);
connector.open("POST", url, true);
connector.
setRequestHeader("Content-type",
"application/x-www-form-urlencoded");
connector.
setRequestHeader("Content-length", data.length);
connector.
setRequestHeader("Connection", "close");
connector.
send(data);
}
}
}
]]>
</script>
<script id="endScript" number="">
<![CDATA[
$ = new $();
function segment(pStr)
/**
segment the callback
*/
{
var reg=new RegExp("~@~+", "g");
return pStr.split(reg);
}
function feedContent()
/**
feed the content of the page by tag expr and Id
*/
{
var ctc = $.oa.getNodesWithId(document.body,"id");
for(var i=0; i < ctc.length ; i++ )
{
//
$.Remote.save({
url: "./main.php",
data: "qry=1&id="+ctc[i].id+"&name=Ben&surname=Abdell",
callback: function(response)
{
var nfo = segment( response.text );
document.getElementById( nfo[0] ).innerHTML = nfo[1];
}
});
}
}
]]>
</script>
</pre>
On teste sur un segment du body. Par convention, le container à nourrir porte l'id composé "id:idDuContainer".
<pre>
<page id="standard">
<![CDATA[
<table width="100%" border="0" id="table1">
<tr><td>
<div id="id:topContent"/>
</td></tr>
<tr><td>
<div id="id:mainContent"/>
</td></tr>
<tr><td>
<div id="id:bottomContent"/>
</td></tr>
</table>
]]>
</page>
</pre>
Un test sur la page "standard" donne en output pour l'appel ajax :
<pre>
$.Remote.save({
url: "./main.php",
data: "qry=1&id="+ctc[i].id+"&name=Ben&surname=Aldell",
callback: function(response)
{
var nfo = segment( response.text );
document.getElementById( nfo[0] ).innerHTML = nfo[1];
}
});
</pre>
=> le résultat suivant
<pre>
<table id="table1" width="100%" border="0">
<tbody>
<tr>
<td>
<div id="id:topContent">id:topContent / Ben Aldell</div>
</td>
</tr>
<tr>
<td>
<div id="id:mainContent">id:mainContent / Ben Aldell</div>
</td>
</tr>
<tr>
<td>
<div id="id:bottomContent">id:bottomContent / Ben Aldell</div>
</td>
</tr>
</tbody>
</table>
</pre>
On est nominal pour l'ajaxification de la complétion du contenu côté client.
==== Implémentation des contrôleurs côté serveur ====
*<b>Common.inc.php :</b>
<pre>
<?php
/*
common application functiunalities
would be fine to merge all request to requestForData
*/
require_once CLA . "/utilities.class.php";
require_once CLA . "/dataManager.class.php";
require_once CLA . "/templateControler.class.php";
function requestForLayout($pTerm,$pOwner="Root",$pTemplate="default")
{
$tmp = new templateControler(array("path"=>"./Owners/" . $pOwner . "/Templates/" . $pTemplate . ".xml"));
$node = $tmp->getNode('//page[@id="' . $pTerm . '"]');
return $node[0]->nodeValue;
}
function requestForData($pTerm)
{
$_lst = explode("|",$pTerm) ;
$_manager = new dataManager();
$_manager->data['path'] = $_manager->data['root'] . "Root/Contents/" . $_lst[0] . ".xml";
$_document = $_manager->initialize();
if ($_lst[1])
$_manager->data['query_1'] = array("attribute"=>"number", "value"=>$_lst[1]);
if($_lst[2])
$_manager->data['query_2'] = array("attribute"=>"id", "value"=>$_lst[2]);
$res = dataManager::getContentByData(
dataManager::getContentByData($_document, $_manager->data['query_1']),
$_manager->data['query_2']
);
$node = isset($_lst[3]) ? $_lst[3] : "content";
$str = $res->getElementsByTagName($node);
return $str->item(0)->nodeValue;
}
</pre>
*<b>Responder.inc.php :</b>
<pre>
<?php
//
$_ = explode(":",$_REQUEST['id']);
$_content = "";
switch( true )
/**
* content type switcher
*/
{
case ($_[0] == "lay") :
// template request
$_content = requestForLayout($_[1]);
break;
case ($_[0] == "data") :
// data request
$_content = requestForData($_[1]);
break;
}
# OUTPUT
echo $_REQUEST['id'] . "~@~" . $_[1] . "~@~" . $_content;
</pre>
==== Auto complétion du layout ====
On implémente feedLayout pour auto compléter et chercher les fragments sur le template pour l'injecter sur les encres
<pre>
<script id="endScript" number="">
<![CDATA[
$ = new $();
function segment(pStr)
/**
*/
{
var reg=new RegExp("~@~+", "g");
return pStr.split(reg);
}
function isToFeed()
/**
check is still to feed
*/
{
return (set = $.oa.getNodesWithId(document.body,"lay") )?set:false;
}
function feedLayout()
/**
feed the layout
*/
{
var ctc = $.oa.getNodesWithId(document.body,"lay");
if(ctc[0])
$.Remote.save({
url: "./main.php",
data: "qry=1&id="+ctc[0].id,
callback: function(response)
{
var nfo = segment( response.text );
var node = document.getElementById( nfo[0] );
node.innerHTML = nfo[2];
node.id = nfo[1];
$.oa.tab = new Array();
feedContent();
}
});
}
function feedContent()
/**
feed the content of the page by tag expr and Id
*/
{
feedLayout();
//feedData();
}
]]>
</script>
</pre>
==== Auto complétion des données ====
<pre>
<script id="endScript" ...>
// ...
function feedData()
/**
feed the data
*/
{
var ctc = $.oa.getNodesWithId(document.body,"data");
if(ctc[0])
$.Remote.save({
url: "./main.php",
data: "qry=1&id="+ctc[0].id+"&request=1",
callback: function(response)
{
var nfo = segment(response.text);
var node = document.getElementById(nfo[0]);
node.innerHTML = nfo[2];
node.id = nfo[1];
$.oa.tab = new Array();
feedContent();
}
});
}
function feedContent()
/**
feed the content of the page by tag expr and Id
*/
{
feedLayout();
feedData();
}
</script>
</pre>
==== Test de réactivité de la couche ajax ====
La fonction feedContent se charge de charger les flux sur la page en provenance du template et des champs de données.
<b>Le template étant :</b>
<pre>
<page id="standard">
<![CDATA[
<div id="lay:frameContent" class="toFeed">
</div>
]]>
</page>
<page id="_frameContent">
<![CDATA[
<table width="100%" border="0" id="table1">
<tr><td>
<div id="lay:topContent" />
</td></tr>
<tr><td>
<div id="lay:mainContent" />
</td></tr>
<tr><td>
<div id="lay:bottomContent" />
</td></tr>
</table>
]]>
</page>
<page id="topContent">
<![CDATA[
<table width="100%" border="1" cellpadding="0" cellspacing="0">
<tr>
<td width="40%">
<div id="lay:loginPane" >:: loginPane ::</div>
</td>
<td width="60%">
<div>My Tiny CMS</div>
<div id="lay:menuPane" >:: menuPane ::</div>
</td>
</tr>
</table>
]]>
</page>
<page id="loginPane">
<![CDATA[
<div>
<div>
<input id="iUsr" type="text" value=" " /><br/>
<input id="iPwd" type="text" value=" " />
</div>
</div>
]]>
</page>
<page id="menuPane">
<![CDATA[
<div> :: menuPane - new :: </div>
]]>
</page>
<page id="mainContent">
<![CDATA[
<div id="data:1|1|title">content</div>
<div id="data:1|2|table">content</div>
<div id="data:1|3|bottom">content</div>
]]>
</page>
<page id="bottomContent">
<![CDATA[
<table width="100%" border="1" cellpadding="0" cellspacing="0">
<tr height="100"><td>
<div align="center"> THIS IS THE BOTTOM CONTENT</div>
</td></tr>
</table>
]]>
</page>
</pre>
<b>et le crop 1.xml :</b>
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<crop id="test" number="1">
<topic>Cultiver son potager</topic>
<scope>légumes potager soupe</scope>
<fields>
<field number="1" id="title">
<subject>les légumes du potager</subject>
<content>
<![CDATA[
les légumes du potager
]]>
</content>
</field>
<field number="2" id="table">
<subject>table content</subject>
<content>
<![CDATA[
<table>
<tr>
<td>
le potagé contient :
</td>
<td>
<ul>
<li>une terre molle.</li>
<li>Parsemé ça et là, on Test carottes, </li>
<li>petits poids et potirons</li>
</ul>
</td>
</tr>
</table>
]]>
</content>
</field>
<field number="3" id="bottom">
<subject>this is the new subject</subject>
<content>
<![CDATA[
le potagé contient une terre molle2.
Parsemé ça et là, on trouve carottes, petits poids et potirons
]]>
</content>
</field>
</fields>
</crop>
</pre>
<b>On a pour rendu en sortie :</b>
[[image:MyTinyCms_001.GIF]]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Premier noyau=
Nous préparons la première version de notre solution myTinyCMS.
Comme nous sommes nominal pour les quatre phases, myTinyCMS permets :
:[1] De gérer les données
:[2] De gérer les utilisateurs
:[3] De gérer un frameset complexe
:[4] D'effectuer des mouvements par son webservice
La première intégration ou version 1 de notre noyau consistera en des contrôleurs de test préfigurant la première beta de myTinyCMS.
L'objectif de ce contrôleur est d'intégrer le frameset sur trois thèmes :
:[1] authentification
:[2] lecture / écriture contenu
:[3] navigation
:[4] gestion de contenu
Le contenu sera géré directement sur le frameset par défaut. Backoffice et Frontoffice étant distingué par le switch Mod||Adm vs Usr, déverrouillant les contrôles de gestion et d'encodage.
Fonctionnellement le noyau présente deux phase d'affichage :
:La première serveur vers client (output total du frameset)
:La seconde cliente appelant l'injection dans les containers
== Le Second noyau ==
MyTinyCMS est maintenant un mini content manager system orienté wiki. L'orientation du CMS génère le code métier suivant
===Classes===
Classes/contentPane.class.php regroupe les flux d'entrée et sortie comme les menus. Il est spécialisé pour notre solution. Le code peut être amélioré.
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Classes/contentPane.class.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
class contentPane
/*
* this class contains the mod menu treats
*/
{
public $data ;
public function setFeed()
/*
* only letters and numbers can be placed in data feed name data["abc.."]
*/
{
$this->data["menuPane"] = '
<div class="panel">
<span onclick="showHide($(\'cMenu\'),1);collapse(this);" class="click">[+]</span>
<span id="cMenu" class="click"> <a href="'.$_SERVER['PHP_SELF'].'?entry='.(@$_REQUEST["entry"]?$_REQUEST["entry"]:'Dummy').'&crop=1">!.crop</a> | <a href="'.$_SERVER['PHP_SELF'].'?entry='.(@$_REQUEST["entry"]?$_REQUEST["entry"]:'Dummy').'&page=1">!.page</a>
<p />
<form action="" method="post" name="form1">
{{loginPane}}
</form>
{{cropPane}}
{{userPane}}
{{pagePane}}
</span>
</div>
' ;
$this->data["loginPane"] = '
<span onclick="showHide($(\'loginPane\'),2);">:: Login Pane ::</span>
<span>
<div>
<input id="iLogin" name="iLogin" type="text" value=" " style="width:96%;" /><br/>
<input id="iButton" type="submit" value="log on" style="width:100%;" />
</div>
</span>
' ;
}
public function setTagFeed($pTag,$pFeed)
{
$this->data[$pTag] = $pFeed ;
}
public function contentPane()
{
$this->init();
}
public function placePane()
{
preg_match_all('/{{[a-z0-9A-Z]*}}/',$this->data["menuPane"],$matches) ;
foreach($matches[0] as $v)
{
$v = str_replace(array('{','[','}',']'),array('',' ','',''),$v);
if(!$this->special($v))
{
$this->data["menuPane"] = preg_replace('/{{'.$v.'}}/',@$this->data[$v],$this->data["menuPane"]) ;
}
}
}
public function special($pTag)
{
return false ;
}
public function tagReserved()
{
switch (true)
{
case (isset( $_SESSION["autho"]['credit'])) :
$this->data["loginPane"] = "<div>
<br/>
<input type='submit' value='unlog' name='iUnlog' style='width:96%;' />
</div>" ;
# CROP PANE
// set the crop list
$cropList = getFileList($_SESSION["path"] . "/Contents") ;
$this->data["cropPane"] = "<p/><div class='dark' onclick='showHide($(\"cCropPane\"),1)' style='cursor:pointer;'>:: Crop pane ::</div><p/><div id='cCropPane'>" ;
foreach($cropList as $v)
{
$this->data["cropPane"] .= '<a href="
' . ($_SERVER["PHP_SELF"]) . '?entry=' . @$_REQUEST["entry"] . '&crop=' . $v . '">' . $v . '</a><br />' ;
}
$this->data["cropPane"] .= "</div>" ;
# USER PANE
$this->data["userPane"] = "<p/><a href='".$_SERVER['PHP_SELF']."?entry=".$_REQUEST['entry']."&user=1'>[ users ]</a> ";
# PAGE PANE
$this->data["pagePane"] = "<a href='".$_SERVER['PHP_SELF']."?entry=".$_REQUEST['entry']."&page=1'>[ pages ]</a>";
break ;
}
}
public function getNewCrop()
{
return "
<script>
function saveNewCrop()
{
var tab = $('cCropId').value
+ '~~'
+$('cTopic').value
+'~~'
+$('cScope').value;
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=22&value=' + tab + '&owner=" . $_REQUEST["entry"] . "',
onSuccess: function(transport)
{
$('cComCrop').innerHTML = transport.responseText ;
},
onFailure: function(){ }
});
}
</script>
<div class='bottomTool' style=''>
<form action='' method='post' name='frm2'>
<div onclick='showHide($(\"cCropPanel\"));collapse(this);' class='click'>[_]</div>
<div id='cCropPanel'>
<table width='600'>
<tr>
<td width='50'>Id</td>
<td width='500'>
<input type='text' value='' class='encoder' id='cCropId'/>
</td>
</tr>
<tr>
<td>Topic</td>
<td><input class='encoder' id='cTopic' type='text' value='' /></td>
</tr>
<tr>
<td>Scope</td>
<td><input class='encoder' id='cScope' type='text' value=''/></td>
</tr>
<tr>
<td colspan='2'>
<p><span class='specButton' onclick='saveNewCrop();/* location.href=\"".($_SERVER['PHP_SELF'].'?'."entry=".$_REQUEST['entry'])."\";*/'><b>[ save ]</b></span> <span id='cComCrop' class='green'></span></p>
<td>
</tr>
</table>
</div>
</div>
</form>
";
}
public function getCropPane($pCrop)
{
$this->getFieldList("field",$pCrop) ;
// set crop dropdownlist
$lst = "<select>" ;$i=1;
foreach($this->data["list"] as $k=>$v)
{
$lst .= "<option value='".$i++."'></option>" ;
}
$lst .= "</select>" ;
return "
<script>
function setNodes(pNode)
{
var dataNodes = _.oa.getNodesWithId(pNode,'|');
for(i=0;i<dataNodes.length;i++)
{
var dataContent = reformat(dataNodes[i].innerHTML);
new Ajax.Request('./main.php',
{
method:'post',
parameters:'qry=30&id='+dataNodes[i].id+'&owner='+$('customerName').value+'&data='+dataContent+'&key='+$('userKey').value,
onSuccess: function(trs)
{
$('cComCrop').innerHTML = trs.responseText;
},
onFailure:function() {}
});
}
}
function getCropContent(itemValue,cropName)
{
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=15&item=' + itemValue + '&crop=' + cropName,
onSuccess: function(transport)
{
var nfo = segment(transport.responseText) ;
$('cTextSubject').value = nfo[0] ;
$('cTextContent').value = nfo[1] ;
},
onFailure: function(){ }
});
}
function saveNewItemCrop()
{
var tab = $('cCropSelect').value+'~~'+$('cCropId').innerHTML
+ '~~'
+$('cTopic').value
+'~~'
+$('cScope').value
+'~~'
+$('cTextSubject').value
+'~~'
+$('cTextContent').value ;
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=21&owner='+$('customerName').value+'&value=' + tab,
onSuccess: function(transport)
{
$('cComCrop').innerHTML = transport.responseText ;
},
onFailure: function(){ }
});
}
function saveCropItem()
{
var tab = $('cCropSelect').value+'~~'+$('cCropId').innerHTML
+ '~~'
+$('cTopic').value
+'~~'
+$('cScope').value
+'~~'
+$('cTextSubject').value
+'~~'
+reformat($('cTextContent').value);
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=20&owner='+$('customerName').value+'&value=' + tab,
onSuccess: function(transport)
{
$('cComCrop').innerHTML = transport.responseText ;
},
onFailure: function(){ }
});
}
</script>
<div class='bottomTool' style=''>
<form action='' method='post' name='frm2'>
<div onclick='showHide($(\"cCropPanel\"));collapse(this);' class='click'>[_]</div>
<div id='cCropPanel'>
<table width='600'>
<tr>
<td width='50'>Id</td>
<td width='500' id='cCropId'>".$_REQUEST["crop"]."</td>
</tr>
<tr>
<td>Topic</td>
<td><input class='encoder' id='cTopic' type='text' value='".@$this->data["list"]["topic"]."' /></td>
</tr>
<tr>
<td>Scope</td>
<td><input class='encoder' id='cScope' type='text' value='".@$this->data["list"]["scope"]."'/></td>
</tr>
<tr>
<td>Item</td>
<td>
<select class='encoder' id='cCropSelect' onchange='getCropContent(this.value,\"".$_REQUEST['crop']."\");'>
".getCropOptions($_REQUEST['crop'])."
</select>
</td>
</tr>
<tr>
<td colspan='2'>
<p>:: Crop Item :: <span class='specButton' onclick='saveCropItem()'>[ save</span> | <span class='specButton' onclick='saveNewItemCrop();'>new ]</span>
<span id='cComCrop' class='green'></span>
</p>
<input type='text' value='' id='cTextSubject' class='encoder'/>
<textarea id='cTextContent' rows='10' class='encoder'></textarea>
</td>
</tr>
</table>
</div>
</div>
</form>
";
}
public function getFieldList($pNodeName,$pCrop)
{
$this->data["list"] = getNodeList($pNodeName,$pCrop) ;
}
public function getPagePane( )
{
return "<div class='bottomTool'><span onclick='showHide($(\"cPagePanel\"));collapse(this);' class='click'>[_]</span>
<div id='cPagePanel'><p>:: Page Pane :: </p>
"
. getPagesPanel($_REQUEST["entry"]) .
"
</div>
</div>";
}
public function getUserPane( )
{
$dom = new DOMDocument() ;
$dom->load("./Owners/" . (@$_REQUEST["entry"]?$_REQUEST["entry"] : 'Dummy'). '/Contents/user.xml') ;
$users = $dom->getElementsByTagName("users") ;
$val = "
<script>
function userEdit(pVal)
/*
edit the user line
*/
{
var tab =[] ;
if($('login_'+pVal).innerHTML.substring(0,1) == '<')
{
$('login_'+pVal).innerHTML = tab[0] = $('iLog_'+pVal).value ;
$('pass_'+pVal).innerHTML = tab[1] = $('iPas_'+pVal).value ;
$('forname_'+pVal).innerHTML = tab[2] = $('iFor_'+pVal).value ;
$('lastname_'+pVal).innerHTML = tab[3] = $('iLas_'+pVal).value ;
$('customer_'+pVal).innerHTML = tab[4] = $('iCus_'+pVal).value ;
$('b'+pVal).innerHTML = '[edit]';
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=25&item=' + pVal + '&value=' + tab + '&owner=' + $('customerName').value + '&key='+$('userKey').value,
onSuccess: function(transport)
{
$('cComUser').innerHTML = transport.responseText ;
location.href='".($_SERVER['PHP_SELF'].'?'."entry=".$_REQUEST['entry'])."&user=1';
},
onFailure: function(){ }
});
}
else
{
$('login_'+pVal).innerHTML = \"<input id='iLog_\"+pVal+\"' type='text' value='\"+ ($('login_'+pVal).innerHTML) +\"' />\" ;
$('pass_'+pVal).innerHTML = \"<input id='iPas_\"+pVal+\"' type='text' value='\"+ ($('pass_'+pVal).innerHTML) +\"' />\" ;
$('forname_'+pVal).innerHTML = \"<input id='iFor_\"+pVal+\"' type='text' value='\"+ ($('forname_'+pVal).innerHTML) +\"' />\" ;
$('lastname_'+pVal).innerHTML = \"<input id='iLas_\"+pVal+\"' type='text' value='\"+ ($('lastname_'+pVal).innerHTML) +\"' />\" ;
$('customer_'+pVal).innerHTML = \"<input id='iCus_\"+pVal+\"' type='text' value='\"+ ($('customer_'+pVal).innerHTML )+\"' />\" ;
$('b'+pVal).innerHTML = '[save]';
}
}
function saveNew()
{
var tab = [] ;
tab[0] = $('iLogNew').value;
tab[1] = $('iPasNew').value;
tab[2] = $('iForNew').value;
tab[3] = $('iLasNew').value;
tab[4] = $('iCusNew').value;
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=26&value=' + tab+ '&owner=' + $('customerName').value +'&key=' + $('userKey').value,
onSuccess: function(transport)
{
$('cComUser').innerHTML = transport.responseText ;
location.href='".($_SERVER['PHP_SELF'].'?'."entry=".$_REQUEST['entry'])."&user=1';
$('new').innerHTML = '';
},
onFailure: function(){ }
});
;
}
function addUser()
{
$('new').innerHTML = '<p/><table width=\"600\"><tr class=\"title\"><td onclick=\"saveNew();\" class=\"specButton\"><b>[save]</b></td><td><input type=\"text\" id=\"iLogNew\"/></td><td><input type=\"text\" id=\"iPasNew\"/></td><td><input type=\"text\" id=\"iForNew\"/></td><td><input type=\"text\" id=\"iLasNew\"/></td><td><input type=\"text\" id=\"iCusNew\"/></td></tr></table><p/>' ;
}
</script>
<table width='600'>
<tr class='title'><td/>
<td>login</td>
<td>pass</td>
<td>lastname</td>
<td>forename</td>
<td>customer</td>
</tr>" ;
$i = 0;
foreach($users->item(0)->childNodes as $v)
{
if($v->nodeType != XML_TEXT_NODE)
{
if(isset($_SESSION['autho']['admin']))
{
$val .= "<tr>
<td onclick='userEdit(".$i.");'><b id='b".$i."' class='specButton'>[edit]</b></td><td id='login_".$i."'>" . trim($v->getAttribute("login")) . "</td><td id='pass_".$i."'>" . trim($v->getAttribute("pass")) . "</td><td id='lastname_".$i."'>" . trim($v->getAttribute("lastname")) . "</td><td id='forname_".$i."'>" . trim($v->getAttribute("forname")) . "</td><td id='customer_".$i."'>" . trim($v->getAttribute("customer")) ."</td>
</tr>" ;
}
}
$i++;
}
if(!isset($_SESSION['autho']['admin']))
$val .= "<tr><td colspan='5'>You're not authorized to modify the privileges</td></tr>";
$val .= "</table>";
return "<div class='bottomTool'>" . $val . "<hr /><div id='new'></div><span class='specButton' onclick='addUser();'>[add user]</span> <span id='cComUser' class='green'></span></div>";
}
public function init()
{
$this->setFeed() ;
$this->tagReserved() ;
$this->placePane() ;
}
}
# Ouput
$_container["content"] = new contentPane() ;
?>
</pre>
</div>
</div>
Voici Classes/dataManager.class.php dans sa version finale. Il est orienté données.
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Classes/dataManager.class.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
class dataManager
{//
private $pattern;
protected $stack;
//
public
$domDoc,
$crop
= array(
"std"=> '<?xml version="1.0" encoding="UTF-8"?>
<crop><topic /><fields /></crop>'
,""=>''
),
$data
= array(
"root"=>"./Owners/"
);
# TODO GETTER/SETTER
public function setDocument($pPath)
{
$this->domDoc->load( $pPath );
}
public static function getNodeByData($pData)
/**
* get the content from data
*/
{
$res = xpathExtension::getNodes($this->domDoc, $pXPath);
return $res['content'];
}
public function dispatch($pData)
/**
* switch direction by value
*/
{
}
public static function getContentByData($pDocument, $pData)
/**
* get the content from data
*/
{
if(@$pData['attribute'])
{
return utilities::searchNodesByAttribute($pDocument, $pData) ;
}
else
{
return utilities::searchNodesByContent($pDocument, $pData) ;
}
}
public function setContentByData($pDocument,$pData)
/**
* set the content to data
*/
{
if(@$pData['attribute'])
{
utilities::writeContentByAttribute($pDocument, $pData) ;
}
else
{
utilities::writeContentByContent($pDocument, $pData) ;
}
$this->saveDocument();
}
public static function setCropForCustomer($pPath, $pCrop)
/**
* set crop to customer directory
*/
{
file_put_contents($pPath, trim($pCrop));
}
# STATE
public function dataManager()
{
$this->initialize();
}
public function initialize()
/**
* init
*/
{
$this->domDoc = new DOMDocument();
if(@$this->data['path'])
{
$this->domDoc->load( $this->data['path'] );
}
return $this->domDoc;
}
public function saveDocument()
/**
* save document
*/
{
$this->domDoc->save($this->data['path']) ;
}
}
?>
</pre>
</div>
</div>
Voici le Classes/templateControler.class.php orienté layout.
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Classes/templateControler.class.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
class templateControler
{
/**
* Control the templates
*
* @var (mixed) ($domDoc, $data, $stack)
*/
//
private $pattern
= array(
'inc'=>'/{{inc:[a-zA-Z]*[\[\]\*a-zA-Z]*}}/' // inclusion patterns
,''=>''
);
protected $domDoc, $stack;
//
public $data;
/*
public function __construct()
{//
$this->domDoc = new DOMDocument();
$this->initialize();
}
*/
/**
* Enter description here...
*
* @param unknown_type $pVar
* @return templateControler
*/
public function templateControler($pVar)
{
$this->domDoc = new DOMDocument();
$this->setter($pVar);
$this->initialize();
}
/**
* Enter description here...
*
* @param unknown_type $pVar
*/
public function setter($pVar)
{//
$this->data = $pVar;
}
// TODO GETTER
public function initialize()
{//
$this->domDoc->load( $this->data['path'] );
}
public function initFrame($pDomValue)
/**
*
*/
{//
$this->data['frameset'] = trim($pDomValue[0]->nodeValue) ;
}
/**
* Enter description here...
*
*/
public function getNode($pXPath)
{//
$this->data['feed'] = utilities::getNodes($this->domDoc, $pXPath);
return $this->data['feed'];
}
/**
* getContent retourne le contenu du/des nœuds demandés
*
* > (nodeName, nodeId) - (string) >
*/
public function getContent($nodeName,$nodeId)
{//
// set expression
$_expr = "//" . $nodeName . ($nodeId&&$nodeId!='*'?"[@id='$nodeId']":"");
$set = $this->getNode($_expr);
$content=null;
if($set)
foreach($set as $k=>$v)
{
$content .= "\r\n" . ($v->nodeValue)."\r\n" ;
}
return $content?$content:"";
}
/**
* replace anchor by content
*/
public function anchorContentReplacer()
{//
$i=0;
foreach($this->data['result'][0] as $k=>$v)
{
// formatting anchor's syllabes
$v = str_replace(array('{','[','}',']'),array('',' ','',''),$v);
$tmp = explode(":",$v);$_ = explode(' ',$tmp[1]);
// replacing
$this->data['result'][0][$i++] = array("node"=>$_[0],"value"=>$this->getContent($_[0], @$_[1]),"id"=>@$_[1]);
}
}
public function setTag($pAnchor,$pValue)
/**
* put the frameset out
*/
{
$this->data['frameset'] = preg_replace('/{{inc:' . $pAnchor . '}}/',$pValue, $this->data['frameset'] ) ;
}
public function setFrame()
/**
* put the frameset out
*/
{
foreach($this->data['result'][0] as $v)
{
if( $v['node'] != 'page' || isset($_REQUEST['admin']))
$this->data['frameset'] = preg_replace('/{{inc:' . $v['node'] . '[\[\]\*'.$v['id'].']*}}/', $v['value'], $this->data['frameset'] ) ;
else
{
$this->data['frameset'] = preg_replace('/{{inc:' . $v['node'] . '[\[\]\*'.$v['id'].']*}}/', getPage(@$_REQUEST['page']?$_REQUEST['page']:$v['id']), $this->data['frameset'] ) ;
}
}
}
public function getAnchors($pFeed)
{//
preg_match_all($this->pattern['inc'], $pFeed, $this->data['result']);
return $this->data['result']; // on prefere les données centralisées dans la classe
}
public function publicTemplate()
{//
return $this->domDoc->saveXML();
}
}
?>
</pre>
</div>
</div>
Classes/utilities.class.php dans sa version finale
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Classes/utilities.class.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
class utilities
{
public static function getNodes($pDomDoc, $pXpathString)
/**
* get the node from DomDoc with XpathString
*/
{
$xp = new DOMXPath($pDomDoc);
$xp->registerNamespace('x', 'http://www.w3.org/1999/xhtml');
$xp->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
$xp->registerNamespace('i18n', 'http://apache.org/cocoon/i18n/2.1');
$ret = array();
$nodes = $xp->query($pXpathString);
foreach ($nodes as $node)
{
array_push($ret, $node);
}
return count($ret)?$ret:false;
}
public static function isContent($pPattern, $pFeed)
/**
* check is pattern in feed
*/
{
return preg_match('/('.$pPattern.')/',$pFeed)?true:false;
}
public static function searchNodesByContent($pDocument, $pQueries)
/**
* return node matching request by content
*/
{
$_fields = $pDocument->getElementsByTagName('fields'); $i = 0;
// ceci est un exemple de mauvais code de service, il ne fonctionne que pour un nombre limité de structure de crop
// c'est donc un code métier à surcharger.
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
foreach ( $u->childNodes as $v )
{
if( $v->nodeName == $pQueries['node'] && ! utilities::isContent ( $pQueries['value'] , $v->nodeValue ) )
{
@$tab[++$i] = $u ;
}
}
}
}
// removing
if(isset($tab))
foreach($tab as $v)
$_fields->item(0)->removeChild($v) ;
/* DEPRECIEE - Malheureusement, ce code ne marche une fois appelée par getNodes
for($i=0; $i<count($pNodeSet); $i++)
{
$_content = $pNodeSet[$i]->childNodes ;
// cette fonction ne descend qu'à un niveau
foreach($_content as $v)
{
if( $v->nodeName == $pQueries['node'] && ! utilities::isContent ( $pQueries['value'] , $v->nodeValue ) )
{
$pNodeSet[$i]->parentNode->removeChild($pNodeSet[$i]) ;
}
}
}
*/
return $pDocument ;
}
public static function searchNodesByAttribute($pDocument, $pQueries)
/**
* return node matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');$i=0;
// ceci est un exemple de mauvais code de service, il ne fonctionne que pour un nombre limité de structure de crop
// c'est donc un code métier à surcharger.
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
if( !($u->getAttribute($pQueries['attribute']) == $pQueries['value']) ) // 1:1 match
@$tab[++$i] = $u ;
}
}
// removing
if(isset($tab))
foreach($tab as $v)
$_fields->item(0)->removeChild($v) ;
return $pDocument ;
}
public static function getAttributesContents($pNode)
/**
*
*/
{
foreach ($pNode->attributes as $attrName => $attrNode)
{
$_tab[$attrName] = $attrNode->value ;
}
return $_tab ;
}
public static function getLineByAttribute($pDocument, $pQueries)
/**
* return node matching request by attribute
*/
{
$_users = $pDocument->getElementsByTagName('user');
// ceci est un exemple de mauvais code de service, il ne fonctionne que pour un nombre limité de structure de crop
// c'est donc un code métier à surcharger.
foreach ($_users as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
$_flg = false ;$i=$j=0;
foreach($pQueries as $_qa )
{
if( ($u->getAttribute($_qa['attribute']) == trim($_qa['value'])) )
{ // 1:1 match
++$j;
} else {
--$j;
}
$i++;
}
if($i==$j)
return utilities::getAttributesContents($u) ;
}
}
return false ;
}
public static function writeContentByContent($pDocument, $pQueries)
/**
* write node content matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
$_flg = false ;
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
foreach ( $u->childNodes as $v )
{
if( $v->nodeName == $pQueries['node'] && utilities::isContent ( $pQueries['value'] , $v->nodeValue ) )
{
$v->nodeValue = $pQueries['content'] ;
$_flg = true ;
}
}
}
}
return $_flg ;
}
public static function writeContentByAttribute($pDocument, $pQueries)
/**
* write node content matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
$_flg = false ;
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
if( $u->getAttribute($pQueries['attribute']) == $pQueries['value'] ) // 1:1 match
{
$v = $u->getElementsByTagName($pQueries['node']) ;
$ct = $v->item(0)->ownerDocument->createCDATASection("\n" . $pQueries['replacement'] . "\n");
$v->item(0)->appendChild($ct) ;
$_flg = true ;
}
}
}
if (!$_flg)
// attribute is note set yet
{
$node = $_fields->item(0)->firstChild->nextSibling->cloneNode(true) ;
$_fields->item(0)->appendChild($node) ;
// filling id
$_fields->item(0)->lastChild->setAttribute("id", $pQueries['value']) ;
// filling number
$_fields->item(0)->setAttribute("count", $cpt = ((int)$_fields->item(0)->getAttribute("count"))+1) ;
$_fields->item(0)->lastChild->setAttribute("number", $cpt) ;
utilities::writeContentByAttribute($pDocument, $pQueries) ;
}
return $_flg ;
}
public static function deleteContentByContent($pDocument, $pQueries)
/**
* delete node matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
$_flg = false ;
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
{
foreach ( $u->childNodes as $v )
{
if( $v->nodeName == $pQueries['node'] && utilities::isContent ( $pQueries['value'] , $v->nodeValue ) )
{
$_fields->item(0)->removeChild($u) ;
$_flg = true ;
}
}
}
}
return $_flg ;
}
public static function deleteContentByAttribute($pDocument, $pQueries)
/**
* delete node matching request by attribute
*/
{
$_fields = $pDocument->getElementsByTagName('fields');
$_flg = false ;
foreach ($_fields->item(0)->childNodes as $u)
{
if ($u->nodeType != XML_TEXT_NODE)
if( $u->getAttribute($pQueries['attribute']) == $pQueries['value'] ) // 1:1 match
{
$_fields->item(0)->removeChild($u) ;
$_flg = true ;
}
}
return $_flg ;
}
}
?>
</pre>
</div>
</div>
===Functions===
Functions/common.inc.php dans sa version finale
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Functions/common.inc.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
/*
common application functiunalities
would be fine to merge all request to requestForData
*/
require_once CLA . "/utilities.class.php" ;
require_once CLA . "/dataManager.class.php" ;
require_once CLA . "/templateControler.class.php" ;
function requestForLayout($pTerm,$pOwner="Root",$pTemplate="default")
/**
* layout request
*/
{
$tmp = new templateControler(array("path"=>"./Owners/" . $pOwner . "/Templates/" . $pTemplate . ".xml")) ;
$node = $tmp->getNode('//page[@id="' . $pTerm . '"]') ;
return $node[0]->nodeValue ;
}
function requestForData($pTerm,$pOwner)
/**
* data request
*/
{
$_lst = explode("|",$pTerm) ;
$_manager = new dataManager() ;
$_manager->data['path'] = $_manager->data['root'] . $pOwner . "/Contents/" . $_lst[0] . ".xml" ; //
$_document = $_manager->initialize() ;
if($_lst[1])
$_manager->data['query_1'] = array("attribute"=>"number", "value"=>$_lst[1]) ;
if($_lst[2])
$_manager->data['query_2'] = array("attribute"=>"id", "value"=>$_lst[2]) ;
$res = dataManager::getContentByData(
dataManager::getContentByData($_document, $_manager->data['query_1'])
,$_manager->data['query_2']) ;
$node = isset($_lst[3]) ? $_lst[3] : "content" ;
$str = $res->getElementsByTagName($node) ;
return $str->item(0)->nodeValue ;
}
function getPagesPanel($pEntry)
{
if( $_SESSION["autho"]["admin"] || $_SESSION["autho"]["moderator"] )
{
$dom = new DOMDocument() ;
$dom->load("./Owners/" . $pEntry . "/Contents/page.xml") ;
$root = $dom->getElementsByTagName("pages");
$str = "
<script>
function pageEdit(pVal)
{
var tab =[] ;
if($('number_'+pVal).innerHTML.substring(0,1) == '<')
{
$('number_'+pVal).innerHTML = tab[0] = $('iNum_'+pVal).value ;
$('id_'+pVal).innerHTML = tab[1] = $('iId_'+pVal).value ;
$('entry_'+pVal).innerHTML = tab[2] = $('iEnt_'+pVal).value ;
$('b'+pVal).innerHTML = '[edit]';
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=35&item=' + pVal + '&value=' + tab + '&owner=' + $('customerName').value + '&key='+$('userKey').value,
onSuccess: function(transport)
{
$('cComPage').innerHTML = transport.responseText ;
location.href='".($_SERVER['PHP_SELF'].'?'."entry=".$_REQUEST['entry'])."&page=1';
},
onFailure: function(){ }
});
}
else
{
$('number_'+pVal).innerHTML = \"<input id='iNum_\"+pVal+\"' type='text' value='\"+ ($('number_'+pVal).innerHTML) +\"' />\" ;
$('id_'+pVal).innerHTML = \"<input id='iId_\"+pVal+\"' type='text' value='\"+ ($('id_'+pVal).innerHTML) +\"' />\" ;
$('entry_'+pVal).innerHTML = \"<input id='iEnt_\"+pVal+\"' type='text' value='\"+ ($('entry_'+pVal).innerHTML) +\"' />\" ;
$('b'+pVal).innerHTML = '[save]';
}
}
function saveNewPage()
{
var tab = [] ;
tab[0] = $('iNumNew').value;
tab[1] = $('iIdNew').value;
tab[2] = $('iEntNew').value;
new Ajax.Request('./main.php',
{
method:'get',
parameters:'qry=36&value=' + tab+ '&owner=' + $('customerName').value +'&key=' + $('userKey').value,
onSuccess: function(transport)
{
$('cComPage').innerHTML = transport.responseText ;
location.href='".($_SERVER['PHP_SELF'].'?'."entry=".$_REQUEST['entry'])."&page=1';
$('new').innerHTML = '';
},
onFailure: function(){ }
});
;
}
function addPage()
{
$('new').innerHTML = '<p/><table width=\"600\"><tr class=\"title\"><td onclick=\"saveNewPage();\" class=\"specButton\"><b>[save]</b></td><td><input type=\"text\" id=\"iNumNew\"/></td><td><input type=\"text\" id=\"iIdNew\"/></td><td><input type=\"text\" id=\"iEntNew\"/></td></tr></table><p/>' ;
}
</script>
<table width='600'>
<tr class='title'>
<td/>
<td>number</td>
<td>id</td>
<td>entry</td>
</tr>
";
$i = 0 ;
foreach($root->item(0)->childNodes as $v)
{
if($v->nodeType != XML_TEXT_NODE)
{
@$str .= "<tr><td><span onclick='pageEdit(".$i.");'><b id='b".$i."' class='specButton'>[edit]</b></td><td id='number_".$i."'>". $v->getAttribute("number") . "</td><td id='id_".$i."'>" . $v->getAttribute("id") . "</td><td id='entry_".$i."'>" . $v->getAttribute("entry") . "</td></tr>" ;
}
$i++ ;
}
return $str .= "</table> <hr /><div id='new'></div><span class='specButton' onclick='addPage();'>[add page]</span> <span id='cComPage' class='green'></span>" ;
}
}
function reformat($pContent)
{
$in = array("``i", "``o","``q","``e","``u","``a");
$out = array("=","\"", "?","&","%","#");
return stripslashes(str_replace($in,$out,$pContent)) ;
}
function formatData($pContent)
/**
* data formatter
*/
{
$pContent = reformat($pContent);
$_pattern = '/(\[)([a-zA-Z0-9]*)(:)?([a-zA-Z0-9]*)?(\[)([<>\-(\"\'a-zA-Z0-9:\[\] ]*)(\]\])/' ;
$_manager = new dataManager() ;
// TODO
$_manager->data['path'] = $_manager->data['root'] . "Root/Contents/" . "1.xml"; //$_lst[0] .
$_document = $_manager->initialize() ;
$flg = preg_match($_pattern, $pContent, $_matches )? true: false ;
if ( $flg )
{
// save feed
$_manager->data['query_1'] = array("attribute"=>"id", "value"=>$_matches[4], "node"=>"content",
"replacement"=>$_matches[6]) ;
// query test
$_manager->setContentByData($_document, $_manager->data['query_1']) ;
$_str = preg_replace($_pattern, ("<" . $_matches[2] . " id='".$_matches[4]."'>" . $_matches[6] . "</" . $_matches[2] . ">"), $pContent) ;
} else {
$_str = $pContent ;
}
return $_str ;
}
function getPage($pId)
{
$dom = new DOMDocument() ;
$fileName = "./Owners/" . $_REQUEST['entry'] . '/Contents/page.xml' ;
$dom->load($fileName) ;
$node = $dom->getElementsByTagName("pages") ;
foreach($node->item(0)->childNodes as $v )
if($v->nodeType != XML_TEXT_NODE)
if($v->getAttribute("id") == $pId)
return "<div id='".$v->getAttribute("entry") . "'></div>" ;
}
function getFileList($pPath)
/*
* Lists all the files in a directory
*/
{
$i = 0 ; $lst = "" ;
if (@$handle = opendir($pPath))
while (false !== ($file = readdir($handle)))
{
if ( $file == "." || $file == ".." || preg_match("/page/",$file) || preg_match("/user/",$file) )
{}
else
{
@$lst[@++$i]=$pPath . "/" . $file ;
}
}
return $lst ;
}
function getCropOptions($pDoc)
{
$dom = new DOMDocument();
$dom->load( $pDoc );
$nodes = $dom->getElementsByTagName("fields");
foreach( $nodes->item(0)->childNodes as $v )
{
if($v->nodeType != XML_TEXT_NODE)
@$opt .= "<option value='" . $v->getAttribute("number") . "'>" . $v->getAttribute("number") . " | " . $v->getAttribute("id") . "</option>" ;
}
return $opt ;
}
function getCropItem($pItemValue,$pCrop)
{
$dom =new DOMDocument();
$dom->load($pCrop);
$node = $dom->getElementsByTagName("fields");
foreach( $node->item(0)->childNodes as $v )
{
if($v->nodeType != XML_TEXT_NODE && $v->getAttribute("number") == trim($pItemValue))
{
$subNodes = $v->childNodes ;
foreach($subNodes as $u)
{
if($u->nodeName == "subject")
$tab = $u->nodeValue ;
if($u->nodeName == "content")
$tab .= "~~" . $u->nodeValue;
}
return @$tab ;
}
}
}
function saveCropItem($pValue,$pOwner)
{
$pValue = reformat($pValue) ;
$tab = explode("~~",$pValue) ;
$dom =new DOMDocument();
$dom->load($tab[1]);
$node = $dom->getElementsByTagName("fields");
$topic = $dom->getElementsByTagName("topic");
$topic->item(0)->nodeValue = $tab[2];
$scope = $dom->getElementsByTagName("scope");
$scope->item(0)->nodeValue = $tab[3];
foreach( $node->item(0)->childNodes as $v )
{
if($v->nodeType != XML_TEXT_NODE && $v->getAttribute("number") == $tab[0])
{
try
{
$subNodes = $v->childNodes ;
foreach($subNodes as $u)
{
if($u->nodeName == "subject")
$u->nodeValue = $tab[4] ;
if($u->nodeName == "content")
$u->nodeValue = $tab[5] ;
}
$dom->save($tab[1]);
return "Crop item saved !" ;
}
catch(Exception $ex)
{
return "Error when saving" ;
}
}
}
}
function saveCropNewItem($pValue,$pOwner)
{
$tab = explode("~~",$pValue) ;
$dom =new DOMDocument();
$dom->load($tab[1]);
$_fields = $dom->getElementsByTagName("fields");
$_fields->item(0)->setAttribute("count", $cpt = ((int)$_fields->item(0)->getAttribute("count"))+1) ;
try
{
$node = $dom->createElement("field");
$node->setAttribute("id",$tab[4]) ;
$node->setAttribute("number",$cpt) ;
$subject = $dom->createElement("subject");
$subject->nodeValue=$tab[4];
$content = $dom->createElement("content");
$ct = $content->ownerDocument->createCDATASection("\n" . $tab[5] . "\n");
$content->appendChild($ct);
$node->appendChild($subject) ;
$node->appendChild($content) ;
$_fields->item(0)->appendChild($node) ;
$dom->save($tab[1]);
return "New item saved !" ;
}
catch(Exception $ex)
{
return "Error when saving" ;
}
}
function createCrop($pOwner,$pPath)
{
copy( OWN . "/Root/Dummies/1.xml", $pPath ) ;
return 'One new crop created but without data !';
}
function saveCrop($pOwner, $pId, $pData,$pKey)
{
$pData = reformat($pData) ;
try
{
$tab = explode("|",$pId) ;
$dom =new DOMDocument();
$cropName = './Owners/'.$pOwner.'/Contents/'.$tab[0].".xml" ;
if(is_file($cropName))
{
$dom->load($cropName);
$nodes = $dom->getElementsByTagName("fields") ;
$flg = false;
foreach($nodes->item(0)->childNodes as $v)
{
if($v->nodeType != XML_TEXT_NODE)
{
if($v->getAttribute("number")==@$tab[1])
{
foreach($v->childNodes as $u)
if($u->nodeType != XML_TEXT_NODE && $u->nodeName=="content")
{
$u->nodeValue = $pData ;$flg=true;
}
}
}
}
$dom->save($cropName) ;
return $flg ? 'Crop updated !' : 'Crop missing reference(s) !' ;
} else {
createCrop($pOwner,$cropName) ;
}
}
catch(Exception $ex)
{
return $ex;
}
}
function saveNewCrop($pOwner,$pValue)
{
try
{
$tab = explode("~~",$pValue) ;
if(!is_file('Owners/'.(@$_REQUEST["entry"]?$_REQUEST["entry"]:'Dummy').'/Contents/'.$tab[0]))
{
$dom =new DOMDocument("1.0");
$root = $dom->createElement("fields");
$root->setAttribute("count", 0) ;
$topic = $dom->createElement("topic");
$topic->nodeValue = $tab[1];
$scope = $dom->createElement("scope");
$scope->nodeValue = $tab[2];
$root->appendChild($topic) ;
$root->appendChild($scope) ;
$dom->appendChild($root) ;
$dom->save('./Owners/'.$pOwner.'/Contents/'.$tab[0].".xml");
return "New crop created !" ;
} else {
return "Crop already exists !" ;
}
}
catch(Exception $ex)
{
return "Error when saving" ;
}
}
function saveUser($pOwner,$pValue,$pKey,$pStatus)
{
require_once CLA . "/dataManager.class.php" ;
require_once CLA . "/utilities.class.php" ;
$_manager = new dataManager() ;
//
$_manager->data['path'] = $_manager->data['root'] . $pOwner . "/Contents/user.xml" ; //
$_manager->data['query_1'] = array(
array("attribute"=>"key", "value"=>$pKey)
) ;
$_res = utilities::getLineByAttribute($_manager->initialize(), $_manager->data['query_1']) ;
$flg=false;
if(isset($_res["login"]))
{
$flg = true;
}
$tab = explode(",", $pValue) ;
//TODO
switch ($pStatus)
{
case 1 :
if($flg)
{
$dom = new DOMDocument() ;
$userFile = "./Owners/" . $pOwner . "/Contents/user.xml" ;
$dom->load($userFile) ;
$root = $dom->getElementsByTagName("users");
$i=0;
$nodes = $root->item(0)->childNodes ;
$nodes->item($_REQUEST["item"])->setAttribute("login",$tab[0]) ;
$nodes->item($_REQUEST["item"])->setAttribute("pass",$tab[1]) ;
$nodes->item($_REQUEST["item"])->setAttribute("forname",$tab[2]) ;
$nodes->item($_REQUEST["item"])->setAttribute("lastname",$tab[3]) ;
$nodes->item($_REQUEST["item"])->setAttribute("customer",$tab[4]) ;
$nodes->item($_REQUEST["item"])->setAttribute("key",md5($tab[2])) ;
$dom->save($userFile) ;
return 'User updated';
}else{
return 'Nothing has been done';
}
break;
case 2 :
if($flg)
{
try
{
$dom = new DOMDocument() ;
$userFile = "./Owners/" . $pOwner . "/Contents/user.xml" ;
$dom->load($userFile) ;
$root = $dom->getElementsByTagName("users");
$new = $dom->createElement("user");
$new->setAttribute("login",$tab[0]);
$new->setAttribute("pass",$tab[1]);
$new->setAttribute("forname",$tab[2]);
$new->setAttribute("lastname",$tab[3]);
$new->setAttribute("customer",$tab[4]);
$new->setAttribute("key",md5($tab[2]));
$root->item(0)->appendChild($new);
$dom->save($userFile);
return 'New user add !' ;
}
catch (Exception $ex) {
return $ex ;
}
}else{
return 'Nothing has been done' ;
}
break;
}
}
function savePage($pOwner,$pValue,$pKey,$pStatus)
{
require_once CLA . "/dataManager.class.php" ;
require_once CLA . "/utilities.class.php" ;
$_manager = new dataManager() ;
//
$_manager->data['path'] = $_manager->data['root'] . $pOwner . "/Contents/user.xml" ; //
$_manager->data['query_1'] = array(
array("attribute"=>"key", "value"=>$pKey)
) ;
$_res = utilities::getLineByAttribute($_manager->initialize(), $_manager->data['query_1']) ;
$flg=false;
if(isset($_res["login"]))
{
$flg = true;
}
$tab = explode(",", $pValue) ;
//TODO
switch ($pStatus)
{
case 1 :
if($flg)
{
$dom = new DOMDocument() ;
$pageFile = "./Owners/" . $pOwner . "/Contents/page.xml" ;
$dom->load($pageFile) ;
$root = $dom->getElementsByTagName("pages");
$i=0;
$nodes = $root->item(0)->childNodes ;
$nodes->item($_REQUEST["item"])->setAttribute("number",$tab[0]) ;
$nodes->item($_REQUEST["item"])->setAttribute("id",$tab[1]) ;
$nodes->item($_REQUEST["item"])->setAttribute("entry",$tab[2]) ;
$dom->save($pageFile) ;
return 'Page updated';
}else{
return 'Nothing has been done';
}
break;
case 2 :
if($flg)
{
try
{
$dom = new DOMDocument() ;
$pageFile = "./Owners/" . $pOwner . "/Contents/page.xml" ;
$dom->load($pageFile) ;
$root = $dom->getElementsByTagName("pages");
$new = $dom->createElement("page");
$new->setAttribute("number",$tab[0]);
$new->setAttribute("id",$tab[1]);
$new->setAttribute("entry",$tab[2]);
$root->item(0)->appendChild($new);
$dom->save($pageFile);
return 'New page add !' ;
}
catch (Exception $ex) {
return $ex ;
}
}else{
return 'Nothing has been done' ;
}
break;
}
}
function getNodeList($pNodeName,$pPath)
/*
* Lists all the files in a directory
*/
{
return xml2phpArray(simplexml_load_string(file_get_contents($pPath)),array());
}
function getAttribute($node)
{// >((dom)node) ((array)tab)>
$tab=array() ;
foreach($node->attributes() as $k1->$v1)
{
$tab[$k1->{''}->name]=$k1->{''}->value ;
}
return $tab;
}//
function xml2phpArray($xml,$arr){
$iter = 0;
foreach($xml->children() as $b){
$a = $b->getName();
if(!$b->children()){
$arr[$a] = trim($b[0]);
}
else{
$arr[$a][$iter] = array();
$arr[$a][$iter] = xml2phpArray($b,$arr[$a][$iter]);
}
$iter++;
}
return $arr;
}
function print_r_html($data,$return_data=false)
/*
* Print_r_htl modified to return array
*/
{
$data = print_r($data,true) ;
$data = str_replace( " "," ", $data) ;
$data = str_replace( "\r\n","<br/>\r\n", $data) ;
$data = str_replace( "\r","<br/>\r", $data) ;
$data = str_replace( "\n","<br/>\n", $data) ;
if (!$return_data)
return $data ;
else
return $data ;
}
?>
</pre>
</div>
</div>
===Backend===
Le backend est implémenter suivant quelques règles métier imposées par l'orientation du CMS également.
Objects/Backend/authorize.inc.php
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Objects/Backend/authorize.inc.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
/*
* Authorize component
*/
require_once CLA . "/dataManager.class.php" ;
require_once CLA . "/utilities.class.php" ;
$_manager = new dataManager() ;
//
$_manager->data['path'] = $_manager->data['root'] . (@$_REQUEST["entry"]?$_REQUEST["entry"]:"Root") . "/Contents/user.xml" ; //
$_credit = isset($_SESSION['autho']['credit'])?trim($_SESSION['autho']['credit']):null ;
//
if ( isset($_REQUEST['iLogin']) )
$_credit = trim($_REQUEST['iLogin']) ;
if( isset($_REQUEST['iUnlog']))
{
unset($_SESSION["autho"]["credit"]) ;
unset($_credit) ;
}
try
{
# QUERY TESTS
$_manager->data['query_1'] = array(
array("attribute"=>"pass", "value"=>@$_credit)) ;
// query test
$_res = utilities::getLineByAttribute($_manager->initialize(), $_manager->data['query_1']) ;
if(isset($_res["login"]))
{
$_SESSION["autho"]["credit"] = $_res["pass"] ;
}
else
{
unset($_SESSION["autho"]["credit"]) ;
}
# REGION [0] - Keep order of privileges
if(preg_match('/u/',@$_res['rights']))
{
$_SESSION["autho"]["user"] = 1 ;
unset($_SESSION["autho"]["moderator"]) ;
unset($_SESSION["autho"]["admin"]) ;
}
if(preg_match('/m/',@$_res['rights']))
{
$_SESSION["autho"]["moderator"] = 1 ;
unset($_SESSION["autho"]["admin"]) ;
}
if(preg_match('/a/',@$_res['rights']))
{
$_SESSION["autho"]["admin"] = 1 ;
}
# END REGION [0]
} catch(Exception $ex) { }
//var_dump($_res);
?>
</pre>
</div>
</div>
Objects/Backend/responder.inc.php dans sa version finale
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Objects/Backend/responder.inc.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
//
$_ = explode(":",@$_REQUEST['id']) ;
$_qry = $_REQUEST['qry'] ;
$_content = "" ;
switch( true )
/**
* content type switcher
*/
{
case ($_[0] == "lay") :
// single query order : template request
$_content = requestForLayout( $_[1], $_REQUEST['owner'], @$_REQUEST['template']?$_REQUEST['template']:'default' ) ;
break ;
case ($_[0] == "data") :
// single query order : data request
$_content = requestForData( $_[1], $_REQUEST['owner'] ) ;
break ;
case(@$_qry == 2) :
// single query order : formatData
$_content = formatData(@$_REQUEST['content']) ;
break ;
case(@$_qry == 3) :
// composed query instruction :
//$_content = ;
break ;
case(@$_qry == 4) :
// single query order : getCustomerEntryList
require_once BCK . "/services.inc.php" ;
if( @$_SESSION["autho"]["credit"] )
{
$_adminPage = new adminPage() ;
$_adminPage->prepareAdminPage( getCustomerEntryList( OWN ) ) ;
$_content = $_adminPage->str ;
} else {
$_content = "no access granted" ;
}
break ;
case (@$_qry == 5) :
// create entries for customer
$_content = $_REQUEST['nu']." !.added [ refresh the page ]" ;
mkdir( OWN . "/" . $_REQUEST['nu'] , 0700);
mkdir( OWN . "/" . $_REQUEST['nu']."/Contents" , 0700);
copy( OWN . "/Root/Dummies/1.xml", OWN . "/" . $_REQUEST['nu'] . "/Contents/1.xml" ) ;
copy( OWN . "/Root/Dummies/user.xml", OWN . "/" . $_REQUEST['nu'] . "/Contents/user.xml" ) ;
copy( OWN . "/Root/Dummies/page.xml", OWN . "/" . $_REQUEST['nu'] . "/Contents/page.xml" ) ;
mkdir( OWN . "/" . $_REQUEST['nu']."/Templates" , 0700);
copy( OWN . "/Root/Dummies/default.xml", OWN . "/" . $_REQUEST['nu'] . "/Templates/default.xml" ) ;
copy( OWN . "/Root/Dummies/admin.xml", OWN . "/" . $_REQUEST['nu'] . "/Templates/admin.xml" ) ;
mkdir( OWN . "/" . $_REQUEST['nu']."/Users" , 0700);
mkdir( OWN . "/" . $_REQUEST['nu']."/Tmp" , 0700);
break ;
case (@$_qry == 6) :
// save file modified
require_once "services.inc.php" ;
$_content = $_REQUEST['path']." !.saved" ;
file_put_contents($_REQUEST['path'], noHtml($_REQUEST['content']));
doFlush();
break ;
case (@$_qry == 7) :
//
$_content = "> crop page" ;
break;
case (@$_qry == 8) :
//
$_content = "> item page" ;
break;
case (@$_qry == 9) :
//
$_content = "> page page" ;
break;
case (@$_qry == 15) :
$_content = getCropItem($_REQUEST['item'],$_REQUEST['crop']) ;
break;
case (@$_qry == 20) :
$_content = saveCropItem($_REQUEST['value'],$_REQUEST['owner']) ;
break;
case (@$_qry == 21) :
$_content = saveCropNewItem($_REQUEST['value'],$_REQUEST['owner']) ;
break;
case (@$_qry == 22) :
$_content = saveNewCrop($_REQUEST["owner"],$_REQUEST['value']) ;
break;
case (@$_qry == 25) :
$_content = saveUser($_REQUEST['owner'], $_REQUEST['value'], $_REQUEST['key'], 1) ;
break;
case (@$_qry == 26) :
$_content = saveUser($_REQUEST['owner'], $_REQUEST['value'], $_REQUEST['key'], 2) ;
break;
case (@$_qry == 30) :
$_content = saveCrop($_REQUEST['owner'], $_REQUEST['id'], $_REQUEST['data'], $_REQUEST['key']) ;
break;
case (@$_qry == 35) :
$_content = savePage($_REQUEST['owner'], $_REQUEST['value'], $_REQUEST['key'],1) ;
break;
case (@$_qry == 36) :
$_content = savePage($_REQUEST['owner'], $_REQUEST['value'], $_REQUEST['key'],2) ;
break;
}
# OUTPUT
switch(true)
{
case ($_qry==6):
echo $_content ;
break;
case ($_qry==15) :
echo $_content;
break;
case ($_qry==20) :
echo $_content;
break;
case ($_qry==21) :
echo $_content;
break;
case ($_qry==22) :
echo $_content;
break;
case ($_qry==25) :
echo $_content;
break;
case ($_qry==26) :
echo $_content;
break;
case ($_qry==30) :
echo $_content;
break;
case ($_qry==35) :
echo $_content;
break;
case ($_qry==36) :
echo $_content;
break;
default:
echo @$_REQUEST['id'] . "~~" . ( @$_[1] ? $_[1] : $_[0] ) . "~~" . $_content ;
break;
}
?>
</pre>
</div>
</div>
Objects/Backend/services.inc.php
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Objects/Backend/services.inc.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
/*
* Backoffices Services
*
* here comes all the backoffice treatments as
*
*/
function noHtml ($pString)
{
$_in = array('/</','/>/') ;
$_out = array('<','>') ;
return preg_replace($_in,$_out,$pString) ;
}
function doFlush ()
{
// check that buffer is actually set before flushing
if (ob_get_length())
{
@ob_flush();
@flush();
@ob_end_flush();
}
@ob_start();
}
function getUserList ()
{
return $lst ;
}
function getCustomerEntryList( $pPath, &$_lst=array() )
/*
* get recursively the files in a given initial path
*/
{
$tmp = getFileList( $pPath ) ;
try
{
if ( $tmp )
foreach ( @$tmp as $k=>$v )
{
if( is_dir($v) )
{
$_lst[$pPath][$v] = array() ;
getCustomerEntryList ( $v, $_lst[$pPath] ) ;
}
else
{
$_lst[$pPath][@++$i] = $v ;
}
}
}
catch (Exception $ex) {}
return $_lst ;
}
class adminPage
/*
* prepare admin page
*/
{
public $str = "
<div id='editPanel' class='adminMenu'>
<input type='text' id='iCustomer' value=':: add customer ::' onclick='this.value=\"\";' />
<input type='button' value='do' onclick='setContent(\"addUserCom\",\"qry=5&nu=\"+$(\"iCustomer\").value) ;' />
<div id='addUserCom' class='green'></div>
<input type='hidden' name='iPath' id='iPath' value='' />
<div id='cMod' style='display:none' ><!-- -->
<textarea id='iMod' rows='10' cols='100' style='width:100%;font-size:10px;font-family:verdana;'>content</textarea>
<input type='button' value='save' onclick='saveMod();' />
</div>
</div>
<br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br />
";
function deployArray ( $pArray, &$str )
/*
*
*/
{
//echo "tmp:".count($pArray) ;
try
{
if (is_array($pArray))
foreach( @$pArray as $k=>$v )
{
$this->str .= ( preg_match( '/\//', $k ) ? "<p><b>" . ((substr_count($k,'/')>2)?" <span class='dirPath'>" . $k:$k) . "</span></b></p>": "" ) . ( ! is_array( $v ) ? " <a style='cursor:pointer;' onclick='editFileContent(\"$v\");'>[edit]</a> <a href='$v'>" . $v . "</a>" : "" ) . " <br />" ;
if( count( $v ) )
$this->deployArray( $v, $this->str ) ;
}
} catch( Exception $ex ) {}
}
function prepareAdminPage( $pArray )
/*
*
*/
{
$tmp = array () ;
$this->deployArray( $pArray, $tmp ) ;
}
}
class cropPage
{
public function cropPage()
{
}
public function getCropList()
{
}
}
?>
</pre>
</div>
</div>
Objects/Backend/webservices.inc.php
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Objects/Backend/webservices.inc.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
?>
</pre>
</div>
</div>
===FrontEnd===
Deux points d'entrées :
Objects/Frontend/admin.inc.php
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Objects/Frontend/entry.inc.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
//
require_once CLA . "/utilities.class.php" ;
require_once CLA . "/templateControler.class.php" ;
require_once FUN. "/common.inc.php" ;
//
$tmp = new templateControler(array("path"=>"./Owners/".(@$_REQUEST["entry"]?@$_REQUEST["entry"]:"Dummy")."/Templates/admin.xml"));
# STATE
// get the content main page
$tmp->initFrame(
$tmp
->getNode("//template/frameset/content")
) ;
// authentification
$tmp->setTag("customer","<input type='hidden' value='".(@$_REQUEST['entry']?$_REQUEST['entry']:"Root")."' id='customerName' />" );
if(@$_SESSION["autho"]["credit"])
$tmp->setTag("logged","<input type='hidden' value='1' id='iLogged' />" );
$tmp->getAnchors( $tmp->data['feed'][0]->nodeValue ) ;
$tmp->anchorContentReplacer() ;
$tmp->setFrame() ;
# OUTPUT
echo $tmp->data['frameset'] ;
?>
</pre>
</div>
</div>
Objects/Frontend/entry.inc.php dans sa version finale
<div class="NavFrame" style="clear:left;"><div class="subpages" style="display:none; clear:left;"></div><div class="NavHead " align="center" style="cursor:default;text-align:center; background-color: #80808020; clear:left;" title="Cliquez pour afficher/masquer">Objects/Frontend/entry.inc.php</div>
<div class="NavContent" align="left" style="clear:left;">
<pre>
<?php
require_once CLA . "/utilities.class.php" ;
require_once CLA . "/templateControler.class.php" ;
require_once FUN. "/common.inc.php" ;
require_once CLA . "/contentPane.class.php" ;
$_SESSION["path"] = $_path = "./Owners/" . (@$_REQUEST['entry']?$_REQUEST['entry']:"Root") ;
//
$tmp = new templateControler
(
array("path"=>$_path
. "/Templates/default.xml")
);
//echo print_r_html($_SESSION) ;
# STATE
$tmp->initFrame(
$tmp
->getNode("//template/frameset/content")
) ;
// authentification
$tmp->setTag("customer","<input type='hidden' value='".(@$_REQUEST['entry']?$_REQUEST['entry']:"Dummy")."' id='customerName' />
<input type='hidden' value='".md5(@$_SESSION["autho"]["credit"])."' id='userKey' />
<input type='hidden' value='" . (@$_cms['status']?1:0) . "' id='contentKey' />
<input type='hidden' value='0' id='iCounter' />
" );
if(@$_SESSION["autho"]["credit"])
{
$tmp->setTag("logged","<input type='hidden' value='1' id='iLogged' />" );
}
// if cms entry - display menu
if(@$_cms["status"] == 1)
{
$tmp->setTag("menuPane", $_container["content"]->data["menuPane"] );
if(@$_SESSION['autho']['credit'])
switch(true)
{
case ( isset($_REQUEST["crop"]) && $_REQUEST["crop"] != 1 && !@$_REQUEST["page"]==1 ) :
$tmp->setTag("bottomPane", $_container["content"]->getCropPane(@$_REQUEST["crop"]) );
break;
case isset( $_REQUEST["page"] ) :
$tmp->setTag("bottomPane", $_container["content"]->getPagePane( ));
break;
case isset( $_REQUEST["user"] ) :
$tmp->setTag("bottomPane", $_container["content"]->getUserPane( ));
break;
case ( @$_REQUEST["crop"] == 1 ) :
$tmp->setTag("bottomPane", $_container["content"]->getNewCrop() );
break;
}
}
$tmp->getAnchors( $tmp->data['feed'][0]->nodeValue ) ;
$tmp->anchorContentReplacer() ;
// customer name
$tmp->setFrame() ;
# OUTPUT
echo $tmp->data['frameset'] ;
?>
</pre>
</div>
</div>
== Le Troisieme noyau ==
MyTinyCMS est ici un CMS orienté données et frontEnd. Avec le frontOffice ajaxifié présenté dans la rubrique relatif à la vue, il permet la création de sites customer directement dans moyennant quelques connaissances en html.
L'applicabilité des modules ayant été démontrée. Cette solution est achevée ici mais peut être augmentée ou améliorée.
myTinyCMS est sous licence GNU copyLeft et utilisable. Si vous implémentez la solution, laissez votre copyright ou alias derrière le mien.
Le copy étant
<pre>
Copyright (C) zulul
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
</pre>
== Télécharger ==
MyTinyCMS est nominal... Il peut être telechargé [http://code.google.com/p/mytinycms/downloads/list ici]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Fournir les données=
Pour la vue, les données sont extraites des champs et ensuite injectées aux points d'ancrages désignés sur les templates.
Un des objectifs étant de bien synchroniser l'injection serveur avec les sorties clientes en mini flux sur les containers de la vue xhtml.
La complétion des données étant sélective.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/IHM=
L'Interface Homme - Machine consistera à fournir à l'utilisateur des options d'encodage et d'identification dans la vue.
Le découpage modulaire dégage la solution des itérations avenir dans l'ajout de nouvelles fonctions.
Deux points d'entrées permettent de découpler
*Site web : http://localhost:888/miniCMS/main.php
*CMS : http://localhost:888/miniCMS/main.php?usr=log
Ce dernier proposant l'authentification.
L'intégrateur pourra créer une interface d'administration à loisir sur cette adresse ou modifier le point d'entrée, en supprimant ou modifiant les paramètres ou en passant les données par POST plutôt que dans l'URL...
Cette partie ne sera pas détaillée dans cet exemple, seule l'interaction avec l'encodeur sera vu.
Le point d'entrée du site devra faire la distinction entre les sous sites ou sous domaines.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Rétrospective=
Si l'envie vous prend d'intégrer ce CMS, c'est à dire de le coder localement chez vous ou de l'adapter à votre environnement ou solution. Je vous conseille de toujours travailler avec l'API approprié, c'est à dire ici : php.net. Cette solution est suffisamment légère pour pouvoir être traduite à loisir en Ruby, Java, Python ou autre.
L'inconvénient majeur actuel étant que l'utilisateur ou le webmaster qui l'utilisera devra avoir des notions d'HTML, la vue ajaxifiée étant ouverte pour une saisie directe au plus au niveau, càd la vue elle même offrant le rendu de ce qui va être encodé.
Les différentes couches ou composantes sous forme de scripts, snippets, classes, functions peuvent être revues, surchargés, améliorées ou étendues. Celles-ci devenant des incréments que vous jugerez utiles de placer à la suite dans ces pages.
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Exemples/MiniCMS/Conclusion=
Développer un collectionneur et publicateur de données comme typo3 ou joomla est plus une affaire d'organisation modulaire que d'approche en terme de service à fournir. Ce dernier n'étant que la collecte et la diffusion, avec quelques options quant au versionning et la mise à jour du contenu. Les règles sont simples, mais à développer...
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=cURL=
PHP possède une extension [[w:cURL|cURL]] pour requêter des URL.
== Installation ==
Sur Linux :
<pre>
sudo apt-get install php-curl
</pre>
== Exemples ==
Voici le POST d'un JSON avec une pièce jointe PDF (du multipart) :
<pre>
private function execCurl(
string $method,
string $url,
string $token,
string $filePath,
string $fileName
): string {
$curlFile = curl_file_create($filePath,'application/pdf', $fileName);
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
'Authorization: ' . $token
],
CURLOPT_POSTFIELDS => [
'File'=> $curlFile,
'jsondata' => '{
"file_name": "' . $fileName . '"
}'
],
]);
$response = curl_exec($curl);
$error = curl_error($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if (!empty($error)) {
throw new ErrorException($status . ' ' . $error);
}
return $response;
}
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=DOMDocument=
== Introduction ==
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 [[w:Document Object Model|DOMDocument]] va repousser ces limites.
== Qu'est-ce que DOMDocument ? ==
DOMDocument est une bibliothèque de fonctions apparue avec PHP5<ref>http://php.net/manual/fr/book.dom.php</ref> et activée par défaut. Elle permet de concevoir des pages [[Programmation HTML|HTML]] sous forme d'objets.
== Les avantages et les inconvénients ==
* 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 [[w:DTD|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 ==
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 :
<pre>
<p>Ce texte est <strong>important</strong></p>
</pre>
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 ==
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.
<pre>
<?php
$doc = DOMDocument::loadHTMLFile("fichier.html");
</pre>
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.
<pre>
<?php
$doc = new DOMDocument();
$doc->loadHTMLFile("fichier.html");
</pre>
Il est également possible d'importer le code à partir d'une chaîne de caractères :
<pre>
<?php
$code = "<html><head></head><body></body></html>";
$doc = new DOMDocument();
$doc->loadHTML( $code );
</pre>
== Enregistrer une page ==
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 :
<pre>
$doc->saveHTMLFile("fichier.html");
</pre>
Si vous voulez l'afficher, il vous suffit d'exécuter la fonction suivante :
<pre>
<?php
echo $doc->saveHTML();
</pre>
La méthode retourne une chaîne de caractères que la fonction <code>echo</code> affiche.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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 !''</p>
</div>
</div>
<pre>
<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>
</pre>
== La classe DOMNode ==
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 :
<pre>
$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
</pre>
Les méthodes sont les suivantes :
;'''AppendChild''':Le nœud $enfant devient enfant de $node
<pre>
$node->appendChild( $enfant );
</pre>
;'''RemoveChild''':Supprime le nœud $enfant du node $node
<pre>
$node->removeChild( $enfant );
</pre>
== La classe DOMElement ==
*Les éléments possèdent les propriétés suivantes :
<pre>
$node->tagName // par exemple "p" pour un paragraphe. Sa valeur ne peut être modifiée
</pre>
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
<pre>
$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
</pre>
- Autres méthodes
<pre>
$newNode = $node->cloneNode(); //duplique un élément
$nodeList = $node->getElementsByTagName("strong");
</pre>
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 :
<pre>
$strong5 = $nodeList->item(4); //Sélectionne la 5e balise <strong>
</pre>
L'attribut ''length'' donne le nombre d'éléments de la liste. Exemple :
<pre>
for ($i=0; $i<$nodeList->length; $i++)
{
echo $nodeList->item( $i )->tagName;
}
</pre>
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 ==
La classe DOMText contient l'unique propriété suivante :
<pre>
$node->wholeText
</pre>
Elle n'est accessible qu'en lecture seule.
La classe ''DOMText'' admet deux méthodes :
<pre>
/* Retourne true si la chaîne de caractère contient des espaces */
$node->isWhitespaceInElementContent();
</pre>
/* 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 */
<pre>
$end = $node->splitText(5)
</pre>
Exemple :
<pre>
<?php
$doc = new DOMDocument();
$text = $doc->createTextNode("Je suis celui qui est");
$text2 = $text->splitText(7);
echo $text->wholeText."<br />";
echo $text2->wholeText
</pre>
Ce code retournera "je suis<br /> celui qui est"
== La classe DOMAttr ==
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 :
<pre>
$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
</pre>
Seule la propriété value n'est pas en lecture seule, c'est à dire qu'il est possible d'avoir recours au code suivant :
<pre>
$node->value = "maValeur";
</pre>
== Accéder à un nœud ==
Il existe plusieurs modes de recherche du nœud. Il est par exemple possible de le sélectionner par son id :
<pre>
$node = $doc->getElementById("sonId"); // Retourne un objet nœud
</pre>
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.
<pre>
$nodeList = $doc->getElementsByTagName("acronym"); //Sélectionne toutes les balises <acronym>
</pre>
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 :
<pre>
$acronym5 = $nodeList->item(4); //Sélectionne le 5e nœud de la liste;
</pre>
== XML ==
Création :
<pre>
$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');
</pre>
Lecture :
<pre>
$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);
}
}
</pre>
==Références==
<references/>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=JSON=
== Bibliothèque PHP-JSON ==
Le format de données [[Programmation JavaScript/Notation JSON|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) ===
==== Linux ====
apt-get install php5-json
==== Windows ====
# 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 : <u>C:\Program Files (x86)\EasyPHP\binaries\php\php_runningversion\php.ini</u>), ajouter :
extension=json.so
=== json_encode() ===
Cette fonction convertit un objet PHP en JSON exploitable en JavaScript<ref>http://php.net/manual/fr/function.json-encode.php</ref>. Ex :
<pre>
$tableau = array('colonne 1' => 'valeur 1', 'colonne 2' => 'valeur 2', 'colonne 3' => 'valeur 3');
echo json_encode($tableau);
</pre>
{"colonne 1":"valeur 1","colonne 2":"valeur 2","colonne 3":"valeur 3"}
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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 <code>JsonSerializable</code> en qui impose une méthode jsonSerialize()<ref>http://php.net/manual/fr/class.jsonserializable.php</ref>. Ex :
<pre>
public function jsonSerialize() {
return $this->array;
}
</pre>
</p>
</div>
</div>
==== Options ====
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Par défaut json_encode() échappe les caractères spéciaux non ASCII (ex : "é" devient "\u00e9"). Pour éviter cela on utilise l'option suivante :
<pre>
$a = json_encode($monTableau, JSON_UNESCAPED_UNICODE);
</pre>
</p>
</div>
</div>
Par ailleurs, les erreurs de json_encode() sont accessibles avec <code>json_last_error()</code> ou <code>json_last_error_msg()</code>. Ex :
<pre>
$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);
}
</pre>
Pour afficher dans un format plus lisible par un humain (indenté), utiliser <code>JSON_PRETTY_PRINT</code>.
=== json_decode() ===
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<ref>http://php.net/manual/fr/function.json-decode.php</ref>.
La gestion des erreurs est semblable à celle de <code>json_encode()</code>.
== PEAR Services_JSON ==
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.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Il requiert PHPUnit.php<sup>[https://pear.php.net/package/PHPUnit/download Télécharger]</sup>.</p>
</div>
</div>
La classe ''Services_JSON'' de JSON.php peut s'utiliser comme décrit dans Test-JSON.php.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=MING=
== Conceptions d'animations pour pages web ==
<div class="noprint" style="float: right; border: solid 2px black; padding: 0px; margin: 5px; width: 250px; -moz-border-radius-topleft: 15px; -moz-border-radius-topright: 15px;"><div style="background-color: #dcb; text-align: center; padding: 2px; margin: 0px; -moz-border-radius-topleft: 15px; -moz-border-radius-topright: 15px;">[[Image:Crystal128-adept manager.svg|20px]] '''Paquet logiciel'''</div><table style="padding: 2px; margin: 0px;" class="plainlinks"><tr><th style="white-space: nowrap; padding-right:0.5em;">[[Image:Ubuntu-logo-no-wordmark-2022.svg|left|15px]] Ubuntu (<span class="noarchive">[https://doc.ubuntu-fr.org/tutoriel/comment_installer_un_paquet ?]</span>)</th><td>php5-ming</td></tr>
</table></div>
Créer les animations en Flash (.swf) se fait par des logiciels payants, cependant la librairie [[w:Ming (bibliothèque logicielle)|MING]] écrite en [[Programmation C|C]], et utilisable en PHP, C++, Python et Ruby, permet de les générer gratuitement (mais pas d'éditer les .swf existant).
<pre>
<?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();
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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.</p>
</div>
</div>
== Liens externes ==
* [http://php.net/manual/fr/book.ming.php Fonctions]
* [http://www.journaldunet.com/developpeur/tutoriel/php/021206php_ming1a.shtml Tutoriel JournalDuNet]
* [http://www.gazbming.com/ Tutoriel GazbMing]
* [http://maxime-ohayon.developpez.com/tutoriels/ming/ Tutoriel developpez.com]
* [http://documentation-php.supportduweb.com/class.swfbutton.documentation-php Tutoriel supportduweb.com]
* [http://www.opaque.net/ming/examples/ Bibliothèque de scripts]
* [http://books.google.fr/books?id=VQKAsErQ_oIC&pg=PA257&lpg=PA257&dq=drawCurveTo+circle&source=bl&ots=6nWRkwIs80&sig=d17nhGChe7hG_YtF3M8C53jE6pQ&hl=fr&ei=xf91TtC7NdDMswbN2-2sCw&sa=X&oi=book_result&ct=result&resnum=9&ved=0CGYQ6AEwCA#v=onepage&q=drawCurveTo%20circle&f=false ''Perl graphics programming'', Shawn P. Wallace, 2002]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=SPL=
La ''Standard PHP Library'' (SPL) est une bibliothèque intégrée depuis PHP 5<ref>http://www.php.net/spl</ref>.
Elle comprend les classes suivantes :
== Structures de données ==
* SplDoublyLinkedList
* SplStack
* SplQueue
* SplHeap
* SplMaxHeap
* SplMinHeap
* SplPriorityQueue
* SplFixedArray
* SplObjectStorage
== Itérateurs ==
== Interfaces ==
== Exceptions ==
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=ADOdb=
== Fonctionnalités ==
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 ==
Les fichiers se téléchargent sur http://adodb.sourceforge.net/#download.
== Exemple ==
Exemple sur une base Access :
<pre>
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
</pre>
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.
<pre>
include('adodb.inc.php');
$conn = &ADONewConnection('mysql');
$conn->PConnect('localhost','userid','','agora');# se connecte à MySQL, agora db
</pre>
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 ==
Un excellent tutoriel se trouve à l'adresse suivante :
*chez phplens : [http://phplens.com/phpeverywhere/adodb_french Tutorial ADODB français]
*chez phpfreaks : [http://www.phpfreaks.com/tutorials/110/0.php Tutorial ADODB anglais]
Une courte description en anglais au database journal : [http://www.databasejournal.com/features/php/article.php/2222651 ADODB class library]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=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 [[w:TCPDF|TCPDF]].
== Exemples ==
<pre>
use Dompdf\Dompdf;
class pdfGenerator
{
public function generate(string $html)
{
$dompdf = new Dompdf();
$dompdf->loadHtml($html);
$dompdf->render();
$dompdf->stream();
}
}
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=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 ==
<pre>
use FPDF;
class pdfGenerator
{
public function generate(string $html)
{
$pdf = new FPDF();
$pdf->AddPage();
$pdf->Cell(10, 10, $html);
$pdf->Output();
}
}
</pre>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=PHPExcel=
== Introduction ==
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<ref>http://g-ernaelsten.developpez.com/tutoriels/excel2007/?page=fondements</ref>.
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 :
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]]
<pre>
composer require phpoffice/phpexcel
</pre>
</div>
</div>
Pour l'utiliser, l'inclure en début de fichiers :
<pre>
include 'PHPExcel/Classes/PHPExcel.php';
</pre>
On appellera ses instances "$objPHPExcel", qui représentent les classeurs.
== Création ==
Pour créer un fichier à partir de rien, soit <u>CreateXLS.php</u> un fichier situé à côté du répertoire de la bibliothèque nommé <u>PHPExcel</u>, brut de téléchargement (on appelle la feuille avec un nom très court car elle est souvent utilisée, "$s" pour "sheet") :
<pre>
$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');
</pre>
== Ouverture ==
Pour ouvrir et lire un fichier existant :
<pre>
$objReader = PHPExcel_IOFactory::createReader('Excel2007');
$objPHPExcel = $objReader->load('./HelloWorld1.xlsx');
print $objPHPExcel->getActiveSheet()->getCell('A1')->getValue();
</pre>
== Conversion ==
Conversion d'un XLSX en CSV :
<pre>
$xlsx = PHPExcel_IOFactory::load('./HelloWorld1.xlsx');
$writer = PHPExcel_IOFactory::createWriter($xlsx, 'CSV');
$writer->setDelimiter(";");
$writer->setEnclosure("");
$writer->save('./HelloWorld1.csv');
</pre>
Conversion d'un CSV en XLSX :
<pre>
$objReader = PHPExcel_IOFactory::createReader('CSV');
$objReader->setDelimiter(';');
$objReader->setEnclosure(' ');
$objPHPExcel = $objReader->load('./HelloWorld1.csv');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$objWriter->save('./HelloWorld3.xlsx');
</pre>
== Modifications ==
Les propriétés des cellules sont présentées sous forme de tableaux multidimensionnels :
<pre>
$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');
</pre>
On peut aussi :
<pre>
// Récupérer la dernière ligne d'une feuille
$ligne = $s->getHighestRow();
// Insérer une ligne
$s->insertNewRowBefore($ligne + 1, 1);
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Voir aussi ==
<div class="noprint bandeau noprint
bandeaujaune
" style="min-height:80px;clear:;">
<div class="bandeautitre" style="margin:0.5em;">
À faire...<span style="display:inline-block;float:right;margin:0.5em 1em;">[[Image:Nuvola apps korganizer.svg|40px|link={{{link}}}]]</span></div>
<div class="bandeautexte" style="padding: 0.5em; background-color: white;">
phpoffice/phpexcel is abandoned, you should avoid using it. Use phpoffice/phpspreadsheet
</div>
</div>[[Catégorie:À faire|Programmation PHP/Version imprimable]]
* https://github.com/PHPOffice/PhpSpreadsheet (.xlsx et .ods)
* https://github.com/PHPOffice/PHPPresentation (PowerPoint)
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=PHPMailer=
[[w:en:PHPMailer|PHPMailer]] est une bibliothèque open source<ref>https://github.com/PHPMailer/PHPMailer</ref> pour envoyer des emails plus rapidement qu'à partir de la commande <code>mail()</code>.
== Installation ==
Télécharger sur GitHub ou bien ajouter à [[../Installer_PHP#Composer|composer.json]] : <code>"phpmailer/phpmailer": "~5.2"</code>.
== Utilisation ==
<pre>
// Pour la v5.0.0 (2009)
require_once('PHPMailer/class.phpmailer.php');
// Pour la v5.2.14 (2016)
require('PHPMailer/PHPMailerAutoload.php');
</pre>
== Exemple ==
Exemple de base :
<pre>
$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é !';
}
</pre>
Bien sûr, on peut ensuite ajouter en une ligne une pièce jointe, une copie cachée, une signature [[w:DKIM|DKIM]]...
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=PHPWord=
== Introduction ==
Cette bibliothèque open source permet de lire et d'écrire des documents de [[w:traitement de texte|traitement de texte]].
Il faut la télécharger sur https://github.com/PHPOffice/PHPWord.
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]]
<syntaxhighlight lang=bash>
composer require phpoffice/phpword
</syntaxhighlight>
</div>
</div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=RabbitMQ=
[[w:RabbitMQ|RabbitMQ]] est un logiciel de messages en protocole [[w:Advanced Message Queuing Protocol|AMQP]]. Il permet donc à des processus de produire des messages JSON dans des files d'attente pour que d'autres les consomme ensuite<ref>https://www.rabbitmq.com/tutorials/tutorial-one-php.html</ref>.
== Installation ==
=== Client PHP ===
<pre>
composer require php-amqplib/php-amqplib
</pre>
=== Serveur ===
L'installation du serveur est multi-plateforme. Sous Linux<ref>https://www.rabbitmq.com/install-debian.html</ref> :
<pre>
apt-get install rabbitmq-server
</pre>
Test de fonctionnement :
<pre>
telnet localhost 5672
</pre>
==== Site de gestion ====
Une interface graphique existe pour lire et manipuler les messages manuellement, c'est le ''management plugin''<ref>https://www.rabbitmq.com/management.html</ref>. Pour l'activer :
<pre>
/usr/sbin/rabbitmq-plugins enable rabbitmq_management
</pre>
Test de fonctionnement depuis le serveur :
<pre>
curl localhost:15672
</pre>
Depuis le client : http://mon_serveur:15672
On la trouve aussi sur Docker<ref>https://hub.docker.com/_/rabbitmq</ref>.
Pour trouver le fichier de configuration :
cat /usr/sbin/rabbitmq-server | grep RABBITMQ_ENV
== Connexion ==
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 :
<pre>
curl -i -u guest:guest http://localhost:15672/api/whoami
</pre>
Si cela ne fonctionne pas, configurer le serveur avec <code>rabbitmqctl</code>. Exemple sous Linux :
<pre>
/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
</pre>
=== Connexion PHP ===
<pre>
$connection = new AMQPStreamConnection($host, $port, $login, $password);
...
$connection->close();
</pre>
<div class="remarque"> Sur le framework Symfony, on peut utiliser le composant Messenger à la place.</div>
== Création de queue et routage ==
Pour créer une queue simple prête à recevoir des messages :
<pre>
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue1', false, false, false, false);
</pre>
=== ''Exchange'' ===
{| align=right style="float:right; border: 1px #aaa solid; font-size: 88%; width: 22em; "
|-
! colspan=2 style="font-size: 110%; text-align: center;" | [[Fichier:Nuvola apps kview.svg|30px|alt=icône image|class=noviewer]] Image externe
|-
|- valign="top"
| [[Image:Searchtool.svg|16px|alt=|link=]]
| [//i2.wp.com/blog.knoldus.com/wp-content/uploads/2018/12/exchanges-topic-fanout-direct.png?resize=1024%2C625&ssl=1 Schéma des différents types de routage RabbitMQ] sur le site : <span class="ouvrage" id="Sachdeva2018"><span class="ouvrage" id="Jyoti_Sachdeva2018"><span class="indicateur-langue">(<abbr class="abbr" title="Langue : anglais">en</abbr>)</span> Jyoti Sachdeva, <span lang="en">[https://dzone.com/articles/getting-started-with-rabbitmq-python-1 « <cite style="font-style:normal;">Getting Started With RabbitMQ: Python</cite> »]</span>, <time class="nowrap" datetime="2018-12-20">20 décembre 2018</time></span></span>
|-
|- valign="top"
|-
|}
Une autre manière de poster des messages est en passant par un ''exchange''. On en distingue plusieurs types<ref>https://www.rabbitmq.com/tutorials/tutorial-three-php.html</ref> :
* ''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)<ref>https://www.rabbitmq.com/tutorials/tutorial-five-php.html</ref>.
* ''headers'' : routage par en-tête de message plutôt que par "routing key".
Dans cet exemple, on rattache la queue à un ''exchange'' "Bus" :
<pre>
$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');
</pre>
Exemple de ''topic'' : on ne publie pas dans la queue mais dans l’''exchange'' qui leur routera ensuite le message.
<pre>
$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');
</pre>
=== QoS ===
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 :
<pre>
$this->rabbitMqConnection->getChannel()->basic_qos(null, 1, null);
</pre>
=== DLX ===
Le mode DLX (''<span class="lang-en" lang="en">Dead Letter Exchanges</span>'') permet de transférer un message d'une queue dans une autre après un certain temps<ref>https://www.rabbitmq.com/dlx.html</ref>.
== Production ==
<pre>
$amqpMessage = new AMQPMessage(json_encode('Hello World!'),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$this->rabbitMqConnection->getChannel()->basic_publish($amqpMessage, 'Bus', 'Wikibooks.Queue1');
</pre>
== Consommation ==
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().
<pre>
$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()));
}
</pre>
En mode "topic", on peut remplacer 'Wikibooks.Queue1' par 'Wikibooks.*' pour récupérer toutes les queues.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Cadriciels=
Les '''[[w:cadriciel|cadriciel]]s''', 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 [[Programmation Java|Java]] ou [[:Catégorie:Ruby|Ruby]] on rails, certains cadriciels PHP se démarquent comme [[w:CakePHP|CakePHP]], [[w:PRADO (framework)|PRADO]] et [[w:Symfony|Symfony]].
== Modèle-vue-contrôleur ==
Ils se basent sur le plus utilisé des [[patrons de conception]] : le [[w:Modèle-Vue-Contrôleur|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 ==
* [[w:Liste de frameworks PHP|Liste de frameworks PHP]] sur Wikipédia [[Image:Wikipedia-logo.svg|20px|Article sur Wikipédia|link=w:Liste de frameworks PHP]][[Catégorie:Pages liées à Wikipédia ]]
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=CakePHP=
== Le cadriciel CakePHP ==
'''[[w:CakePHP|CakePHP]]''' est un [[w:framework|framework]] de type ''[[w:Rapid Application Developpement|Rapid Application Developpement]]'' (RAD) utilisant le motif de conception [[w:modèle-vue-contrôleur|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<ref name="">http://manual.cakephp.org/chapter/installing</ref>.
== Prérequis ==
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 ===
*PHP 5 objet : [[Programmation PHP/La programmation orientée objet]]
*Avoir complété : [[Programmation PHP/Exemples/Formulaire|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 ===
*Un [[w:EDI|EDI]] comme [[Eclipse|Eclipse]].
== Installation ==
=== Base de données ===
Créez une nouvelle base de données <code>cake_monapp</code> pour l'utilisateur propriétaire <code>cakedev</code> :
MySQL :
<pre>
CREATE DATABASE cake_monapp;
GRANT ALL PRIVILEGES ON cake_monapp.* TO 'cakedev'@'localhost' IDENTIFIED BY 'plop';
</pre>
PostgreSQL :
<pre>
CREATE USER cakedev WITH PASSWORD 'plop';
CREATE DATABASE cake_monapp OWNER cakedev;
GRANT ALL PRIVILEGES ON DATABASE cake_monapp TO cakedev;
</pre>
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).
<code>/cake/app/config/database.php</code> :
Pour MySQL :
<pre>
var $default = array('driver' => 'mysql',
'connect' => 'mysql_connect',
'host' => 'localhost',
'login' => 'cakedev',
'password' => 'plop',
'database' => 'cake_monapp',
'prefix' => '');
</pre>
Pour PostgreSQL :
<pre>
var $default = array('driver' => 'postgres',
'connect' => 'pg_connect',
'host' => 'localhost',
'login' => 'cakedev',
'password' => 'plop',
'database' => 'cake_monapp',
'prefix' => '');
</pre>
=== Configurations ===
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 [[w:Environnement de développement intégré|EDI]] ==
Vous créerez un nouveau projet qui contiendra la copie locale de l'arborescence sous app/.
== Notes et références ==
<references />
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=PEAR=
== Qu'est-ce que PEAR ? ==
[[w:PEAR|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<ref>http://pear.php.net/</ref>.
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 <code>pear install Nom_du_package</code>.
# La commande <code>php pyrus.phar install pear/Nom_du_package</code>.
== DB ==
L'extension PEAR DB fournit une gamme de fonctions de gestion de [[../Bases de données|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<ref>http://pear.php.net/package/DB</ref>, ce qui offre un inconvénient par rapport à [[#MDB2|PEAR MDB2]].
=== Connexion à la base ===
Se connecter à une base de données revêt la syntaxe suivante :
<pre>
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();
}
</pre>
Il est également possible de remplacer la chaîne de caractères par un tableau contenant vos informations :
<pre>
$dsn = array(
'phptype' => 'mysql',
'username' => 'myAccount',
'password' => '****',
'hostspec' => '127.0.0.1',
'database' => 'tests',
);
</pre>
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 ===
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 :
<pre>
$db->disconnect();
</pre>
=== Envoyer une requête ===
Une fois connecté, vous allez pouvoir envoyer des requêtes à votre BDD comme suit :
<pre>
$query = "SELECT * FROM table WHERE id=5";
$rsc = $db->query($query);
</pre>
=== Récupérer des informations ===
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 <code>mysql_fetch_array()</code> :
<pre>
$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";
}
</pre>
== MDB2 ==
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.
<div style="margin: 5px 5px 5px 50px; padding: 0px 2px; background-color: #ffcc7740; display:flow-root;"><div style="border: solid 1px #FF993380; padding: 0.5em;" class="plainlinks">[[Image:Logo travaux orange.svg|24px|lien=//fr.wikibooks.org/w/index.php?title=Programmation_PHP/Version_imprimable&action=edit]] Cette section est vide, pas assez détaillée ou incomplète. <span class="noprint">[//fr.wikibooks.org/w/index.php?title=Programmation_PHP/Version_imprimable&action=edit Votre aide] est la bienvenue !</span></div></div>[[Catégorie:À faire]]
== Spreadsheet_Excel_Writer ==
Cette bibliothèque fournit des classes de [[Translinguisme/Programmation#Manipulation_de_fichier_Excel|manipulation de fichier .xls sont]]<ref>[http://pear.php.net/package/Spreadsheet_Excel_Writer/docs/latest/Spreadsheet_Excel_Writer/Spreadsheet_Excel_Writer_Format.html pear.php.net]</ref><ref>[http://g-ernaelsten.developpez.com/tutoriels/excelphp/ developpez.com]</ref>.
Pour l'installer, il faut simplement télécharger le paquetage [http://pear.php.net/package/Spreadsheet_Excel_Writer/download Spreadsheet_Excel_Writer], qui utilise [http://pear.php.net/package/OLE/download OLE] et [http://pear.php.net/package/Console_Getopt/download Getopt].
Pour l'utiliser :
<pre>
include "Spreadsheet/Excel/Writer.php;
</pre>
=== Limites ===
Cette bibliothèque n'est plus maintenue depuis 2012<ref>http://pear.php.net/package/Spreadsheet_Excel_Writer/</ref>, on lui préfèrera donc [[../PHPExcel/]]<ref>https://github.com/PHPOffice/PHPExcel</ref><ref>https://phpexcel.codeplex.com/</ref>, 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 : <code>Object of class Spreadsheet_Excel_Writer_Format could not be converted to int</code>.
== phpDocumentor ==
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<ref>https://www.phpdoc.org/</ref> :
<pre>
/**
* @param bool $condition
*
* @return String|null
*/
</pre>
Une méthode qui hérite d'une autre peut aussi hériter de sa doc :
@inheritdoc
== PECL ==
[[w:PECL|PECL]] (prononcé "pickle", pour ''PHP Extension Community Library''), est un gestionnaire de paquets. Exemples :
<pre>
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
</pre>
Pour désinstaller :
<pre>
sudo pecl uninstall memcached
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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<ref>https://github.com/Homebrew/homebrew-core/issues/26108</ref>. Sinon l'erreur suivante survient : ''Unable to load dynamic library 'memcached.so'''. Plusieurs solutions :
<pre>
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
</pre>
ou
<pre>
sudo phpdismod -v 7.2 memcached; sudo phpenmod -v 7.2 memcached
</pre>
Pour vérifier :
<pre>
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
</pre></p>
</div>
</div>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
<div style="page-break-before:always"></div>
<div style="clear:both;page-break-before:always;"></div>
=Symfony=
[[Image:Symfony2.svg|vignette|center]]
:''Pour plus de détails voir'' : '''''[[Programmation PHP avec Symfony]]'''''.
== Présentation ==
'''[[w:Symfony|Symfony]]''' (parfois abrégé '''SF''') est un [[w:cadriciel|cadriciel]] [[w:Modèle-Vue-Contrôleur|MVC]] [[w:Logiciel libre|libre]] écrit en [[w:PHP: Hypertext Preprocessor|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 [[w:Modèle-Vue-Contrôleur|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'[[w:Ajax|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 [[w:YAML|YAML]].
* Un générateur de back-office et un "démarreur de module" ([[w:Échafaudage (programmation)|scaffolding]]).
* Un support de l'[[w:I18N|I18N]] - Symfony est nativement multi-langue.
* Une architecture extensible, permettant la création et l'utilisation de [[Programmation PHP/Symfony/Composant|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 [[w:Twig|Twig]] dont la syntaxe est plus simples.
** Une couche de mapping objet-relationnel ([[w:ORM|ORM]]) et une couche d'abstraction de données (cf. [[w:Doctrine (ORM)|Doctrine]] et son langage DQL<ref>http://guidella.free.fr/General/symfony2AnnotationsReference.html</ref>).
=== Utilisations ===
Plusieurs autres projets notables utilisent Symfony, parmi lesquels :
* https://github.com/drupal/drupal : le [[:Catégorie:système de gestion de contenu|système de gestion de contenu]] (CMS) [[Drupal|Drupal]].
* https://github.com/joomla/joomla-cms : le CMS [[Joomla|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 ===
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 ==
[[Image:Primeros-pasos-symfony2_4.png|vignette|upright=2|Page d'accueil par défaut de Symfony 2.]]
Pour créer un nouveau projet sous Symfony, tapez la commande suivante :
<pre>
composer create-project "symfony/skeleton:^7.2" mon_projet
</pre>
ou avec Symfony CLI :
<pre>
wget <nowiki>https://get.symfony.com/cli/installer</nowiki> -O - | bash
symfony new mon_projet
</pre>
Cette commande a pour effet la création d'un dossier contenant les bases du site web à développer.
== Lancer le projet ==
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 ===
Symfony intègre un serveur web local qu'on peut lancer avec la commande (se placer dans le répertoire du projet auparavant) :
<code>$ symfony server:start -d</code>
En passant <code>open:local</code> en argument de la commande <code>symfony</code>, le projet s'ouvre dans un navigateur :
<code>$ symfony open:local</code>
Ou bien en utilisant le serveur web intégré à php
$ php -S localhost:8000 -t public
=== Serveur web de production ===
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 profesionnels. Il faudra alors ajouter un vhost et un nom de domaine dédiés au site Symfony<ref>http://mikinfo.free.fr/index.php/configurer-les-virtualhosts-pour-symfony/</ref><ref>https://symfony.com/doc/current/setup/web_server_configuration.html</ref>. Pour le test, le domaine peut juste figurer dans <code>/etc/hosts</code>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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.</p>
</div>
</div>
== Configurer le projet ==
=== Paramètres dev et prod ===
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 <code>config/packages</code>. 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 :
<pre>
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
</pre>
Les fichiers .yml contenant les variables globales sont dans <u>app\config\</u>.
Par exemple en SF2 et 3, le mot de passe et l'adresse de la base de données sont modifiables en éditant <code>parameters.yml</code> (non versionné et créé à partir du <code>parameters.yml.dist</code>). L'environnement de test passe par web/app_dev.php, et le mode debug y est alors activé par la ligne <code>Debug::enable();</code> (testable avec <code>%kernel.debug% = 1</code>).
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 : <code>'%env(APP_SECRET)%'</code>. Le mode debug est activé avec <code>APP_DEBUG=1</code> dans ce fichier .env.
<div class="remarque"> En YAML, on préfèrera déclarer les services avec des simples quotes car les doubles nécessitent d'échapper les antislashs.</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Les variables d'environnement du système d'exploitation peuvent remplacer celles des .env.</p>
</div>
</div>
== Upgrade de version majeure ==
Installation ou mise à jour des versions précédentes :
* [[Programmation PHP avec Symfony/Symfony 3|Symfony 3]]
* [[Programmation PHP avec Symfony/Symfony 4|Symfony 4]]
* [[Programmation PHP avec Symfony/Migration de Symfony 5 à 6|Migration de Symfony 5 à 6]]
* [[Programmation PHP avec Symfony/Migration de Symfony 6 à 7|Migration de Symfony 6 à 7]]
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
* <span class="ouvrage">[http://trac.symfony-project.org/wiki/Resources/fr_FR « <cite style="font-style:normal;">Wiki officiel</cite> »]</span>
* <span class="ouvrage">[https://openclassrooms.com/courses/developpez-votre-site-web-avec-le-framework-symfony « <cite style="font-style:normal;">Tutoriel openclassrooms.com</cite> »]</span>
* <span class="ouvrage">[http://c-maneu.developpez.com/tutorial/web/php/symfony/intro/ « <cite style="font-style:normal;">Tutoriel developpez.com</cite> »]</span>
* <span class="ouvrage"><span class="indicateur-langue">(<abbr class="abbr" title="Langue : anglais">en</abbr>)</span> <span lang="en">[http://symfony.com/pdf/Symfony_cookbook_3.1.pdf « <cite style="font-style:normal;">Symfony 3.1 cookbook</cite> »]</span></span> : livre officiel de 500 pages
* <span class="ouvrage"><span class="indicateur-langue">(<abbr class="abbr" title="Langue : anglais">en</abbr>)</span> <span lang="en">[https://knpuniversity.com/screencast/symfony « <cite style="font-style:normal;">Charming Development in Symfony 5</cite> »]</span></span> (texte et vidéo)
== Voir aussi ==
* [irc://irc.freenode.net/symfony #symfony] : canal IRC (#symfony sur [[w:Freenode|Freenode]])
* [irc://irc.freenode.net/symfony-fr #symfony-fr] : canal IRC francophone (#symfony-fr sur [[w:Freenode|Freenode]])
* https://sonata-project.org/get-started : un CMS basé sur Symfony
== Principe ==
Le principe des services Symfony est d'éviter d'instancier la plupart des classes avec des "new" dispersés dans le code, pour les déclarer une seule fois, grâce au ''container''. Ils sont alors instanciés uniquement s'ils sont utilisés (ex : sur la page web courante), grâce au [[wikt:lazy loading|lazy loading]] du ''container''<ref>https://symfony.com/doc/3.4/service_container.html</ref>.
Cette déclaration peut se faire en PHP, en YAML ou en XML. On baptise alors le service (il peut y en avoir plusieurs par classe), et on appelle ses arguments par leur nom de service. Exemple :
<pre>
services:
app.my_namespace.my_service:
class: App\myNamespace\myServiceClass
arguments:
- '%parameter%'
- '@app.my_namespace.my_other_service'
</pre>
=== Pas de include ou require ===
Les classes natives de PHP doivent être introduites par leur namespace ou bien par l'espace de nom global. Ex :
<pre>
use DateTime;
echo new DateTime();
</pre>
ou
<pre>
echo new \DateTime();
</pre>
== Autowiring ==
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'"[[wikt:autowiring|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<ref>https://symfony.com/doc/current/service_container/autowiring.html</ref>.
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 :
<pre>
imports:
- { resource: services1.yaml }
- { resource: services2.yaml }
</pre>
ou :
<pre>
imports:
- { resource: services/* }
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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 <code>_defaults</code> dans le nouveau .yaml.</p>
</div>
</div>
Exemple d'exclusion récursive de plusieurs dossiers de même nom, avec ** :
<pre>
App\:
resource: '../src/*'
exclude:
- '../src/UnDossier'
- '../src/**/Entity' # Tous les sous-dossiers "Entity"
</pre>
=== bind ===
Par défaut, l'autowiring ne fonctionne pas avec les classes avec des tags, ou ayant autre chose que des services dans leurs constructeurs<ref>https://symfony.com/doc/current/service_container/autowiring.html#fixing-non-autowireable-arguments</ref>. Néanmoins pour injecter des scalaires automatiquement, il suffit que ces derniers soit déclarés aussi. Ex :
<pre>
services:
_defaults:
bind:
$salt: 'ma_chaine_de_caractères'
$variableSymfony: '%kernel.project_dir%'
$variableDEnvironnement: '%env(resolve:APP_DEBUG)%'
</pre>
=== _instanceof ===
Pour ajouter un tag ou injecter un service si on implémente une interface. Ex :
<pre>
services:
_instanceof:
Psr\Log\LoggerAwareInterface:
calls:
- [ 'setLogger', [ '@logger' ] ]
</pre>
Ici, toutes les classes qui implémentent <code>LoggerAwareInterface</code> verront leurs méthodes <code>setLogger(LoggerInterface $logger)</code> appelées automatiquement à l’instanciation.
=== En SF <2.8 ===
Les contrôleurs sont des services qui peuvent en appeler avec la méthode héritée de leur classe mère :
<pre>
$this->get('app.my_namespace.my_service')
</pre>
Pour déterminer si un service existe depuis un contrôleur :
<pre>
$this->getContainer->hasDefinition('app.my_namespace.my_service')
</pre>
== Paramètres ==
Chaque service doit donc être déclaré avec un paramètre "class", puis peut ensuite facultativement contenir les paramètres suivants :
{| class="wikitable sortable"
|+ Paramètres des services en YAML
! 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<ref>https://symfony.com/doc/current/service_container/factories.html</ref>.
|-
| configurator || Exécute un invocable donné après l'instanciation de la classe<ref>https://symfony.com/doc/current/service_container/configurators.html</ref>.
|-
| 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<ref>https://symfony.com/doc/current/testing.html</ref>).
|-
| parent || Nom de la [[wikt:superclasse|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 [[wikt:singleton|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''<ref>https://symfony.com/doc/current/service_container/compiler_passes.html</ref>.
|-
| 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)<ref>https://symfony.com/doc/current/service_container/service_decoration.html</ref>
|}
=== Injecter des services tagués ===
Dans un constructeur :
<pre>
App\Service\FactoriesHandler:
arguments:
- !tagged_iterator app.factory
</pre>
Dans une autre méthode :
<pre>
App\Service\FactoriesHandler:
calls:
- [ 'setFactories', [!tagged_iterator app.factory] ]
</pre>
Par défaut, l'itérateur contient des clés numériques, mais on peut les personnaliser<ref>https://symfony.com/doc/5.4/service_container/tags.html#tagged-services-with-index</ref>. Ex :
<pre>
App\Factory\FactoryOne:
tags:
- { name: 'app.factory', my_key: 'factory_one' }
App\Service\FactoriesHandler:
arguments:
- !tagged_iterator { tag: 'app.factory', key: 'my_key' }
</pre>
=== Service abstrait ===
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 <code>logger</code> (comme l'exemple <code>_instanceof</code> ci-dessus), plus la méthode <code>setLogger()</code> de leur classe abstraite, sans avoir à toucher à leurs constructeurs :
<pre>
App\Controller\:
resource: '../src/Controller'
parent: App\Controller\AbstractEntitiesController
tags: ['controller.service_arguments']
App\Controller\AbstractEntitiesController:
abstract: true
autoconfigure: false
calls:
- [ 'setLogger', [ '@logger' ] ]
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Principe ==
Les contrôleurs Symfony sont les classes qui définissent les opérations à réaliser quand on visite les pages du sites<ref>https://symfony.com/doc/current/controller.html</ref> : 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" :
<pre>
class HelloWorldController extends AbstractController
{
#[Route(path: '/', name: 'helloWorld')]
#[Route(path: '/helloWorld', name: 'helloWorld')]
public function indexAction(Request $request): Response
{
return new Response('Hello World!');
}
}
</pre>
NB : en PHP < 8, remplacer l'attribut par une annotation :
<pre>
/**
* @Route("/", name="helloWorld")
* @Route("/helloWorld")
*/
</pre>
== Retours ==
Ces méthodes peuvent déboucher sur plusieurs actions :
* <code>Response()</code> : affiche un texte, et facultativement un [[w:Liste des codes HTTP|code HTTP]] en deuxième paramètre (ex : erreur 404).
** <code>JsonResponse()</code> ou <code>$this->json()</code> : affiche du JSON.
** <code>RedirectResponse()</code> : renvoie vers une autre adresse. Si elle se trouve dans la même application, on peut aussi utiliser le <code>$this->forward()</code> hérité du contrôleur abstrait.
** <code>BinaryFileResponse()</code> : renvoie un fichier à télécharger (à partir de son chemin).
* <code>$this->redirect('mon_url')</code> : redirige à une autre adresse.
* <code>$this->redirectToRoute('nom_de_la_route');</code> : redirige vers une route du site par son nom.
* <code>$this->generateUrl('app_mon_chemin', []);</code> : redirige vers une URL relative (ajouter <code>UrlGeneratorInterface::ABSOLUTE_URL</code> en paramètre 3 pour l'absolue, car il est à <code>UrlGeneratorInterface::ABSOLUTE_PATH</code> par défaut dans SF3).
* <code> $this->container->get('router')->generate('app_mon_chemin', ['paramètre' => 'mon_paramètre']);</code>.
* <code>$this->render()</code> : affiche une page à partir d'un template, par exemple HTML ou Twig.
<div class="remarque"> On peut changer les options d'encodage en JSON ainsi :
<pre>
$response = new JsonResponse();
$response->setEncodingOptions(JSON_UNESCAPED_UNICODE);
$response->setData($data);
return $response;
</pre>
</div>
== Requêtes ==
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 ==
On peut injecter un ID dans l'URL ou la requête pour le CRUD d'une [[Programmation PHP avec Symfony/Doctrine|entité]], mais grâce au paramConverter on peut aussi injecter directement l'entité. Ex :
<pre>
#[Route('/my_entity/{id}', methods: ['GET'])]
public function getProduct(MyEntity $myEntity): JsonResponse
{
return new JsonResponse($myEntity);
}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Avant Symfony 6.2 cela fonctionne avec un <code>composer require sensio/framework-extra-bundle</code>.</p>
</div>
</div>
== Flashbag ==
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<ref>https://stackoverflow.com/questions/14449967/symfony-setting-flash-and-checking-in-twig</ref> :
<pre>
{% for flashMessage in app.session.flashbag.get('success') %}
{{ flashMessage }}
{% endfor %}
</pre>
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''.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Le fait de lire les flash (au moins depuis les Twig avec app.flashes) vide leur tableau.</p>
</div>
</div>
== Accès aux paramètres et services ==
Les contrôleurs étendent la [[w:classe abstraite|classe abstraite]] <code>Symfony\Bundle\FrameworkBundle\Controller\AbstractController</code>. Cela leur permettait entre autres dans Symfony 2, de récupérer les services et paramètres ainsi :
<syntaxhighlight lang=php>
dump($this->get('session'));
dump($this->getParameter('kernel.project_dir'));
</syntaxhighlight>
Depuis Symfony 4, il faut injecter le service ''service_container'' pour accéder à la liste des services publiques (<code>public: true</code> en YAML), mais la bonne pratique est d'injecter uniquement les services nécessaires dans le constructeur<ref>https://symfony.com/doc/current/best_practices.html#use-dependency-injection-to-get-services</ref><ref>https://symfony.com/doc/4.0/best_practices/controllers.html#fetching-services</ref>.
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 ==
Par exemple pour créer une nouvelle page sur l'URL :
<pre>
http://localhost:8000/test
</pre>
Installer le routage :
<pre>
composer require sensio/framework-extra-bundle
composer require symfony/routing
</pre>
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 [[w:Modèle-vue-contrôleur|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 :
<pre>
<?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);
}
}
</pre>
NB : en PHP < 8, remplacer l'attribut par une annotation :
<pre>
/**
* @Route("/test/{numero}", name="test", requirements={"id"="\d*"}, methods={"GET|POST"}, priority=-1)
*/
</pre>
Autres exemples de prérequis :
* <code>requirements={"id"="fr|en"}</code>
* <code>requirements={"id"="MaClasse::MA_CONSTANTE1|MaClasse::MA_CONSTANTE2"}</code>
* <code>requirements={"id"="(?!api/doc|_profiler).*"}</code>
<div class="remarque"> On peut placer plusieurs attributs ou annotations "route" sur une même méthode. Il est alors possible de changer son comportement selon la route avec <code>if ($request->get('_route') === 'test')</code>.</div>
=== Alias ===
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 <u>config/routes.yaml</u> (anciennement <u>app\config\routing.yml</u> sur Symfony < 4) :
<pre>
test:
path: /test/{numero}
defaults: { _controller: AppBundle:Test:HelloWorld }
</pre>
À présent http://localhost:8000/test/1 ou http://localhost:8000/test/2 affichent "Hello World!".
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">
* 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.
</p>
</div>
</div>
=== Redirection vers la dernière page visitée ===
Une astuce pour rediriger l'utilisateur vers la dernière page qu'il avait visité :
<pre>
$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)
);
</pre>
=== Annotations.yaml ===
Ce fichier permet de définir des groupes de contrôleurs, dont les routes sont préfixées. Ex :
<pre>
back_controllers:
resource: ../../src/Controller/BackOffice
type: annotation
prefix: admin
front_controllers:
resource: ../../src/Controller/FrontOffice
type: annotation
prefix: api
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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.</p>
</div>
</div>
<div class="remarque"> security.yaml utilise les voteurs : des classes qui écoutent des évènements pour vérifier les permissions de l'utilisateur logué<ref>https://symfony.com/doc/current/security/voters.html</ref>.</div>
=== Paramètres spéciaux ===
Il existe quatre paramètres spéciaux que l'on peut placer dans routes.yaml ou en argument des méthodes des contrôleurs<ref>https://symfony.com/doc/current/routing.html#special-routing-parameters</ref> :
* _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 :
<pre>
#[Route('/controller_route', requirements: ['_locale' => 'en|fr'])]
class MyController extends AbstractController
</pre>
== Vue ==
Pour commencer à créer des pages plus complexes, il suffit de remplacer :
<pre>
return new Response('Hello World!');
</pre>
par une vue issue d'un [[w:Catégorie:Moteur de template|moteur de template]]. Celui de Symfony est [[Programmation PHP avec Symfony/Twig|Twig]] :
<pre>
return $this->render('helloWorld.html.twig');
</pre>
Pour installer les bibliothèques JavaScript qui agiront sur ces pages, se positionner dans <u>/public</u>. Exemple :
<pre>
cd public/
sudo apt-get install npm
npm install --save jquery
npm install --save bootstrap
</pre>
Ensuite il suffit de les appeler dans <u>/templates/helloWorld.html.twig</u> pour pouvoir les utiliser :
<pre>
<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>
</pre>
== Modèle ==
Pour gérer le modèle du MVC, c'est-à-dire la structure des données stockées, l'[[w:Mapping objet-relationnel|ORM]] officiel de Symfony se nomme [[Programmation PHP avec Symfony/Doctrine|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 ==
:''Pour plus de détails voir'' : '''''[[Programmation PHP avec Symfony/HttpClient#Tests]]'''''.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Principe ==
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 [[w:Interface en ligne de commande|CLI]]).
La liste des commandes disponibles en console est visible avec :
* Sur Linux :
bin\console
* Sur Windows :
php bin\console
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Dans Symfony 2 c'était <code>php app\console</code>.</p>
</div>
</div>
Parmi les principales commandes natives au framework et à ses bundles, on trouve :
* <code>php bin/console list</code> : liste toutes les commandes du projet.
* <code>php bin/console debug:router</code> : liste toutes les routes (URL) du site.
* <code>php bin/console debug:container</code> : liste tous les services avec leurs alias (qui sont des instanciations des classes).
* <code>php bin/console debug:container --parameters</code> : liste les paramètres.
* <code>php bin/console debug:container --env-vars</code> : liste les variables d'environnement.
* <code>php bin/console debug:autowiring --all</code> : liste tous les services automatiquement déclarés.
* <code>php bin/console debug:config NomDuBundle</code> : liste tous les paramètres disponibles pour paramétrer un bundle donné. Ex : <code>bin/console debug:config FrameworkBundle</code>
* <code>php bin/console cache:clear</code> : vide la mémoire cache du framework.
* <code>php bin/console generate:bundle</code> : crée un bunble (surtout pour SF2).
* <code>php bin/console generate:controller</code> : crée un contrôleur (en SF2).
* <code>php bin/console doctrine:migrations:generate; chown 1001:1001 -R app/DoctrineMigrations</code> : génère un fichier vide de migration SQL ou DQL.
* <code>php bin/console doctrine:migrations:list</code> : 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 ==
Lors du lancement d'une commande, on distingue deux types de paramètres<ref>https://symfony.com/doc/current/console/input.html</ref> :
# Les arguments : non nommés
# Les options : nommées.
Exemple :
bin/console app:ma_commande argument1 --option1=test
<syntaxhighlight lang=php>
#[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;
}
}
</syntaxhighlight>
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 <code>InputArgument::IS_ARRAY</code> et séparer les valeurs par un espace. Ex :
bin/console app:my_command arg1.1 arg1.2 arg1.3
=== Ajout de logs ===
Pour que la commande logue ses actions, la documentation de Symfony propose deux solutions<ref>https://symfony.com/doc/current/console/style.html</ref> :
* $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<ref>https://www.php-fig.org/psr/psr-3/</ref> et si on veut utiliser ces logs comme ceux des autres services (pour les stocker ailleurs par exemple), mieux vaut utiliser <code>LoggerInterface $logger</code> (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 ==
Ex :
<syntaxhighlight lang=php>
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);
}
</syntaxhighlight>
<div class="remarque"> L'option "dry-run" est recommandée pour éviter que la commande modifie des ressources externes, comme une base de données ou une API. Il faut pour cela que la commande en tienne compte au moment d'appeler ces ressources.</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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<ref>https://stackoverflow.com/questions/30664606/how-to-assert-a-line-is-logged-using-monolog-inside-symfony/31999172#31999172</ref> :
<pre>
if ($this->logger instanceof Logger) {
$this->logger->pushHandler(new ConsoleHandler($output));
}
</pre>
</p>
</div>
</div>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Description ==
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 : [[Patrons de conception/Injection de dépendance|Injection de dépendance]]
* decorator : [[Patrons de conception/Décorateur|Décorateur]].
* shared : [[Patrons de conception/Singleton|Singleton]].
* factory : [[Patrons de conception/Fabrique|Fabrique]]<ref>https://symfony.com/doc/current/service_container/factories.html</ref>.
Toutefois, on peut lui ajouter des composants<ref>https://symfony.com/components</ref>, dont il convient de connaitre les fonctionnalités pour ne pas réinventer la roue. Pour les installer :
<pre>
composer require symfony/nom_du_composant
</pre>
Les quatre premiers ci-dessous sont inclus par défaut dans le microframework <code>symfony/skeleton</code>.
== framework-bundle ==
Structure la configuration principale du framework sans laquelle aucun composant n'est installable<ref>https://symfony.com/doc/current/reference/configuration/framework.html</ref>.
== console ==
[[Patrons de conception/Commande|Patrons de conception "Commande"]].
Fournit la possibilité d'exécuter le framework avec des commandes shell<ref>https://symfony.com/doc/current/components/console.html</ref>. Par exemple pour obtenir la liste de toutes les commandes disponibles dans un projet :
<pre>
php bin/console help list
</pre>
== dotenv ==
Gère les variables d'environnement non versionnées, contenues dans un fichier .env<ref>https://symfony.com/doc/current/components/dotenv.html</ref>. Elles peuvent aussi bénéficier de [[wikt:type checking|type checking]] en préfixant les types avec ":". Ex de .env :
<pre>
IS_DEV_SERVER=1
</pre>
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") :
<pre>
is_dev_server: '%env(bool:IS_DEV_SERVER)%'
</pre>
Il existe plusieurs processeurs de variable d'environnement (en plus de "bool" et des autres types)<ref>https://symfony.com/doc/current/configuration/env_var_processors.html</ref> :
* <code>base64:</code> encode en base64.
* <code>default:</code> remplace le deuxième paramètre par le premier si absent. Ex :
** <code>$addTestValues: '%env(bool:default::ADD_TEST_VALUES)%'</code> injecte "null" si ADD_TEST_VALUES n'est pas défini.
** <code>$addTestValues: '%env(bool:default:ADD_TEST_VALUES2:ADD_TEST_VALUES1)%'</code> injecte le contenu de ADD_TEST_VALUES2 si ADD_TEST_VALUES1 n'est pas défini.
* <code>file:</code> remplace le chemin d'un fichier par son contenu.
* <code>not:</code> renvoie l'inverse.
* <code>require:</code> fait un require() PHP.
* <code>resolve:</code> remplace le nom d'une variable par sa valeur.
* <code>trim:</code> fait un trim() PHP.
Pour définir une valeur par défaut en cas de variable d'environnement manquante (sans utiliser <code>default:</code>), dans ''services.yaml, parameters:'' :
<pre>
env(MY_MISSING_CONSTANT): '0'
</pre>
== yaml ==
Ajoute la conversion de fichier [[w:YAML|YAML]] en tableau PHP<ref>https://symfony.com/doc/current/components/yaml.html</ref>. 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 ==
[[Patrons de conception/Façade|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 ==
Permet de convertir des objets en tableaux ou dans les principaux formats de notation : JSON, XML, YAML et CSV<ref>https://symfony.com/doc/current/components/serializer.html</ref>.
<pre>
composer require symfony/serializer
</pre>
Ce composant est notamment utilisé pour créer des APIs.
== form ==
Construit des formulaires HTML.
:''Pour plus de détails voir'' : '''''[[Programmation PHP avec Symfony/Formulaire]]'''''.
== validator ==
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<ref>https://symfony.com/doc/current/validation/sequence_provider.html</ref>.
=== Exemples ===
* Dans une entité :
<pre>
use Symfony\Component\Validator\Constraints as Assert;
...
#[Assert\Email]
private ?string $email = null;
</pre>
* Dans un formulaire (inutile à faire si c'est déjà dans l'entité) :
<pre>
use Symfony\Component\Validator\Constraints\Email;
...
$builder->add('email', EmailType::class, [
'required' => false,
'constraints' => [new Email()],
])
</pre>
== translation ==
Les traductions sont stockées dans un fichier différent par domaine et par langue (code [[w:ISO 639|ISO 639]]). Les formats acceptés sont YAML, XML, PHP<ref>https://symfony.com/doc/current/translation.html</ref>.
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 <code>translations/messages.fr.yml</code> ou <code>translations/messages.fr-FR.yml</code>.
=== Installation ===
<pre>
composer require symfony/translation
</pre>
Pour avoir les traductions inutilisées en anglais :
<pre>
bin/console debug:translation en --only-unused
</pre>
Pour les traductions manquantes en anglais :
<pre>
bin/console debug:translation en --only-missing
</pre>
On peut restreindre à un seul domaine avec une option : <code>--domain=mon_domaine</code>
=== Traduction en PHP ===
Le domaine et la langue sont facultatifs (car ils ont des valeurs par défaut) :
<pre>
$translator->trans('Hello World', domain: 'login', locale: 'fr_FR');
</pre>
<div class="remarque"> le composant Symfony Form appelle automatiquement Translations pour ses "labels".</div>
=== Traduction en Twig ===
Les traductions en Twig sont appelées par le [[Programmation PHP avec Symfony/Twig#Filtre_trans|filtre "trans"]] :
<pre>
{% trans_default_domain 'login' %}
{{ 'Hello World' |trans }}
</pre>
Ou :
<pre>
{{ 'Hello World' |trans({}, 'login', 'fr-FR') }}
</pre>
=== Variables ===
* YAML : la variable est entre accolades (selon la norme de l'[[w:International Components for Unicode|ICU]]<ref>https://symfony.com/doc/current/reference/formats/message_format.html</ref>)
<pre>
Hello World: 'Hello World name!'
</pre>
* Twig :
<pre>
{{ Hello World |trans({"name": userName}) }}
</pre>
* PHP
<pre>
$translator->trans('Hello World', ['name' => $userName]);
</pre>
Dans un formulaire Symfony :
<pre>
$builder
->add('hello', TextType::class,([
'label' => 'Hello World',
'label_translation_parameters' => [
'name' => $userName,
]
]))
;
</pre>
Par défaut le domaine de traduction est "message" mais on peut désactiver ces dernières avec : <code>choice_translation_domain => false</code>.
== event-dispatcher ==
[[Patrons de conception/Observateur|Patrons de conception "Observateur"]]<ref>http://www.jpsymfony.com/design_patterns/le-design-pattern-observer-avec-symfony2</ref> et [[Patrons de conception/Médiateur|"Médiateur"]]<ref>https://github.com/certificationy/symfony-pack/blob/babd3fee68a7e793767f67c6df140630f52e7f8d/data/architecture.yml#L13</ref>.
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 ==
Permet de lancer des sous-processus en parallèle<ref>https://symfony.com/doc/current/components/process.html</ref>. Exemple qui lance une commande shell :
<pre>
$process = new Process(['ls']);
$process->run();
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">En l'absence de <code>$process->stop()</code> ou de timeout, le sous-processus peut être stoppé en redémarrant le serveur PHP.</p>
</div>
</div>
Exemple de requête SQL asynchrone<ref>https://gist.github.com/appaydin/42eaf953172fc7ea6a8b193694645324</ref> :
<pre>
$sql = 'SELECT * FROM ma_table LIMIT 1';
$process = Process::fromShellCommandline(sprintf('../bin/console doctrine:query:sql "%s"', $sql));
$process->setTimeout(3600);
$process->start();
</pre>
== cache ==
Gère les connexions, lectures et écritures vers des serveurs de mémoire caches tels que [[w:Redis|Redis]] ou [[w:Memcached|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 ==
Ajoute la fonction Twig <code>asset()</code> pour accéder aux fichiers CSS, JS ou images selon leurs versions<ref>https://symfony.com/doc/current/components/asset.html</ref>.
== webpack-encore ==
Intégration de [[w:en:Webpack|Webpack]] pour gérer la partie [[wikt:front end|front end]] (ex : minifications des CSS et JS).
=== Installation<ref>https://symfonycasts.com/screencast/stimulus/encore</ref> ===
<pre>
composer require symfony/webpack-encore-bundle
yarn install
yarn build
</pre>
NB : si [[w:Yarn|Yarn]] n'est pas installé, le faire avec [[w:npm|npm]] : <code>apt install nodejs npm; npm install --global yarn</code>.
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 : <code>encore_entry_link_tags()</code> et <code>encore_entry_script_tags()</code>.
Par ailleurs, cela installe le framework JS Stimulus, et interprète les [[wikt:attributs de données|attributs de données]] pour appeler ses contrôleurs ou méthodes.
=== Rebuild ===
Pour que le code se build en cours de frappe, deux solutions<ref>https://symfony.com/doc/current/frontend/encore/simple-example.html</ref> :
* 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 ==
[[Patrons de conception/Chaîne de responsabilité|Patrons de conception "Chaîne de responsabilité"]].
Messenger permet d'utiliser des queues au protocole [[w:AMQP|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''<ref>https://vria.eu/delve_into_the_heart_of_the_symfony_messenger/</ref>. On peut aussi persister ces messages en les envoyant dans des ''transports'' via un [[wikt:DSN|DSN]], par exemple dans RabbitMQ, Redis ou Doctrine (donc une table des SGBD les plus populaires).
<pre>
php bin/console debug:messenger
</pre>
Chaque middleware doit passer le relais au suivant ainsi :
<pre>
return $stack->next()->handle($envelope, $stack);
</pre>
Pour stopper le message dans un middleware sans qu'il arrive aux handlers :
<pre>
return $envelope;
</pre>
== workflow ==
[[Patrons de conception/Commande|Patrons de conception "État"]].
Ce composant nécessite de créer (en YAML, XML ou PHP) la configuration d'un [[w:automate fini|automate fini]]<ref>https://symfony.com/doc/current/workflow.html</ref>, c'est-à-dire la liste de ses transitions et états (appelés "places").
Ces graphes sont ensuite visualisables en image ainsi :
<pre>
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Dumper\StateMachineGraphvizDumper;
class WorkflowDisplayer
...
$definition = new Definition($places, $transitions);
echo (new StateMachineGraphvizDumper())->dump($definition);
</pre>
<pre>
sudo apt install graphviz
php WorkflowDisplayer.php | dot -Tpng -o workflow.png
</pre>
== browser-kit ==
Simule un navigateur pour les tests d'intégration.
== config ==
Permet de manipuler des fichiers de configurations.
== contracts ==
Pour la [[w:programmation par contrat|programmation par contrat]].
== css-selector ==
Pour utiliser [[w:XPath|XPath]].
== debug ==
Fournit des méthodes statiques pour déboguer le PHP.
== dependency-injection ==
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''<ref>https://symfony.com/doc/current/components/dependency_injection/compilation.html</ref>.
== dom-crawler ==
Fournit des méthodes pour parcourir le [[w:DOM|DOM]].
== expression-language ==
[[Patrons de conception/Interpréteur|Patrons de conception "Interpréteur"]].
''Expression language ''sert à évaluer des expressions, ce qui peut permettre de définir des règles métier<ref>https://symfony.com/doc/current/components/expression_language.html</ref>.
Installation :
composer require symfony/expression-language
Exemple :
<pre>
$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
</pre>
== filesystem ==
Méthodes de lecture et écriture dans les dossiers et fichiers.
== finder ==
Recherche dans les dossiers et fichiers.
== security ==
Ensemble de sous-composants assurant la sécurité d'un site. Ex : authentification, anti-[[w:CSRF|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 <code>$this->tokenStorage->getToken()->getUser()</code>.
== guard ==
Extension de sécurité pour des authentifications complexes.
== http-client ==
Pour lancer des requêtes HTTP depuis l'application.
:''Pour plus de détails voir'' : '''''[[Programmation PHP avec Symfony/HttpClient]]'''''.
== http-foundation ==
Fournit des classes pour manipuler les requêtes HTTP, comme ''Request'' et ''Response'' que l'on retrouve dans les contrôleurs.
Par exemple :
<pre>
use Symfony\Component\HttpFoundation\Response;
//...
echo Response::HTTP_OK; // 200
echo Response::HTTP_NOT_FOUND; // 404
</pre>
== http-kernel ==
Permet d'utiliser des évènements lors des transformations des requêtes HTTP en réponses.
== inflector ==
Deprecated depuis Symfony 5.
Accorde les mots anglais au pluriel à partir de leurs singuliers.
== intl ==
Internationalisation, comme par exemple la classe "Locale" pour gérer une langue.
== ldap ==
Connexion aux serveur [[w:LDAP|LDAP]].
== lock ==
Pour verrouiller les accès aux ressources<ref>https://symfony.com/doc/current/components/lock.html</ref>.
Par exemple, pour ne pas qu'une commande soit lancée deux fois simultanément, bien que le composant console aie aussi cette fonctionnalité :
<pre>
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;
}
</pre>
== Maker bundle ==
Pour créer ou recréer des classes à partir de déductions<ref>https://symfony.com/bundles/SymfonyMakerBundle/current/index.html</ref>.
<pre>
composer require --dev symfony/maker-bundle
</pre>
NB : ce composant ne permet pas de générer des entités à partir d'une base de données.
== mailer ==
Pour envoyer des emails.
== mime ==
Manipulation des messages [[w:MIME|MIME]].
== notifier ==
Pour envoyer des notifications telles que des emails, des SMS, des messages instantanés, etc.
== options-resolver ==
Gère les remplacements de propriétés par d'autres, avec certaines par défaut.
== phpunit-bridge ==
[[Patrons de conception/Pont|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 ==
Pour lire les attributs de classe à partir de leurs getters, ou des tableaux.
== property-info ==
Pour lire les métadonnées des attributs de classe.
== stopwatch ==
Chronomètre pour mesurer des temps d'exécution.
== string ==
API convertissant certains objets en chaine de caractères. Ex :
<pre>
use Symfony\Component\String\Slugger\AsciiSlugger;
$slugger = new AsciiSlugger();
echo $slugger->slug('caractères spéciaux € $');
</pre>
Résultat :
caracteres-speciaux-EUR
== templating ==
Extension de construction de templates.
:''Pour plus de détails voir'' : '''''[[Programmation PHP avec Symfony/Templating]]'''''.
== var-dumper ==
Ajoute une fonction globale <code>dump()</code> pour déboguer des objets en les affichant avec une coloration syntaxique et des menus déroulant.
Ajoute aussi <code>dd()</code> pour ''dump() and die()''.
== var-exporter ==
Permet d'instancier une classe sans utiliser son constructeur.
== polyfill* ==
On trouve aussi une vingtaine de composants [[wikt:polyfill|polyfill]], fournissant des fonctions PHP retirées dans les versions les plus récentes.
== Composants désuets ==
=== locale (<= v2.3) ===
Arrêté en 2011, car remplacé par le composant ''intl''<ref>https://symfony.com/components/Locale</ref>.
=== icu (<= v2.6) ===
Arrêté en 2014, car remplacé par le composant ''intl''<ref>https://symfony.com/components/Icu</ref>.
=== class-loader (<= v3.3) ===
Arrêté en 2011, car remplacé par composer.json<ref>https://symfony.com/components/ClassLoader</ref>.
== Ajoutés en 2020 ==
=== Uid ''(sic)'' (>= v5.1) ===
Pour générer des [[w:UUID|UUID]]<ref>https://symfony.com/doc/current/components/uid.html</ref>.
=== RateLimiter (>= v5.2) ===
[[Patrons de conception/Proxy|Patron de conception "Proxy"]], qui permet de limiter la consommation de ressources du serveur par les clients<ref>https://symfony.com/doc/current/rate_limiter.html</ref>
Installation :
<pre>
composer require symfony/rate-limiter
</pre>
Pour l'activer, ajouter la ligne suivante dans les pare-feux de security.yaml concernés :
<pre>
login_throttling: true
</pre>
=== Semaphore (>= v5.2) ===
Pour donner l'exclusivité d'accès à une ressource<ref>https://symfony.com/doc/current/components/semaphore.html</ref>.
== Ajoutés en 2021 ==
=== PasswordHasher (>= v5.3) ===
Pour gérer les chiffrements<ref>https://symfony.com/blog/new-in-symfony-5-3-passwordhasher-component</ref>.
=== Runtime (>= v5.3) ===
Pour le démarrage (bootstrap) : permettre de découpler l'application de son code de retour.
<ref>https://symfony.com/blog/new-in-symfony-5-3-runtime-component</ref>.
== Ajoutés en 2022 ==
=== HtmlSanitizer (>= v6.1) ===
=== Clock (>= v6.2) ===
=== Symfony UX (>= v5.4) ===
==== ux-autocomplete ====
==== ux-chartjs ====
Utilise Chart.js via Stimulus pour afficher des graphiques, via la fonction Twig <code>render_chart()</code><ref>https://symfony.com/bundles/ux-chartjs/current/index.html</ref>.
==== ux-react ====
Ajoute le framework [[w:React.js|React.js]].
:''Pour plus de détails voir'' : '''''[[Programmation PHP avec Symfony/Stimulus]]'''''.
==== ux-vue ====
Ajoute le framework [[w:Vue.js|Vue.js]].
== Ajoutés en 2023 ==
=== Webhook et RemoteEvent (>= v6.3) ===
<ref>https://symfony.com/blog/new-in-symfony-6-3-webhook-and-remoteevent-components</ref>
=== AssetMapper (>= v6.3) ===
<ref>https://symfony.com/blog/new-in-symfony-6-3-assetmapper-component</ref>
=== Scheduler (>= v6.3) ===
<ref>https://symfony.com/blog/new-in-symfony-6-3-scheduler-component</ref>
== Composants non listés comme tels ==
=== apache-pack ===
Pour faire tourner le site sans passer par le serveur <code>symfony server:start</code>.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Installation ==
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<ref>https://symfony.com/doc/current/components/http_client.html</ref> :
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]] <pre>composer require symfony/http-client</pre>
</div>
</div>
== Utilisation ==
Deux solutions :
<pre>
HttpClient::create();
</pre>
ou
<pre>
public function __construct(private readonly HttpClientInterface $httpClient)
</pre>
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 ===
On peut forcer l'utilisation de HTTP 2 à la création :
<pre>
$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'));
}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Ce code ne lève pas les exceptions de résolution DNS.</p>
</div>
</div>
=== POST ===
Exemple en POST avec authentification :
<pre>
$response = $httpClient->request('POST', 'https://fr.wikibooks.org/w/api.php', [
'auth_bearer' => 'mon_token',
'jsonA' => $keyValuePairs,
]);
</pre>
Pour lancer plusieurs appels asynchrones, il suffit de placer leurs <code>$response->getContent()</code> ensemble, après tous les <code>$httpClient->request()</code>.
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'<ref>https://dev.to/timoschinkel/sending-multipart-data-with-psr-18-2lb5</ref>.
=== Problèmes connus ===
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 :
<pre>
$options = array_merge($options, [
'resolve' => ['localhost' => '127.0.0.1']
]);
</pre>
* <code>$httpClient->request()</code> renvoie une <code>Symfony\Contracts\HttpClient\ResponseInterface</code>, mais en cas d'erreur, elle ne contient qu'une ligne de résumé, soit moins d'informations qu'un client comme Postman.
== Tests ==
Ce composant peut aussi serveur aux tests fonctionnels via PhpUnit. On l'appelle alors avec <code>static::createClient</code> si le test <code>extends WebTestCase</code>. Dans le cas d'un projet API Platform, on l'appelle de la même manière mais le test <code>extends ApiTestCase</code>.
Exemple :
<pre>
$client = static::createClient();
$client->request('GET', '/home');
var_dump($client->getResponse()->getContent());
</pre>
Pour simuler plusieurs clients en parallèle : <code>$client->insulate()</code>.
Pour simuler un utilisateur : <code>$client->loginUser($monUser)</code>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Pour un test de bundle, il faut créer une classe Kernel qui charge les routes en plus<ref>https://symfonycasts.com/screencast/symfony-bundle/controller-functional-test</ref>.</p>
</div>
</div>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
Un [[w:Programmation événementielle|évènement]] est une action pouvant en déclencher d'autres qui l'attendaient, à la manière du [[w:Observateur (patron de conception)|patron de conception ''observateur'']], via un ''[[wikt:hook|hook]]''.
== Installation ==
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]] <pre>composer require symfony/event-dispatcher</pre>
</div>
</div>
== Commande ==
Pour lister les évènements et écouteurs d'un projet (avec leurs priorités) :
<pre>
php bin/console debug:event-dispatcher
</pre>
Ex :
<pre>
"console.terminate" event
-------------------------
------- ----------------------------------------------------------------------------- ----------
Order Callable Priority
------- ----------------------------------------------------------------------------- ----------
#1 Symfony\Component\Console\EventListener\ErrorListener::onConsoleTerminate() -128
#2 Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate() -255
------- ----------------------------------------------------------------------------- ----------
</pre>
== Event ==
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 [[w:conception pilotée par le domaine|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'' :
<pre>
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;
}
}
</pre>
Une fois la classe crée, il faut choisir où l'instancier :
<pre>
use Symfony\Component\EventDispatcher\EventDispatcher;
...
$this->eventDispatcher->dispatch(new AddExtraDataEvent($userGroup));
</pre>
<div class="remarque"> Le setter permet de transférer un résultat issu des observateurs de l’évènement, à l'endroit qui les dispatch (qu'il peut récupérer via le getter de l'évènement). Une alternative serait d'injecter à la place de EventDispatcher, un tableau des services des modules concernés, définis par un tag.</div>
<div class="remarque"> Certains évènements de Symfony possèdent une méthode "setResponse" pour renvoyer directement quelque chose à l'utilisateur.</div>
== Listener ==
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''<ref>https://symfony.com/doc/current/event_dispatcher.html</ref>), 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 ===
<pre>
services:
App\EventListener\MyViewListener:
tags:
- { name: kernel.event_listener, event: kernel.view }
</pre>
<pre>
class MyViewListener
{
public function onKernelException(ExceptionEvent $event)
{
echo "Triggered!";
}
}
</pre>
== Subscriber ==
Un souscripteur doit forcément implémenter ''EventSubscriberInterface'' :
<pre>
class ViewSubscriber implements EventSubscriberInterface
{
public function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['onView']
];
}
public function onView(ViewEvent $event): void
{
echo "Triggered!";
}
}
</pre>
Autre exemple où on veut embarquer dans un évènement maison une information de ses souscripteurs :
<pre>
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');
}
}
}
</pre>
== Débogage ==
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 :
<pre>
$this->getEventDispatcher()->dispatch('mon_service');
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Principe ==
Le principe est d'ajouter des champs de [[Programmation PHP/Formulaire|formulaire en PHP]], qui seront automatiquement convertis en code HTML correspondant.
En effet, en [[Le langage HTML/Formulaires|HTML on utilise habituellement la balise <code>{{#parsoid\0fragment:19}}</code>]] pour afficher les champs à remplir par le visiteur. Puis sur validation on récupère leurs valeurs en PHP avec la superglobale <code>$_REQUEST</code> (ou ses composantes <code>$_GET</code> et <code>$_POST</code>). Or ce système ne fonctionne pas en <code>$_POST</code> dans Symfony : si on affiche un tel formulaire et qu'on le valide, <code>$_POST</code> est vide, et l'équivalent Symfony de <code>$_REQUEST</code>, <code>$request->request</code><ref>https://symfony.com/doc/current/components/http_foundation.html</ref> aussi.
Les formulaires doivent donc nécessairement être préparés en PHP.
== Installation ==
=== Form ===
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]]
composer require symfony/form
</div>
</div>
Les formulaires présents sont ensuite listables avec :
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]]
bin/console debug:form
</div>
</div>
Et vérifiables individuellement :
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]]
bin/console debug:form "App\Service\Form\MyForm"
</div>
</div>
Avec le composant ''maker'', on peut créer un formulaire pour chaque entité Doctrine à modifier :
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]]
composer require symfony/maker-bundle
bin/console make:form
</div>
</div>
=== Validator ===
Pour ajouter des contrôles sur les champs, il existe un deuxième composant Symfony<ref>https://symfony.com/doc/current/forms.html#form-validation</ref> :
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]]
composer require symfony/validator
</div>
</div>
== Contrôleur ==
=== Injection du formulaire dans un Twig ===
<pre>
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(),
]);
}
}
</pre>
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 ===
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 :
<pre>
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');
}
</pre>
== Fichier du formulaire ==
Dans SF4, l'espace de nom <u>Symfony\Component\Form\Extension\Core\Type</u> 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 ===
Exemple<ref>https://symfony.com/doc/current/reference/forms/types/form.html#empty-data</ref> :
<pre>
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'],
]);
}
</pre>
Pour préremplir des valeurs dans les champs :
<pre>
$form->get('email')->setData($user->getEmail());
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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.</p>
</div>
</div>
=== NumberType ===
Cette classe génère une balise <code>input type="number"</code>, 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 :
<pre>
$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',
],
]);
</pre>
=== ChoiceType ===
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<ref>https://symfony.com/doc/current/reference/forms/types/choice.html</ref>.
Ex :
<pre>
$builder
->add('civility', ChoiceType::class, [
'choices' => ['Choisir' => null, 'M.' => 'M.', 'Mme' => 'Mme'],
])
</pre>
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.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Si Symfony envoie une string vide au lieu du null de la liste, on peut mettre 0 dans la liste et <code>'empty_data' => null,</code> dans le champ du formulaire.</p>
</div>
</div>
==== Avec liste modifiable ====
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<ref>https://github.com/symfony/symfony/issues/42451</ref> :
<pre>
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
...
})
</pre>
=== EntityType ===
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<ref>https://symfony.com/doc/master/reference/forms/types/entity.html</ref>.
Ex :
<pre>
$builder->add('company', EntityType::class, ['class' => Company::class]);
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">En SF4, il n'y avait pas encore les types ''CheckboxType'' ou ''RadioType'' : il fallait jouer sur deux paramètres de <code>EntityType</code> ainsi :</p>
</div>
</div>
{| class="wikitable"
! Élément !! Expanded !! Multiple
|-
| Sélecteur || false || false
|-
| Sélecteur multiple || false || true
|-
| Boutons radio || true || false
|-
| Cases à cocher || true || true
|}
Exemple :
<pre>
$builder->add('gender', EntityType::class, ['expanded' => true, 'multiple' => false]);
</pre>
Pour lui donner une valeur par défaut, il faut lui injecter un objet :
<pre>
$builder->add('company', EntityType::class, [
'class' => Company::class,
'choice_label' => 'name',
'data' => $company,
]);
</pre>
=== Sous-formulaire ===
Utiliser le nom du sous-formulaire comme type :
<pre>
$builder->add('company', MySubformType::class, [
'label' => false,
]);
</pre>
== Validation ==
=== Validation depuis les entités ===
Le validateur de formulaire d'entité peut utiliser les annotations des entités. Ex :
<pre>
use Symfony\Component\Validator\Constraints as Assert;
...
#[Assert\Type('string')]
#[Assert\NotBlank]
#[Assert\Length(
min: 1,
max: 255,
)]
</pre>
En PHP < 8 :
<pre>
use Symfony\Component\Validator\Constraints as Assert;
...
/**
* @Assert\Type("string")
* @Assert\NotBlank
* @Assert\Length(
* min = 2,
* max = 50
* )
*/
</pre>
Plusieurs types de données sont déjà définis, comme l'email ou l'URL<ref>https://symfony.com/doc/current/validation.html#string-constraints</ref>. Ex :
<pre>
@Assert\Email()
</pre>
=== Validation depuis les formulaires ===
Sinon il permet aussi des contrôles plus personnalisés dans les types (qui étendent Symfony\Component\Form\AbstractType). Ex :
<pre>
'constraints' => [
new Assert\NotBlank(),
new GreaterThanOrEqual(2),
new Assert\Callback([ProductChecker::class, 'check']),
],
</pre>
=== Validation avec un service ===
Pour valider une entité depuis le service validateur<ref>https://symfony.com/doc/current/validation.html#using-the-validator-service</ref> :
use Symfony\Component\Validator\Validator\ValidatorInterface;
...
$validator->validate(
$entity,
$entityConstraint
);
NB : le second paramètre est optionnel.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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.</p>
</div>
</div>
Exemple pour valider un email :
<pre>
php bin/console debug:container |grep -i validator |grep -i email
validator.email Symfony\Component\Validator\Constraints\EmailValidator
</pre>
<pre>
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Validator\ValidatorInterface;
...
$this->validator->validate(
'mon_email@example.com',
new Email()
);
</pre>
== Appel du formulaire Symfony dans la vue ==
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 :
<pre>
{{ form_start(form) }}
{{ form_end(form) }}
</pre>
Pour n'afficher qu'un seul champ :
<pre>
{{ form_widget(form.choosen_credit_card) }}
</pre>
Les mêmes attributs qu'en PHP peuvent être définis en paramètre. Ex :
<pre>
{{ form_widget(form.name, {'attr': {'class': 'address', 'placeholder': 'Entrer une adresse'} }) }}
{{ form_label(form.name, null, {'label_attr': {'class': 'address'}}) }}
</pre>
Exemple complet :
<pre>
{{ 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) }}
</pre>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Mailer ==
Depuis Symfony 4.3, un composant Symfony Mailer a été ajouté.
Pour l'installer<ref>https://symfony.com/doc/current/mailer.html</ref> :
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]] <pre>composer require symfony/mailer</pre>
</div>
</div>
Ajouter ensuite le SMTP dans le .env :
MAILER_DSN=smtp://mon_utilisateur:mon_mot_de_passe@smtp.example.com
== Utilisation ==
<pre>
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);
}
</pre>
== Swift Mailer ==
Avant Symfony 4.3 et la création du composant Mailer<ref>https://symfony.com/doc/current/email.html</ref>, on pouvait utiliser Swift Mailer.
Swift Mailer est ensuite remplacé en novembre 2021 par le composant Mailer.
=== Installation ===
<div class="boite" style="box-shadow: 0 0 0.2em #999999; border-radius: 0.2em; margin: 0.5em 0.5em 1em 0.5em; background: #f8f9fa; clear:left;">
<div class="entete-boite" style="background: #000000; border-radius: 0.2em 0.2em 0 0; padding: 0.5em 1em 0.5em 1em;"><span class="entete-boite-titre" style="color: #FFF; font-weight: bold;">Terminal</span>
</div>
<div style="padding: 0.5em 1em 0.5em 1em; border-radius: 0 0 0.2em 0.2em;">
[[File:Utilities-terminal.svg|50px|alt=Logo|link=]] <pre>composer require symfony/swiftmailer-bundle</pre>
</div>
</div>
=== Utilisation ===
Par exemple, pour un envoi d'email sans passer par config.yml :
<pre>
$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);
</pre>
<div class="remarque"> Il existe des applications comme Mailcatcher<ref>https://mailcatcher.me/</ref> pour intercepter les emails envoyés en environnement de développement, et les lire dans une interface graphique.</div>
== Templates ==
Pour simplifier les templates d'email, une alternative au HTML / CSS existe, il s'agit de Inky<ref>https://symfony.com/doc/current/mailer.html#inky-email-templating-language</ref>.
Elle utilise d'autres balises XML, comme <code>callout</code> ou <code>spacer</code><ref>https://get.foundation/emails/docs/spacer.html</ref>.
Installation :
composer require twig/extra-bundle twig/inky-extra
Utilisation :
{% apply inky_to_html %}
...
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Introduction ==
Stimulus est le framework JavaScript officiel de Symfony<ref>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</ref>. Il est installé avec Webpack :
composer require symfony/webpack-encore-bundle
Pour utiliser le framework [[w:React.js|React.js]] dans Symfony<ref>https://symfony.com/bundles/ux-react/current/index.html</ref> :
composer require symfony/ux-react
Lancer ensuite <code>npm run watch</code> pour que le code JS exécuté soit toujours identique à celui écris. Cela va lancer le <code>npm run build</code> en cours de frappe.
== Hello World on ready ==
=== Partie Twig ===
La première étape consiste à connecter un contrôleur Stimulus depuis un fichier Twig, en lui injectant les variables dont il a besoin. Ex :
<pre>
<div {{ stimulus_controller('ticket', {
subject: 'Hello World'
} )}}>
</div>
</pre>
Une syntaxe alternative est :
<pre>
<div data-controller="ticket"
data-ticket-subject-value="Hello World"
>
</div>
</pre>
=== Partie Stimulus ===
Dans le fichier <u>assets/controllers/ticket_controller.js</u>, créer une classe héritant de Stimulus :
<pre>
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = {
subject: String,
body: String,
};
connect() {
alert(this.subjectValue);
}
}
</pre>
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 [[w:Document Object Model|DOM]] de la page<ref>https://stimulus.hotwired.dev/reference/lifecycle-callbacks</ref>. 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''.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Si le contrôleur Stimulus est dans un sous-dossier, la syntaxe des séparateurs de dossiers côté Twig n'est pas "/" mais "--".</p>
</div>
</div>
Ex : <code>stimulus_controller('sousDossier--ticket', ...)</code> connectera le fichier <u>assets/controllers/sousDossier/ticket_controller.js</u>.
== Hello World on click ==
On utilise l'action "click"<ref>https://stimulus.hotwired.dev/reference/actions</ref>.
=== Partie Twig ===
<pre>
<div {{ stimulus_controller('ticket', {
subject: 'Hello World'
} )}}>
<button {{ stimulus_action('ticket', 'onCreate', 'click') }}>
Créer un ticket
</button>
</div>
</pre>
Une syntaxe alternative est :
<pre>
<div data-controller="ticket"
data-ticket-subject-value="Hello World"
>
<button data-action="click->ticket#onCreate" >
Créer un ticket
</button>
</div>
</pre>
=== Partie Stimulus ===
Par rapport au premier exemple, on remplace juste "connect" par une méthode maison.
<pre>
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = {
subject: String,
body: String,
};
onCreate() {
alert(this.subjectValue);
}
}
</pre>
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 ===
On veut maintenant déclencher l'ouverture d'une fenêtre modale [[Programmation JavaScript/React.js|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 :
<pre>
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);
}
}
</pre>
* HelloWorld.js :
<pre>
export default function (props) {
alert(props.subject);
}
</pre>
<div class="remarque"> Pour appeler directement un composant React depuis le Twig, il existe aussi <code>react_component()</code>.</div>
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
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'' ==
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 :
<pre>
config/bundles.php
</pre>
Pour l'activer ou le désactiver de certains environnements, il suffit de l'ajouter un paramètre. Ex :
<pre>
<?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],
];
</pre>
== Créer un ''bundle'' ==
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<ref>https://symfony.com/doc/current/bundles/best_practices.html</ref>. 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''<ref>https://symfony.com/doc/current/bundles/configuration.html#processing-the-configs-array</ref>.
== Principaux bundles ==
Packagist propose une liste des bundles Symfony les plus utilisés<ref>https://packagist.org/packages/symfony/?query=symfony%20bundle&tags=symfony</ref>.
=== SensioFrameworkExtraBundle ===
Permet de créer des annotations<ref>https://symfony.com/bundles/SensioFrameworkExtraBundle/current/index.html</ref>.
=== FOS ===
FriendsOfSymfony<ref>https://github.com/FriendsOfSymfony</ref>
propose plusieurs bundles intéressants, parmi lesquels :
* FOSUserBundle : pour gérer des utilisateurs.
* FOSRestBundle : pour les API REST.
=== KNP ===
KNP Labs offre également plusieurs ''bundles'' connus, dont un paginateur<ref>https://github.com/KnpLabs</ref>.
=== SonataAdmin ===
Ce bundle permet de créer rapidement un back-office pour lire ou modifier une base de données<ref>https://github.com/sonata-project/SonataAdminBundle</ref>.
=== EasyAdmin ===
[[Image:Filter words list in EasyAdminBundle-fr.png|vignette|upright=2|Fonctionnalité filtre de EasyAdminBundle.]]
Mêmes principales fonctions que SonataAdmin mais plus léger<ref>https://symfony.com/bundles/EasyAdminBundle/current/index.html</ref>.
==== Installation ====
<pre>
composer require easycorp/easyadmin-bundle
</pre>
==== Configuration ====
Pour créer la page d'accueil :
<pre>
bin/console make:admin:dashboard
</pre>
Pour une liste paginée d'entités Doctrine modifiables, avec liens vers leurs CRUD :
<pre>
bin/console make:admin:crud
</pre>
Pour modifier les actions d'une page, dans son contrôleur :
<pre>
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é');
})
;
</pre>
=== The PHP League ===
Voir https://github.com/thephpleague.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Installation ==
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<ref>https://twig.symfony.com/pdf/2.x/Twig.pdf</ref>, 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/.
<pre>
composer require symfony/templating
</pre>
Anciennement sur Symfony 3 :
<pre>
composer require twig/twig
</pre>
== Syntaxe native ==
Les mots réservés suivants s'ajoutent au HTML déjà interprété :
* {{#parsoid\0fragment:20}} : appel à une variable ou une fonction PHP, ou un template Twig parent (<code>{{#parsoid\0fragment:21}}</code>).
* {# ... #} : commentaires.
* {% ... %} : commande, comme une affectation, une condition, une boucle ou un bloc HTML.
** {% set foo = 'bar' %} : assignation<ref>https://twig.sensiolabs.org/doc/2.x/tags/set.html</ref>.
** {% 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 ===
==== Concaténation ====
Il existe de multiples manière de concaténer des chaines<ref>https://www.designcise.com/web/tutorial/how-to-concatenate-strings-and-variables-in-twig</ref>. Par exemple avec l'opérateur de concaténation ou par interpolation :
<pre>
"{{ variable1 ~ variable2 }}"
"#{variable1} #{variable2}"
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Les apostrophes ne fonctionnent pas avec l'interpolation.</p>
</div>
</div>
=== Tableaux ===
==== Création ====
Pour créer un tableau itératif :
<pre>
{% set myArray = [1, 2] %}
</pre>
Un tableau associatif :
<pre>
{% set myArray = {'key': 'value'} %}
</pre>
À plusieurs lignes :
<pre>
{% set months = {
1: 'janvier',
2: 'février',
3: 'mars',
} %}
{{ dump(months[1]) }} {# 'janvier' #}
</pre>
Ajouter une ligne :
<pre>
{% set months = months|merge({4: 'avril'}) %}
</pre>
Ajouter une ligne avec clé variable :
<pre>
{% set key = 5 %}
{% set months = months|merge({(key): 'mai'}) %}
</pre>
Ajouter une ligne en préservant les clés numériques :
<pre>
{% set key = 6 %}
{% set months = months + {(key): 'juin'} %}
</pre>
Multidimensionnel :
<pre>
{% set myArray = [
{'key1': 'value1'},
{'key2': 'value2'}
] %}
</pre>
Dans un "for ... in", pour séparer chaque élément avec une virgule :
<pre>
{% if loop.first != true %}
,
{% endif %}
</pre>
Pour créer un tableau associatif JavaScript à partir d'un tableau Twig :
<pre>
<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>
</pre>
==== Modification d'une ligne ====
Pour modifier une ligne, utiliser "merge()"<ref>https://twig.symfony.com/doc/3.x/filters/merge.html</ref>. Ex :
<pre>
{% set tests = {'a': 1} %}
{% set tests = tests|merge({'b': 2}) %}
{{ dump(tests) }}
{% set tests = tests|merge({'b': 3}) %}
{{ dump(tests) }}
</pre>
<pre>
array:2 [▼
"a" => 1
"b" => 2
]
array:2 [▼
"a" => 1
"b" => 3
]
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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 :</p>
</div>
</div>
<pre>
{% set tests = {'1': 1} %}
{% set tests = tests|merge({'2': 2}) %}
{{ dump(tests) }}
{% set tests = tests|merge({'2': 3}) %}
{{ dump(tests) }}
</pre>
<pre>
array:2 [▼
0 => 1
1 => 2
]
array:3 [▼
0 => 1
1 => 2
2 => 3
]
</pre>
==== Modification des lignes ====
Pour ajouter une ou plusieurs lignes à un tableau, utiliser "merge()" aussi :
<pre>
{% set oldArray = [1] %}
{% set newArray = oldArray|merge([2,3]) %}
{{ dump(newArray) }}
</pre>
<pre>
0 => 1
1 => 2
2 => 3
</pre>
Pour ajouter une ligne associative :
<pre>
{% set oldArray = {'key1': 'value1'} %}
{% set newArray = oldArray|merge({'key2': 'value2'}) %}
{{ dump(newArray) }}
</pre>
<pre>
[
"key1" => "value1"
"key2" => "value2"
]
</pre>
Pour ajouter une ligne de sous-tableau :
<pre>
{% set oldArray = [{'key1': 'value1'}] %}
{% set newArray = oldArray|merge([{'key2': 'value2'}]) %}
{{ dump(newArray) }}
</pre>
<pre>
[
0 => ["key1" => "value1"]
1 => ["key2" => "value2"]
]
</pre>
==== Lecture ====
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''<ref>https://twig.symfony.com/doc/3.x/filters/filter.html</ref>. 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 ===
Du moins au plus prioritaire<ref>http://twig.sensiolabs.org/doc/templates.html</ref> :
{|class="wikitable"
! 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
|-
| {{#parsoid\0fragment:22}} || Inférieur ou égal
|-
| in || Dans (ex : <code>{% if x in [1, 2] %}</code>)
|-
| matches || Correspond
|-
| starts with || Commence par
|-
| ends with || Se termine par
|-
| .. || Séquence (ex : <code>1..5</code>)
|-
| + || Plus
|-
| - || Moins
|-
| ~ || Concaténation
|-
| * || Multiplication
|-
| / || Division
|-
| // || Division arrondie à l'inférieur
|-
| % || Modulo
|-
| is || Test (ex : <code>is defined</code> ou <code>is not empty</code>)
|-
| ** || Puissance
|-
| {{#parsoid\0fragment:23}} || Filtre
|-
| [] || Entrée de tableau
|-
| . || Attribut ou méthode d'un objet (ex : <code>country.name</code>)
|}
Pour afficher la valeur NULL dans un opérateur ternaire, il faut la mettre entre apostrophes :
<pre>
{{ (myVariable is not empty) ? '"' ~ myVariable.value ~ '"' : 'null' }}
</pre>
=== Fonctions usuelles ===
==== Chemins, routes et URLs ====
* <code>url('route_name')</code> : affiche l'URL complète d'une route. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex : <code>url('ma_route_de_controleur', {'parametre1': param1})</code>).
* <code>absolute_url('path')</code> : affiche l'URL complète d'un chemin.
* <code>path('route_name')</code> : affiche le chemin, en absolu par défaut, mais il existe le paramètre <code>relative=true</code>. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex : <code>path('ma_route_de_controleur', {'parametre1': param1}</code>).
* <code>asset('path')</code> : pointe le dossier des "assets" ("web" dans SF2, "public" dans SF4). Ex : <code>{{#parsoid\0fragment:24}}</code>.
* <code>controller('controller_name')</code> : exécute la méthode d'un contrôleur. Ex : <code>{{#parsoid\0fragment:25}}</code>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;"><code>absolute_url()</code> renvoie l'URL de l'application si l'appel provient d'un contrôleur, mais http://localhost s'il vient d'une commande ([[w:Interface en ligne de commande|CLI]])<ref>https://stackoverflow.com/questions/73026340/absolute-url-in-template-returns-localhost-in-email-templates</ref>. La solution est donc de définir l'URL de l'environnement dans une variable, soit <code>default_uri</code> de routing.yaml, soit maison et injectée par le contrôleur dans le Twig.</p>
</div>
</div>
<div class="remarque"> render_esi() peut remplacer render() (en PHP aussi), pour inclure un Twig avec le cache [[w:Edge Side Includes|Edge Side Includes]]<ref>https://symfony.com/doc/current/reference/twig_reference.html</ref><ref>https://symfony.com/doc/current/http_cache/esi.html</ref>.</div>
==== Divers ====
* <code>constant(constant_name)</code> : importe une constante d'une classe PHP<ref>https://twig.symfony.com/doc/2.x/functions/constant.html</ref>.
* <code>attribute(object, method)</code> : accède à l'attribut d'un objet PHP. C'est équivalent au "." mais la propriété peut être dynamique<ref>https://twig.symfony.com/doc/2.x/functions/attribute.html</ref>.
* <code>date()</code> : convertit en date, ce qui permet leur comparaison. Ex : <code>{% if date(x) > date(y) %}</code>. 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 ===
Les filtres fournissent des traitements sur une expression, si on les place après elle séparés par des pipes. Par exemple :
* <code>capitalize</code> : équivaut au PHP <code>ucfirst()</code>, met une majuscule à la première lettre d'une chaine de caractères, et passe les autres en minuscules.
* <code>upper</code> : équivaut au PHP <code>strtoupper()</code>, met la chaine en lettres capitales. Exemple pour ne mettre la majuscule que sur la première lettre : <code>{{ variable[:1]|upper ~ variable[1:] }}</code>.
* <code>first</code> : affiche la première ligne d'un tableau, ou la première lettre d'une chaine.
* <code>length</code> : équivaut au PHP <code>sizeof()</code>, renvoie la taille de la variable (chaine ou tableau).
* <code>format</code> : équivaut au PHP <code>printf()</code>.
* <code>date</code> : équivaut au PHP <code>date()</code> mais son format est du type DateInterval<ref>https://twig.symfony.com/doc/3.x/filters/date.html</ref>.
* <code>date_modify</code> : équivaut au PHP DateTime->modify(). Ex : <code>{% set tomorrow = 'now'|date_modify("+1 day") %}</code>.
* <code>replace</code> : équivaut au PHP <code>str_replace()</code>. Ex : <code>{{#parsoid\0fragment:26}}</code>.
* <code>join</code> : équivaut au PHP <code>implode()</code> : convertit un tableau en chaine avec un séparateur en paramètre.
* <code>split</code> : équivaut au PHP <code>explode()</code> : convertit une chaine en tableau avec un séparateur en paramètre.
* <code>slice(début, fin)</code> : équivaut au PHP <code>array_slice()</code> + <code>substr()</code> : découpe un tableau ou une chaine selon deux positions<ref>https://twig.symfony.com/doc/3.x/filters/slice.html</ref>.
* <code>trim</code> : équivaut au PHP <code>trim()</code>.
* <code>raw</code> : ne pas échapper les balises HTML.
* <code>json_encode</code> : transforme un tableau en chaine de caractères JSON.
* <code>default</code> : ce filtre lève les exceptions sur les variables non définies ou vides<ref>https://twig.symfony.com/doc/2.x/filters/default.html</ref>. Ex :
<pre>
{{ variable1 |default(null) }}
</pre>
=== Variables spéciales ===
* <code>loop</code> contient les informations de la boucle dans laquelle elle se trouve. Par exemple <code>loop.index</code> donne le nombre d'itérations déjà survenue (commence par 1 et pas par 0).
* Les variables globales commencent par des underscores, par exemple<ref>https://twig.symfony.com/doc/3.x/templates.html#global-variables</ref> :
** <code>_route</code> : partie de l'URL située après le domaine.
** <code>_self</code> : nom de du fichier courant.
** <code>_charset</code> : jeu de caractères de la page. Ex : UTF-8.
** <code>_context</code> : variables injectées dans le template. Cela peut donc permettre d'y accéder en variables variables. Ex :
*** <code>{{#parsoid\0fragment:27}}</code>
*** <code>{{#parsoid\0fragment:28}}</code> pour un champ de formulaire.
* Les [[w:variables d'environnement CGI|variables d'environnement CGI]], telles que <code>{{#parsoid\0fragment:29}}</code>
Pour obtenir la route d'une page : <code>{{#parsoid\0fragment:30}}</code>
L'URL courante : <code>{{#parsoid\0fragment:31}}</code>
La page d'accueil du site Web : <code>{{#parsoid\0fragment:32}}</code>
<code>app.environment</code> renvoie la valeur de APP_ENV.
== Gestion des espaces ==
=== spaceless ===
Un Twig bien formaté ne correspond pas forcément au rendu qu'il doit apporter. Pour supprimer les espaces du formatage dans ce rendu :
<pre>
{% apply spaceless %}
<b>
Hello World!
</b>
{% endspaceless %}
</pre>
NB : en Twig < 2.7, c'était<ref>https://twig.symfony.com/doc/2.x/tags/spaceless.html</ref> :
<pre>
{% spaceless %}
{% autoescape false %}
<b>
Hello World!
</b>
{% endspaceless %}
</pre>
Par ailleurs, il existe un filtre |spaceless<ref>https://twig.symfony.com/doc/2.x/filters/spaceless.html</ref>.
=== - ===
De plus, on peut apposer le symboles "-" aux endroits où ignorer les espacements (dont retours chariot) du formatage :
<pre>
Hello {% ... -%}
{%- ... %} World!
</pre>
Cela fonctionne aussi entre <code>{{#parsoid\0fragment:33}}</code>.
== Utilisation du traducteur ==
=== Configuration ===
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 :
<pre>
{{ app.request.getLocale() }}
</pre>
<div class="remarque"> D'ailleurs il est aussi possible de récupérer n'importe quel paramètre de l'URL avec :
<pre>
{{ app.request.get('mon_query_param') }}
</pre></div>
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 :
<pre>
{% trans_default_domain 'homepage' %}
</pre>
Par ailleurs, la commande PHP pour lister les traductions les traductions d'une langue est<ref>https://symfony.com/doc/current/translation/debug.html</ref> :
<pre>
php bin/console debug:translation en --only-unused // Pour les inutilisées
php bin/console debug:translation en --only-missing // Pour les manquantes
</pre>
=== Filtre trans ===
Une fois la configuration effectuée, on peut apposer le filtre <code>trans</code> aux textes traduis dans le Twig.
<pre>
{{ MessageInMyLanguage |trans }}
</pre>
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 <code>trans</code> :
<pre>
{{ 'punctuation_separator'|trans({}, 'common') }}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Si le YAML contient des balises HTML à interpréter, il faut apposer le filtre <code>raw</code> après <code>trans</code>.</p>
</div>
</div>
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 <code>trans</code> :
<pre>
{{ FrenchMessage |trans({}, 'common', 'fr') }}
</pre>
Si le YAML doit contenir une variable, on la place entre pourcentages pour la remplacer en Twig avec le premier paramètre du filtre <code>trans</code> :
<pre>
{{ variableMessage |trans({"%price%": formatPrice(myPrice)}) }}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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 :
<pre>
{% set variableMessage = 'constante.' ~ variable %}
{{ variableMessage |trans }}
</pre>
</p>
</div>
</div>
=== Opération trans ===
Il existe aussi une syntaxe alternative au filtre. Par exemple les deux paragraphes ci-dessous sont équivalents :
<pre>
{{ 'punctuation_separator'|trans({}, 'common') }}
{% trans from 'common' %}
punctuation_separator
{% endtrans %}
</pre>
De plus, on peut injecter une variable avec "with". Voici deux équivalents :
<pre>
{{ 'Bonjour %name% !' |trans({"%name%": name}) }}
{% trans with {'%name%': name}%}Bonjour %name% !{% endtrans %}
</pre>
== Méthodes PHP appelables en Twig ==
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 :
<pre>
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, ',', ' ') . ' €';
}
}
</pre>
== Héritages et inclusions ==
=== extends ===
Si une fichier appelé doit être inclus dans un tout, il doit en hériter avec le mot <code>extends</code>. 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 :
<pre>
{% extends "base.html.twig" %}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Twig ne supporte pas l'héritage multiple<ref>https://twig.symfony.com/doc/3.x/tags/extends.html</ref>.</p>
</div>
</div>
Il est possible de surcharger totalement ou en partie les blocs du template parent. Exemple depuis le template qui hérite :
<pre>
{% block header %}
Mon en-tête qui écrase le parent
{% endblock %}
{% block footer %}
Mon pied de page qui complète le parent
{{ parent() }}
{% endblock %}
</pre>
=== include ===
À 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 <code>include</code>. Ex :
<pre>
{% include("partials/footer.html.twig") %}
</pre>
En lui injectant des paramètres :
<pre>
{% include("partials/footer.html.twig") with {'clé': 'valeur'} %}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">On trouvait en Twig 1 la syntaxe <code>{{#parsoid\0fragment:34}}</code><ref>https://twig.symfony.com/doc/1.x/functions/include.html</ref> au lieu de <code>{% include() %}</code><ref>https://twig.symfony.com/doc/2.x/tags/include.html</ref> en Twig 2.</p>
</div>
</div>
=== embed ===
Enfin, <code>embed</code> combine les deux précédentes fonctions :
<pre>
{% embed "footer.html.twig" %}
...
{% endembed %}
</pre>
=== import ===
<code>import</code> récupère certaines fonctions d'un fichier en contenant plusieurs :
<pre>
{% from 'mes_macros.html' import format_price as price, format_date %}
</pre>
=== Macros ===
Les macros sont des fonctions globales, appelables depuis un fichier Twig<ref>https://twig.symfony.com/doc/2.x/tags/macro.html</ref>.
Exemple :
<pre>
{% 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 %}
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Lors de l'appel, les paramètres nommés ne fonctionnent que si 100 % des paramètres appelés le sont.</p>
</div>
</div>
== Exemples ==
<pre>
{% 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>
{% endfor %}
</ul>
{% endblock navigation %}
</pre>
Pour ne pas qu'un bloc hérité écrase son parent, mais l'incrémente plutôt, utiliser :
<pre>
{{ parent() }}
</pre>
== Bonnes pratiques ==
Les noms des fichiers .twig doivent être rédigés en snake_case<ref>https://symfony.com/doc/current/contributing/code/standards.html</ref>.
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
== Installation ==
[[w:Doctrine (ORM)|Doctrine]] est l'ORM par défaut de Symfony. Il utilise [[w:PHP Data Objects|PDO]]. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la [[Patrons de conception/Chaîne de responsabilité|chaîne de responsabilité]].
Installation en SF4<ref>https://symfony.com/doc/current/doctrine.html</ref> :
<pre>
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
</pre>
Renseigner l'accès au SGBD dans le .env :
<pre>
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
</pre>
Ensuite la base de données doit être créée avec :
<pre>
php bin/console doctrine:database:create
</pre>Si la commande précédente échoue avec le message d'erreur suivant:
''Could not create database "database_name" for connection named default''
''An exception occurred in the driver: could not find driver''
Ce qui veut dire que vous devez installer le driver approprié.
'''Exemple:'''
sudo apt install php8.3-pgsql<div class="remarque"> symfony/orm-pack équivaut aux paquets suivants, qui peuvent bien sûr être installés séparément à la place :
*doctrine/doctrine-bundle
*doctrine/doctrine-migrations-bundle
*doctrine/orm
*symfony/proxy-manager-bridge</div>
== Commandes Doctrine ==
Exemples de commandes :
<pre>
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
</pre>
== Entity ==
Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs [[wikt:getter|getter]]s et [[wikt:setter|setter]]s respectifs. Pour en générer une :
<pre>
php bin/console generate:doctrine:entity
</pre>
Cette association est définie par des attributs Doctrine. Pour les vérifier :
<pre>
php bin/console doctrine:schema:validate
</pre>
=== Exemple ===
Voici par exemple plusieurs types d'attributs :
<syntaxhighlight lang=php>
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
</syntaxhighlight>
<div class="NavFrame" style="clear:both; "><div class="NavHead " align="center" style="cursor:default;clear:both; text-align:center; background-color: #80808020; " title="Cliquez pour afficher/masquer">
Exemple avec annotation (avant PHP 8)
</div>
<div class="NavContent" align="left" style="clear:both; column-count: 1; -webkit-column-count: 1; -moz-column-count: 1; ">
<syntaxhighlight lang=php>
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="spelling", type="string", length=255, nullable=false)
*/
private $spelling;
/**
* @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
*/
private $pronunciation;
/**
* @var Language
*
* @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
* @ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
*/
private $homophons;
</syntaxhighlight>
</div>
<div class="NavEnd" style="clear:both;"> </div>
</div>
Et leurs modificateurs (getters et setters) :
<syntaxhighlight lang=php>
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
</syntaxhighlight>
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec <code>$word->setLanguage($language)</code>.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi un deuxième attribut <code>#[ORM\JoinColumn</code> (anciennement <code>@ORM\JoinColumn</code>) mentionnant la clé étrangère en base de données (et pas en PHP).
=== Bonnes pratiques ===
L'attribut <code>#[ORM\Table(name: 'word')]</code> était facultatif dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.
Avant PHP 8, les contraintes d'unicité (utiles entre autres pour les clés composites) étaient encapsulées dans l'annotation <code>Table</code>, mais ce n'est plus le cas avec les attributs :
<pre>
#[ORM\UniqueConstraint(name: 'spelling-pronunciation', columns: ['spelling', 'pronunciation'])]
</pre>
<div class="NavFrame" style="clear:both; "><div class="NavHead " align="center" style="cursor:default;clear:both; text-align:center; background-color: #80808020; " title="Cliquez pour afficher/masquer">
Exemple avec annotation (avant PHP 8)
</div>
<div class="NavContent" align="left" style="clear:both; column-count: 1; -webkit-column-count: 1; -moz-column-count: 1; ">
<pre>
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
</pre>
</div>
<div class="NavEnd" style="clear:both;"> </div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Dans les relations *toMany :
* il faut initialiser l'attribut dans le constructeur en <code>ArrayCollection()</code>.
* on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
* cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
</p>
</div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Il faut ajouter le <code>#[ORM\JoinColumn(</code> dans les deux entités liées, car :
* dans aucune cela renvoie <code>Could not resolve type of column "id"</code>
* dans une seule cela provoque un problème N+1 (celle qui ne l'a pas appelle celle qui l'a pour chacun de ses enregistrements, même si celle qui l'a n'est pas utilisée ensuite).
</p>
</div>
</div>
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec <code>length=0</code><ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string</ref>. Le type "text" par contre n'a pas de limite.
=== ArrayCollection ===
Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
* Dans une entité : <code>#[ORM\OrderBy(['sort_order' => 'ASC'])]</code> (anciennement <code>@ORM\OrderBy({"sort_order" = "ASC"})</code>).
* Sinon, instancier un critère :
<pre>
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
</pre>
=== GeneratedValue ===
L'annotation ''GeneratedValue'' peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID<ref>https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations</ref>. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur '' Integrity constraint violation'' puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
<syntaxhighlight lang=php>
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
</syntaxhighlight>
</p>
</div>
</div>
=== Triggers ===
Les opérations en cascade sont définies sous deux formes d'attributs :
* <code>#[ORM\OneToMany(cascade: ['persist', 'remove'])]</code> : au niveau ORM.
* <code>#[ORM\JoinColumn(onDelete: 'CASCADE')]</code> : au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
=== Concepts avancés ===
Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware<ref>https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html</ref>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Les types des attributs peuvent être quelque peu différents du SGBD<ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix</ref>.</p>
</div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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").</p>
</div>
</div>
L'autojointure est appelé ''self-referencing association mapping'' par Doctrine<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing</ref>).
=== Héritage ===
Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html</ref> :
<syntaxhighlight lang=php>
/** @MappedSuperclass */
class MyEntityParent
...
</syntaxhighlight>
=== Tables sans classe ===
Doctrine peut créer des tables de mapping sans entité, si on précise son nom dans les deux tables reliées :
<pre>
#[ORM\JoinTable(name: 'table_de_mapping')]
#[ORM\JoinColumn(name: 'table_source_id', referencedColumnName: 'table_source_id')]
#[ORM\InverseJoinColumn(name: 'table_cible_id', referencedColumnName: 'table_cible_id')]
#[ORM\ManyToMany(targetEntity: TableCible::class)]
</pre>
== EntityManager ==
L'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données, définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
* 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 <code>$createdDate</code> :
<syntaxhighlight lang=php>
$entity = new MyEntity();
$em->persist($entity);
$em->flush($entity);
// Trigger SGBD déclenché ici en parallèle
$em->refresh($entity);
$entity->setCreatedDate($createdDate);
$em->flush($entity);
</syntaxhighlight>
== Repository ==
On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de <code>Doctrine\ORM\EntityRepository</code>. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés ''NomDeLEntitéRepository''.
<div class="remarque"> D'un point de vue architectural, avant d'instancier une nouvelle entité, on utilise généralement le repository pour savoir si son enregistrement existe en base ou si on doit le créer. Dans ce deuxième cas, la bonne pratique en [[wikt:DDD|DDD]] est d'utiliser une Factory pour faire le new de l'entité, mais aussi pour les new de son agrégat si elle est le nœud racine. Par exemple une <code>CarFactory</code> fera un <code>new Car()</code> mais aussi créera et lui associera ses composants : <code>new Motor()</code>...</div>
<div class="remarque"> Il est possible de préciser le nom du repository d'une entité dans cette dernière :
<pre>
#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]
</pre>
<div class="NavFrame" style="clear:both; "><div class="NavHead " align="center" style="cursor:default;clear:both; text-align:center; background-color: #80808020; " title="Cliquez pour afficher/masquer">
Exemple avec annotation (avant PHP 8)
</div>
<div class="NavContent" align="left" style="clear:both; column-count: 1; -webkit-column-count: 1; -moz-column-count: 1; ">
<pre>
@ORM\Entity(repositoryClass="App\Repository\WordRepository")
</pre>
</div>
<div class="NavEnd" style="clear:both;"> </div>
</div>
</div>
=== SQL ===
==== Depuis Doctrine ====
<syntaxhighlight lang=php>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
</syntaxhighlight>
==== Sans Doctrine ====
Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :
<syntaxhighlight lang=php>
return $this->connection->fetchAll($sql);
</syntaxhighlight>
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut ''entity manager'' (''em'', ou ''_em'' pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
<syntaxhighlight lang=php>
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
</syntaxhighlight>
2. En injectant le service de connexion dans le constructeur (<code>'@database_connection'</code>) :
<syntaxhighlight lang=php>
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
</syntaxhighlight>
=== DQL ===
==== Méthodes magiques ====
Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
* <code>$repo->find($id)</code> : cherche par la clé primaire définie dans l'entité.
* <code>$repo->findAll()</code> : récupère tous les enregistrements (sans clause <code>WHERE</code>).
* <code>$repo->findById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id</code> dans la table associée au repo.
* <code>$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])</code> engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname</code>.
* <code>$repo->findOneById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id LIMIT 1</code>.
* <code>$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])</code> : engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1</code>.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "<code>findById</code>" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser <code>findBy()</code>.
</p>
</div>
</div>
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
<syntaxhighlight lang=php>
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
</syntaxhighlight>
==== createQuery ====
DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec <code>IDENTITY()</code> pour les jointures. Ex :
<pre>
public function findComplicatedStuff()
{
$em = $this->getEntityManager();
$query = $em->createQuery("
SELECT
u.last_name, u.first_name
FROM
App\Entity\Users u
INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users)
WHERE
i.status='waiting'
");
return $query->getResult();
}
</pre>
==== createQueryBuilder ====
L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel <code>createQueryBuilder()</code> :
<syntaxhighlight lang=php>
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
</syntaxhighlight>
Pour éviter le <code>SELECT *</code> dans cet exemple, on peut y ajouter la méthode <code>->select()</code>.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
===== Jointures =====
Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
<pre>
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
</pre>
Pour filtrer quand une jointure toMany contient des résultats, utiliser <code>EMPTY</code> :
<pre>
...
->andWhere('files IS NOT EMPTY')
</pre>
===== Résultats =====
Doctrine peut renvoyer avec :
* <code>getResult()</code> : un objet ArrayCollection (iterable, pour rechercher dedans : <code>->contains()</code>), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;
* <code>getArrayResult()</code> ou <code>getScalarResult()</code> : un tableau de tableaux (entité normalisée) ;
* <code>getSingleColumnResult()</code> : un tableau unidimensionnel.
===== Cache =====
====== Configuration globale ======
Doctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
<syntaxhighlight lang=yaml>
framework:
cache:
pools:
doctrine.metadata_cache_pool:
adapter: cache.system
doctrine.query_cache_pool:
adapter: cache.system
doctrine.result_cache_pool:
adapter: cache.app
</syntaxhighlight>
Puis dans doctrine.yaml, les utiliser :
<syntaxhighlight lang=yaml>
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.metadata_cache_pool
query_cache_driver:
type: pool
pool: doctrine.query_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
</syntaxhighlight>
À partir de là le cache des métadonnées est utilisé partout.
====== Configuration par entité ======
Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
* Dans l'entité, avec un attribut <code>#[ORM\Cache(usage: 'READ_ONLY', region: 'write_rare')]</code> (anciennement <code>@ORM\Cache(usage="READ_ONLY", region="write_rare")</code><ref>https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6</ref>), utilisant la configuration doctrine.yaml :
<pre>
doctrine:
orm:
second_level_cache:
enabled: true
regions:
write_rare:
lifetime: 864000
cache_driver: { type: service, id: cache.app }
</pre>
* Dans le repository :
<syntaxhighlight lang=php>
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
</syntaxhighlight>
Dans cet exemple, on n'utilise pas <code>cache.system</code> pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. <code>cache.app</code> pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
==== Expressions ====
Pour ajouter une expression en DQL, utilise <code>$qb->expr()</code>. Ex<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class</ref> :
* <code>$qb->expr()->count('u.id')</code>
* <code>$qb->expr()->between('u.id', 2, 10)</code> (entre 2 et 10)
* <code>$qb->expr()->gte('u.id', 2)</code> (plus grand ou égal à 2)
* <code>$qb->expr()->like('u.name', '%son')</code>
* <code>$qb->expr()->lower('u.name')</code>
* <code>$qb->expr()->substring('u.name', 0, 1)</code>
==== Injection de dépendances ====
Les repository DQL deoivent ''ServiceEntityRepository'' :
<syntaxhighlight lang=php>
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
</syntaxhighlight>
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
* Étendre une classe qui étend ''ServiceEntityRepository''.
* Le redéfinir dans services.yaml.
* Utiliser un trait.
== Transactions ==
Pour garantir d'intégrité d'une transaction<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly</ref> :
<syntaxhighlight lang=php>
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
</syntaxhighlight>
Il existe aussi une syntaxe alternative :
<syntaxhighlight lang=php>
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
</syntaxhighlight>
== Évènements ==
Pour ajouter des triggers sur la mise à jour d'une table, il y a deux solutions :
* ajouter dans son entité l'attribut <code>#[ORM\HasLifecycleCallbacks]</code>, et à ses méthodes l'attribut de l'évènement concerné. Ex :
<pre>
#[ORM\PrePersist]
public function setCreatedAt(): self
{
$this->createdAt = new DateTime();
return $this;
}
</pre>
* ajouter des tags dans services.yaml. Ex :
<pre>
App\EventListener\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: PrePersist }
</pre>
Voici les [[Programmation PHP avec Symfony/Évènement|évènements]] utilisables ensuite (dans les listeners / subscribers) :
=== prePersist ===
Se produit avant la persistance d'une entité (paramètre : <code>PrePersistEventArgs $args</code>).
=== postPersist ===
Se produit après la persistance d'une entité (<code>PostPersistEventArgs $args</code>).
=== preUpdate ===
Se produit avant l'update d'une entité (<code>PreUpdateEventArgs $args</code>).
=== postUpdate ===
Se produit après l'update d'une entité (<code>PostUpdateEventArgs $args</code>).
=== preRemove ===
Se produit avant l'update d'une entité (<code>PreRemoveEventArgs $args</code>).
=== postRemove ===
Se produit après l'update d'une entité (<code>PostRemoveEventArgs $args</code>).
=== preFlush ===
Se produit avant la sauvegarde d'une entité (<code>PreFlushEventArgs $args</code>).
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">
* Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
* Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
* Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler <code>$unitOfWork->computeChangeSet($classMetadata, $entity)</code><ref>https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener</ref>.
</p>
</div>
</div>
<div class="remarque"> On peut aussi appeler computeChangeSet() depuis ailleurs pour savoir si une entité va occasionner une requête SQL lors de son flush<ref>https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2</ref>.</div> Ex :
<pre>
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
//...
}
</pre>
<div class="remarque"> On peut aussi utiliser le paramètre <code>LifecycleEventArgs $args</code> dans ces fonctions.</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Parfois le <code>$object::class</code> peut renvoyer <code>Proxies\__CG__\App\Entity\MyEntity</code> au lieu de <code>App\Entity\MyEntity</code>, selon le cache utilisé.</p>
</div>
</div>
=== postFlush ===
Se produit après la sauvegarde d'une entité (<code>PostFlushEventArgs $args</code>).
== Migrations ==
Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
<syntaxhighlight lang=bash>
composer require doctrine/doctrine-migrations-bundle
</syntaxhighlight>
=== Création ===
Ensuite, on peut créer un squelette de "migration" :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:generate
</syntaxhighlight>
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée <code>migration_versions</code> enregistre leur succession).
==== Exemple SQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->connection->fetchAll('SHOW DATABASES;');
$this->addSql(<<<SQL
CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
}
public function down(Schema $schema) : void
{
$this->addSql('DROP TABLE ma_table');
}
}
</syntaxhighlight>
==== Exemple DQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$table = $schema->createTable('ma_table');
$table->addColumn('ma_colonne', 'string');
}
public function down(Schema $schema) : void
{
$schema->dropTable('ma_table');
}
}
</syntaxhighlight>
==== Exemple PHP ====
Depuis Symfony 7.0, il faut implémenter <code>MigrationFactory</code> pour injecter des dépendances dans les migrations (et on ne peut plus injecter tout le conteneur)<ref>https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies</ref>.
<div class="NavFrame" style="clear:both; "><div class="NavHead " align="center" style="cursor:default;clear:both; text-align:center; background-color: #80808020; " title="Cliquez pour afficher/masquer">
Avant Symfony 7.0, il fallait juste utiliser <code>ContainerAwareTrait</code>
</div>
<div class="NavContent" align="left" style="clear:both; column-count: 1; -webkit-column-count: 1; -moz-column-count: 1; ">
Exemple :
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function up(Schema $schema) : void
{
$em = $this->container->get('doctrine.orm.entity_manager');
$monEntite = new MonEntite();
$em->persist($monEntite);
$em->flush();
}
}
</syntaxhighlight>
</div>
<div class="NavEnd" style="clear:both;"> </div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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.</p>
</div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;"><code>$this->container->getParameter()</code> ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple <code>$_SERVER['SUBAPI_URI']</code> renvoie la variable d'environnement et <code>$this->containergetParameter('env(SUBAPI_URI)')</code> sa valeur par défaut (définie dans services.yaml).</p>
</div>
</div>
=== Exécution ===
La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate
</syntaxhighlight>
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --up 20170321095644
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
</syntaxhighlight>
Pour le rollback :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --down 20170321095644
</syntaxhighlight>
Pour éviter que Doctrine pose des questions durant les migrations, ajouter <code>--no-interaction</code> (ou <code>-n</code>).
Pour voir le code SQL au lieu de l'exécuter : <code>--write-sql</code>.
==== Sur plusieurs bases de données ====
Pour exécuter sur plusieurs bases :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
</syntaxhighlight>
Avec des migrations.yaml de type :
<syntaxhighlight lang=bash>
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
</syntaxhighlight>
=== Synchronisation ===
==== Vers le code ====
===== Vers les entités =====
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
</pre>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer [[Programmation_PHP_avec_Symfony/Migration_de_Symfony_6_à_7#Rector|Rector]] pour convertir les annotations. Ex :
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable
vendor/bin/rector process src/Entity/MyNewEntity.php
</pre>
</p>
</div>
</div>
===== Vers les migrations =====
Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
==== Vers la base ====
À contrario, pour mettre à jour la BDD à partir des entités :
php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
php bin/console doctrine:schema:update --dump-sql
== Fixtures ==
Il existe plusieurs bibliothèques pour créer des [[wikt:fixture|fixture]]s, dont une de Doctrine<ref>https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html</ref> :
<syntaxhighlight lang=bash>
composer require --dev orm-fixtures
</syntaxhighlight>
Pour charger les fixtures du code dans la base :
<syntaxhighlight lang=bash>
php bin/console doctrine:fixtures:load -n
</syntaxhighlight>
== Types de champ ==
La liste des types de champ Doctrine se trouve dans <code>Doctrine\DBAL\Types</code>. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre <code>JsonType</code> pour surcharger le type JSON par défaut afin de lui faire faire <code>json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)</code> automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne<ref>https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23</ref>.
== Réplication SQL ==
Anciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml.
Ex<ref>https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35</ref> :
<pre>
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
replicas:
replica1:
url: '%env(resolve:REPLICA_DATABASE_URL)%'
</pre>
== Critique ==
# Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
# Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le [[wikt:lazy loading|lazy loading]] est contourné par au choix :
## Avant l'appel null, un <code>ObjetJoint->get()</code>.
## Dans l'entité, un <code>@ManyToOne(…, fetch="EAGER")</code>.
## Dans le repository, un <code>$this->queryBuilder->addSelect()</code>. NB : si cela ajoute un problème N+1, joindre aussi la deuxième entité qui le provoque.
# Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
# Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
# Attention aux <code>$this->queryBuilder->setMaxResults()</code> et <code>$this->queryBuilder->setFirstResult()</code> en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar du <code>LIMIT</code> SQL). La solution consiste à ajouter un paginateur<ref>https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939</ref>.
# L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
# Pas de hints.
# Bug des <code>UNION ALL</code> quand on joint deux entités non liées dans le repo.
<div class="noprint bandeau noprint
bandeaujaune
" style="min-height:80px;clear:;">
<div class="bandeautitre" style="margin:0.5em;">
À faire...<span style="display:inline-block;float:right;margin:0.5em 1em;">[[Image:Nuvola apps korganizer.svg|40px|link={{{link}}}]]</span></div>
<div class="bandeautexte" style="padding: 0.5em; background-color: white;">
* Ajouter la connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL (patrons à copier-coller ?)
</div>
</div>[[Catégorie:À faire|Programmation PHP/Version imprimable]]
== Références ==
<div class="references-small" style="column-count:1; -moz-column-count:1; -webkit-column-count:1;"><references group=""></references></div>
Pour créer une [[w:interface de programmation|interface de programmation]] (API) [[w:Representational state transfer|REST]] avec Symfony, il existe plusieurs bibliothèques :
* [[w:API Platform|API Platform]]<ref>https://api-platform.com/</ref>, 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<ref>https://api-platform.com/docs/core/content-negotiation/</ref>.
* Sinon il faut combiner plusieurs éléments : routeur, générateur de doc en ligne et sérialiseur.
== API Platform ==
=== Installation ===
<pre>
composer require api
</pre>
=== Utilisation ===
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">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''.</p>
</div>
</div>
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''<ref>https://api-platform.com/docs/core/data-providers/</ref> ou des ''data persisters''<ref>https://api-platform.com/docs/core/data-persisters/</ref>, pour transformer les données, respectivement à l'affichage et à la sauvegarde.
==== Sécurité ====
Par défaut toutes les routes sont accessibles sans identification (selon security.yaml). Pour changer cela, on peut utiliser les ''Custom Doctrine ORM Extension''<ref>https://api-platform.com/docs/core/extensions/#custom-doctrine-orm-extension</ref> :
<pre>
class CurrentApiUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{ ... }
</pre>
==== Attributs ====
===== ApiResource =====
Définit les noms et méthodes REST (GET, POST...) des routes de l'API.
Exemple sur la V3<ref>https://api-platform.com/docs/core/operations/</ref> :
<pre>
#[ApiResource(
operations: [
new Get(),
new GetCollection()
]
)]
class MyEntity...
</pre>
Avec personnalisation de la vue OpenAPI :
<pre>
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
],
openapiContext: [
'summary' => '',
'tags' => ['Enums'],
]
)]
</pre>
<div class="NavFrame" style="clear:both; "><div class="NavHead " align="center" style="cursor:default;clear:both; text-align:center; background-color: #80808020; " title="Cliquez pour afficher/masquer">
Idem sur la V2
</div>
<div class="NavContent" align="left" style="clear:both; column-count: 1; -webkit-column-count: 1; -moz-column-count: 1; ">
<pre>
#[ApiResource(
collectionOperations: [
'get' => [
'openapiContext' => [
'summary' => '',
'tags' => ['Enums'],
],
],
],
itemOperations: [
'get' => [
'openapiContext' => [
'summary' => '',
'tags' => ['Enums'],
],
],
],
)]
</pre>
</div>
<div class="NavEnd" style="clear:both;"> </div>
</div>
===== ApiProperty =====
Par exemple pour masquer un champ d'entité sur la route d'API :
<pre>
#[ApiProperty(readable: false, writable: false, required: false, fetchable: false)]
</pre>
===== MaxDepth =====
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 =====
API Platform ajoute plus d'une dizaine d'évènements donc les priorités sont définies dans EventPriorities<ref>https://api-platform.com/docs/core/events/</ref>.
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 ==
=== Installation ===
==== FOS REST ====
FOSRestBundle apporte des annotations pour créer des contrôleurs d'API<ref>https://github.com/FriendsOfSymfony/FOSRestBundle</ref>. Installation :
composer require "friendsofsymfony/rest-bundle"
Puis dans <u>config/packages/fos_rest.yaml</u> :
<syntaxhighlight lang=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 ] }
</syntaxhighlight>
==== Documentation ====
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<ref>https://github.com/nelmio/NelmioApiDocBundle</ref>, qui permet en plus de tester en ligne. En effet, pour éviter de tester les API en copiant-collant leurs chemins dans une commande [[w:cURL|cURL]] ou dans des logiciels plus complets comme Postman<ref>https://www.postman.com/</ref>, on peut installer une interface graphique ergonomique qui allie documentation et test en ligne :
<pre>
composer require "nelmio/api-doc-bundle"
</pre>
Son URL se configure ensuite dans <u>routes/nelmio_api_doc.yml</u> :
<syntaxhighlight lang=yaml>
app.swagger_ui:
path: /api/doc
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
</syntaxhighlight>
À 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.
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Pour documenter /api/* sauf /api/doc, il faut préciser l'exception en regex dans <u>packages/nelmio_api_doc.yaml</u> :</p>
</div>
</div>
<syntaxhighlight lang=yaml>
nelmio_api_doc:
areas:
path_patterns:
- ^/api/(?!/doc$)
</syntaxhighlight>
<div style="display: flex;flex-wrap: wrap;">
<div style="width: 40px; margin-right: 1em;">
[[Fichier:OOjs UI icon alert-warning.svg|40px|alt=Logo|link=]]
</div>
<div style="width: calc(100% - 90px); margin-left: 1em;">
<p style="color:
#f16600;">Il faut vider le cache de Symfony à chaque modification de nelmio_api_doc.yaml.</p>
</div>
</div>
===== Authentification =====
Pour tester depuis la documentation des routes nécessitant un token JWT, ajouter dans <u>packages/nelmio_api_doc.yaml</u> :
<syntaxhighlight lang=yaml>
nelmio_api_doc:
documentation:
securityDefinitions:
Bearer:
type: apiKey
description: 'Value: Bearer {jwt}'
name: Authorization
in: header
security:
- Bearer: []
</syntaxhighlight>
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 :
<pre>
security:
- JWT: []
</pre>
===== Exemple =====
Dans un contrôleur, au-dessus de l'attribut <code>#[Route]</code> d'un CRUD :
<pre>
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
),
]
)
)
]
)]
</pre>
==== Sérialiseur ====
Enfin pour la [[wikt:sérialisation|sérialisation]], on distingue plusieurs solutions :
* symfony/serializer, qui donne des contrôleurs <code>extends AbstractFOSRestController</code> et des méthodes aux annotations <code>@Rest\Post()</code><ref>https://www.thinktocode.com/2018/03/26/symfony-4-rest-api-part-1-fosrestbundle/</ref>.
* jms/serializer-bundle, avec des contrôleurs <code>extends RestController</code> et des méthodes aux annotations <code>@ApiDoc()</code>.
* Le service <code>fos_rest.service.serializer</code>.
===== symfony/serializer =====
<pre>
composer require "symfony/serializer"
</pre>
===== jms/serializer-bundle =====
<pre>
composer require "jms/serializer-bundle"
</pre>
=== Utilisation ===
Maintenant /api/doc affiche les méthodes des différents contrôleurs API. Voici un exemple :
<syntaxhighlight lang=php>
<?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');
}
}
</syntaxhighlight>
<div class="NavFrame" style="clear:both; "><div class="NavHead " align="center" style="cursor:default;clear:both; text-align:center; background-color: #80808020; " title="Cliquez pour afficher/masquer">
Dans PHP < 8
</div>
<div class="NavContent" align="left" style="clear:both; column-count: 1; -webkit-column-count: 1; -moz-column-count: 1; ">
<syntaxhighlight lang=php>
...
/**
* @Route("/api/test", methods={"GET"})
*/
public function testAction(Request $request): View
...
}
</syntaxhighlight>
</div>
<div class="NavEnd" style="clear:both;"> </div>
</div>
Maintenant dans /api/doc, cliquer sur /api/test, puis "Ty it out" pour exécuter la méthode de test.
== Sécurité ==
Une API étant ''stateless'', l'authentification est assurée à chaque requête, par l'envoi par le client d'un token [[w:JSON Web 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<ref>https://symfony.com/doc/current/security/guard_authentication.html</ref> :
<pre>
composer require "symfony/security-bundle"
</pre>
<syntaxhighlight lang=yaml>
security:
firewalls:
main:
guard:
authenticators:
- App\Security\TokenAuthenticator
Manipuler les JWT
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
Pour tester en shell :
TOKEN=123 curl -H 'Accept: application/json' -H "Authorization: Bearer ${TOKEN}" http://localhost
Références
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
Via composer
composer require --dev phpunit/phpunit ^8
Via wget
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
wget https://phar.phpunit.de/phpunit-8.phar mv phpunit.phar /usr/local/bin/phpunit chmod +x phpunit.phar
Windows
- 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
Test de l'installation :
phpunit --version
Utilisation
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
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
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
Pour afficher les noms des tests et le temps qu'ils prennent, utiliser : --testdox
Rapports
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
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
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class SuiteDeTests1 extends TestCase { /** @var MockObject */ private $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()
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
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
.willReturn()
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()
Renvoie l'argument dont le numéro est en paramètre.
willThrowException()
Pour qu'un mock simule une erreur. Ex :
$monMock->method('MaMéthode3')->willThrowException(new Exception());
expects()
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
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()
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()
Cette méthode s'emploie quand il est inutile de passer par le constructeur du mock.
expectException()
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
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
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
En PHP, Selenium peut s'interfacer avec PHPUnit[9] pour tester du JavaScript.
Avec Symfony, il existe aussi Panther[10].
Symfony
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 :
/** @var string */ private static $maVariableYaml; /** @var Translator */ private static $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
- ↑ 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
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
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
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
Lancer les tests avec en ligne de commande.
Syntaxe
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
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 homepageCatégorie:À faire
Compléments
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
- ↑ http://mink.behat.org/en/latest/
- ↑ (en) Junade Ali, Mastering PHP Design Patterns, Packt Publishing Ltd, (lire en ligne)
Logiciels métier
PHP propose de nombreuses solutions clés en mains pour construire des blogs, forums, e-commerces, etc.
CMS
Parmi les CMS les plus connus on peut citer Wordpress, SPIP, Joomla ou encore Drupal.
Certains moins connus sont e107.
Moteur de blog
Il en existe principalement Dotclear et Wordpress, même si ce dernier s'est étendu à un usage plus généraliste.
E-commerce
Parmi les solutions les plus éprouvées on peut utiliser Wordpress avec le plugin WooCommerce, Prestashop, osCommerce ou encore Magento.
Forums
Bien que passés de mode, PHP a accompagné la mode des forums. Les plus célèbres sont phpBB, IPB, Vbulletin ou encore Simple machines forum. Des alternatives plus légères comme punBB, FluxBB ou myBB existent.
Flarum est une solution moderne.
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
IIS tourne dans le vide après la mise à jour de PHP
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
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
Erreur sous Firefox provenant d'un mysql_close()
ou d'une directive Apache.
La page n'est pas redirigée correctement
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é)
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
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
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
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
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
Connect Error, 2002: Aucune connexion n'a pu être établie car l'ordinateur cible l'a expressément refusée.
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.
Ouvrir les pare-feux.
Invalid body indentation level (expecting an indentation level of at least 8)
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)
Ajouter le paramètre 4 : JSON_THROW_ON_ERROR.
json_decode throw "Control character error, possibly incorrectly encoded"
La string à décoder est trop longue.
Malformed UTF-8 characters, possibly incorrectly encoded
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
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
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
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
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
Un iterator_to_array($generator)
le ferme en le convertissant en tableau.
child exited on signal 7 (SIGBUS)
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
Le paramètre n°2 de fopen()
ne permet pas la lecture[7].
Each stream must be readable
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
On essaie de fermer un fichier fermé : utiliser if (is_resource($f))
avant.
SSL peer certificate or SSH remote key was not OK
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
[] operator not supported for strings
// 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 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
La méthode est invoquée sur une variable qui n'est pas une classe.
Call to undefined function
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()
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
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
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
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
É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
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é.
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.
Il faut certainement rerégler la Configuration DCOM voire le serveur Web.
Uncaught exception 'PDOException' with message
could not find driver
Se référer à la liste des pilotes connus dans Programmation PHP/PDO.
SQLSTATE[HY000]: General error
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
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
Se référer à Programmation PHP/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é
Se référer à Programmation PHP/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
Se référer à Programmation PHP/PDO : le paramètre 'host' est valide pour MySQL mais pas pour MS-SQL.
SQLSTATE[IMSSP]: The DSN string ended unexpectedly
Se référer à Programmation PHP/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
Se référer à Programmation PHP/PDO : utiliser la syntaxe ODBC.
Notice
Undefined property
La propriété de la variable n'est pas déclarée.
Undefined index: SERVER_NAME in ...php
Certaines versions de PHP utilisent $_SERVER['HTTP_HOST']
au lieu de $_SERVER['SERVER_NAME']
.
Undefined variable
La variable n'est pas déclarée.
Use of undefined constant
La constante n'est pas déclarée.
Parse error
syntax error, unexpected '(', expecting variable (T_VARIABLE) or '$' in...
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)
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)
Dans une classe en dehors des méthodes, il faut déclarer les variables avec leur portée :
private $Variable;
Strict Standards
Declaration of extFunctions::logs() should be compatible with functions::logs($chaine)
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
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
Cannot use a scalar value as an array
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
Se produit quand on appelle l'attribut d'un objet NULL.
date_diff() expects parameter 1 to be DateTimeInterface
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
On invoque une entrée inexistante dans un tableau associatif. Lever l'exception avec Try
ou if (!isset(
.
mkdir(): File exists
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'
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'
sudo pecl install redis
Erreurs SMTP
La connexion a échoué
Vérifier le serveur HTTP qui interprète le fichier .php
.
SMTP Error: Could not connect to SMTP host
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
- 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
1 package has known vulnerabilities
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
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
Lors d'un composer require, spécifier une version inférieure du paquet requis.
No driver found to handle VCS repository
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
Ajouter le paramètre suivant : composer require mon_paquet --update-with-all-dependencies
Permission denied (public key)
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
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".
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
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
Désactiver Xdebug et relancer.
PHPUnit
Les tests ne se lancent pas
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
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
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"
Un var_dump() sature la mémoire.
Did you forget a "use" statement for MaClasse ou Class 'MaClasse' not found
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...
Supprimer vendor/ et relancer composer.
THE ERROR HANDLER HAS CHANGED!
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
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
use
n'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
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
Si cela survient dans tous les tests qui invoquent un trait, retirer le constructeur de ce trait.
Warning No tests found in class "Xxx".
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

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
Retirer le :
$uow = $em->getUnitOfWork(); $uow->computeChangeSets();
Le contrôle de formulaire avec name=‘xxx’ ne peut recevoir le focus.
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
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
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)
composer remove friendsofsymfony/rest-bundle
Attempted to load class "WebProfilerBundle" from namespace "Symfony\Bundle\WebProfilerBundle"
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.
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
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.
composer require sensio/framework-extra-bundle
Circular reference detected
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
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
composer update symfony/flex --no-plugins --no-scripts
CURLPIPE_HTTP1 is no longer supported
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.
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.
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.
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
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
yarn install && yarn add --dev @symfony/webpack-encore && yarn build
Maximum function nesting level of '6000' reached
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)
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
Utiliser $request->getContent() ou $request->toArray() à la place[16].
SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known
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
Utiliser l'installation avec "Composer.phar" plutôt que "Symfony.phar".
InvalidArgumentException Invalid env(bool:xxx) name: only "word" characters are allowed
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.
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
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"?
À 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."
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
Le champ ne se sauvegarde pas en base
- 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
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"})
- Dans le cas d'un Doctrine\ORM\ORMInvalidArgumentException:
- Si on est en train de suppriemr 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 suppriemr 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, on tente de flusher une entité récupérée, ou dont une de ses entités liées a été récupérée, par l’entityManager d'un replica SQL (potentiellement non répliqué). Utiliser la base de données maitresee à la place.
- Sinon c'est que le même flush est réalisé plusieurs fois : ajouter du cache d'instance pour ne pas ré-exécuter ce code.
Binding an entity with a composite primary key to a query is not supported
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
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.
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
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?
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'
Il faut juse ajouter la colonne de jointure sous l'annotation *To*. Ex :
@ORM\JoinColumn(name="id", referencedColumnName="id_personne")
Could not find the entity manager for class '...'
Dans doctrine.yaml, retirer type: annotation
.
Entity has to be managed or scheduled for removal for single computation
- 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
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'.
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
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'])
No alias was set before invoking getRootAlias()
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 defined
quand 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
Survient quand une entité étend une autre sans discriminator[19].
The association mon_entité1#entité2 refers to the owning side field mon_entité2#id which does not exist
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
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
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
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
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
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
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
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-sync
pour bypasser cette partie du test.
Transaction commit failed because the transaction has been marked for rollback only
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"
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"
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 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
Lancer doctrine:database:create :
bin/console d:d:c
Puis rajouter les éventuelles tables :
bin/console doctrine:schema:update --force
Unrecognized field (ORMException)
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"
Ajouter apc.enable_cli=1
dans le fichier php.ini
.
DoctrineMigrations
S'il n'exécute pas tout (sans logs même en -vvv)
Séparer en plusieurs migrations, notamment les créations de tables et de fonctions.
The schema provider is not available
Remplacer "connection" par "em" dans doctrine_migrations.yaml :
doctrine_migrations: em: default
Syntax error or access violation
Il faut probablement échapper des caractères, par exemple avec la syntaxe heredoc ou $this->connection->quote().
API Platform
Invalid IRI
Ajouter le getId() dans l'entité récupérée par IRI.
InvalidArgumentException: "No item route associated with the type xxx
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
/** * @ApiProperty(identifier=true) */ private $id;
Si aucun ID n'est possible, en renvoyer "1" par exemple.
Unable to generate an IRI for
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
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
Un template Twig ne se rafraichit pas dans la navigateur
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 (:)
Il faut probablement mettre des parenthèses autour des variables dans un merge de tableau. Ex :
myArray|merge([{(myKey): (myValue)}])
Array to string conversion
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
Une virgule de trop après une clé.
key \"id\" for array with keys \"0\" does not exist.
Appel d'une clé absente d'un tableau.
The CSRF token is invalid. Please try to resubmit the form
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 "{"
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
Un "&" n'est pas échappé en XML. Il faut ajouter un filtre "| escape" en Twig pour le faire.
PhpStorm
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
Fermer le projet, supprimer le .idea, et le rouvrir.
Impossible de CTRL + clic dans un .twig (chemin introuvable)
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é
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
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
Il n'est pas ou plus associé à un type de fichier, aller dans File\Associate with File Type.
incoming connection from xdebug
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
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
Une classe existe mais PhpStorm ne la voit pas : ajouter son dossier dans File\Settings\Directories, retirer l'exclusion (rouge) du dossier concerné.
Xdebug
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
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
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
Cannot find file '/usr/local/php/php/auto_prepends/default_prepend.php' locally
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'
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
Le serveur est introuvable, changer :
- En V2, xdebug.remote_host ou remote_port.
- En V3, xdebug.client_host ou xdebug.client_port.
Gateway Timeout
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
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
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
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
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'
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
Fields "maRoute" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.
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
imagecreatefromstring(): gd-png: libpng warning: Interlace handling should be turned on when using png_read_image
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
- ↑ 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. |