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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]PHP 8.2 est sorti le 8 décembre 2022[7] :
- classes en lecture seule :
readonly classsignifie que tous les arguments de son constructeur son implicitement enreadonly. - types
null,trueetfalse. - constantes dans les traits.
- Forme normale disjonctive de type (combinaison de l'union et de l'intersection).
8.3
[modifier | modifier le wikicode]PHP 8.3 sort le 23 novembre 2023[8] :
- classes anonymes en lecture seule.
- constantes typées.
- fonction "json_validate".
8.4
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]À 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ↑ 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]LAMP
[modifier | modifier le wikicode]Logiciel tout-en-un pour Linux (Apache + MySQL + PHP), comme WAMP pour Windows.
# apt-get install tasksel
# tasksel install lamp-server
Installation manuelle
[modifier | modifier le wikicode]L'installation des logiciels séparément permet plus de personnalisation, par exemple de remplacer Apache par Nginx.
Apache sur Debian / Ubuntu
[modifier | modifier le wikicode]# apt-get install apache2Le 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 startOn 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
[modifier | modifier le wikicode]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.phpPour 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 sudo update-rc.d php8.4-fpm disable
Mise à jour
[modifier | modifier le wikicode]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
Enfin, s'il doit communiquer avec Apache via son module :
sudo a2enmod php8.4 sudo systemctl reload apache2
Bibliothèques
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Remplacer "install" par "remove" dans la commande d'installation :
sudo apt remove php8.4 php8.4-common php8.4-cli php8.4-fpm
Sinon, 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]$ sudo apt-get install mysql-server mysql_secure_installation
Variante
[modifier | modifier le wikicode]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 : [1])
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
[modifier | modifier le wikicode]emerge mysql
Modules
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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)
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Tout-en-un
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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 ?
[modifier | modifier le wikicode]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
.phpconcerne 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Il existe de nombreuses images de conteneur Docker pouvant fournir PHP. Exemple de Dockerfile[6] :
FROM php:8.4-cliFROM php:8.4-fpm
PHP 7.4 FPM
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]vim dispose de plugins pour programmer en PHP[8].
Références
[modifier | modifier le wikicode]- ↑ 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Le programme Composer lit et modifie la liste des bibliothèques du projet dans le fichier composer.json.
init
[modifier | modifier le wikicode]Pour créer le fichier composer.json :
composer init
create-project
[modifier | modifier le wikicode]Alternative à init pour les apps initialisée par un framework.
composer create-project "symfony/skeleton:^5" mon_projet
require
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
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
[modifier | modifier le wikicode]Affiche toutes les bibliothèques installées sur une ligne chacune, ce qui est plus lisible que composer.lock.
depends
[modifier | modifier le wikicode]Affiche les bibliothèques qui dépendent du package donné en paramètre.
clear-cache
[modifier | modifier le wikicode]composer clear-cache
Syntaxe du composer.json
[modifier | modifier le wikicode]La syntaxe JSON de ce fichier contient quelques extensions[2] :
name
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Après la version à trois nombres, on peut suffixer un arobase puis un Stability Flag[3]. Exemples :
@dev@stable
require-dev
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Ensemble de paramètres de composer[4], par exemple le timeout ou les identifiants vers certains dépôts.
autoload
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Complément de "scripts".
conflict
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Pour spécifier des dépôts privés (qui ne sont pas accessibles sans communiquer leur URL).
Exemple complet
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ↑ 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Refactorisation
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]| 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]
NetBeans

Installation de NetBeans
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]
NetBeans permet de comparer deux fichiers : Tools, Diff...
Gestion de versions
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
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
[modifier | modifier le wikicode]- ↑ 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
Commentaires
Principe
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
Premier programme
Hello World
[modifier | modifier le wikicode]É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
[modifier | modifier le wikicode]Soit le fichier HelloWorld.php ci-dessous, il peut être lancé par :
- les commandes shell
php -q HelloWorld.phpouphp -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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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,$_POSTet$_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
[modifier | modifier le wikicode]
Variables
Définition
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]<?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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- Pour plus de détails voir : Programmation PHP/Variables superglobales.
Variables de sessions
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]$_REQUEST est un tableau associatif constitué du contenu des variables $_GET, $_POST, $_COOKIE.
Variables de fichiers
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]Le mot clé mixed permet de définir une variable mixte.
Variables de serveurs
[modifier | modifier le wikicode]$_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.barsera/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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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 MaClasseest vrai si l'objet est de type "MaClasse".
Conversions
[modifier | modifier le wikicode]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) $vconvertit en entier.floatval($v)ou(float) $vconvertit en flottant.chr($v)convertit en caractère.strval($v)ou(string) $vconvertit 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ↑ 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- « Extraire l’url de la page en cours » [html]
- (en) « $_SERVER » [html]
Variables d'environnement : $_ENV
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Elle stocke les informations sur un fichier envoyé via HTTP par le client par un formulaire.
Variables de session : $_SESSION
[modifier | modifier le wikicode]Elle contient les valeurs de la session en cours pour le client.
Variables globales : $GLOBALS
[modifier | modifier le wikicode]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()
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Type de données
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Il en existe de deux types[3] :
- fixes en valeur
- dont la valeur est dynamique
Constantes intégrées à valeur fixe
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
Opérateurs
Les opérateurs sont des symboles qui permettent de manipuler les variables.
Opérateurs arithmétiques
[modifier | modifier le wikicode]= Opérateur d'affectation
[modifier | modifier le wikicode]<?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
[modifier | modifier le wikicode]<?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
[modifier | modifier le wikicode]<?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
[modifier | modifier le wikicode]<?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
[modifier | modifier le wikicode]<?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
[modifier | modifier le wikicode]<?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
[modifier | modifier le wikicode]<?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
}
fmod()[2].Opérateurs logiques booléens
[modifier | modifier le wikicode]Les opérateurs logiques agissent sur les types booléens (true ou false).
- ! : négation. Transforme
falseentrueettrueenfalse. - && : 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
[modifier | modifier le wikicode]Les opérateurs logiques bit à bit agissent sur chaque bit des valeurs entières.
- ~ Négation :
0->1et1->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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ++ : 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
[modifier | modifier le wikicode]- == 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
[modifier | modifier le wikicode]Opérateur ?
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]"&" 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ↑ 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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).
Quand on passe d'un tableau à un Generator, on ne peut plus compter avec count() ou boucler plusieurs fois sur les données retournées.
Portée des variables
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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_substren 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
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ↑ 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
[modifier | modifier le wikicode]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 : <syntaxhighlight lang=php>
$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);
print ne fonctionne pas pour les tableaux, il faut utiliser var_dump ou print_r. Par ailleurs, pour récupérer la chaine affichée par ces fonctions, utiliser print_r(MonTableau1, true).
Autres exemples :
$tab[0] = 1; // entier
$tab[1] = 2.0; // flottant
array_push($tab,'Ligne 3');
$tab[] = 'Ligne 4';
var_dump($tab);
Il en est de même pour les tableaux à deux dimensions.
$tab = [];
$tab[0][0] = '0-0';
$tab[0][1] = '0-1';
$tab[1][0] = '1-0';
$tab[1][1] = '1-1';
var_dump($tab);
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 tableau associatif, auquel on accède par le nom d'une clé en chaine de caractères.
Tableau itératifs
[modifier | modifier le wikicode]Les clés du tableaux sont des nombres. Ils ont l'avantage de pouvoir être parcourus par un compteur.
|
Exemple
|
$tab = ['val1', 'val2', 'val3']; // $tab[0] vaut val1 /-/ $tab[1] vaut val2 /-/ etc.
for($i = 0; $i<2; $i++)
echo $tab[$i];
|
Ce code affichera : val1val2.
En PHP, on peut aussi directement affecter des indices du tableau, comme suit :
Notez que les indices ne sont pas typés (on pourra indifféremment utiliser $tab[1] et $tab['1']).
Tableaux associatifs
[modifier | modifier le wikicode]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 :
|
Exemple
|
$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.". ";
|
Résultat : cle1 : val1. cle2 : val2. cle3 : val3.
Pour ne garder que les valeurs on peut utiliser implode(), qui convertit un tableau en chaine avec séparateur :
print implode(". ", $tab).". ";
Résultat : val1. val2. val3.
Fonctions de lecture
[modifier | modifier le wikicode]count: cette fonction renvoie le nombre d'éléments présent dans le tableau.
key: clé de l'élément courant du tableau, celui vers lequel le pointeur fait référence.current: valeur de l'élément courant.reset: valeur du premier élément.end: valeur du dernier élément.each: valeur de l'élément courant, et avance le pointeur au suivant.prev: valeur de l'élément précédent.next: valeur de l'élément suivant.array_values($tab): 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.array_keys($botteDeFoin, $aiguille): 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.array_key_exists($cle, $tab): renvoie "vrai" si la clé est dans le tableau.array_key_first($tab): renvoie la première clé d'un tableau.array_key_last($tab): renvoie la dernière clé d'un tableau.array_diff($t1, $t2): renvoie le tableau des différences entre ceux en paramètres (peut servir pour supprimer par valeur).array_sum($tab): renvoie la somme des valeurs du tableau.array_intersect($t1, $t2): intersection entre plusieurs tableaux.array_find($tab, function): renvoie un sous-tableau qui match la fonction en argument.
Exemple
[modifier | modifier le wikicode] $tab = ["mixte valeur<sub>1</sub>","mixte valeur<sub>2</sub>","...","mixte valeur<sub>n</sub>"];
echo key($tab);
echo ' : ';
echo current($tab);
Affiche 0 : mixte valeur1
Les fonctions key() et current() peuvent accéder aux autres éléments du tableau après each() ou next().
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
[modifier | modifier le wikicode]in_array($aiguille, $botteDeFoin): recherche de présence par valeur. Renvoie un booléen si l'élément est trouvé.array_search($aiguille, $botteDeFoin): recherche de position par valeur. Renvoie la clé de l'élément trouvé, ou false sinon.array_keys($botteDeFoin, $aiguille): recherche par clé (déjà décrit au paragraphe précédent).
php -r "var_dump(in_array(0, ['test']));" = true
Comparaison
[modifier | modifier le wikicode]Pour comparer deux tableaux :
$a1 == $a2; // compare le contenu et la taille
$a1 === $a2; // compare le contenu, la taille et l'index
Condition
[modifier | modifier le wikicode]Un tableau vide dans une condition vaudra "faux", alors qu'un non vide vaudra "true".
Fonctions d'écriture
[modifier | modifier le wikicode]Pour manipuler des tableaux il est indispensable de connaitre les fonctions suivantes :
str_split($string): convertit une chaine de caractères en tableau itératif, chaque ligne étant composée d'un caractère.mb_str_split($string): idem mais en caractères multi-octets.explode($separateur, $tableau): 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é.implode($separateur, $tableau): convertit un tableau en chaine de caractères. Le séparateur à placer dans la chaine est facultatif.sizeof($tableau): renvoie la taille du tableau (le nombre d'objets qu'il contient). Attention : avant PHP 7.2 cette fonction pouvait aussi remplacerstrlen().array_push($monTableau, $valeur): ajoute une ligne à la fin du tableau, équivaut à$monTableau[][1] (empile).array_unshift($monTableau, $valeur): ajoute une ligne au début du tableau[2].array_pop($monTableau): retire la dernière ligne du tableau, en la renvoyant[3] (dépile).array_shift($monTableau): retire la première ligne du tableau, en la renvoyant[4].array_merge($monTableau1, $monTableau2, $monTableau3...): fusionne plusieurs tableaux[5].array_merge_recursive(): idem en multidimensionnel.array_replace($monTableau1, $monTableau2, $monTableau3...): 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.array_replace_recursive(): idem en multidimensionnel.array_unique($tableau): filtre les valeurs en doublon (quelles que soient leurs clés).array_filter($tableau, fonction): filtre les lignes selon une fonction exécutée sur chaque élément. Pour injecter des variables dans la fonction, utiliser "use" (ex :array_filter($tableau, function($ligne) use($variable1){...).array_column($tableau, colonne): filtre par colonne. Renvoie uniquement les valeurs d'un champ donné pour chaque élément.array_reduce($tableau, fonction): transforme le tableau selon une fonction exécutée sur chaque élément.array_map(fonction, $tableau)exécute une fonction sur chaque valeur du tableau[6]. Exemples :- Pour trimer chaque ligne d'un tableau :
array_map('trim', $tableau) - Pour créer un tableau de tableau : <syntaxhighlight lang=php>
- Pour trimer chaque ligne d'un tableau :
array_map(function($ligne) {
return explode('=', $ligne);
}
, $tableau)
array_walk($tableau, fonction): exécute une fonction sur chaque élément (clé ou valeur).array_chunk($tableau, $taille): découpe le tableau fourni en tableaux de la taille fournie.array_slice($tableau, $début, $taille): 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.array_flip($tableau): 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).unset($tableau[$index]): supprimer la ligne.
|
Exemple
|
$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
|
Comme le premier indice du tableau est zéro, le dernier est égal à sa taille moins un.
|
Joindre les éléments "id" d'un tableau de tableaux
|
echo implode(', ', array_map(function ($ligne) {
return $ligne['id'];
}, $tableau));
|
Tris
[modifier | modifier le wikicode]array_multisort($tableau, SORT_ASC)permet de trier un tableau dans l'ordre croissant de ses valeurs.sort($tableau): trie le tableau par valeurs croissantes, en recréant des clés numériques.asort($tableau): trie le tableau par valeurs croissantes, en conservant les clés associées.arsort, fonction): trie le tableau par valeurs décroissantes, en conservant les clés associées.ksort($tableau): trie par clés croissantes par défaut.krsort($tableau): trie par clés décroissantes par défaut.usort($tableau, fonction): trie selon une fonction donnée[7].
|
Exemple
|
$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" }
|
Pour trier de l'Unicode il faut utiliser le flag "SORT_LOCALE_STRING".
Exemple :
$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);
donne : Array ( [0] => i [1] => o [2] => u [3] => à [4] => é )
Array ( [0] => à [1] => é [2] => i [3] => o [4] => u )
Tableaux multi-dimensionnels
[modifier | modifier le wikicode]La clé d’un tableau peut pointer sur un second tableau créant ainsi un tableau multi-dimensionnel.
$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 />';
}
}
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
[modifier | modifier le wikicode]Cette classe native permet de redéfinir l'opérateur d'index ([]) 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[8].
Destructuration
[modifier | modifier le wikicode]PHP ne gère pas la destructuration avec l'opérateur égal comme le fait JavaScript ou Python (ex : x, y = getXY()). À la place, il propose plusieurs fonctions :
list($x, $y): traite les variables en paramètre comme un tableau[9]. Ex :list($x, $y) = getArrayWithTwoLines();compact($x, $y): crée un tableau avec les noms des variables en paramètre comme clés, et leurs valeurs comme valeurs[10].extract($tableau): 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[11] (contraire decompact()).
Références
[modifier | modifier le wikicode]- ↑ http://php.net/manual/fr/function.array-push.php
- ↑ http://php.net/manual/fr/function.array-unshift.php
- ↑ http://php.net/manual/fr/function.array-pop.php
- ↑ http://php.net/manual/fr/function.array-shift.php
- ↑ http://php.net/manual/fr/function.array-merge.php
- ↑ http://php.net/manual/fr/function.array-map.php
- ↑ http://php.net/manual/fr/function.usort.php
- ↑ http://php.net/manual/fr/class.arrayaccess.php
- ↑ http://php.net/manual/fr/function.list.php
- ↑ http://php.net/manual/fr/function.compact.php
- ↑ http://php.net/manual/fr/function.extract.php
Structures de contrôle
Une structure conditionnelle fonctionne de manière à ce que si la valeur de la condition est true alors tel schéma est appliqué, et si la valeur est false, un autre schéma est réalisé.
if - else
[modifier | modifier le wikicode]La structure la plus simple se présente sous la forme d'un if() {} et d'un else {}. Le if 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
else {}; - Si elle n'est pas réalisée c'est ce qui est entre les accolades suivant le
elsequi sera exécuté.
<?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;
}
Emploi de if seul
[modifier | modifier le wikicode]Un if peut être employé seul, en fait le else é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.
<?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
Emploi de elseif
[modifier | modifier le wikicode]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 else), il peut être plus judicieux de tester une autre condition (déclarée par elseif). En clair la structure est semblable à un ordre de préférence :
| Symboliquement | Traduction en code |
|---|---|
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 |
<?php
if (condition 1) {
code 1;
} elseif(condition 2) {
code 2;
} elseif(condition 3) {
code 3;
} else {
code par défaut;
}
|
Imbrication
[modifier | modifier le wikicode]On peut imbriquer les if les uns dans les autres. Simplement lorsqu'un if imbriqué aura fini d'être exécuté, il retournera à l'étape logique suivante du rang hiérarchique supérieur.
<?php
if() {
if() { }
elseif() { }
else { }
}
elseif() {}
else {}
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
[modifier | modifier le wikicode]Lorsque l'on teste des conditions en nombre important sur une même valeur, l'utilisation de if est fastidieuse. Il existe bien heureusement une structure créée à cet usage : le switch. On déclare la variable à tester avec switch : switch($surlasellette) {}. Dans ce switch on utilise case pour déclarer la valeur de la variable pour laquelle une action est envisagée : case "valeur": (ici ne pas oublier les deux points !) une suite d'instructions s'ensuit et est généralement clôturée par break; pour que les autres cas ne soient pas traités à la suite. La valeur par défaut, corollaire du else pour if, est introduite par default:.
| Avec if | Traduction en switch |
|---|---|
<?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";
}
|
<?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);
}
|
switch s'imbrique aussi comme if.
Le plus important étant de ne pas oublier de mettre l'instruction break avant un nouveau case.
Le switch compare avec "==" et pas "===".
match
[modifier | modifier le wikicode]Depuis PHP8, cette structure retourne une valeur selon plusieurs conditions[1]. Ex :
echo match ($variable) {
'clé_1' => 'valeur 1',
'clé_2' => 'valeur 2',
default => 'unknown value',
};
Syntaxe alternative
[modifier | modifier le wikicode]PHP propose une syntaxe alternative aux accolades pour ses structures de contrôle, qui sont le ":" puis "end_nom_de_la_structure;"[2]. Ex :
if ($a == $b):
echo "Test";
endif;
C'est notamment pratique si vous utilisez php comme moteur de templating
<?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; ?>
Références
[modifier | modifier le wikicode]
Boucles
Concept
[modifier | modifier le wikicode]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) :
while(): le programme se répète tant qu'une condition est vraie (ex : tant que x est inférieur à 10).for(): 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
[modifier | modifier le wikicode]while est un mot anglais signifiant "tant que" en français. Le programme exécute une routine tant que la condition est vraie.
while (condition) {
instructions(s);
}
Deuxième syntaxe (moins courante) :
while (condition):
instructions(s);
endwhile;
Troisième syntaxe (assez rare), quand la condition contient une méthode qui change à chaque itération, jusqu'à renvoyer faux :
while (condition);
do while
[modifier | modifier le wikicode]Idem avec premier passage obligatoire : la condition est vérifié en fin de bloc.
do {
instructions(s);
} while (condition);
for
[modifier | modifier le wikicode]for 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.
for (première expression ; condition d'arrêt ; itération) {
instruction;
}
- 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
TRUEl'instruction sera exécutée, siFALSEest 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 for( ; ; )
foreach
[modifier | modifier le wikicode]Les boucles foreach 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.
foreachsimplifie une tache qui aurait certes été possible avecfor, mais fastidieuse :
-- 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"
}
as 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
$array = array( "ville" => "Montargis", "température" => "15 degrés" );
Ainsi on récupère le nom de la clé et la valeur du champ. En fait la structure $cle => $valeur est celle de la déclaration du tableau.
foreach ($array as $cle => $valeur) {
commandes;
}
Par ailleurs, il est possible d'itérer des objets depuis PHP 5.
break et continue
[modifier | modifier le wikicode]Les mots clés :
break: sort de la boucle avant la fin.continue: passe immédiatement à l'itération suivante.
Pour sortir ou continuer la boucle mère :
break 2;continue 2;
Dates
date()
[modifier | modifier le wikicode]La fonction date() 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[1] :
- 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
setlocale(LC_TIME, 'fr_FR');) - 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 (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
[modifier | modifier le wikicode]echo date('Y-m-d'); // affiche 2016-07-10
echo date('Y-m-d H:i:s'); // affiche 2016-07-10 20:06:34
strtotime()
[modifier | modifier le wikicode]Cette fonction transforme un texte (en anglais) en date[2].
Exemples courants :
$demain = date('Y-m-d', strtotime('+1 day'));
$hier = date('Y-m-d', strtotime('-1 day'));
Pour afficher la plage des dates de la semaine précédente[3] :
$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
checkdate()
[modifier | modifier le wikicode]Il est impératif dans un formulaire de vérifier si une date est au bon format. Pour ce faire il existe checkdate()[4] qui demande de séparer le mois, le jour puis l'année. Exemple :
var_dump(checkdate(0, 0, 2000)); // false
var_dump(checkdate(1, 1, 2000)); // true
DateTime
[modifier | modifier le wikicode]Cette classe peut être instanciée en style POO ou en style procédural[5]. Exemple :
$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');
Résultat : 2018-01-01 00:00:00.
Pour générer une date relative à l'actuelle :
echo (new DateTime('now -2 days'))->format('Y-m-d H:i:s')
Pour date relative à une absolue :
echo (new DateTime('2018-01-01 -2 days'))->format('Y-m-d H:i:s')
Elle possède également des méthodes pour modifier les dates :
- Via une string :
$date->modify('-1 day'); - Via un objet de type DateInterval.
- Pour modifier les heures, par exemple pour obtenir la date du jour à minuit :
(new DateTime())->setTime(0, 0);.
add
[modifier | modifier le wikicode]La méthode DateTime::add() (et son alias date_add()), permet d'ajouter deux dates[6].
diff
[modifier | modifier le wikicode]La méthode DateTime::diff() (et son alias date_diff()), permet de soustraire deux dates[7].
Exemple :
$date1 = new DateTime('2017-01-01');
$date2 = new DateTime('2020-11-01');
$dateInterval = $date2->diff($date1);
echo $dateInterval->format('%a jours');
Résultat : 1400 jours.
Conversion Timestamp en DateTime
[modifier | modifier le wikicode]$date = new DateTime('@1676564671');
DateTimeImmutable
[modifier | modifier le wikicode]Idem que DateTime mais immutable[8].
DateInterval
[modifier | modifier le wikicode]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 :
$dateInterval = new DateInterval('P1Y2M3D');
echo $dateInterval->format('%y an %m mois %d jours');
Résultat : 1 an 2 mois 3 jours.
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")[9], et aussi :
- a : nombre de jours au total.
- N : numéro du jour de la semaine (1 = lundi, 7 = dimanche).
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 :
var_dump(new DateInterval('P1D')); // un jour
var_dump(new DateInterval('PT1H')); // une heure
Les objets de cette classe peuvent être placés en paramètre des méthodes de DateTime add() et sub(). Ils peuvent aussi être obtenus en résultat de diff() vu ci-dessus.
DatePeriod =
[modifier | modifier le wikicode]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)[10].
Timestamps
[modifier | modifier le wikicode]time()
[modifier | modifier le wikicode]Cette fonction affiche la date courante au format 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 date().
mktime()
[modifier | modifier le wikicode]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 strtotime(), qui accepte les timestamps en second paramètre :
$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
date_default_timezone_set()
[modifier | modifier le wikicode]Ex :
date_default_timezone_set('Europe/Paris');
Références
[modifier | modifier le wikicode]- ↑ http://php.net/manual/fr/function.date.php
- ↑ http://php.net/manual/fr/function.strtotime.php
- ↑ http://stackoverflow.com/questions/21644002/how-can-i-get-last-week-date-range-in-php
- ↑ http://php.net/manual/fr/function.checkdate.php
- ↑ http://php.net/manual/fr/datetime.format.php
- ↑ https://www.php.net/manual/fr/function.date-add.php
- ↑ https://www.php.net/manual/fr/function.date-diff.php
- ↑ http://php.net/manual/fr/class.datetimeimmutable.php
- ↑ http://php.net/manual=/fr/dateinterval.format.php
- ↑ https://www.php.net/manual/en/class.dateperiod.php
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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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 ?
[modifier | modifier le wikicode]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é
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Initialiser une session
[modifier | modifier le wikicode]Pour pouvoir utiliser la fonctionnalité de session de PHP, il faut lancer le moteur de session en utilisant la fonction session_start().
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é
[modifier | modifier le wikicode]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 session_name() entre en scène.
Ce code va donc charger une session avec l'identifiant provenant du cookie ou du paramètre GET portant le nom nom_de_session.
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
[modifier | modifier le wikicode]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 (md5() ou sha1()) 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 session_id(). 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.
|
Exemple
|
<?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. */ |
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).
Lire et écrire des données dans la session
[modifier | modifier le wikicode]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 $_SESSION. Dans les versions plus anciennes, il s'appelait $HTTP_SESSION_VARS et nécessitait le mot clé global 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Voici le code du formulaire en HTML, il va afficher une boîte de texte et un bouton "Connexion".
La page de vérification
[modifier | modifier le wikicode]Le formulaire ci-dessus pointe vers une page nommée verification.php. 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.
L'appel à la fonction session_start() a fabriqué le tableau $_SESSION. 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 true (vrai) si le code est le bon. Nous pouvons alors rediriger le navigateur de l'internaute ou afficher un message d'erreur.
Si vous ne comprenez pas la ligne if ($mdp == $_POST['mdp']) {, vous devriez lire (ou relire) le chapitre sur les formulaires.
Pour résumé, le code suivant suffit à l'identification du visiteur :
La page d'administration
[modifier | modifier le wikicode]Cette page doit se nommer admin.php. 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.
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 true. 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 isset() qui vérifie si la variable (ou l'élément du tableau) existe.
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é.
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 403 si l'utilisateur n'existe pas, ou s'il existe mais que son mot de passe est incorrect.
Fermer une session
[modifier | modifier le wikicode]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 session_write_close().
Si vous voulez également détruire la session, vous pouvez utiliser la fonction session_destroy() couplée à la fonction session_unset().
Cookies
Introduction
[modifier | modifier le wikicode]setcookie('wiki', 'user');.Les 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[1] :
- Firefox :
C:\Users\%USERNAME%\AppData\Roaming\Mozilla\Firefox\Profiles\xxxxxxxx.default\cookies.sqlite(lisible par exemple avec https://addons.mozilla.org/en-US/firefox/addon/sqlite-manager/). - Chrome
C:\Users\%USERNAME%\AppData\Local\Google\Chrome\User Data\Safe Browsing Cookies. - Internet Explorer :
C:\Users\%USERNAME%\AppData\Roaming\Microsoft\Windows\Cookies.
- 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.
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 (path) permet de ne les rendre opérants que dans certaines parties d'un site.
- Le domaine fonctionne même avec d'autres serveurs (voir pixel espion).
Syntaxe
[modifier | modifier le wikicode]<?php
setcookie('cookie1', 'valeur1');
echo $_COOKIE['cookie1'];
Pour définir la durée du cookie (à 30 jours) et la page du site qui lui est associée :
<?php
setcookie('cookie2', 'valeur2', time() + 3600 * 24 * 30, '/');
Pour supprimer un cookie, on lui confère une durée de vie négative :
<?php
setcookie('cookie2', 'valeur2', time() - 1, '/');
Exemples
[modifier | modifier le wikicode]<?php
if (isset($_COOKIE["cookie1"])) {
echo 'Authentifié';
} else {
echo 'Non authentifié';
}
Références
[modifier | modifier le wikicode]- http://php.net/manual/fr/features.cookies.php
- http://www.php.net/manual/fr/function.setcookie.php
- http://fr2.php.net/explode/
Cache
La mémoire cache stocke des données calculées afin de pouvoir y ré-accéder sans les recalculer, donc plus rapidement.
Classification
[modifier | modifier le wikicode]Il existe plusieurs systèmes de cache en PHP pour accélérer l'exécution du code rappelé[1] :
| Nom | Données stockées | Flush |
|---|---|---|
| Cache d'instance | Objet PHP. Ex :if (null === $x) {
$x = 1;
} |
Relancer le script (ex : rafraichir la page Web). |
| Cache de session | Objet PHP[2] | Vider les cookies du navigateur. |
| OPcache | Opcode[3] | opcache_reset();
|
| APCu | Variables utilisateurs dans la RAM[4] | apcu_clear_cache();
|
| Cache du navigateur | Rendering, voire plus dans les cookies, LocalStorage et Indexed DB | CTRL + F5 |
| ESI | Partie de pages Web | Dépend du CDN ou proxy utilisé qui interprétera l'en-tête HTTP Cache-Control
|
| Proxy | Page web entière | Exemples, voir Varnish, HAProxy |
| Cache de framework | Configuration, traductions, résultat de requêtes HTTP | Exemple de Symfony où il s'agit de fichiers temporaires dans var/cache/ :
php bin/console cache:clear |
| Base de données NoSQL | Paire clé-valeur | Voir les pages Memcached et Redis ci-après. |
| Cache d'ORM | Annotations, requêtes SQL ou leurs résultats | Exemple de Doctrine :
php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:cache:clear-query
php bin/console doctrine:cache:clear-result
ou : bin/console cache:pool:clear --all ou en PHP : $qb = $entityManager->createQuery();
$cacheDriver = $qb->getResultCacheDriver();
$cacheDriver->delete('ma_cle_de_cache');
|
| 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[5], 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
[modifier | modifier le wikicode]OPcache
[modifier | modifier le wikicode]Dans Docker :
RUN docker-php-ext-install opcache
APCu
[modifier | modifier le wikicode]Dans Docker
[modifier | modifier le wikicode]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
&& pecl install apcu_bc \Sur machine hôte Linux
[modifier | modifier le wikicode]sudo pecl install apcu echo "extension=apcu.so" >> php.ini
Sur machine hôte Windows
[modifier | modifier le wikicode]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 :
extension=apcu
[apcu] apc.enabled=1 apc.enable_cli=1
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[6] :
apc.shm_size=32M apc.ttl=7200 apc.enable_cli=1 apc.serializer=php
ou[7]
apc.shm_size=64M apc.shm_segments=1 apc.max_file_size=10M apc.stat=1
Références
[modifier | modifier le wikicode]- ↑ http://www.php-cache.com/en/latest/
- ↑ https://www.php.net/manual/fr/session.security.php
- ↑ https://www.php.net/manual/fr/intro.opcache.php
- ↑ https://www.php.net/manual/fr/intro.apcu.php
- ↑ https://www.php-fig.org/psr/psr-6/
- ↑ https://stackoverflow.com/questions/24448261/how-to-install-apcu-in-windows
- ↑ https://phpflow.com/php/how-to-install-apc-cache-on-wamp-and-xampp/?utm_content=cmp-true
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[1].
Installation
[modifier | modifier le wikicode]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[2].
Installation :
sudo apt-get install memcached
Client
[modifier | modifier le wikicode]Sur Docker PHP :
RUN pecl install memcached \
&& docker-php-ext-enable memcache
Test
[modifier | modifier le wikicode] telnet localhost 11211
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 :
netstat -an | grep ":11211"
tcp 0 0 127.0.0.1:11211 0.0.0.0:* LISTEN
ou
ss -nlt | grep 11211
LISTEN 0 1024 127.0.0.1:11211
Pour le résoudre :
sudo vim /etc/memcached.conf
sudo /etc/init.d/memcached restart
Commandes
[modifier | modifier le wikicode]- Reset mémoire :
echo "flush_all" | nc -q 1 localhost 11211
Utilisation
[modifier | modifier le wikicode]Memcached propose plusieurs commandes[3]. Pour tester si le serveur fonctionne avant de l'utiliser en PHP, on peut donc les lancer avec telnet nom_du_serveur 11211.
- 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 :
stats items
STAT items:1:number 1
...
STAT items:2:number 1
...
STAT items:3:number 1
...
Puis l'appeler par son numéro pour voir son nom (le zéro représente l'absence de limite) :
stats cachedump 1 0
Utilisation en PHP
[modifier | modifier le wikicode]Installation
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]$memcached = new \Memcached();
$memcached->addServer('127.0.0.1', 11211);
$memcached->set('nom du test', 'valeur du test');
echo $memcached->get('nom du test');
Références
[modifier | modifier le wikicode]- ↑ (en) « License of memcached »
- ↑ « Amplification d'attaque DDoS : Memcached fait exploser les compteurs » (consulté le 19 octobre 2018)
- ↑ https://github.com/memcached/memcached/wiki/Commands
Redis
Installation
[modifier | modifier le wikicode]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[1]. 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[2].
Serveur
[modifier | modifier le wikicode]Pour l'installer :
sudo apt-get install redis-server
Obtenir la version :
redis-server -v Redis server v=6.2.5 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=19d2f4c94e0b6820
Client
[modifier | modifier le wikicode]sudo apt-get install redis
Sur Docker PHP :
RUN pecl install redis \
&& docker-php-ext-enable redis
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
[modifier | modifier le wikicode]Pour se loguer au serveur Redis :
telnet nom_du_serveur 6379
Les commandes Redis les plus utiles[3] :
Lecture
[modifier | modifier le wikicode]- 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
nilsi elle n'existe pas). - MGET : affiche les valeurs des clés en paramètre.
- QUIT : quitter.
Écriture
[modifier | modifier le wikicode]- SET : définit une valeur associée à la clé en paramètre[4].
- EXPIRE : définit une durée avant expiration pour la clé en paramètre, en secondes.
- SETEX : SET + EXPIRE[5].
- 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
[modifier | modifier le wikicode]Pour afficher les clés de la base en shell :
redis-cli KEYS '*'
Par défaut, redis-cli pointe sur 127.0.0.1. Pour regarder une autre machine :
redis-cli -h redis.example.com KEYS '*'
Supprimer des clés par leurs noms[6] (exemple de celles qui ont le préfixe "users:") :
redis-cli KEYS "users:*" | xargs redis-cli DEL
ou :
redis-cli --scan --pattern users:* | xargs redis-cli DEL
Plusieurs bases
[modifier | modifier le wikicode]Chaque instance de Redis peut accueillir jusqu'à 16 bases de données[7].
Elles sont accessibles par un suffixe dans leur DSN. Par défaut, redis://localhost:6379 pointe sur la base zéro : redis://localhost/0:6379.
Utilisation en PHP
[modifier | modifier le wikicode]$redis = new \Redis();
$redis->connect('localhost', 6379);
$redis->set('nom du test', 'valeur du test');
echo $redis->get('nom du test');
predis
[modifier | modifier le wikicode]Cette bibliothèque permet d'utiliser Redis en clustering, avec des masters et slaves[8].
Dans Symfony
[modifier | modifier le wikicode]Dans le framework PHP Symfony.
Session
[modifier | modifier le wikicode]SncRedisBundle
[modifier | modifier le wikicode]Avant Symfony 4.1, il fallait passer par un bundle tel que SncRedisBundle[9].
Depuis :
composer require snc/redis-bundle predis/predis
Pour que les sessions soient stockées dans Redis au lieu de var/cache/, remplacer dans framework.yaml, session.handler_id: null par snc_redis.session.handler. Cela permet par exemple de les partager entre plusieurs conteneurs.
RedisSessionHandler
[modifier | modifier le wikicode]Depuis Symfony 4.1, le composant HttpFoundation contient une classe RedisSessionHandler[10].
Installation dans services.yaml :
redis:
class: Redis
calls:
- connect:
- '%env(REDIS_HOST)%'
- '%env(int:REDIS_PORT)%'
Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
arguments:
- '@redis'
Puis dans config/packages/framework.yaml[11] :
framework:
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
Ensuite le service session utilisera automatiquement Redis.
Doctrine
[modifier | modifier le wikicode]Pour mettre le cache Doctrine de requête et de résultat dans Redis, on peut utiliser SncRedisBundle[12][13].
Pour vider les caches :
bin/console cache:pool:clear doctrine.query_cache_pool doctrine.result_cache_pool
Cache chainé
[modifier | modifier le wikicode]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[14] :
# 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
# services.yaml
services:
chain_adapter_provider:
class: Symfony\Component\Cache\Adapter\ChainAdapter
arguments:
- ['@cache.redis', '@cache.apcu', '@cache.filesystem']
- '%env(int:CACHE_DEFAULT_LIFETIME)%'
Références
[modifier | modifier le wikicode]- ↑ https://aws.amazon.com/fr/elasticache/redis-vs-memcached/
- ↑ https://www.disko.fr/reflexions/technique/redis-vs-memcached/
- ↑ https://redis.io/commands
- ↑ https://redis.io/commands/set/
- ↑ https://redis.io/commands/setex/
- ↑ https://rdbtools.com/blog/redis-delete-keys-matching-pattern-using-scan/
- ↑ https://www.mikeperham.com/2015/09/24/storing-data-with-redis/
- ↑ https://github.com/predis/predis
- ↑ https://github.com/snc/SncRedisBundle/blob/master/Resources/doc/index.md
- ↑ https://github.com/symfony/symfony/blob/4.1/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php
- ↑ https://symfony.com/doc/current/session/database.html
- ↑ https://kunicmarko20.github.io/2017/07/20/Doctrine-Second-Level-Cache-with-Translations-and-Redis.html
- ↑ https://github.com/snc/SncRedisBundle/issues/554
- ↑ https://symfony.com/doc/current/components/cache/adapters/chain_adapter.html
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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.
En gros, ce formulaire enverra sur la page traitement.php la valeur de l'entrée "mdp".
Le traitement
[modifier | modifier le wikicode]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 $_POST pour des raisons de sécurité et de compatibilité.
<?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) */
La zone administration
[modifier | modifier le wikicode]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.
<?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. */
Types de champ
[modifier | modifier le wikicode]Pour des <input type="checkbox">, on vérifie si leurs valeurs sont 'on' ou 'off'.
Types MIME
[modifier | modifier le wikicode]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[1].
Références
[modifier | modifier le wikicode]
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
[modifier | modifier le wikicode]Voici les fonctions usuelles de navigation et manipulation du système de fichier.
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é").
Lecture
[modifier | modifier le wikicode]PHP_OS: contient de système d'exploitation courant (ex : WINNT, Linux).basename($path): extraire le nom du fichier à partir de son chemin.dirname(__DIR__): extraire le répertoire parent du dossier en paramètre, au niveau précisé en second paramètre (le parent direct par défaut).chdir($dossier): changer de dossier courant ;opendir($dossier): ouvrir un dossier ;closedir($dossier): fermer le dossier ;is_dir($dossier): vérifier la présence d'un dossier ;is_file($fichier): vérifier la présence d'un fichier ;file_exists($fichier): vérifier la présence d'un fichier ou un dossier local[1] ;filesize($fichier): renvoie la taille du fichier en octet[2] ;readdir($dossier): lire le contenu du dossier ligne par ligne ;scandir($dossier): lire le contenu du dossier en une fois ;glob($regex): lire le contenu du dossier courant, avec filtrage regex (ex : *.txt)[3].pathinfo($fichier): renvoie un tableau avec les caractéristiques du fichier : dossier, nom, extension.mime_content_type($fichier): renvoie le type MIME.
Généralement le suffixe du type MIME et identique à l'extension du pathinfo, mais il y a des exceptions, car le premier se base sur le contenu du fichier alors que le second sur son nom. Ex :
$filePath = 'test.jpg'; var_dump(pathinfo($filePath, PATHINFO_EXTENSION)); // jpg var_dump(mime_content_type($filePath)); // image/jpeg
Récupérer certains fichiers d'un dossier
[modifier | modifier le wikicode]Par exemple, pour trouver tous les fichiers .sql du dossier "sql" :
chdir('sql');
$files = glob('*.sql');
var_dump($files);
Écriture
[modifier | modifier le wikicode]shell_exec('pwd'): exécuter n'importe quelle commande shell. Le nom du répertoire courant (pwd) dans cet exemple.mkdir()(make directory) : créer un dossier.rmdir()(remove directory) : supprime un dossier non vide.touch(): créer un fichier (ou le rafraichir s'il existe).rename(): déplacer un fichier.
Les commandes du shell_exec dépendent du système d'exploitation. Par exemple, sur Linux shell_exec('ls') fonctionne mais sur Windows il renvoie null et il faut utiliser shell_exec('dir').
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 : DIRECTORY_SEPARATOR qui fonctionne pour les deux[4].
array_map('unlink', glob('your/path/*.*'));
rmdir('your/path');
Droits des fichiers
[modifier | modifier le wikicode]Windows
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]chmod est le système de droit d'accès a un fichier Unix. Il s'agit d'un 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 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 :
fileperms($localFilePath) & 0777 - Le propriétaire :
fileowner($localFilePath))
Ouvrir un fichier
[modifier | modifier le wikicode]Pour déclencher l'ouverture d'un fichier chez celui qui exécute le script, on précise son type puis son emplacement :
header("Content-Type: application/pdf");
header("Content-Disposition: inline; filename='".$fichier."'");
Télécharger un fichier
[modifier | modifier le wikicode]header("Content-Type: application/pdf");
header("Content-Disposition: attachment; filename='".$fichier."'");
Le téléchargement ne doit pas être précédé d'affichages (ex : echo ou logs de warning) sinon ils apparaitront dans l'en-tête du fichier, le rendant illisible.
Zipper un fichier
[modifier | modifier le wikicode]Installation
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Pour compresser des fichiers, il faut d'abord créer l'archive vide, puis les y ajouter un par un avec la méthode addFile(Fichier source, Nom du fichier dans l'archive)[6] :
$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();
Dézipper un fichier
[modifier | modifier le wikicode]$zip = new ZipArchive();
$f = '../Temp/'.$nomFichier . '.zip';
if ($zip->open($f) === true) {
$zip->extractTo('../Temp/');
$zip->close();
}
Pour les .gz[7] :
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);
Éditer et fermer un fichier
[modifier | modifier le wikicode]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à :
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 :
| Mode | Description |
|---|---|
| r | (read) Ouvre en lecture seule, et place le pointeur de fichier au début du fichier. |
| r+ | Ouvre en lecture et écriture, et place le pointeur de fichier au début du fichier. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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|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. |
| 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. |
| 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. |
| c+ | Ouvre le fichier en lecture et écriture, puis se comporte comme "c". |
Pour le fermer maintenant, il y a la fonction fclose.
Copier un fichier
[modifier | modifier le wikicode]if (!copy($ancienFichier, $nouveauFichier)) {
echo 'La copie a échoué.';
}
Pour les images il existe aussi imagecopyresampled[8].
Supprimer un fichier
[modifier | modifier le wikicode]if (!unlink($fichier)) {
echo 'La suppression a échoué.';
}
Interagir avec le fichier
[modifier | modifier le wikicode]Lecture
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]file
[modifier | modifier le wikicode]Pour stocker le fichier entier dans un tableau, on peut utiliser file() qui renvoie un tableau séparant chaque ligne du fichier :
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[9].
file_get_contents
[modifier | modifier le wikicode]Pour stocker le fichier entier dans un scalaire, on utilise cette fonction.
readfile
[modifier | modifier le wikicode]Pour afficher tout le fichier dans la sortie standard[10].
Ligne par ligne
[modifier | modifier le wikicode]fgets
[modifier | modifier le wikicode]La méthode générale pour lire ligne par ligne avec la fonction fgets, dont la définition est la suivante :
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.
|
Exemple de parcours d'un fichier ligne par ligne
|
<?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.";
}
|
fgetc
[modifier | modifier le wikicode]La fonction équivalente pour lire caractère par caractère se nomme fgetc :
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
[modifier | modifier le wikicode]Cette fonction fonctionne comme "fgets" sauf qu'en plus elle utilise le séparateur "," (qui peut être changé par le paramètre "delimiter"[11]) pour créer un sous-tableau de champs par ligne.
Écriture
[modifier | modifier le wikicode]Une fois le fichier ouvert, l'écriture se fait via la fonction fwrite.
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.
|
Exemple d'écriture
|
<?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.";
}
|
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[12].
Se déplacer
[modifier | modifier le wikicode]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 :
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 :
Fichiers uploadés
[modifier | modifier le wikicode]Copier-coller ces lignes dans un fichier vierge upload.php pour qu’il affiche le nom des fichiers qu'on upload avec :
<?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'];
}
Il existe la fonction is_uploaded_file() pour vérifier si un fichier est bien issu d'un upload[13], et move_uploaded_file() pour le déplacer.
Fichiers temporaires
[modifier | modifier le wikicode]Pour créer des fichiers qui seront automatiquement détruits en fin de connexion, le dossier temporaire est accessible avec : sys_get_temp_dir().
La fonction tempnam() quant-à elle nomme automatiquement un nouveau fichier temporaire avec un nom unique :
$fileName = tempnam(sys_get_temp_dir(), 'MonFichier1');
Fichiers distants
[modifier | modifier le wikicode]HTTP
[modifier | modifier le wikicode]Pour lire un fichier en HTTP (par exemple cette page web) :
<?php
$page = file_get_contents("http://fr.wikibooks.org/wiki/PHP/Fichiers");
echo $page;
ou[14]
<?php
$url = 'http://fr.wikibooks.org/wiki/PHP/Fichiers';
echo htmlspecialchars(implode('', file($url)));
Pour tester si un fichier distant existe, utiliser get_headers()[15].
FTP
[modifier | modifier le wikicode]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)[16] :
<?php
copy('fichier_local.txt', 'ftp://login:password@server/repertoire/fichier_distant.txt');
Souvent le répertoire porte le nom de l'utilisateur, et on écrase le fichier s'il existe :
<?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))));
Fonctions propres au FTP, pour lire un serveur distant, y télécharger et déposer des fichiers[17] :
$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";
}
Pour définir le timeout, voir ftp_set_option()[18].
SFTP
[modifier | modifier le wikicode]Prérequis : libssh2-1-dev && libssl-dev. Activer ssh2 dans phpinfo.
On utiliser les trois fonctions suivantes pour construire l'URL ouverte par fopen[19] :
$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);
php://
[modifier | modifier le wikicode]Ce protocole donne accès aux flux (entrant et sortant) de PHP[20].
- php://fd : file descriptor.
- 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ↑ http://php.net/manual/fr/function.file-exists.php
- ↑ https://www.php.net/manual/en/function.filesize.php
- ↑ http://php.net/manual/fr/function.glob.php
- ↑ http://php.net/manual/fr/dir.constants.php
- ↑ https://stackoverflow.com/questions/1653771/how-do-i-remove-a-directory-that-is-not-empty
- ↑ http://php.net/manual/fr/ziparchive.addfile.php
- ↑ https://stackoverflow.com/questions/11265914/how-can-i-extract-or-uncompress-gzip-file-using-php
- ↑ http://php.net//manual/fr/function.imagecopyresampled.php
- ↑ https://www.startutorial.com/articles/view/php-generator-reading-file-content
- ↑ https://www.php.net/manual/en/function.readfile.php
- ↑ http://php.net/manual/fr/function.fgetcsv.php
- ↑ http://php.net/manual/fr/function.file-put-contents.php
- ↑ https://www.php.net/manual/en/function.is-uploaded-file.php
- ↑ developpez.net
- ↑ http://php.net/manual/fr/function.get-headers.php
- ↑ http://php.net/manual/fr/book.ftp.php
- ↑ http://php.net/manual/fr/function.ftp-nlist.php
- ↑ http://php.net/manual/fr/function.ftp-set-option.php
- ↑ http://php.net/manual/fr/function.ssh2-sftp.php
- ↑ http://php.net/manual/fr/wrappers.php.php
Objets COM
Installation
[modifier | modifier le wikicode]Le système d'exploitation Windows fournit des objets COM (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[1] :
ini_set('com.allow_dcom','1');
Exemple
[modifier | modifier le wikicode]Création d'un .xls :
$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();
On peut aussi créer des .doc.
Références
[modifier | modifier le wikicode]
Images
Introduction
[modifier | modifier le wikicode]PHP peut créer et modifier dynamiquement des images, par exemple avec la 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
[modifier | modifier le wikicode]Pour créer une image ex-nihilo, on utilise la fonction :
imagecreatetruecolor($hauteur, $largeur)
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[1] :
imagecreate($largeur, $hauteur)
Pour charger en mémoire une image sauvegardée sur le disque :
imagecreatefrom<type>($chemin)
Exemple :
$img = imagecreatefrompng('image.png');
Autre fonction :
imagecreatefromstring($texte)
qui crée une image à partir de son format texte, spécifié en paramètre.
S'il survient une erreur, ces fonctions renvoient false.
Travailler avec les couleurs
[modifier | modifier le wikicode]Pour allouer une couleur il faut en définir les paramètres RVB :
$couleur = imagecolorallocate($image, $r, $v, $b)
Pour définir une transparence dans un PNG :
imagecolortransparent($image, $couleur)
où $couleur est le résultat de imagecolorallocate.
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 :
imagecolorallocatealpha($image, $r, $v, $b, $transparence)
Sinon, un fond transparent peut également être assuré par les deux fonctions ci-dessous :
imageAlphaBlending($image, false);
imageSaveAlpha($image, true);
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
[modifier | modifier le wikicode]Pour dessiner un pixel, on utilise ses coordonnées (x, y ci-dessous) :
imagesetpixel(image, x, y, couleur)
Pour tracer une ligne entre deux points :
imageline(image, x1, y1, x2, y2, couleur)
Pour créer un rectangle par sa diagonale :
imagerectangle(image, x1, y1, x2, y2, couleur)
Pour représenter une ellipse par son centre, sa hauteur et sa largeur :
imageellipse(image, x, y, h, l, couleur)
ou en précisant son arc par ses angles en gradient (numérotés dans le sens horaire) :
imagearc(image, x, y, h, l, angle1, angle2, couleur)
Retravailler les pixels existants
[modifier | modifier le wikicode]Une des fonctions les plus utilisées pour retravailler des images comme des photos, est certainement imagecopyresized, qui permet de copier une zone rectangulaire pour la coller dans une autre image[2]. Exemple :
imagecopyresized(dst_image, src_image, dst_x, dst_y, src_x, src_y, dst_w, dst_h, src_w, src_h);
où :
src_imageest l'image source ;dst_imageest l'image de destination ;dst_x, dst_ysont les coordonnées dedst_image;src_x, src_ysont les coordonnées desrc_imageen partant d'en haut à gauche ;dst_w, dst_h, src_w, src_hsont les largeurs et hauteurs des rectangles de source et destination.
On peut ensuite comprendre que si dst_w est égal à src_w, et dst_h à src_h, la portion rectangulaire de l'image restera de la même taille. Dans le cas contraire on allonge ou on élargit.
La fonction imagecopyresampled reçoit les mêmes paramètres que imagecopyresized, mais avec la différence que, en cas de redimensionnement, la qualité est améliorée.
Puis il existe la fonction imagefilter, qui permet de nombreux effets tels que les niveaux de gris, le relief, ou la recoloration[3].
Imprimer l'output
[modifier | modifier le wikicode]Le format de l'image obtenue ("png", "jpeg" ou "gif") peut être spécifié par la fonction header, via le content-type (par défaut text/html) ainsi :
header("Content-type: image/<type>");
Pour visualiser l'image ensuite, la placer en paramètre dans une fonction dépendant de son type : imagepng, imagejpeg ou imagegif.
Puis, libérer la mémoire avec imagedestroy($image). Cette étape facultative est particulièrement recommandée pour les images volumineuses.
Exemple
[modifier | modifier le wikicode]Le code suivant affiche dans le navigateur, un carré rouge de 50 pixels dans un noir de 100.
$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);
Références
[modifier | modifier le wikicode]
Mails
Mail()
[modifier | modifier le wikicode]Le code suivant se connecte à localhost:25 pour envoyer un mail[1] :
<?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);
Si la machine hébergeant le script n'est pas pourvue d'un serveur SMTP, le freeware portable Simple Mail Server[2] peut jouer ce rôle rapidement sans installation.
Utilisation de Mail() plus complexe...
[modifier | modifier le wikicode]|
Exemple
|
<?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);
//==========
|
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
[modifier | modifier le wikicode]|
Exemple
|
<?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);
//==========
|
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
[modifier | modifier le wikicode]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 :
- PHPMailer[3]
- PEAR Mail[4], Net SMTP[5] et Net Socket[6], et Mail Mime[7] pour ajouter un fichier joint.
Références
[modifier | modifier le wikicode]- ↑ http://php.net/manual/fr/function.mail.php
- ↑ https://sourceforge.net/projects/simplemailsvr/
- ↑ http://sourceforge.net/projects/phpmailer/
- ↑ http://pear.php.net/package/Mail/download
- ↑ http://pear.php.net/package/Net_SMTP/download
- ↑ http://pear.php.net/package/Net_Socket/download
- ↑ http://pear.php.net/package/Mail_Mime/download
Sécurité
Sécuriser les en-têtes HTTP
[modifier | modifier le wikicode]Pour commencer, il est préférable d'utiliser HTTPS à HTTP pour complexifier l'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 HSTS, qui utilise l'en-tête HTTP Strict-Transport-Security.
Masquer les versions
[modifier | modifier le wikicode]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 :
ini_set('expose_php', 'Off');
header('X-Powered-By: UnknownWebServer');
Restreindre les connexions
[modifier | modifier le wikicode]De plus, il existe d'autres sécurisations des en-têtes HTTP pour se prémunir des attaques de type Cross-site request forgery (CSRF).
Exemple de couche de protection utilisant le Content Security Policy (CSP) :
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=()"');
Voir aussi Cross Origin Ressource Sharing (CORS) avec Access-Control-Allow-Origin.
OpenSSL
[modifier | modifier le wikicode]PHP propose des fonctions des bibliothèques OpenSSL, pour le chiffrement et déchiffrement symétrique et asymétrique[1].
Par exemple, pour signer un en-tête HTTP (et vérifier ensuite) :
$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);
Protéger l'accès à l'application
[modifier | modifier le wikicode]Pour éviter que des robots spammeurs polluent l'application via ses formulaires et ses API, ou la saturent par une 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 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 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 injection SQL (généralement les apostrophes), et cross-site scripting (alias XSS, qui injecte du JavaScript entre balises HTML "script"), ou clickjacking (balises "iframe"). En échappant les chevrons cela suffit)[2].
- Imposer aux utilisateurs des mots de passe complexes (c'est-à-dire impossible à deviner par robots avant plusieurs années de tentatives infructueuses).
- Installer un captcha sur les formulaires accessibles aux utilisateurs non connectés. Par exemple reCAPTCHA, ou sur MediaWiki ConfirmEdit.
- Filtrer les messages postés sur l'application en les comparant aux listes partagées de spams connus, comme l'API Akismet[3], ou sur MediaWiki TitleBlacklist.
- Utiliser PHP >= 8 qui protège par défaut contre les attaques XML external entity attack (XXE)[4].
Protéger les données sensibles
[modifier | modifier le wikicode]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)[5] : avec un algorithme de la liste
hash_algos()[6]. Ex :hash('sha256', $maChaine). Déconseillé car pas adapté pour les mots de passe. - crypt()[7] est une fonction de hachage à sens unique d'une chaine, qui accepte en deuxième paramètre optionnel un salage. Déconseillé car vulnérable à certaines attaques.
- sha1() : déconseillé car facile à casser[8].
- md5() : déconseillé car facile à casser[9].
La méthode actuellement recommandée pour hasher un mot de passe est l'utilisation de la fonction password_hash() conjointement avec password_verify() pour vérifier si un mot de passe saisi par l'utilisateur est correct.[10][11]
Scanners
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- ↑ https://www.php.net/manual/fr/ref.openssl.php
- ↑ https://github.com/WebGoat/WebGoat
- ↑ https://symfony.com/doc/current/the-fast-track/fr/16-spam.html
- ↑ https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#php
- ↑ https://www.php.net/manual/fr/function.hash.php
- ↑ https://www.php.net/manual/fr/function.hash-algos.php
- ↑ http://php.net/manual/fr/function.crypt.php
- ↑ https://www.php.net/manual/fr/function.sha1.php
- ↑ https://www.php.net/manual/fr/function.md5.php
- ↑ https://www.php.net/manual/fr/function.password-hash.php
- ↑ https://www.php.net/manual/fr/function.password-verify.php
Programmation orientée objet
Introduction
[modifier | modifier le wikicode]Une classe est un format de variable non scalaire, comprenant trois types de composants :
- des constantes, accessibles par réflexion avec
$maClasse::getConstants(). - des variables appelées "propriétés", accessibles avec
$maClasse::getProperties(). - des fonctions appelées "méthodes", accessibles avec
$maClasse::getMethods().
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 static sont persistants, et accessibles sans instanciation préalable.
Opérateur objet
[modifier | modifier le wikicode]Pour accéder aux propriétés et méthodes d'un objet, on utilise l'opérateur object : ->.
Opérateur de résolution de portée
[modifier | modifier le wikicode]Pour accéder aux constantes, propriétés et méthodes statiques d'une classe, on utilise l'opérateur de résolution de portée : ::.
Cet opérateur peut également être précédé de noms de classes ou des mots réservés suivants[1] :
$this: l'objet courant.parent: la classe parente.static: la classe courante.self: la classe parente puis la courante s'il n'y a rien[2].
Le mot-réservé static a donc deux sens : un pour les déclarations et un pour les appels.
Inclusion
[modifier | modifier le wikicode]À 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 :
include('ma_classe.php');
// ou
include_once('ma_classe.php');
// ou
require('ma_classe.php');
// ou
require_once('ma_classe.php');
Mais la syntaxe à privilégier est celle par espace de nom :
use mon_namespace/ma_classe;
En PHP, l'inclusion doit précéder les appels du code qui y figure.
use function ma_fonction pour déclarer l'utilisation d'une fonction.Instanciation
[modifier | modifier le wikicode]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 :
$c = new stdClass(); var_dump($c);
Définition des classes
[modifier | modifier le wikicode]Définir une nouvelle classe adopte la syntaxe suivante :
class nomObjet
{
var $variable1;
var $variable2;
...
function maFonction1()
{
...code
}
function maFonction2()
{
}
}
Il est possible d’attribuer une valeur par défaut. Le code dans la classe est alors var $variable1 = valeur;. 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, $variable doit être :
$this->variablepour cibler l'objet instancié (et$this::constante,$this->méthode()).self::variablepour cibler la classe statique.
De même pour exécuter une autre méthode de sa classe. ex :
class client
{
var $aDitBonjour = false;
function direBonjour()
{
$this->message("Bonjour");
}
function message($message)
{
echo $message;
$this->aDitBonjour = true;
}
}
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 global :
class client
{
function message($message)
{
global $InstanceAutreClasse;
$InstanceAutreClasse->aDitBonjour = true;
}
}
Utilisation d’un objet
[modifier | modifier le wikicode]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 :
class client
{
for ($i=0; $i<5; $i++)
echo "$i\n";
}
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 :
$objet = new client();
Il faut bien entendu avoir préalablement défini la classe client. La variable $objet contient donc un objet. Pour accéder à une variable pour lui faire subir des modifications, il suffit d’entrer le code suivant :
$objet->variable1 = "Hello world";
Il est possible de lui faire subir les mêmes opérations qu’à une variable normale. De même pour exécuter une fonction :
$objet->maFonction();
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 :
$objet->variable = "valeur"; // définition de variable unset($objet->variable); // suppressions
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 :
//Le code reprend l'ancien script $objet = new client(); $objet2 = $objet; $objet2->direBonjour(); echo $objet->aDitBonjour; //affiche true
Pour dupliquer une variable de type objet, il faut donc entrer le code suivant :
$objet2 = clone $objet;
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 :
class Message
{
function direBonjour()
{
echo "salut";
}
}
/* Exécute la méthode */
Message::direBonjour();
Héritage
[modifier | modifier le wikicode]PHP était initialement un langage à héritage simple[4], 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 extends. Ex :
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();
L'héritage permet le 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[5]).
Les interfaces peuvent par contre bénéficier d'un héritage multiple.
On peut aussi invoquer les méthodes parentes depuis la classe enfant :
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();
Traits
[modifier | modifier le wikicode]Depuis PHP 5.4.0, une structure de données appelée "trait" permet l'héritage multiple. Exemple d'utilisation :
<?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;
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.
De plus, ce type d'injection de dépendance est contraire au principe SOLID d'inversion des dépendances.
Final
[modifier | modifier le wikicode]Pour empêcher une classe ou une méthode d'être étendue (et en faire donc une classe finale ou une méthode finale), on peut la déclarer avec le mot-clé final. Ex :
final class MaClasseFinale
{
...
}
Classes abstraites
[modifier | modifier le wikicode]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 classe abstraite :
abstract class MaClasseAbstraite
{
public $var="Bonjour";
abstract protected function MaMethode($var1, $var2);
protected function MaMethode2($var1, $var2)
{
return 'TODO';
}
}
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[6]
Les méthodes abstraites sont obligatoirement à implémenter par les classes filles.
Closures
[modifier | modifier le wikicode]Apparues avec PHP 5.3[7], les closures sont des classes avec des méthodes gérant les fonctions anonymes.
Classes anonymes
[modifier | modifier le wikicode]Apparues avec PHP 7[8], les classes anonymes sont des classes sans nom, déclarées lors de l'exécution.
Interfaces
[modifier | modifier le wikicode]Voici un exemple d'interface :
interface MonInterface
{
public function setName($name);
public function getName();
}
Et son utilisation : la classe doit reprendre les méthodes de l'interface sous peine d'erreur.
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;
}
}
- 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[9].
- 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
extendsavant leimplements.
Namespaces
[modifier | modifier le wikicode]Exemple d'espace de noms :
namespace MonEspace\Nom;
class MaClasse {}
function MaMethode() {}
const MYCONST = 1;
$a = new MaClasse;
$c = new \MonEspace\Nom\MaClasse;
$d = new \ClasseGlobale;
Pour utiliser un namespace, "use" conserve son nom mais on peut le changer avec "as" :
use MonEspace\Nom; use SonEspace\Nom as NomExterne;
Depuis PHP7 on peut même importer plusieurs classes, fonctions ou constantes sur la même ligne :
use MonEspace\{MaClasseA, MaClasseB as B};
Portée des variables
[modifier | modifier le wikicode]Il est possible depuis PHP5 de préciser l'accès à certaines variables ou méthodes, en les déclarant à la place de var avec :
public: visible dans tout le programme.protected: visible uniquement dans les instances de la classe et de ses sous-classes.private: visible uniquement dans les instances de la classe.
Exemple :
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);
En effet, il faut gagner de l’argent avant d’en ajouter à la banque (quoique...).
Ce code retournera un message d’erreur s'il est exécuté sous PHP5 ou une version ultérieure.
Les méthodes prédéfinies
[modifier | modifier le wikicode]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[10], 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
var_dump(). - __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 :
echo $object(1)). - __isset() : à l'appel de
isset()(ouempty()) sur des propriétés inexistantes ou interdites. - __serialize() : à l'appel de
serialize(). - __set() : à l'écriture de propriétés inexistantes ou interdites.
- __set_state() : modifie les résultats des
var_export(). - __sleep() : à l'appel de
serialize(), pour en modifier le résultat. - __toString() : à l'appel de l'objet comme une chaine de caractères (ex :
echo $object). - __unserialize() : à l'appel de
serialize(). - __unset() : à l'appel de
unset()sur des propriétés inexistantes ou interdites. - __wakeup() : à l'appel de
unserialize(), pour en modifier le résultat.
Constructeur et destructeur
[modifier | modifier le wikicode]__construct()- 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"
__destruct()- 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 :
//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
*/
Sous php4, le constructeur avait pour nom celui de la classe. Sous php5, si la fonction __construct() n’est pas trouvée, l’interpréteur cherchera une méthode du même nom que la classe.
Copie en profondeur
[modifier | modifier le wikicode]Il existe une méthode qui s’exécute lors d’une duplication de l’objet. Son nom est __clone().
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
[modifier | modifier le wikicode]Ces méthodes permettent de rendre dynamique l'utilisation de la classe, et permettent la surcharge magique[11].
__call()
[modifier | modifier le wikicode]La méthode __call() s'exécute quand une méthode appelée est inaccessible ou inexistante. Exemple :
function __call($method,$arguments)
{
echo "On a appelé $method sans succès avec les paramètres :<br/>";
var_dump($arguments);
}
_get()
[modifier | modifier le wikicode]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 $nom. Important : le contenu de la variable $nom ne sera pas prioritaire sur le nom d'une variable interne à la classe.
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)"
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 __get.
Ne jamais accéder à une variable de classe privée dans son __get() sous peine de boucle infinie.
Exemple de réécriture de la méthode __get ci-dessus pour accéder à la variable privée :
function __get($nom)
{
if ($nom == 'b') {
echo $this->b;
}
}
__set()
[modifier | modifier le wikicode]Cette méthode s'exécute quand on modifie une variable inaccessible ou inexistante. Exemple :
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')
__sleep() et __wakeup()
[modifier | modifier le wikicode]Ces méthodes ne fonctionnent plus avec l'interface Serializable depuis PHP 8.1, au profit de serialize et unserialize[12].
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()
[modifier | modifier le wikicode]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 :
$maClasse = new MaClasse(); return $maClasse();
Quelques fonctions intégrées
[modifier | modifier le wikicode]Voici quelques fonctions en relation avec la programmation orientée objet qui peuvent vous être utiles.
self()
[modifier | modifier le wikicode]Une classe peut s'instancier elle-même avec new self();.
class_exists()
[modifier | modifier le wikicode]Vérifie qu’une classe existe. Renvoie une valeur booléenne. ex :
if (class_exists('maClasse'))
$var = new maClasse();
get_class_methods()
[modifier | modifier le wikicode]Retourne toutes les méthodes d’une classe sous forme de tableau. Ex :
$maClasse = new Classe(); $methodes = get_class_methods($maClasse); print_r($methodes);
get_class_vars()
[modifier | modifier le wikicode]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 :
$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);
Peut donc servir pour un "foreach" propriétés de la classe.
Pour récupérer ou filtrer les attributs privés, utiliser \ReflectionClass::getProperties[13] :
$reflecttion = new \ReflectionClass('Classe');
print_r($reflection->getProperties());
get_object_vars()
[modifier | modifier le wikicode]Idem avec les valeurs courantes de l'objet instance de classe.
method_exists($classe, $méthode)
[modifier | modifier le wikicode]Teste sur une méthode existe dans une classe.
serialize() et unserialize()
[modifier | modifier le wikicode]Assurent la transformation du flux de données, en précisant les types des variables et index des tableaux. Exemple :
$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";}
Le préfixe "a:2" signifie "array of 2 lines", et il est obligatoire pour désérialiser.
Références
[modifier | modifier le wikicode]- ↑ http://php.net/manual/fr/language.oop5.paamayim-nekudotayim.php
- ↑ https://www.php.net/manual/fr/language.oop5.static.php#104823
- ↑ https://www.php.net/manual/fr/filesystem.configuration.php
- ↑ Sébastien Rohaut, Algorithmique : Techniques fondamentales de programmation (avec des exemples en PHP), Editions ENI, (lire en ligne)
- ↑ http://php.net/manual/fr/language.oop5.basic.php
- ↑ http://php.net/manual/fr/language.oop5.abstract.php
- ↑ http://php.net/manual/fr/class.closure.php
- ↑ https://secure.php.net/manual/fr/language.oop5.anonymous.php
- ↑ https://www.safaribooksonline.com/library/view/php-5-power/013147149X/013147149X_ch03lev1sec14.html
- ↑ http://php.net/manual/fr/language.oop5.magic.php
- ↑ http://php.net/manual/fr/language.oop5.overloading.php
- ↑ https://www.php.net/manual/en/class.serializable.php
- ↑ https://www.php.net/manual/fr/reflectionclass.getproperties.php
Bases de données
Introduction
[modifier | modifier le wikicode]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 :
| 0 | "DUPOND" | "LOUIS" | "1,Petite rue" | "2,Petite rue" | 0543454654 | 0543454352 |
| 1 | "DUSS" | "Jean-Claude" | "1,Grande rue" | "2, Grande rue" | null | 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
[modifier | modifier le wikicode]À 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 :
$sql = sprintf('
SELECT *
FROM ma_table
WHERE (mon_champ1 = "%1$s" AND mon_champ2 != "") OR mon_champ2 = "%1$s"
', $maChaine);
NB : \'%1$s\' serait équivalent à "%1$s".
Documentation sur http://php.net/manual/fr/book.oci8.php.
Documentation sur http://php.net/manual/fr/book.pgsql.php.
Voir aussi
[modifier | modifier le wikicode]
Microsoft SQL Server
Installation
[modifier | modifier le wikicode]On distingue plusieurs pilotes PHP pour MS-SQL Server :
Windows
[modifier | modifier le wikicode]Pour se connecter au serveur MS-SQL à partir d'un tout-en-un comme EasyPHP, il suffit de télécharger les pilotes .dll[1] correspondant à sa version de PHP, puis d'indiquer leurs chemins dans le PHP.ini :
- Sous PHP 4, copier le fichier
php_mssql.dlldans les extensions. - Pour PHP 5.4 :
- Télécharger les .dll SQL30
- Les copier dans
C:\PROGRA~2\EasyPHP\binaries\php\php_runningversion\ext - Les ajouter dans
C:\PROGRA~2\EasyPHP\binaries\php\php_runningversion\php.inivia les lignes suivantes[2] :extension=php_sqlsrv_54_ts.dllextension=php_pdo_sqlsrv_54_ts.dll
- Dans PHP 5.5 on obtient toujours
Fatal error: Call to undefined function sqlsrv_connect(), 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 pdo_sqlsrv s’est bien ajoutée dans la configuration (ex : http://127.0.0.1/home/index.php?page=php-page&display=extensions).
Linux
[modifier | modifier le wikicode]Pour se connecter au serveur MS-SQL, il suffit de télécharger les pilotes .so[3] correspondant à sa version de PHP, puis d'indiquer leurs chemins dans le PHP.ini[4] :
extension=php_pdo_sqlsrv_7_nts.so
extension=php_sqlsrv_7_nts.so
Pour vérifier l'installation, redémarrer le serveur Web, puis vérifier que la ligne pdo_sqlsrv s’est bien ajoutée dans la configuration (ex : php -r "phpinfo();" |grep sql).
Connexion
[modifier | modifier le wikicode]Pilote 1.1 SQL Server pour PHP
[modifier | modifier le wikicode]Ce pilote fournit des fonctions d’interaction avec Microsoft SQL Server. Il peut s'utiliser avec les version de SQL Server postérieure à 2005[5].
Alternative désuète
[modifier | modifier le wikicode]Les fonctions suivantes sont supprimées depuis PHP 7.0[6] :
$serverName = "(local)";
$connect = mssql_connect($serverName, "login", "password");
mssql_select_db("my_db", $connect)
// ... votre script
mssql_close($connect);
Alternative Doctrine
[modifier | modifier le wikicode]Les bibliothèques Doctrine utilisent cette syntaxe[8] :
$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();
Requête SQL
[modifier | modifier le wikicode] // ... connexion
$sql = "DELETE FROM ma_table WHERE colonne = '1' LIMIT 0,30";
if (!sqlsrv_query($connect, $sql)) {
die (sqlsrv_errors());
}
// ... déconnexion
Récupération des données
[modifier | modifier le wikicode] // ... 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
Par défaut, sqlsrv_fetch_array() 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[9] :
SQLSRV_FETCH_ASSOC: pour le tableau associatif.SQLSRV_FETCH_NUMERIC: pour le tableau itératif.SQLSRV_FETCH_BOTH: pour les deux (déjà par défaut).
Références
[modifier | modifier le wikicode]- ↑ http://www.microsoft.com/en-us/download/details.aspx?id=20098
- ↑ http://www.php.net/manual/fr/ref.pdo-sqlsrv.php
- ↑ https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server
- ↑ https://www.barryodonovan.com/2016/10/31/linux-ubuntu-16-04-php-and-ms-sql
- ↑ http://msdn.microsoft.com/fr-fr/library/cc296152%28v=sql.90%29.aspx
- ↑ http://php.net/manual/fr/function.mssql-connect.php
- ↑ http://www.php.net/manual/fr/function.mssql-select-db.php
- ↑ http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html
- ↑ http://php.net/manual/fr/function.sqlsrv-fetch-array.php
MySQL
PHP offre plusieurs fonctions d’interaction avec MySQL.
mysql_connect()
[modifier | modifier le wikicode]
La fonction mysql_connect() est obsolète en PHP7, et remplacée par la classe mysqli.
Connexion
[modifier | modifier le wikicode] $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);
Exécution d'une requête
[modifier | modifier le wikicode]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
mysql_fetch_assoc) $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'];
}
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 :
mysql_num_rows(): retourne le nombre de lignes données par la requête.mysql_fetch_row(): identique àmysql_fetch_assocmais retourne un tableau simple indice->valeur.mysql_fetch_object(): identique àmysql_fetch_assocmais retourne un objet.mysql_fetch_array(): retourne les tableauxmysql_fetch_assoc()etmysql_fetch_row().
Exemple :
mysql_fetch_object) $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;
}
Par défaut, mysql_fetch_array() 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[1] :
MYSQL_ASSOC: pour le tableau associatif.MYSQL_NUM: pour le tableau itératif.MYSQL_BOTH: pour les deux (déjà par défaut).
Fermeture d'une connexion
[modifier | modifier le wikicode] //Ferme la connexion MySQL
mysql_close ($ressource);
mysqli_connect()
[modifier | modifier le wikicode]Cette fonction fonctionne un peu comme mysql_connect()[2] :
$link = mysqli_connect("BDDADRESSE","BDDUSER","BDDPASS","BDDNAME");
if (!$link) { die(mysqli_connect_errno()); }
mysqli_query($link, $query);
mysqli_close($link);
Les fonctions de manipulation de données ont pour préfixe "mysqli" avec les mêmes suffixes que celles du paragraphe précédent :
mysql_num_rows()mysql_fetch_assoc()mysql_fetch_row()mysql_fetch_object()
Classe mysqli
[modifier | modifier le wikicode]La classe mysqli 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...) :
$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();
query accepte les valeurs suivantes en second paramètre :
MYSQLI_STORE_RESULT: récupère tous les résultats du serveur (valeur par défaut).MYSQLI_USE_RESULT: récupère ligne par ligne.MYSQLI_ASYNC: requête SQL asynchrone (non bloquante).
Références
[modifier | modifier le wikicode]
PDO
Introduction
[modifier | modifier le wikicode]PDO (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[1] :
Et aussi via ODBC.
Installation
[modifier | modifier le wikicode]Machine hôte
[modifier | modifier le wikicode]Pour activer les différents SGBD qui doivent communiquer avec PDO, il faut ajouter ou décommenter les lignes extension=php_pdo_[SGBD utilisé].dll 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[3].
Docker
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]L'extension PDO comporte trois classes[4] :
- La classe
PDOcorrespond à une connexion à la base de données. - La classe
PDOStatementrepré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
PDOExceptionreprésente une erreur émise par PDO.
Accès à la base de données avec PDO
[modifier | modifier le wikicode]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[5] :
$connexion = new PDO('mysql:host=mon_serveur;dbname=ma_bdd;port=3306','mon_id','mon_mot_de_passe');
- Pour MS-SQL[6] les noms des paramètres diffèrent :
$connexion = new PDO('sqlsrv:server=mon_serveur:mon_port;Database=ma_bdd','mon_id','mon_mot_de_passe');
- MS-SQL via une source de données ODBC (à créer dans C:\Windows\SysWOW64\odbcad32.exe) :
$connexion = new PDO('odbc:nom_de_la_source','mon_id','mon_mot_de_passe');
- Pour PostgreSQL[7] :
$connexion = new PDO('pgsql:host=mon_serveur;dbname=ma_bdd;port=5432;user=mon_id;password=mon_mot_de_passe');
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 Access denied for user.
Passer des requêtes en PDO
[modifier | modifier le wikicode]Des méthodes permettent de passer des requêtes à l'objet récupéré lors de la connexion.
PDO::exec(string $statement)
La méthode exec() permet de passer et exécuter une requête SQL de type INSERT, UPDATE, DELETE.
Elle retourne le nombre de lignes affectées par la requête.
$requete="DELETE * FROM matable WHERE champ1='mavaleur'";
$resultat=$connexion->exec($requete);
echo $resultat.' suppressions effectuées';
PDO::query(string $statement)
La méthode query() permet de passer et exécuter une requête SQL de type SELECT.
Elle retourne le jeu de résultats (s'il y en a) sous forme d'objet PDOStatement.
$requete="SELECT champ1, champ2 FROM matable";
$resultat=$connexion->query($requete);
Quelques méthodes de PDOStatement
[modifier | modifier le wikicode]PDOStatement::fetch()récupère la ligne suivante d'un jeu de résultats PDO.PDOStatement::fetchAll()retourne un tableau contenant toutes les lignes du jeu d'enregistrements PDO.PDOStatement::fetchObject()récupère la ligne suivante et la retourne en tant qu'objet.PDOStatement::fetchColumn()retourne une colonne depuis la ligne suivante d'un jeu de résultats PDO.PDOStatement::rowCount()retourne le nombre de lignes affectées par le dernier appel à la fonction.PDOStatement::closeCursor()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.PDOStatement::execute()exécute une requête préparée.
Les paramètres de la méthode fetch
[modifier | modifier le wikicode]La méthode fetch() peut avoir en paramètre le type de retour du résultat. Par défaut, sans paramètre, le paramètre implicite est PDO::FETCH_BOTH.
PDO::FETCH_ASSOCretourne le jeu de résultats sous forme d'un tableau associatif, dont la clé est le nom de colonnes.
Exemple d'utilisation :
while ($ligne = $resultat ->fetch(PDO::FETCH_ASSOC)) {
echo $ligne['champ3'].' '.$ligne['champ1'].'<br/>';
}
PDO::FETCH_NUMretourne le jeu de résultats sous forme d'un tableau indexé par numéro commençant à 0.
Exemple d'utilisation :
while ($ligne = $resultat ->fetch(PDO::FETCH_NUM)) {
echo $ligne[2].' '.$ligne[0].'<br/>';
}
PDO::FETCH_BOTHretourne un tableau indexé et un tableau associatif.PDO::FETCH_OBJretourne 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
[modifier | modifier le wikicode]PDO::prepare() permet de préparer une requête que l'on exécutera ensuite avec la méthode PDOStatement::execute().
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 execute(). Ce dispositif protège l'application d'attaque de type injection SQL.
Exemple :
$pdoStatement = PDO::prepare('SELECT * FROM people WHERE name = :name');
$pdoStatement->bindParam(':name', $name, PDO::PARAM_STR);
$pdoStatement->execute();
return $pdoStatement->fetchAll(PDO::FETCH_ASSOC);
Notes et références
[modifier | modifier le wikicode]- ↑ http://php.net/manual/fr/pdo.drivers.php
- ↑ http://fr2.php.net/manual/fr/pdo.installation.php
- ↑ https://www.microsoft.com/en-us/download/details.aspx?id=20098
- ↑ http://php.net/manual/fr/class.pdo.php
- ↑ http://php.net/manual/fr/ref.pdo-mysql.php
- ↑ http://php.net/manual/fr/ref.pdo-sqlsrv.connection.php
- ↑ https://www.php.net/manual/fr/ref.pdo-pgsql.php
SQLite
SQLite est le moteur de base de données intégré à PHP5.
Connexion
[modifier | modifier le wikicode] // On se connecte à la base
// CHEMIN_BDD constitue de chemin physique de la base de données
$db = new SQLiteDatabase(CHEMIN_BDD);
Exécution d'une requête
[modifier | modifier le wikicode]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
$requete = "SELECT * FROM NOMTABLE";
$res = $db->arrayQuery ($requete, SQLITE_ASSOC);
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 :
foreach ($res as $enregistrement) {
$nom = $enregistrement['NOM'];
$prenom = $enregistrement['PRENOM'];
$adresse1 = $enregistrement['ADRESSE1'];
$adresse2= $enregistrement['ADRESSE2'];
$tel1 = $enregistrement['TEL1'];
$tel2 = $enregistrement['TEL2'];
}
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...
numRows (): retourne le nombre de lignes données par la requête.
Fermeture d'une connexion
[modifier | modifier le wikicode]$db->close();
Expressions rationnelles
En informatique, une 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 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 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 preg_match :
<?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');
}
Syntaxe
[modifier | modifier le wikicode]PHP utilise la norme PCRE.
Les expressions rationnelles peuvent être analysées et testées via un débogueur en ligne comme https://regex101.com/.
| Caractère | Type | Explication |
|---|---|---|
.
|
Point | N'importe quel caractère |
[...]
|
crochets | 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 : [0-9a-z] pour tout l'alphanumérique en minuscule, ou [0-Z] pour tous les caractères de la table Unicode entre "0" et "Z", c'est-à-dire l'alphanumérique majuscule plus ":;<=>?@"[1].
|
[^...]
|
crochets et circonflexe | classe complémentée : tous les caractères sauf ceux énumérés. |
^
|
circonflexe | Marque le début de la chaîne ou de la ligne. |
$
|
dollar | Marque la fin de la chaîne ou de la ligne. |
|
|
barre verticale | Alternative - ou reconnaît l'un ou l'autre |
(...)
|
parenthèses | 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 |
\n
|
référence | Même séquence que celle capturée précédemment par le nème groupe de capture |
\g{n}
|
référence | Même séquence que celle capturée précédemment par le nème groupe de capture |
(?P<nom>pattern)
|
Sous-motif nommé | Nomme le résultat d'un groupe de capture par un nom. |
\g{nom}
|
référence | Même séquence que celle capturée précédemment par le 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é.
| Caractère | Type | Explication |
|---|---|---|
*
|
astérisque | 0, 1 ou plusieurs occurrences |
+
|
plus | 1 ou plusieurs occurrences |
?
|
interrogation | 0 ou 1 occurrence |
{...}
|
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 :
|
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.
| Caractère | Type | Explication |
|---|---|---|
?
|
réticent | Le quantificateur qui précède recherchera la plus petite séquence possible. |
+
|
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 (
^et$) ne fonctionnent pas dans[]où ils ont un autre rôle. - Les opérateurs
*et+sont toujours avides, pour qu'ils laissent la priorité il faut leur apposer un?à leur suite[2].
| Classe | Signification |
|---|---|
[[:alpha:]]
|
n'importe quelle lettre |
[[:digit:]]
|
n'importe quel chiffre |
[[:xdigit:]]
|
caractères hexadécimaux |
[[:alnum:]]
|
n'importe quelle lettre ou chiffre |
[[:space:]]
|
n'importe quel espace blanc |
[[:punct:]]
|
n'importe quel signe de ponctuation |
[[:lower:]]
|
n'importe quelle lettre en minuscule |
[[:upper:]]
|
n'importe quelle lettre capitale |
[[:blank:]]
|
espace ou tabulation |
[[:graph:]]
|
caractères affichables et imprimables |
[[:cntrl:]]
|
caractères d'échappement |
[[:print:]]
|
caractères imprimables exceptés ceux de contrôle |
| Expression | Signification |
|---|---|
\\ |
Antislash |
\C |
Caractère spécial C non interprété : [ ] { } ( ) ? * . : \ & - ^ $
|
\Q...\E |
Séquence littérale non interprétée |
\0xxx |
Caractère Unicode (1 à 3 chiffres octaux) |
\a |
Alarme (ASCII 07) |
\A |
Début de chaîne |
\b |
Caractère de début ou fin de mot |
\B |
Caractère qui n'est pas début ou fin de mot |
\cX |
Caractère de contrôle ASCII (X étant une lettre) |
\d |
Chiffre |
\D |
Non chiffre |
\e |
Escape (ASCII 1B) |
\f |
Form-feed (ASCII 0C) |
\G |
Fin de la correspondance précédente |
\h |
Espace blanc horizontal [ \t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]
|
\H |
Non espace blanc horizontal [^\h]
|
\n |
Fin de ligne |
\pL, \p{L}, \p{Letter} |
Lettre (dans tout langage) |
\r |
Retour charriot |
\R |
Retour à la ligne, équivaut à \u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]
|
\s |
Caractères espace [ \t\n\x0B\f\r]
|
\S |
Non caractères espace [^\s]
|
\t |
Tabulation |
\v |
Espace blanc vertical [\n\x0B\f\r\x85\u2028\u2029]
|
\V |
Non espace blanc vertical [^\v]
|
\w |
Caractère alphanumérique : lettre, chiffre ou underscore |
\W |
Caractère qui n'est pas lettre, chiffre ou underscore |
\xxx |
Caractère Unicode (2 chiffres hexadécimaux) |
\x{xx...x} |
Caractère Unicode (chiffres hexadécimaux) |
\X |
Caractère Unicode du groupe de graphèmes étendu |
\z |
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.
?:: groupe non capturant. Ignorer le groupe de capture lors de la numérotation des backreferences. Exemple :((?:sous-chaine_non_renvoyée|autre).*).- 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
?:juste après la parenthèse ouvrante, et en décalant les numéros des groupes référencés.
- 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
?>: groupe non capturant indépendant.?<=: 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 :
(?<=q)u
- Chercher une lettre u précédée d'une lettre q :
?<!: negative lookbehind, vérifier (sans consommer) que ce qui précède ne correspond pas au motif spécifié.?=: positive lookahead, vérifier (sans consommer) que ce qui suit correspond au motif spécifié.?!: negative lookahead, vérifier (sans consommer) que ce qui suit ne correspond pas au motif spécifié. Exemples :
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[7].
(?optionsactivées-optionsdésactivées)
Exemples :
- Chercher un mot composé de voyelles sans tenir compte de la casse :
(?i)[AEIOUY]+
- Chercher un mot composé de voyelles en tenant compte de la casse, ici en majuscules :
(?-i)[AEIOUY]+
Les options s'appliquent à toute l'expression quelle que soit leur position dans l'expression.
$1: résultat du premier groupe de capture dans les remplacements ($2 correspond au deuxième, etc.).
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 : '\$'.
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 (`), mais on trouve aussi souvent / et #.
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[8] :
| 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
[modifier | modifier le wikicode]La fonction ereg() qui permettait de rechercher en regex a été remplacée par preg_match() depuis PHP 5.3.
preg_match()
[modifier | modifier le wikicode]La fonction preg_match()[9] est la principale fonction de recherche[10]. 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 :
<?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');
}
- Exemple avancé :
<?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');
}
Exemples de flags[11] :
- PREG_OFFSET_CAPTURE : affiche la position de la sous-chaine recherchée dans la chaine.
- PREG_GREP_INVERT : affiche l'inverse dans
preg_grep().
Exemples élaborés
[modifier | modifier le wikicode]- Recherche des balises images HTML sans attribut "alt"[12] :
/(<img(?!.*?alt=(['"]).*?\2)[^>]*)(>)/ - Savoir si la chaine est une année (à quatre chiffres) :
'/^[0-9]{4}$/' - Comparer une URL et un host sans tenir compte du protocole ou du slash de fin :
'`^https?://'.$hostUrl.'/?$`' - Caractères ASCII et ASCII étendu (ex : "e" ou "E" ou "é") :
'/^[a-z\x7f-\xff]+$/i*'
preg_match() peut ne pas marcher sur un résultat de file_get_contents() (avec plusieurs lignes), car il n'a pas de flag global (/g)[13]. Il faut alors utiliser preg_match_all() (voir ci-dessous).
preg_grep()
[modifier | modifier le wikicode]Cette fonction recherche dans les tableaux[14].
preg_match_all()
[modifier | modifier le wikicode]Pour obtenir tous les résultats dans un tableau, remplacer preg_match par preg_match_all[15], 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 :
$regex = "/\(([^)]*)\)/"; preg_match_all($regex, file_get_contents($nomFichier), $matches); print_r($matches);
Remplacement
[modifier | modifier le wikicode]preg_replace()
[modifier | modifier le wikicode]La fonction preg_replace comprend trois paramètres : remplacé, remplaçant, chaine à traiter.
<?php
// Remplace tous les espaces par des underscores
$chaine="Test regex PHP pour Wikibooks francophone.";
$chaineTriee=preg_replace('`( )`','_',$chaine);
echo $chaineTriee;
Seul le dernier groupe de capture sera pris en compte.
preg_filter()
[modifier | modifier le wikicode]Idem que preg_replace() mais son résultat ne contient que ce qui est effectivement remplacé.
preg_split()
[modifier | modifier le wikicode]Décompose une chaine de caractères.
Références
[modifier | modifier le wikicode]- ↑ https://unicode-table.com/fr/
- ↑ https://docstore.mik.ua/orelly/webprog/pcook/ch13_05.htm
- ↑ https://www.regular-expressions.info/posixbrackets.html
- ↑ https://www.regular-expressions.info/unicode.html
- ↑ https://www.regextester.com/15
- ↑ Jan Goyvaerts, Steven Levithan, Regular Expressions Cookbook, O'Reilly Media, Inc., (lire en ligne)
- ↑ Les options sont appelées modificateurs (modifiers en anglais), voir https://www.regular-expressions.info/modifiers.html
- ↑ http://php.net/manual/fr/reference.pcre.pattern.modifiers.php
- ↑ http://php.net/manual/en/function.preg-match.php
- ↑ http://php.net/manual/fr/ref.pcre.php
- ↑ http://php.net/manual/fr/pcre.constants.php
- ↑ https://stackoverflow.com/questions/4031948/using-regular-expressions-to-find-img-tags-without-an-alt-attribute
- ↑ https://stackoverflow.com/questions/3578671/unknown-modifier-g-in-when-using-preg-match-in-php
- ↑ http://php.net/manual/fr/function.preg-grep.php
- ↑ http://www.expreg.com/pregmatchall.php
Concevoir du code de haute qualité
Introduction
[modifier | modifier le wikicode]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 PHP mais leur application est toujours valable quel que soit le langage de programmation.
Pourquoi bien programmer ?
[modifier | modifier le wikicode]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é
[modifier | modifier le wikicode]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é
[modifier | modifier le wikicode]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é
[modifier | modifier le wikicode]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 :
/* Code qui va afficher "salut" par un echo. Ce programme est fait en PHP compatible php3*/ echo "salut"; //affiche "salut" #fin du programme
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
[modifier | modifier le wikicode]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 :
$maSuperVariable // Écriture en "CamelCase" $ma_super_variable // Écriture avec des underscores
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)
[modifier | modifier le wikicode]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 :
<?php
$connect = mysql_connect('host','account','password');
//actions sur la BDD
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 :
include 'connect.php';
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 duplication de code est donc considérée comme un anti-patron de programmation.
Exemple concret
[modifier | modifier le wikicode]Créez un fichier conf/config.php dans lequel vous mettez vos informations de connexion à la base de données :
define('HOST', 'localhost');
define('USER', 'moi');
define('PASS', 'mon_mdp');
define('DB', 'mon_site');
define('PREFIX', 'mon_site_'); // Préfixe des tables SQL
Lorsque vous ferez une requête, vous la ferez de la manière suivante :
mysql_connect(HOST, USER, PASS);
mysql_select_db(DB);
$temp = mysql_query('SELECT * FROM '. PREFIX .'ma_table');
La gestion des erreurs
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Par exemple, vous décidez un jour de concevoir un 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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é
[modifier | modifier le wikicode]Parfois le code le plus court n'est pas le plus rapide d'exécution[1]. En voici un exemple :
//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];
}
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 ?
[modifier | modifier le wikicode]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[2]. En voici la raison : à l'intérieur des guillemets doubles, les variables sont interprétées et substituées correctement.
Exemple :
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.';
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 : echo "$a - $b,$c + $d $e $sigma $epsilon";). 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.
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 <br/>.
Conventions de codage
[modifier | modifier le wikicode]Lors d'une comparaison entre une variable et un littéral, on place ce dernier en premier. Ex : if (1 == $x). Cette technique sert à éviter de confondre les assignations avec les égalités, et les déréférencements de pointeurs null.
PSR
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Les voici résumées ici[3] :
- Un fichier .php doit être encodé en UTF8 sans BOM.
- Les noms de classe doivent être rédigés en StudlyCaps (commencer par une majuscule).
- Les noms des variables et méthodes de classe doivent être écris en camelCase (en commençant par une minuscule).
- Les noms des constantes doivent être en lettres capitales et snake_case (en séparant les mots par des underscores). Comme par exemple la native
DIRECTORY_SEPARATOR.
PSR-3 : interface du logger
[modifier | modifier le wikicode]Définit une méthode par niveau de criticité du log :
| Numéro Graylog | Niveau |
|---|---|
| 0 | emergency |
| 1 | alert |
| 2 | critical |
| 3 | error |
| 4 | warning |
| 5 | notice |
| 6 | informational |
| 7 | debug |
PSR-4 : Autoloading
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]PSR-7 : interface des messages HTTP
[modifier | modifier le wikicode]PSR-11 : interface des conteneurs
[modifier | modifier le wikicode]PSR-12 : guide de style étendu
[modifier | modifier le wikicode]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 PHP Standard Recommendation sur Wikipédia (en anglais)
).
PSR-13 : liens hypermédia
[modifier | modifier le wikicode]PSR-14 : Event Dispatcher
[modifier | modifier le wikicode]PSR-15 : HTTP Handlers
[modifier | modifier le wikicode]PSR-16 : interface des éléments du cache
[modifier | modifier le wikicode]PSR-17 : interface des fabriques de requêtes HTTP
[modifier | modifier le wikicode]PSR-18 : interface des clients HTTP
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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[4], ou un tableau).
Séparer le code SQL dans un dossier "repository"
[modifier | modifier le wikicode]À 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Plusieurs outils de profilage permettent de classer les codes par temps d'exécution[5].
Xdebug's Profiler
[modifier | modifier le wikicode]Le profilage Xdebug a été décrit dans le chapitre Programmation PHP/Xdebug.
XHProf
[modifier | modifier le wikicode]
XHProf est une extension PHP dédiée au profilage du code[6], développée par Facebook et open source depuis mars 2009.
Installation
[modifier | modifier le wikicode]Linux
[modifier | modifier le wikicode]pecl install -f xhprof
Dans php.ini :
extension=xhprof.so
Docker
[modifier | modifier le wikicode]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 | modifier le wikicode]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
[modifier | modifier le wikicode]Plusieurs solutions parmi lesquelles Grafana, 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
[modifier | modifier le wikicode]Outil payant développé par le concepteur du framework Symfony[7] qui fournit des organigrammes des exécutions.
Choisir la condition sans négation
[modifier | modifier le wikicode]Cela permet de gagner une opération NOT dans le processeur, et cela simplifie également la lecture du code en simplifiant la condition.
Avant :
if (x !== 1) {
y = 1;
} else {
y = 0;
}
Après :
if (x === 1) {
y = 0;
} else {
y = 1;
}
Return early
[modifier | modifier le wikicode]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[8]. 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 :
$parent = $this->getParent();
$children = $this->getChildren($parent);
if (empty($parent) || empty($children)) {
return 404;
}
- Bien :
$parent = $this->getParent();
if (empty($parent)) {
return 404;
}
$children = $this->getChildren($parent);
if (empty($children)) {
return 404;
}
Passage par référence des tableaux
[modifier | modifier le wikicode]Utiliser les références dans les arguments tableaux volumineux (function fonction(&$tableau)), pour éviter sa duplication en mémoire.
Tester les invariants avant les boucles
[modifier | modifier le wikicode]Une condition est testée dans une boucle comme dans l'exemple ci-dessous.
for ($i=0; $i<$count; $i++) {
if ($mode === 'display') {
echo $array[$i];
}
}
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.
if ($mode === 'display') {
for ($i=0; $i<$count; $i++) {
echo $array[$i];
}
}
Tests de charge
[modifier | modifier le wikicode]Plusieurs outils existent :
Autres bonnes pratiques
[modifier | modifier le wikicode]- Pour nommer une variable, éviter $data car trop générique : choisir un nom le plus descriptif possible.
- Dans les
sprintf(), 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 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
SELECT *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 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).
- ne pas lancer de
Références
[modifier | modifier le wikicode]- ↑ http://www.phpbench.com/
- ↑ https://www.keycdn.com/blog/php-performance#11-stick-with-single-quotes
- ↑ http://www.php-fig.org/psr/psr-1/
- ↑ https://github.com/jupeter/clean-code-php
- ↑ https://www.novaway.fr/blog/tech/les-outils-de-profiling-php-open-source-en-2017
- ↑ https://www.php.net/manual/fr/book.xhprof.php
- ↑ https://blackfire.io/docs/up-and-running/installation
- ↑ https://pear.php.net/manual/en/standards.bestpractices.php
- ↑ https://github.com/locustio/locust
Analyse statique de programmes
PHP offre plusieurs outils d'analyse statique de programmes (ou analyse de code statique).
microtime() et memory_get_usage()
[modifier | modifier le wikicode]Pour mesurer un temps d'exécution dans le code, on peut utiliser des fonctions natives :
$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)
);
phpcs
[modifier | modifier le wikicode]PHP_CodeSniffer liste ou corrige les violations des normes de codage[1][2].
Installation
[modifier | modifier le wikicode]composer require squizlabs/php_codesniffer --dev
Lancement
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- 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 :
// phpcs:ignoreFile - Sur une ligne :
// phpcs:ignore
Configuration
[modifier | modifier le wikicode]Voici un exemple de fichier phpcs.xml.dist :
<?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>
phpinsights
[modifier | modifier le wikicode]Ajoute declare(strict_types=1); dans tous les fichiers, et formate code code et le style.
Installation
[modifier | modifier le wikicode]composer require nunomaduro/phpinsights --dev
Lancement
[modifier | modifier le wikicode]Pour la vérification :
vendor/bin/phpinsights analyse src -n
Pour réparer automatiquement, ajouter "--fix".
Configuration
[modifier | modifier le wikicode]Voici un exemple de fichier phpinsights.php :
<?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,
];
phpmd
[modifier | modifier le wikicode]PHP Mess Detector recense les mauvaises pratiques du type "code mort", paramètre inutilisé, mauvais nommage camelCase, présence d'un exit, etc.[3].
Installation
[modifier | modifier le wikicode]composer require phpmd/phpmd --dev
Lancement
[modifier | modifier le wikicode]vendor/bin/phpmd src ansi rulesets.xml
Configuration
[modifier | modifier le wikicode]Comme CodeSniffer, il utilise un fichier XML pour lister les vérifications à inclure ou exclure.
Exemple de fichier rulesets.xml :
<?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>
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
[modifier | modifier le wikicode]Outil d'analyse fournissant un rapport graphique.
Installation
[modifier | modifier le wikicode]composer require phpmetrics/phpmetrics --dev
Lancement
[modifier | modifier le wikicode]vendor/bin/phpmetrics --report-html=reports/phpmetrics-$$(date '+%Y%m%d_%H%M%S').html src
Configuration
[modifier | modifier le wikicode]Éviter de versionner les rapports avec l'application. Par exemple ajouter "reports" au .gitignore.
phpstan
[modifier | modifier le wikicode]PHP Static Analysis détecte des erreurs potentielles à l'exécution (ex : mauvais type) sans réellement exécuter le code[4].
Installation
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]vendor/bin/phpstan analyse --no-progress --memory-limit=256M src
Configuration
[modifier | modifier le wikicode]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 :
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\(\)#'
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[5].
psalm
[modifier | modifier le wikicode]Psalm cherche à détecter des bugs, par exemple des erreurs de type[6].
Installation
[modifier | modifier le wikicode]composer require --dev vimeo/psalm vendor/bin/psalm --init
Lancement
[modifier | modifier le wikicode]vendor/bin/psalm
Configuration
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Comprend des analyses de sécurité et de performances[7].
GrumPHP
[modifier | modifier le wikicode]Idem[8].
composer require --dev phpro/grumphp

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.
/.scannerwork/
Exemples
[modifier | modifier le wikicode]Pour lancer le scan sur un projet Symfony installé sur Windows (dans un .cmd) :
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
Sur Docker :
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
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
[modifier | modifier le wikicode]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 :
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
Références
[modifier | modifier le wikicode]- ↑ https://github.com/squizlabs/PHP_CodeSniffer
- ↑ http://pear.php.net/package/PHP_CodeSniffer/redirected
- ↑ https://github.com/phpmd/phpmd
- ↑ https://github.com/phpstan/phpstan
- ↑ https://phpstan.org/user-guide/rule-levels
- ↑ https://psalm.dev/docs/
- ↑ https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/
- ↑ https://github.com/phpro/grumphp
Exceptions
Tester l'existence
[modifier | modifier le wikicode]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 :
if (!isset($maVariable)) { $maVariable = ''; }
if (!defined('MA_CONSTANTE')) { define('MA_CONSTANTE', ''); }
if (!function_exists('maFonction')) { function maFonction() {} }
empty()
[modifier | modifier le wikicode]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 :
isset($v) and $v == "".
ou
- Booléen :
isset($v) and $v == false.
ou
- Tableau :
isset($v) and sizeof($v) == 0.
try... catch
[modifier | modifier le wikicode]Tout comme en Java, la levée d'exception est assurée par un bloc try... catch. 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 Throwable, qui a deux classes filles : Error et Exception[1].
Exemples
[modifier | modifier le wikicode] 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
}
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 :
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);
finally ajouté après les catch sera exécuté après les instructions du try et des catch.
La bonne pratique est de ne jamais envoyer \Exception 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 Symfony\Component\HttpKernel\Exception.
Par ailleurs, il est possible de créer ses propres classes d'exceptions, et de modifier les exceptions natives PHP avec set_exception_handler()[2].
Recréer une exception ou une erreur
[modifier | modifier le wikicode]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 getPrevious(), dont le résultat injecté en troisième argument du constructeur d'un autre, permet de cloner l'objet. Ex :
try {
crash();
} catch (\Exception $e) {
throw new \Exception('Nouveau message + ancien : '.$e->getMessage(), $e->getCode(), $e->getPrevious());
}
NB : la classe Throwable n'est pas instanciable directement.
trigger_error()
[modifier | modifier le wikicode]Cette fonction lance une exception du type placé en second paramètre, dont certains peuvent stopper l'exécution du programme[3]. Par exemple :
trigger_error('Message d'erreur', E_USER_ERROR);
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
[modifier | modifier le wikicode]Affichage d'une trace lisible
[modifier | modifier le wikicode]Afin de clarifier la trace de l'exécution des scripts (il peut être utile de formater celle-ci avec la balise <pre>) :
if ($debogage) {
var_dump(scandir('.'));
}
NB : on rappelle que les commandes ini_set('display_errors', 1); et error_reporting(E_ALL); permet d'afficher à l'écran toutes les erreurs et avertissements.
Voici une manière de n'afficher les erreurs que lors du débogage :
if ($debogage) {
error_reporting(E_ALL);
} else {
error_reporting(0);
}
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 (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
[modifier | modifier le wikicode]Ces fonction affichent automatiquement la trace de l'exécution menant à elles.
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 :
var_dump(debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
$logFile = fopen('debug.log', 'a+');
fwrite($logFile, json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)).PHP_EOL);
fclose($logFile);
throw
[modifier | modifier le wikicode]Si l'affichage de la trace doit être suivi d'une interruption du programme, utiliser :
throw new \Exception('Trace');
Exception::getTraceAsString()
[modifier | modifier le wikicode]Si l'exception ne surgit pas avec un throw, on peut afficher la trace depuis avec sa méthode :
echo $e->getTraceAsString();
Fichier de log maison
[modifier | modifier le wikicode]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 :
$logFile = fopen('debug.log', 'a+');
fwrite($logFile, $maVariable.PHP_EOL);
fclose($logFile);
Symfony
[modifier | modifier le wikicode]Depuis une commande Symfony, on peut afficher des logs en console :
$output->writeln('Update complete');
Mais le mieux est d'utiliser la bibliothèque Monolog[4] (compatible PSR-3[5]), 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[6] (ce qui est fait par défaut sur la v3.3 dans config/packages/dev/monolog.yaml). composer require symfony/monolog-bundle
Pour booster ses performances, on peut le régler ainsi dans monolog.yaml :
monolog:
use_microseconds: false
Quand l'application tourne sur plusieurs serveurs frontaux, il vaut mieux centraliser les logs dans un agrégateur de logs comme 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 :
$a = 'HelloWorld!';
$this->logger->info('Test d\'affichage.', [
'$a' => $a, // absent de Graylog
'a' => $a, // visible
'b' => null, // absent de Graylog
]);
Références
[modifier | modifier le wikicode]- ↑ https://www.php.net/manual/fr/language.errors.php7.php
- ↑ http://php.net/manual/fr/function.set-exception-handler.php
- ↑ http://php.net/manual/fr/function.trigger-error.php
- ↑ https://symfony.com/doc/current/logging.html
- ↑ https://www.php-fig.org/psr/psr-3/
- ↑ https://symfony.com/doc/current/logging/monolog_console.html
Xdebug

Xdebug est un système qui permet de calculer le taux de couverture de code, de le 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
[modifier | modifier le wikicode]Linux
[modifier | modifier le wikicode]Installation sur Linux[1][2] :
sudo apt-get install php8.4-xdebug
ou
pecl install xdebug
Puis dans php.ini :
xdebug.remote_enable = On
ou sur PHP via Apache :
sudo vim /etc/php/8.4/apache2/conf.d/20-xdebug.ini
Ajouter :
xdebug.remote_enable=1
Configuration complète
[modifier | modifier le wikicode]Exemple en V3[3] :
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
Exemple en V2
On peut aussi forcer d'autres paramètres[4] :
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
Docker
[modifier | modifier le wikicode]Avec Docker, il faut aussi spécifier le "client_host"[6].
Exemple d'installation et activation :
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
Windows
[modifier | modifier le wikicode]Wamp fournit Xdebug en mode "develop". Pour activer le débogage pas à pas, il faut donc éditer le php.ini :
xdebug.mode=debug xdebug.client_host=127.0.0.1 xdebug.start_with_request=trigger
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".
xdebug.start_with_request=yes
Lancement
[modifier | modifier le wikicode]Dans un navigateur
[modifier | modifier le wikicode]Il faut installer un module sur son navigateur (ex : Xdebug-ext sur Firefox[7]) pour pouvoir activer ou désactiver le débogage d'une page. Ce module s'interface avec les principaux IDE, par exemple PhpStorm[8], pour leur faire lancer le débogage lors du chargement d'une page.
En ligne de commande
[modifier | modifier le wikicode]On peut ajouter un argument à PHP[9]. Ex :
php -d xdebug.mode=debug,profile bin/console MaCommande.php
Pour PhpStorm et Docker, il faut ajouter l'interpréteur dans les paramètres[10].
Débogage pas à pas
[modifier | modifier le wikicode]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 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
[modifier | modifier le wikicode]Ajouter le mode au précédent[11] : xdebug.mode=debug,profile
Un ensemble de vidéos explicatives existent en anglais[12].
Références
[modifier | modifier le wikicode]- ↑ « PHPStorm : Xdebug »
- ↑ « Tutoriel PHP : Xdebug, l'exécution pas à pas »
- ↑ https://xdebug.org/docs/upgrade_guide
- ↑ https://xdebug.org/docs/all_settings
- ↑ https://www.jetbrains.com/help/phpstorm/2023.2/configuring-xdebug.html#5a0181d2
- ↑ https://blog.eleven-labs.com/fr/debug-run-phpunit-tests-using-docker-remote-interpreters-with-phpstorm/
- ↑ https://addons.mozilla.org/fr/firefox/addon/xdebug-ext-quantum/?src=search
- ↑ https://www.jetbrains.com/help/phpstorm/configuring-xdebug.html
- ↑ https://stackoverflow.com/questions/2288612/how-to-trigger-xdebug-profiler-for-a-command-line-php-script
- ↑ https://www.jetbrains.com/help/phpstorm/configuring-remote-interpreters.html#remote-interpreter-docker
- ↑ https://xdebug.org/docs/profiler
- ↑ https://xdebug.org/docs/profiler#related_content
Mots réservés
Mots du langage
[modifier | modifier le wikicode]Les 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.
| 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 | interface | |
| require | require_once | return | static | throw |
| switch | unset | use | var | protected |
| while | __FUNCTION__ | __CLASS__ | __METHOD__ | abstract |
| extends | cfunction* | old_function* | yield[1] | |
| * : depuis PHP4 seulement | ||||
Liste des 72 mots réservés par ordre alphabétique[2] :
__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
Nouveautés PHP 7
[modifier | modifier le wikicode]Plusieurs opérateurs composés permettent de réduire la syntaxe d'opérations courantes :
??: opérateur de fusion null. Équivalent d'un opérateur ternaire avec unis_null().<=>: opérateur vaisseau spatial. Équivalent d'unswitchà trois cas : inférieur, égal et supérieur.intdiv(): division entière. Équivalent de/+intval().
De plus, on peut maintenant utiliser :
- plusieurs classes dans le même
use. define()pour définir un tableau de constantes.
Depuis PHP 7.4, les propriétés typées. Ex : public int $id;
Extensions
[modifier | modifier le wikicode]Liste des 48 bibliothèques natives PHP 5.5.0 avec EasyPHP[3] : Core PDO Phar Reflection SPL SimpleXML apache2handler bcmath bz2 calendar ctype curl date dom ereg filter ftp gd hash iconv json libxml mbstring mcrypt mhash mysql mysqli mysqlnd odbc openssl pcre pdo_mysql pdo_sqlite pdo_sqlsrv session sockets sqlite3 sqlsrv standard tokenizer wddx xdebug xml xmlreader xmlwriter xsl zip zlib
Voir aussi List of PHP extensions sur Wikipédia (en anglais)
.
Références
[modifier | modifier le wikicode]
Exemples/Variables
Utilisation de variables
[modifier | modifier le wikicode]Un exemple de programme
[modifier | modifier le wikicode] <!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>
Explications
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode] <!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>
Les guillemets
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode] <!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>
Explications
[modifier | modifier le wikicode]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 :
la variable $a vaut 100
Exemples/Sommaire
Un sommaire simple
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Fichier index.php
<!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>
fichier page1.html
<h1>Page 1</h1>
bla bla bla
fichier page2.html
<h1>Page 2</h1>
ble ble ble
fichier page3.html
<h1>Page 3</h1>
bli bli bli
fichier page4.html
<h1>Page 4</h1>
blo blo blo
Explications
[modifier | modifier le wikicode]- 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 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é
pagechange : il vaut 1, 2, 3 ou 4 selon le lien cliqué. - Dans la partie « page » de index.php, la valeur du paramètre
pageest récupérée de l'url cliquée en écrivant$_GET['page']. 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
if (isset($_GET['page']))$numero=$_GET['page']; else $numero='1';
- 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
require'nom_du_fichier';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é.
Exemples/Formulaire
Interaction avec un formulaire
[modifier | modifier le wikicode]Les principaux concepts
[modifier | modifier le wikicode]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 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 $_POST['nom']. De la même manière, pour récupérer la valeur du champ prénom, il faut écrire $_POST['prenom']. 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
[modifier | modifier le wikicode]Le fichier index.html :
<!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>
Le fichier go.php :
<!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>";
?>
Captures d'écran
[modifier | modifier le wikicode]
Exemples/BD 1
Afficher le résultat d'une requête
[modifier | modifier le wikicode]Présentation
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]Les requêtes suivantes permettent la création et l'initialisation des données dans notre table :
Fichier BD.sql :
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'
);
La table créée par ces requêtes est la suivante :
| ID | NOM | PRENOM | SALAIRE |
| 1 | Dupond | Marcel | 8000 |
| 2 | Martin | Xavier | 4000 |
| 3 | Gogol | Henri | 3000 |
| 4 | Hugo | Victor | 2000 |
| 5 | Gali | Daniel | 6000 |
| 6 | Martin | Georges | 9000 |
Configuration de la base de données
[modifier | modifier le wikicode]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 EasyPHP : il est vivement recommandé de mettre un vrai mot de passe pour une plateforme en exploitation.
Les requêtes utilisées
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.
<?php
$host='localhost';
$user='root';
$password='';
$base='toto';
?>
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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
while ($a=mysql_fetch_object($r)) {
...
}
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 echo à partir des différents champs des enregistrements.
Les pages du site (version 1)
[modifier | modifier le wikicode]Fichier index.html
<!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>
Fichier params.php
<?php
$host='localhost';
$user='root';
$password='';
$base='toto';
?>
Fichier liste1.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
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>
Fichier liste2.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 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>
Fichier liste3.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 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>
Les pages du site (version 2)
[modifier | modifier le wikicode]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 :
<!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>
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 :
<!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>
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.
Exemples/BD 2
Modifier une table
[modifier | modifier le wikicode]Les fichiers de l'application
[modifier | modifier le wikicode]Le fichier BD.sql
[modifier | modifier le wikicode] 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'
);
Le fichier index.php
[modifier | modifier le wikicode] <?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>
Le fichier liste.php
[modifier | modifier le wikicode] <?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>
Le fichier add.php
[modifier | modifier le wikicode] <?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';
}
?>
Le fichier addAction.php
[modifier | modifier le wikicode] <?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">';
}
}
?>
Le fichier connect.php
[modifier | modifier le wikicode] <?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;
?>
Le fichier disconnect.php
[modifier | modifier le wikicode] <?php
session_start();
require_once'Appli/appli.php';
disconnect();
?>
<meta http-equiv="Refresh" content="0;URL=index.php">
Le fichier delete.php
[modifier | modifier le wikicode] <?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';
}
?>
Le fichier deleteAction.php
[modifier | modifier le wikicode] <?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">';
}
?>
Le fichier Appli/.htaccess
[modifier | modifier le wikicode]deny from all
Le fichier Appli/params.php
[modifier | modifier le wikicode] <?php
$host = 'localhost';
$user = 'root';
$password = '';
$base = 'toto';
$admin_login = 'admin';
$admin_password = 'aligator';
?>
Le fichier Appli/appli.php
[modifier | modifier le wikicode] <?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();
}
?>
Le fichier Appli/connectForm.php
[modifier | modifier le wikicode] <?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>
Le fichier Appli/addForm.php
[modifier | modifier le wikicode] <?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>
Le fichier Appli/deleteForm.php
[modifier | modifier le wikicode] <?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>
Exemples/Livre
Créer le livre
[modifier | modifier le wikicode]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 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.
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` )
);
Ajouter un message
[modifier | modifier le wikicode]Voici la requête SQL qui permet d'insérer les données dans la table:
INSERT INTO `livre` ( `nom` , `email` , `date` , `message` )
VALUES ( `UnNom` , `Une@` , `xx/xx/xxxx` , `blalblablabla...` );
Saisie des données
[modifier | modifier le wikicode]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
<!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>
Traitement des données
[modifier | modifier le wikicode]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 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
<?php
$host='localhost';
$user='root';
$password='';
$base='toto';
?>
fichier ajout_message.php
<?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";
?>
La fonction htmlspecialchar() 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 htmlentities(), qui permet en plus de spécifier les caractères de remplacement, ou n12br() qui permet de remplacer les retours chariots par le caractère HTML de retour à la ligne (br).
Récupérer les messages et les faire afficher
[modifier | modifier le wikicode]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:
SELECT * FROM `livre` ORDER BY `id`
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 :
SELECT * FROM `livre` ORDER BY `id` DESC
Après, il suffit d'afficher les informations obtenues grâce à la requête.
fichier affiche_message.php
<!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>
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.
Exemples/MiniForum
Faire un mini forum de discussion
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Voici un forum modulaire, universel, illimité, fonctionnel et très réactif mais à sécuriser avec MySQL en backend et HTML en frontend.
Architecture
[modifier | modifier le wikicode]À bâtir en MVC, le forum est segmenté comme :
./index.php ./config.inc.php // STATICAL MODULES ./Objects/ ./Objects/frameset.inc.php // main frameset ./Objects/mainPage.inc.php // main pages content // FUNCTIONNAL MODULES ./Libraries/
Déploiement
[modifier | modifier le wikicode]La méthodologie 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 incrément 0 étant l'ébauche et incrément 1, la première itération de l'applicatif.
<?php
/* */
define('OBJ', './Objects/');
define('LIB', './Libraries/');
$cnx = array('host'=>'localhost','user'=>'root','db'=>'miniForum','pass'=>'');
?>
increment 0 : index.php
<?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;
?>
increment 1 : index.php
<?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;
?>
increment 2 : index.php
<?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;
?>
Objects
[modifier | modifier le wikicode] <?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;
?>
increment 3 : frameset.inc.php
<head>
{$_content['title']}
{$_content['meta']}
{$_content['script']}
{$_content['style']}
</head>
increment 0 : mainPage.inc.php
<?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;
?>
increment 1 : listener.inc.php
<?php
/* application listener
*/
/* flag cases
*/
switch(true)
{
case(@$_REQUEST['aut']=='0'):
//action disconnect
unset($_SESSION['autho'],$_SESSION['userMod']);
break;
/*
case ():
break;
*/
}
?>
increment 1 : controler.inc.php
<?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';
}
?>
increment 1 : mainPage.inc.php
- À cette itération, l'authentification du user est fonctionnelle.
- Le snippet content['authorize'] change de mode.
...
// 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';
...
increment 2 : mainPage.inc.php
- À cette itération, l'attribution des privilèges du user est fonctionnelle.
- Le snippet content['authorize'] affiche le statut user mode.
// 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>
';
increment 3 : jsScript.inc.php
- intégration des javascripts
<?php
$script = '
/* javascript scripts
*/
function showHide(obj)
{// >(element)
if(obj.style.display!="none")
obj.style.display="none";
else
obj.style.display="";
}
';
?>
increment 3 : pages.inc.php
- contenus des pages
<?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>
';
}
}
?>
increment 4 : pages.inc.php
- contenus des pages complétés
<?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>
';
}
?>
increment 4 : controler.inc.php
- controler complété
<?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']);
}
}
?>
- À ce stade on a une ébauche de layout pour notre forum :
Constatations
[modifier | modifier le wikicode]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 ?)
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Nous aurons d'emblée besoin des deux utilitaires :
[classes]
- Les classes usuelles
- Connectivity
- Request
[functions]
- Les fonctionnalités usuelles
[classes]
<?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");
}
}
}
?>
<?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);
}
}
?>
[functions]
<?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;
}
}
?>
increment 2 : utils.fnc.php
/* 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];
}
}
}
increment 4 : utils.fnc.php
- add function
/* get record by id
*/
function getRecordNfo($cnx, $table, $id)
{
return count($res=Request::SQL($cnx,"SELECT * FROM $table WHERE id=$id"))?$res[0]:false;
}
Modélisation
[modifier | modifier le wikicode]La modélisation est fonction du métier et des affinités. En voici une fonctionnelle pour notre exemple :
-- 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`
--
un script d'installation pourrait être :
<?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 ;');
}
?>
Conclusion
[modifier | modifier le wikicode]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.
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
[modifier | modifier le wikicode]- É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
[modifier | modifier le wikicode]- Écriture d'un document XML valide à imbrications multiples d'éléments hétéroclites comportant des attributs ou non...
<?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>
- Sauvegarde de ce document.xml bien formé dans le même répertoire.
Création de la fonction
[modifier | modifier le wikicode]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
getAttribute()pour optimiser le code
function getAttribute($node)
{// >((dom)node) ((array)tab)>
$tab=array();
foreach($node->attributes() as $k1->$v1) {
$tab[$k1->{''}->name]=$k1->{''}->value;
}
return $tab;
}
Description :
- Pour chaque attribut, on place le contenu à une clé du tableau
tabà retourner.
On s'attaque ensuite au plus gros du travail de notre convertisseur à savoir domxml2array() :
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;
}
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 :
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;
}
Création de la classe
[modifier | modifier le wikicode]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
DomTree.Class.php.
<?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;
}//
}
?>
Application
[modifier | modifier le wikicode]Dans un fichier test.php on instancie la classe et on l'exécute:
<?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);
?>
Aperçu
[modifier | modifier le wikicode]On obtient un arbre structuré easy2use pour le web2
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
)
)
En bref
[modifier | modifier le wikicode]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.
Exemples/MVC
Historiquement, PHP est un langage glue, il peut être intégré avec le langage de balisage 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 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 Cadriciel web sont basés sur ce principe.
Développement
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]La vue comprendra un formulaire HTML pour la saisie des données utilisateur :
<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>
Spécialisation des composantes
[modifier | modifier le wikicode]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) :
<?php
$page['container']['frameSet'] = '
<html>
<head>'
.$page['css']
.$page['script'].
'</head>
<body>'
.$page['container']['header']
.$page['container']['main']
.$page['container']['footer'].
'</body>
</html>';
?>
2. Les formulaires deviendraient (vueFormulaire.inc.php) :
<?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>';
?>
Création du modèle
[modifier | modifier le wikicode]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)
<?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);
}//
*/
?>
! 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
[modifier | modifier le wikicode]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
<?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>';
}
?>
! utilisation de $page['container']['main'] ne sert a rien ici car sera écrasé dans l'appel de la page principale.
Gestion des containers
[modifier | modifier le wikicode]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. :
<?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'];
?>
! 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
[modifier | modifier le wikicode]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
<?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';
?>
2. controlAffichage.inc.php devient
<?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>'; // <--
}
?>
3. ici rien est à faire
Application
[modifier | modifier le wikicode]L'application se construit ensuite par inclusion des composantes
<?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'];
?>
! 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
[modifier | modifier le wikicode]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.
Exemples/Webservice
Introduction
[modifier | modifier le wikicode]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 SOAP et REST.
Qu'est-ce que SOAP (Simple Object Access Protocol) ?
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Utilisation de la bibliothèque NuSOAP
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.
<?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';
?>
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
[modifier | modifier le wikicode]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 :
<?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);
?>
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
[modifier | modifier le wikicode]On utilise ici la bibliothèque SOAP[1].
WSDL
[modifier | modifier le wikicode] <?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>
SERVER
[modifier | modifier le wikicode]Pour que soap soit actif, il faut décommenter extension=php_soap.dll dans php.ini
<?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;
}
?>
Client
[modifier | modifier le wikicode] <?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');
?>
Conclusion
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
Exemples/Vérification RIO
Le 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
[modifier | modifier le wikicode]- le champ
coderiodoit être renseigné avec le code RIO sans blanc. - le champ
mobiledoit être renseigné avec un numéro de téléphone mobile, sans blanc.
code
[modifier | modifier le wikicode]$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>";
}
}
Ajax/Sommaire
Ajax : comment créer un sommaire
[modifier | modifier le wikicode]Intérêt de l'utilisation d'Ajax
[modifier | modifier le wikicode]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 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
[modifier | modifier le wikicode]Le fichier index.html
[modifier | modifier le wikicode]<!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>
Le fichier ajax.php
[modifier | modifier le wikicode]<?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';
?>
Le fichier page1.html
[modifier | modifier le wikicode]<h1>Page 1</h1> bla bibib blan
Le fichier page2.html
[modifier | modifier le wikicode]<h1>Page 2</h1> bonjour
Le fichier page3.html
[modifier | modifier le wikicode]<h1>Page 3</h1> bli bli bli
Le fichier page4.html
[modifier | modifier le wikicode]<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>
Ajax/Date
Démonstration
[modifier | modifier le wikicode]Le résultat est visible sur http://xavier.merrheim.free.fr/date.
Les fichiers
[modifier | modifier le wikicode]Le fichier index.html
[modifier | modifier le wikicode]<!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>
Le fichier ajax.php
[modifier | modifier le wikicode]Ce fichier peut contenir tout script php dont le résultat doit être mis à jour régulièrement.
Pour cet exemple :
<?php
echo "(Cadre mise à jour le ".date("r").")";
?>
Exemples/MiniCMS
Les CMS ou Content Management Systems (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".
Exemples/MiniCMS/Faire un miniCMS
Faire un miniCMS
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Ne seront développés et intégrés que les modules nécessaires à la démonstration.
Ces modules peuvent s'organiser à desseins.
- Backend(s)manager
- Users
- Contents
- Customizations
- FrontEnds Manager
- Templates
- Controlers
- Front-Ends
- Templates
Analyse
[modifier | modifier le wikicode]Les schémas suivants décrivent le déploiement des composants.
./index.php
<Utilities>_____<Backend>
|
[Functions] | [ContentModule]
[Classes] | [TemplatesModule]
[Objects] | [UsersModule]
|
|__<Frontend>
[Outputer]
[AjaxLayer]
On a ici trois couches à distribuer en MVC.
Déploiements
[modifier | modifier le wikicode]Les parties "clientes" et "administratives" sont séparées par simplicité. On prend un déploiement admins vs customers sur deux répertoires.
./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
/...
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
[modifier | modifier le wikicode]- Les données : Xmlisation
- Le code : Php5 Object
- Dumping & archiving : mysql en soutien pour la recherche indexée et le harvesting
Méthodologie
[modifier | modifier le wikicode]- RUP et patterns
- pas de frameworks (zend ou autre...) ou rapatriement total des snippets
Exemples/MiniCMS/Développement
Développement
[modifier | modifier le wikicode]- 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
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).
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.
Exemples/MiniCMS/Modélisation
La modélisation se veut simple et polyvalente.
Champs de données
[modifier | modifier le wikicode][champs contenus]
Sous customer1/contents, les champs ou "espaces de données" sont organisés en paquets d'enregistrements. D'autres modèles sont possibles.
<?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>
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.
[champs utilisateurs]
Même logique de service :
<?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>
Mêmes accès que pour le modèle précédent.
[Champs page.xml]
L'indexation des pages pointeront sur des données data ou layout
<?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>
[Champs templates]
<?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>
La version suivante a une complétion par points d'ancrages. Cette version va orienter le projet.
<?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>
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().
Exemples/MiniCMS/Implémentation
Nous avons besoin des fonctionnalités :
TODO
- [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
- (do ) php snippet acter
- [6] Vue xhtml fonctionnelle ajaxifiée
- (done) xhtml template "nested"
NICE TO HAVE
- [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.
Concernant l'approche modulaire :
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. 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.
'Classes & Fermes de fonctions' contre 'scriptlets'
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[1].
- ↑ 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.
[1] Complétion des données
[modifier | modifier le wikicode]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
<?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']) ;
}
}
?>
On implémente ces fonctions utilitaires à utilities.class.php
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 ;
}
On implémente la méthode getContentByData du dataManager
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) ;
}
}
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é...
<?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();
?>
- -> Sur l'input crop1.xml
<?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>
- -> On a pour résultat :
<?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>
Le deuxième test de get par l'id est également concluant
# 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();
Son résultat étant :
<?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>
On implémente à utilities.class.php les méthodes :
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 ;
}
et à la classe dataManager.class.php
public function setContentByData($pDocument,$pData)
/**
* set the content to data
*/
{
if(@$pData['attribute'])
{
utilities::writeContentByAttribute($pDocument, $pData) ;
}
else
{
utilities::writeContentByContent($pDocument, $pData) ;
}
$this->saveDocument();
}
Le test suivant permet de valider la phase d'écriture :
# 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']);
avec le resultat
<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>
Liste des défauts
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]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 :
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 ;
}
Un test concluant de ces méthodes nous rend opérationnel pour cette phase
<?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);
?>
Résultat :
Array
(
[login] => user2
[pass] => pass2
[lastname] => name2
[forname] => forName2
[rights] => u
[customer] => customer2
)
Liste des défauts
[modifier | modifier le wikicode]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
user.
[3] Complétion du frameset
[modifier | modifier le wikicode]- Pour commencer nous déclarons une classe utilities.class.php qui reprendra toutes les fonctionnalités utiles pour notre solution
<?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;
}
}
?>
- La classe templateControler regroupe les fonctions sur les templates. Le contenu à publier est segmenté en flux.
- Son objectif est de :
- - reformater,
- - assembler,
- - publier.
<?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();
}
}
Le test suivant permet de voir si on prend bien les ancres d'injection, par exemple, pour le nœud frameset/content
//
$tmp = new templateControler(array("path"=>"object.xml"));
$tst = $tmp->getNode("//template/frameset/content");
print_r_html($tmp->getAnchors($tst[0]->nodeValue));
Array
(
[0] => Array
(
[0] => {{inc:styles[*]}}
[1] => {{inc:scripts[*]}}
[2] => {{inc:bodies[*]}}
)
)
On implémente ces deux nouvelles fonctions à templateControler
/**
* 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.
Le test suivant permet de voir si on remplace bien l'ancre par son contenu :
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']
);
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] =>
)
)
)
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.
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);
}
}
Liste des défauts
[modifier | modifier le wikicode]- 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
[modifier | modifier le wikicode]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.
<?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;
}
?>
Exemples/MiniCMS/Déploiement de la vue
Le frontOffice
[modifier | modifier le wikicode]Le front office est orienté ajax.
<?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>
Le backOffice
[modifier | modifier le wikicode]- Config.inc.php
<?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") ;
?>
- main.php
- point d'entrée back&front end
- un switch distingue les appels entrants avec et sans paramètres
<?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" ;
}
?>
- responder.inc.php
- appels entrant avec interrogation
<?php
# TESTING 1.0
echo $_REQUEST["id"] . "~@~" . $_REQUEST['id'] . " / " . $_REQUEST["name"] . " " . $_REQUEST["surname"] ;
?>
- entry.inc.php
- appels entrant sans interrogation
<?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'] ;
?>
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é.
Pour ce faire :
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.
<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>
On teste sur un segment du body. Par convention, le container à nourrir porte l'id composé "id:idDuContainer".
<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>
Un test sur la page "standard" donne en output pour l'appel ajax :
$.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];
}
});
=> le résultat suivant
<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>
On est nominal pour l'ajaxification de la complétion du contenu côté client.
Implémentation des contrôleurs côté serveur
[modifier | modifier le wikicode]- Common.inc.php :
<?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;
}
- Responder.inc.php :
<?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;
Auto complétion du layout
[modifier | modifier le wikicode]On implémente feedLayout pour auto compléter et chercher les fragments sur le template pour l'injecter sur les encres
<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>
Auto complétion des données
[modifier | modifier le wikicode]
<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>
Test de réactivité de la couche ajax
[modifier | modifier le wikicode]La fonction feedContent se charge de charger les flux sur la page en provenance du template et des champs de données.
Le template étant :
<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>
et le crop 1.xml :
<?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>
On a pour rendu en sortie :
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
[modifier | modifier le wikicode]MyTinyCMS est maintenant un mini content manager system orienté wiki. L'orientation du CMS génère le code métier suivant
Classes
[modifier | modifier le wikicode]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é.
<?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() ;
?>
Voici Classes/dataManager.class.php dans sa version finale. Il est orienté données.
<?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']) ;
}
}
?>
Voici le Classes/templateControler.class.php orienté layout.
<?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();
}
}
?>
Classes/utilities.class.php dans sa version finale
<?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 ;
}
}
?>
Functions
[modifier | modifier le wikicode]Functions/common.inc.php dans sa version finale
<?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 ;
}
?>
Backend
[modifier | modifier le wikicode]Le backend est implémenter suivant quelques règles métier imposées par l'orientation du CMS également.
Objects/Backend/authorize.inc.php
<?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);
?>
Objects/Backend/responder.inc.php dans sa version finale
<?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;
}
?>
Objects/Backend/services.inc.php
<?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()
{
}
}
?>
Objects/Backend/webservices.inc.php
<?php ?>
FrontEnd
[modifier | modifier le wikicode]Deux points d'entrées : Objects/Frontend/admin.inc.php
<?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'] ;
?>
Objects/Frontend/entry.inc.php dans sa version finale
<?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'] ;
?>
Le Troisieme noyau
[modifier | modifier le wikicode]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
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".
Télécharger
[modifier | modifier le wikicode]MyTinyCMS est nominal... Il peut être telechargé ici
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.
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.
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.
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...
cURL
PHP possède une extension cURL pour requêter des URL.
Installation
[modifier | modifier le wikicode]Sur Linux :
sudo apt-get install php-curl
Exemples
[modifier | modifier le wikicode]Voici le POST d'un JSON avec une pièce jointe PDF (du multipart) :
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;
}
DOMDocument
Introduction
[modifier | modifier le wikicode]Une des recherches majeures du développeur a toujours été de chercher à séparer les langages de programmation, clarifiant ainsi ses scripts et simplifiant sa tâche. Ainsi, il est possible d'enregistrer son CSS dans des fichiers externes comportant l'extension .css, de séparer le Javascript du HTML en l'enregistrant dans des fichiers .js.
Il reste cependant le problème de la séparation du PHP et du XML (incluant le HTML). La bibliothèque DOMDocument va repousser ces limites.
Qu'est-ce que DOMDocument ?
[modifier | modifier le wikicode]DOMDocument est une bibliothèque de fonctions apparue avec PHP5[1] et activée par défaut. Elle permet de concevoir des pages HTML sous forme d'objets.
Les avantages et les inconvénients
[modifier | modifier le wikicode]- Concevoir des pages HTML par cette méthode permet d’annihiler un problème majeur de la programmation procédurale : l'édition du code n'est plus en fonction de sa position dans le script. Pour être plus clair, chaque balise jusqu'à la DTD peut être modifiée à tout moment dans l'objet HTML.
- Il est possible d'enregistrer la page HTML dans un fichier sans l'afficher.
mais...
- Le code est plus long à éditer.
Principe du DOM
[modifier | modifier le wikicode]Cette bibliothèque présente de nombreuses similitudes avec le Javascript aussi bien dans le fonctionnement que dans le nom de ses fonctions.
Le DOM (Document Object Model) est basé sur un système de nodes (nœuds). Un node est un élément qui est - soit une balise (nodes Tag) - soit du texte - soit un attribut de balise Les nodes sont liés par un système hiérarchique :
<p>Ce texte est <strong>important</strong></p>
On dit alors que le node Tag <strong> est fils du node <p> Le node texte "Ce texte est" est également fils de <p> qui est parent du node texte.
Il existe un certain nombre de classes prédéfinies : DOMDocument, DOMNode, DOMElement, DOMText, DOMAttr, DOMList... Certaines sont très simples, d'autres possèdent des fonctionnalités très avancées.
Importer une page préexistante
[modifier | modifier le wikicode]Il est possible d'importer une page HTML. Cela simplifiera considérablement la tâche du programmeur qui n'aura qu'à apporter les modifications nécessaires avant de l'afficher ou de réenregistrer la page. Voici le code important la page.
<?php
$doc = DOMDocument::loadHTMLFile("fichier.html");
La variable $doc contient donc un objet DOMDocument avec toutes les balises sous formes de nodes. Il est maintenant possible d'accéder aux nodes par le biais de fonctions préexistantes.
NB : il est également possible d'avoir recours au code suivant.
<?php
$doc = new DOMDocument();
$doc->loadHTMLFile("fichier.html");
Il est également possible d'importer le code à partir d'une chaîne de caractères :
<?php $code = "<html><head></head><body></body></html>"; $doc = new DOMDocument(); $doc->loadHTML( $code );
Enregistrer une page
[modifier | modifier le wikicode]Un des grands avantages de cette bibliothèque est la capacité à enregistrer la page générée dans un fichier pour un affichage ultérieur. Il suffit d'avoir recours au code suivant :
$doc->saveHTMLFile("fichier.html");
Si vous voulez l'afficher, il vous suffit d'exécuter la fonction suivante :
<?php echo $doc->saveHTML();
La méthode retourne une chaîne de caractères que la fonction echo affiche.
Le résultat n'est pas en Unicode, donc les lettres avec diacritiques seront mal affichées par défaut, en français : àâçéèêëîïôöüù. Cela peut aussi générer un Warning: DOMDocumentFragment::appendXML(): Entity: line 1: parser error : Input is not proper UTF-8, indicate encoding !
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR"> <head> <meta http-equiv="Content-type" content="text/html; charset=UTF-8"/> </head> <body> <?php $code = 'Les àâçéèêëîïôöüù'; echo $code; // affichage normal $page = new DOMDocument(); $page->loadHTML($code); echo $page->saveHTML(); // Les à âçéèêëîïôöüù // Solution $page2 = new DOMDocument(); $page2->loadHTML(utf8_decode($code)); echo $page2->saveHTML(); // Les àâçéèêëîïôöüù ?> </body> </html>
La classe DOMNode
[modifier | modifier le wikicode]Les classes DOMElement, DOMText et DOMAttribute sont dérivées de cette classe. Ainsi, les méthodes et propriétés présentées ici seront disponibles pour leurs classes filles.
Attention : un nœud une fois créé ne se trouve pas dans le document. Ajouter un nœud va se dérouler en deux étapes :
- on crée le nœud
- on l'insère dans le nœud parent ou à la racine du document.
Voici les propriétés accessibles à tous les nœuds :
$node->nodeType // Type de nœud. Vaut 1 pour un élément XML, 3 pour un texte $node->childNodes //Retourne un objet NodeList qui contient tous les éléments enfants de ce nœud $node->firstChild //Retourne le premier nœud enfant $node->lastChild // Retourne le dernier nœud enfant $node->previousSibling // Retourne le nœud juste avant celui-ci $node->nextSibling // Retourne le nœud juste après celui-ci
Les méthodes sont les suivantes :
- AppendChild
- Le nœud $enfant devient enfant de $node
$node->appendChild( $enfant );
- RemoveChild
- Supprime le nœud $enfant du node $node
$node->removeChild( $enfant );
La classe DOMElement
[modifier | modifier le wikicode]- Les éléments possèdent les propriétés suivantes :
$node->tagName // par exemple "p" pour un paragraphe. Sa valeur ne peut être modifiée
Attention : seules les principales propriétés sont présentées. Si vous voulez en avoir la liste complète, vous pouvez consulter la documentation de référence sur http://php.net/manual/fr/class.domnode.php.
- Et les méthodes suivantes :
- Méthodes d'attributs
$node->hasAttribute(); //Renvoie true s'il possède des attributs
$node->getAttribute("name"); //Retourne la valeur de l'attribut
$node->removeAttribute("name"); //Supprime l'attribut ''name''
$node->setAttribute("name","value"); //Modifie un attribut
$node->getAttributeNode ("name" ); //Retourne un objet DOMAttr
- Autres méthodes
$newNode = $node->cloneNode(); //duplique un élément
$nodeList = $node->getElementsByTagName("strong");
Cette fonction retourne un objet nodeList qui contient une liste des balises <strong> enfants du nœud. Pour récupérer le n+1ème nœud de la liste, il suffit d'avoir recours à la méthode de l'objet nodeList suivante :
$strong5 = $nodeList->item(4); //Sélectionne la 5e balise <strong>
L'attribut length donne le nombre d'éléments de la liste. Exemple :
for ($i=0; $i<$nodeList->length; $i++)
{
echo $nodeList->item( $i )->tagName;
}
Comme vous le savez, tagName retourne le nom de la balise. Ici, le code retournera "strongstrongstrong…". En effet, seuls les nœuds strong ont "été sélectionnés. Comme vous avez pu le remarquer, il est possible d'exécuter plusieurs méthodes et propriétés en même temps. Voici l'ordre d'exécution :
- La méthode item($i) est exécutée et retourne un nodeTag.
- La propriété tagName du nœud est appelée. Attention : c'est celle de l'objet retourné.
- La fonction echo affiche le nom de la balise retournée par la propriété tagName.
La classe DOMText
[modifier | modifier le wikicode]La classe DOMText contient l'unique propriété suivante :
$node->wholeText
Elle n'est accessible qu'en lecture seule.
La classe DOMText admet deux méthodes :
/* Retourne true si la chaîne de caractère contient des espaces */ $node->isWhitespaceInElementContent();
/* Retourne dans $end un objet Text qui contient la fin du texte de $node. $node ne contiendra plus que les 5 premiers caractères de sa chaîne */
$end = $node->splitText(5)
Exemple :
<?php
$doc = new DOMDocument();
$text = $doc->createTextNode("Je suis celui qui est");
$text2 = $text->splitText(7);
echo $text->wholeText."<br />";
echo $text2->wholeText
Ce code retournera "je suis<br /> celui qui est"
La classe DOMAttr
[modifier | modifier le wikicode]La classe DOMAttr est comme son nom l'indique un attribut, qui est donc dépendant de la balise. Elle contient les propriétés suivantes :
$node->name // Nom de l'attribut $node->value // Valeur de l'attribut /* Nom de la balise qui contient l'attribut. La valeur retournée est un objet DOMElement */ $node->ownerElement
Seule la propriété value n'est pas en lecture seule, c'est à dire qu'il est possible d'avoir recours au code suivant :
$node->value = "maValeur";
Accéder à un nœud
[modifier | modifier le wikicode]Il existe plusieurs modes de recherche du nœud. Il est par exemple possible de le sélectionner par son id :
$node = $doc->getElementById("sonId"); // Retourne un objet nœud
Sachant qu'il ne peut y avoir qu'un nœud possédant l'id recherché, la méthode retournera un objet nœud au lieu d'un objet nodeList.
Il est cependant possible de récupérer une liste de nœuds en les sélectionnant par leur nom de balise. Évidemment, seuls les nœuds Tag peuvent être sélectionnés.
$nodeList = $doc->getElementsByTagName("acronym"); //Sélectionne toutes les balises <acronym>
Comme nous l'avons déjà dit, il faut, pour récupérer un nœud particulier de la liste, utiliser la méthode suivante :
$acronym5 = $nodeList->item(4); //Sélectionne le 5e nœud de la liste;
XML
[modifier | modifier le wikicode]Création :
$xml = new DOMDocument("1.0", "ISO-8859-15");
$xml_node1 = $xml->createElement("Node_1");
$xml_node2 = $xml->createElement("Node_2", "Feuille");
$xml_node2->setAttribute("Attribut_1", "Valeur_1");
$xml_node1->appendChild($xml_node2);
$xml->appendChild($xml_node1);
$xml->save('Fichier_1.xml');
Lecture :
$xml = new DomDocument;
$xml->load('Fichier_1.xml');
$fields = $xml->getElementsByTagName('fields');
foreach ($fields as $field) {
/** @var DOMElement|DomText $fieldChild */
foreach ($field->childNodes as $fieldChild) {
var_dump($fieldChild->nodeValue);
}
}
Références
[modifier | modifier le wikicode]
JSON
Bibliothèque PHP-JSON
[modifier | modifier le wikicode]Le format de données JavaScript Object Notation (JSON) peut être utilisé en PHP grâce à différentes fonctions natives depuis PHP 5.2.0.
Installation (pour PHP < 5.2)
[modifier | modifier le wikicode]Linux
[modifier | modifier le wikicode]apt-get install php5-json
Windows
[modifier | modifier le wikicode]- Télécharger le fichier json-1.2.1.tgz sur https://pecl.php.net/package/json.
- Décompresser et compiler le code source en json.so.
- Le copier dans le dossier des extensions PHP.
- Dans le php.ini (ex : C:\Program Files (x86)\EasyPHP\binaries\php\php_runningversion\php.ini), ajouter :
extension=json.so
json_encode()
[modifier | modifier le wikicode]Cette fonction convertit un objet PHP en JSON exploitable en JavaScript[1]. Ex :
$tableau = array('colonne 1' => 'valeur 1', 'colonne 2' => 'valeur 2', 'colonne 3' => 'valeur 3');
echo json_encode($tableau);
{"colonne 1":"valeur 1","colonne 2":"valeur 2","colonne 3":"valeur 3"}
json_encode() sur une instance de classe n'en n'affiche que les attributs publics. Pour encoder les privés, il faut que la classe implémente JsonSerializable en qui impose une méthode jsonSerialize()[2]. Ex :
public function jsonSerialize() {
return $this->array;
}
Options
[modifier | modifier le wikicode]
Par défaut json_encode() échappe les caractères spéciaux non ASCII (ex : "é" devient "\u00e9"). Pour éviter cela on utilise l'option suivante :
$a = json_encode($monTableau, JSON_UNESCAPED_UNICODE);
Par ailleurs, les erreurs de json_encode() sont accessibles avec json_last_error() ou json_last_error_msg(). Ex :
$a = json_encode($monTableau);
if (json_last_error() === JSON_ERROR_INF_OR_NAN) {
$this->logger->error(json_last_error_msg());
$a = json_encode($monTableau, JSON_PARTIAL_OUTPUT_ON_ERROR);
}
Pour afficher dans un format plus lisible par un humain (indenté), utiliser JSON_PRETTY_PRINT.
json_decode()
[modifier | modifier le wikicode]Convertit une chaine de caractères JSON en :
- Si aucun paramètre 2 n'est passé, un objet PHP dont chaque attribut correspond à une clé du tableau.
- Si le paramètre 2 vaut "true", un tableau associatif[3].
La gestion des erreurs est semblable à celle de json_encode().
PEAR Services_JSON
[modifier | modifier le wikicode]Le framework PEAR possède aussi un package Services_JSON contenant des .php avec des exemples, à télécharger en tant que JSON.tar.gz sur http://pear.php.net/pepr/pepr-proposal-show.php?id=198.
Il requiert PHPUnit.phpTélécharger.
La classe Services_JSON de JSON.php peut s'utiliser comme décrit dans Test-JSON.php.
Références
[modifier | modifier le wikicode]
MING
Conceptions d'animations pour pages web
[modifier | modifier le wikicode]Créer les animations en Flash (.swf) se fait par des logiciels payants, cependant la librairie MING écrite en C, et utilisable en PHP, C++, Python et Ruby, permet de les générer gratuitement (mais pas d'éditer les .swf existant).
<?php
// Dessine deux boutons interactifs
function BoutonCarré($r, $g, $b)
{
$s = new SWFShape();
$s->setRightFill($s->addFill($r, $g, $b));
$s->movePenTo(-20,-20);
$s->drawLineTo(20,-20);
$s->drawLineTo(20,20);
$s->drawLineTo(-20,20);
$s->drawLineTo(-20,-20);
return $s;
}
function BoutonRond($r, $g, $b)
{
$s = new SWFShape();
$s->setRightFill($s->addFill($r, $g, $b));
$s->movePenTo(20, 20);
$s->drawCircle(20);
return $s;
}
$carré = new SWFButton();
$carré->setUp(BoutonCarré(0xff, 0, 0));
$carré->setOver(BoutonCarré(0, 0xff, 0));
$carré->setDown(BoutonCarré(0, 0, 0xff));
$carré->setHit(BoutonCarré(0, 0, 0));
$rond = new SWFButton();
$rond->setUp(BoutonRond(0xff, 0, 0));
$rond->setOver(BoutonRond(0, 0xff, 0));
$rond->setDown(BoutonRond(0, 0, 0xff));
$rond->setHit(BoutonRond(0, 0, 0));
$m = new SWFMovie();
$m->setDimension(320, 240);
$m->setBackground(0xff, 0xff, 0xff);
$i = $m->add($carré);
$i->moveTo(50, 50);
$i = $m->add($rond);
$i->moveTo(100, 50);
header('Content-type: application/x-shockwave-flash');
$m->output();
Depuis février 2016, Firefox bloque tous les contenus Flash par défaut, pour des raisons de sécurité. Cette technologie est donc amenée à être remplacée par JavaScript.
Liens externes
[modifier | modifier le wikicode]- Fonctions
- Tutoriel JournalDuNet
- Tutoriel GazbMing
- Tutoriel developpez.com
- Tutoriel supportduweb.com
- Bibliothèque de scripts
- Perl graphics programming, Shawn P. Wallace, 2002
SPL
La Standard PHP Library (SPL) est une bibliothèque intégrée depuis PHP 5[1].
Elle comprend les classes suivantes :
Structures de données
[modifier | modifier le wikicode]- SplDoublyLinkedList
- SplStack
- SplQueue
- SplHeap
- SplMaxHeap
- SplMinHeap
- SplPriorityQueue
- SplFixedArray
- SplObjectStorage
Itérateurs
[modifier | modifier le wikicode]Interfaces
[modifier | modifier le wikicode]Exceptions
[modifier | modifier le wikicode]Références
[modifier | modifier le wikicode]
ADOdb
Fonctionnalités
[modifier | modifier le wikicode]Cette librairie permet comme PEAR DB de supporter différents types de bases de données (MySQL, Oracle ...). Il est ainsi possible par le biais d'un fichier de configuration de modifier le type de base de données sans que cela n'aie d'impact dans le code de l'application en elle-même.
Il s'agit ici, comme pour les autres produits décrits plus haut, d'exploiter les possibilités de programmation orienté objet en ce sens qu'elles optimisent la programmation, la rendant plus efficiente.
Reformulons l'intérêt d'utilisation de cette classe. Nous savons par exemple que le langage Java permet "d'attaquer" différents types de bases de données sans qu'une ligne de programmation ne soit modifiée, déportant cette disparité en concentrant notre attention sur une seule ligne de configuration : la déclaration du driver de base de données.
C'est un peu ce que veut faire pour PHP ADODB, tout comme PEAR DB, qui furent des produits concurrents, dans le but de tester un développement PHP.
Installation
[modifier | modifier le wikicode]Les fichiers se téléchargent sur http://adodb.sourceforge.net/#download.
Exemple
[modifier | modifier le wikicode]Exemple sur une base Access :
include('adodb.inc.php'); # charge le code de ADOdb
$conn = &ADONewConnection('access'); # crée une connexion
$conn->PConnect('northwind'); # se connecte à MS-Access, northwind DSN
mais ensuite, que vous créez, puis exploitez en base réelle sur Postgres ou encore MySQL, les modifications sur les lignes de codes pour une version en exploitation seront alors des modifications de type paramétrage dans l'appel de la méthode d’accès, mais non point sur chaque ligne d'instruction, qu'il s'agisse d'un accès en lecture, en mise à jour, etc.
include('adodb.inc.php');
$conn = &ADONewConnection('mysql');
$conn->PConnect('localhost','userid','','agora');# se connecte à MySQL, agora db
Il existe aussi des méthodes permettant de passer rapidement en conversion HTML après exploitation en séquence de tuples de la base de données cible, ou encore de créer rapidement un fichier CSV, ce qui est très prisé en bureautique (pour les logiciel de type "Office").
Voir aussi
[modifier | modifier le wikicode]Un excellent tutoriel se trouve à l'adresse suivante :
- chez phplens : Tutorial ADODB français
- chez phpfreaks : Tutorial ADODB anglais
Une courte description en anglais au database journal : ADODB class library
DOMPDF
DOMPDF permet de générer des fichiers PDF à partir d'une page HTML. C'est une alternative à HTML2PDF, qui lui est basé sur TCPDF.
Exemples
[modifier | modifier le wikicode]use Dompdf\Dompdf;
class pdfGenerator
{
public function generate(string $html)
{
$dompdf = new Dompdf();
$dompdf->loadHtml($html);
$dompdf->render();
$dompdf->stream();
}
}
FPDF
FPDF est d'origine française, il est gratuit, et facile d'utilisation. Cette librairie permet d'exploiter les possibilités de production de documents en PDF à l'aide de PHP. Elle se télécharge sur http://www.fpdf.org/.
Exemples
[modifier | modifier le wikicode]use FPDF;
class pdfGenerator
{
public function generate(string $html)
{
$pdf = new FPDF();
$pdf->AddPage();
$pdf->Cell(10, 10, $html);
$pdf->Output();
}
}
PHPExcel
Introduction
[modifier | modifier le wikicode]Cette bibliothèque open source permet de lire et d'écrire dans des tableurs, XLS et XLSX. Mais il peut aussi générer des CSV, des PDF, et des HTML[1].
Elle comprend toute sorte de fonctions de manipulations de tableurs, telles que le changement de couleur des champs, l'ajout de graphiques et de filtres, la protection de feuilles...
Il faut la télécharger sur https://github.com/PHPOffice/PHPExcel :
composer require phpoffice/phpexcel
Pour l'utiliser, l'inclure en début de fichiers :
include 'PHPExcel/Classes/PHPExcel.php';
On appellera ses instances "$objPHPExcel", qui représentent les classeurs.
Création
[modifier | modifier le wikicode]Pour créer un fichier à partir de rien, soit CreateXLS.php un fichier situé à côté du répertoire de la bibliothèque nommé PHPExcel, brut de téléchargement (on appelle la feuille avec un nom très court car elle est souvent utilisée, "$s" pour "sheet") :
$objPHPExcel = new PHPExcel;
$s = $objPHPExcel->getActiveSheet();
$s->setCellValue('A1','Hello');
$s->setCellValueByColumnAndRow(2, 1, 'World!');
$writer = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
// Option 1 : fichier .xlsx apparaissant à côté du .php
$writer->save('./HelloWorld1.xlsx');
// Option 2 : fichier à télécharger par le navigateur
header('Content-Disposition: attachment;filename="HelloWorld2.xlsx"');
$writer->save('php://output');
Ouverture
[modifier | modifier le wikicode]Pour ouvrir et lire un fichier existant :
$objReader = PHPExcel_IOFactory::createReader('Excel2007');
$objPHPExcel = $objReader->load('./HelloWorld1.xlsx');
print $objPHPExcel->getActiveSheet()->getCell('A1')->getValue();
Conversion
[modifier | modifier le wikicode]Conversion d'un XLSX en CSV :
$xlsx = PHPExcel_IOFactory::load('./HelloWorld1.xlsx');
$writer = PHPExcel_IOFactory::createWriter($xlsx, 'CSV');
$writer->setDelimiter(";");
$writer->setEnclosure("");
$writer->save('./HelloWorld1.csv');
Conversion d'un CSV en XLSX :
$objReader = PHPExcel_IOFactory::createReader('CSV');
$objReader->setDelimiter(';');
$objReader->setEnclosure(' ');
$objPHPExcel = $objReader->load('./HelloWorld1.csv');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$objWriter->save('./HelloWorld3.xlsx');
Modifications
[modifier | modifier le wikicode]Les propriétés des cellules sont présentées sous forme de tableaux multidimensionnels :
$style = array(
'borders' => array(
'outline' => array(
'style' => PHPExcel_Style_Border::BORDER_THICK,
'color' => array('argb' => 'FFFF0000'),
),
),
'font' => array(
'bold' => true,
'name' => 'Tahoma',
'size' => 10,
'color' => array('rgb' => 'FF0000'),
),
'fill' => array(
'type' => PHPExcel_Style_Fill::FILL_SOLID,
'color' => array('rgb' => 'C3C3E5')
),
'alignment' => array(
'horizontal' => PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true // retour à la ligne automatique
)
);
// Ajout du style ci-dessus en feuille 2 d'un nouveau fichier
$objPHPExcel = new PHPExcel;
$writer = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$s = $objPHPExcel->createSheet();
$s->setTitle('Feuille style');
$s = $objPHPExcel->setActiveSheetIndex($objPHPExcel->getSheetCount()-1);
$s->setCellValue('A1','Hello style');
$s->getStyle('A1')->applyFromArray($style);
$s->getStyle('B1')->getNumberFormat()->setFormatCode( PHPExcel_Style_NumberFormat::FORMAT_TEXT );
$s->setCellValue('B1','9999999999999999999'); // Sans le format texte les nombres de plus de 15 chiffres sont arrondis
$writer->save('./HelloWorld4.xlsx');
On peut aussi :
// Récupérer la dernière ligne d'une feuille
$ligne = $s->getHighestRow();
// Insérer une ligne
$s->insertNewRowBefore($ligne + 1, 1);
Références
[modifier | modifier le wikicode]Voir aussi
[modifier | modifier le wikicode]- https://github.com/PHPOffice/PhpSpreadsheet (.xlsx et .ods)
- https://github.com/PHPOffice/PHPPresentation (PowerPoint)
PHPMailer
PHPMailer est une bibliothèque open source[1] pour envoyer des emails plus rapidement qu'à partir de la commande mail().
Installation
[modifier | modifier le wikicode]Télécharger sur GitHub ou bien ajouter à composer.json : "phpmailer/phpmailer": "~5.2".
Utilisation
[modifier | modifier le wikicode]// Pour la v5.0.0 (2009)
require_once('PHPMailer/class.phpmailer.php');
// Pour la v5.2.14 (2016)
require('PHPMailer/PHPMailerAutoload.php');
Exemple
[modifier | modifier le wikicode]Exemple de base :
$mail = new PHPMailer();
$mail->Subject = 'Hello World!';
$mail->SetFrom('expediteur@mon_domaine.com');
$mail->AddAddress('destinataire1@son_domaine.com');
$mail->MsgHTML('Corps de l\'email');
if (!$mail->Send()) {
echo 'Erreur : ' . $mail->ErrorInfo;
} else {
echo 'Message envoyé !';
}
Bien sûr, on peut ensuite ajouter en une ligne une pièce jointe, une copie cachée, une signature DKIM...
Références
[modifier | modifier le wikicode]
PHPWord
Introduction
[modifier | modifier le wikicode]Cette bibliothèque open source permet de lire et d'écrire des documents de traitement de texte.
Il faut la télécharger sur https://github.com/PHPOffice/PHPWord.
composer require phpoffice/phpword
RabbitMQ
RabbitMQ est un logiciel de messages en protocole AMQP. Il permet donc à des processus de produire des messages JSON dans des files d'attente pour que d'autres les consomme ensuite[1].
Installation
[modifier | modifier le wikicode]Client PHP
[modifier | modifier le wikicode]composer require php-amqplib/php-amqplib
Serveur
[modifier | modifier le wikicode]L'installation du serveur est multi-plateforme. Sous Linux[2] :
apt-get install rabbitmq-server
Test de fonctionnement :
telnet localhost 5672
Site de gestion
[modifier | modifier le wikicode]Une interface graphique existe pour lire et manipuler les messages manuellement, c'est le management plugin[3]. Pour l'activer :
/usr/sbin/rabbitmq-plugins enable rabbitmq_management
Test de fonctionnement depuis le serveur :
curl localhost:15672
Depuis le client : http://mon_serveur:15672
On la trouve aussi sur Docker[4].
Pour trouver le fichier de configuration : cat /usr/sbin/rabbitmq-server | grep RABBITMQ_ENV
Connexion
[modifier | modifier le wikicode]Les identifiants par défaut de RabbitMQ dépendent des versions. On trouve soit le login / mot de passe "user / password", soit "guest / guest". Pour tester :
curl -i -u guest:guest http://localhost:15672/api/whoami
Si cela ne fonctionne pas, configurer le serveur avec rabbitmqctl. Exemple sous Linux :
/usr/sbin/rabbitmqctl add_user userDev mon_mot_de_passe
/usr/sbin/rabbitmqctl set_permissions -p / userDev '.*' '.*' '.*'
/usr/sbin/rabbitmqctl set_user_tags userDev management
/usr/sbin/rabbitmqctl list_users
Connexion PHP
[modifier | modifier le wikicode] $connection = new AMQPStreamConnection($host, $port, $login, $password);
...
$connection->close();
Création de queue et routage
[modifier | modifier le wikicode]Pour créer une queue simple prête à recevoir des messages :
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue1', false, false, false, false);
Exchange
[modifier | modifier le wikicode]| Schéma des différents types de routage RabbitMQ sur le site : (en) Jyoti Sachdeva, « Getting Started With RabbitMQ: Python », | |
Une autre manière de poster des messages est en passant par un exchange. On en distingue plusieurs types[5] :
- direct : une seule queue recevra le message (patron de conception producteur/consommateur).
- fanout : toutes les queues liée à l’exchange recevront le message (patron de conception producteur/abonné).
- topic : les queues de l’exchange inscrites aux sujets concernés recevront le message (selon un motif dans la "routing key" où "*" représente un seul mot séparé par un point, et "#" au moins un)[6].
- headers : routage par en-tête de message plutôt que par "routing key".
Dans cet exemple, on rattache la queue à un exchange "Bus" :
$this->rabbitMqConnection->getChannel()->exchange_declare('Bus', 'fanout', false, true, false);
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue2', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue2', 'Bus');
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue3', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue3', 'Bus');
Exemple de topic : on ne publie pas dans la queue mais dans l’exchange qui leur routera ensuite le message.
$this->rabbitMqConnection->getChannel()->exchange_declare('Topic_bus', 'topic', false, false, false);
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue4', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue4', 'Topic_bus');
$this->rabbitMqConnection->getChannel()->queue_declare('Wikibooks.Queue5', false, true, false, false);
$this->rabbitMqConnection->getChannel()->queue_bind('Wikibooks.Queue5', 'Topic_bus');
QoS
[modifier | modifier le wikicode]Pour demander à RabbitMQ de ne pas surcharger les consommateurs d'une queue en leur répartissant les messages que s'ils ont terminé de traiter le précédent :
$this->rabbitMqConnection->getChannel()->basic_qos(null, 1, null);
DLX
[modifier | modifier le wikicode]Le mode DLX (Dead Letter Exchanges) permet de transférer un message d'une queue dans une autre après un certain temps[7].
Production
[modifier | modifier le wikicode] $amqpMessage = new AMQPMessage(json_encode('Hello World!'),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$this->rabbitMqConnection->getChannel()->basic_publish($amqpMessage, 'Bus', 'Wikibooks.Queue1');
Consommation
[modifier | modifier le wikicode]Par défaut on consomme un seul message de la queue. Pour tous les lire un par un, utiliser basic_ack() après basic_consume().
$this->rabbitMqConnection->getChannel()->basic_consume(
'Wikibooks.Queue1',
gethostname() . '#' . rand(1, 9999),
false,
false,
false,
false,
[$this, 'consumeCallback']
);
while (count($this->rabbitMqConnection->getChannel()->callbacks)) {
$this->rabbitMqConnection->getChannel()->wait();
}
public function consumeCallback(?AMQPMessage $msg)
{
if (empty($msg)) {
return null;
}
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
var_dump(json_decode($msg->getBody()));
}
En mode "topic", on peut remplacer 'Wikibooks.Queue1' par 'Wikibooks.*' pour récupérer toutes les queues.
Références
[modifier | modifier le wikicode]- ↑ https://www.rabbitmq.com/tutorials/tutorial-one-php.html
- ↑ https://www.rabbitmq.com/install-debian.html
- ↑ https://www.rabbitmq.com/management.html
- ↑ https://hub.docker.com/_/rabbitmq
- ↑ https://www.rabbitmq.com/tutorials/tutorial-three-php.html
- ↑ https://www.rabbitmq.com/tutorials/tutorial-five-php.html
- ↑ https://www.rabbitmq.com/dlx.html
Cadriciels
Les cadriciels, plus connus sous le terme framework, sont un ensemble de bibliothèques, scripts destinés aux projets professionnels et au travail en équipe.
Issu d'une longue réflexion et de maturation, le cadriciel permet au développeur de s'affranchir des contingences de sécurité, ergonomie (bouton, habillage) afin de ne se concentrer que sur les fonctionnalités de son application.
Sur le modèle des cadriciels Java ou Ruby on rails, certains cadriciels PHP se démarquent comme CakePHP, PRADO et Symfony.
Modèle-vue-contrôleur
[modifier | modifier le wikicode]Ils se basent sur le plus utilisé des patrons de conception : le modèle-vue-contrôleur qui sépare le modèle de données, l'interface utilisateur et les traitements, ce qui donne trois parties fondamentales dans l'application finale : le modèle, la vue et le contrôleur.
- Le modèle ne s'occupe que du traitements des données, lire depuis une base de données, insérer des lignes dans une base, vérifier que les données sont bien formatées (validation).
- La vue correspond à tout ce qui concerne l'affichage pour l'utilisateur. Cela ne contient généralement que des modèles (template) de pages avec la logique de présentation. C'est une caractéristique forte de MVC, seule cette partie est à mettre-à-jour pour changer l'apparence de votre application.
- Le contrôleur prend en charge le déroulement du programme. La liste des actions sera dans le contrôleur.
Le programmeur peut être dérouté par la séparation en morceaux imposée mais le premier objectif de MVC est la maintenance à long terme.
Voir aussi
[modifier | modifier le wikicode]- Liste de frameworks PHP sur Wikipédia

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

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

