Aller au contenu

Programmation PHP/Version imprimable

Un livre de Wikilivres.

Ceci est la version imprimable de Programmation PHP.
  • Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
  • Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
  • Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.


Programmation PHP

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

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. Une copie de cette licence est incluse dans l'annexe nommée « Licence de documentation libre GNU ».

Introduction

Répartition des langages de programmation côté serveur, des sites Internet le 28 avril 2016.

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].

Les nouvelles fonctionnalités du PHP 5 (paru en 2004) concernent surtout la programmation orientée objet[2] :

  1. interfaces
  2. classes abstraites
  3. constructeurs et destructeurs de classes (ainsi que d'autres méthodes magiques)
  4. portée des attributs et méthodes (public, protected, private)
  5. attributs et méthodes statiques
  6. attributs et méthodes finaux
  7. type hinting de classe.

Les principales fonctionnalités apportées par PHP 7 (depuis 2015) sont[3] :

  1. typage strict par classe
  2. paramètres typés
  3. retours de méthode typés
  4. opérateur de coalescence null (??)
  5. opérateur vaisseau spatial (<=>)
  6. tableaux de constantes
  7. classes anonymes
  8. groupage des déclarations (use, avec des accolades).

Nouvelles fonctionnalités du PHP 8.0 en 2020[4][5] :

  1. paramètres nommés
  2. types d'union (X|Y)
  3. propriétés promues (déclarations dans le constructeur)
  4. autorisation d'une virgule de fin dans les paramètres
  5. autorisation des méthodes abstraites dans les traits
  6. autorisation des indices négatifs dans les tableaux autoincrémentés
  7. l'instruction match
  8. opérateur null-safe (?->)
  9. fonction str_contains()
  10. fonction str_starts_with() et str_ends_with()
  11. fonction get_debug_type() : il s'agit d'un gettype() plus précis, car il renvoie le nom de la classe au lieu de "object"
  12. exécution juste-à-temps (JIT), pour améliorer les performances : plus de 20 % de requêtes en plus par seconde[6].

Pour PHP 8.1, sorti en novembre 2021 :

  1. types d'intersection (X&Y)
  2. type de retour never
  3. constantes de classe finales
  4. énumérations
  5. fibres (threads virtuels pilotés par la classe Fiber)
  6. fonction array_is_list()
  7. attributs en lecture seule (readonly).

PHP 8.2 est sorti le 8 décembre 2022[7] :

  1. classes en lecture seule : readonly class signifie que tous les arguments de son constructeur son implicitement en readonly.
  2. types null, true et false.
  3. constantes dans les traits.
  4. Forme normale disjonctive de type (combinaison de l'union et de l'intersection).

PHP 8.3 sort le 23 novembre 2023[8] :

  • classes anonymes en lecture seule.
  • constantes typées.
  • fonction "json_validate".

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.

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);


Installer PHP

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.

Logiciel tout-en-un pour Linux (Apache + MySQL + PHP), comme WAMP pour Windows.

commande nécessitant les privilèges root
# 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]
commande nécessitant les privilèges root
# apt-get install apache2

Le service peut ne pas être lancé par défaut, mais même s'il l'est on peut quand-même essayer de l'activer avec :

commande nécessitant les privilèges root
# /etc/init.d/apache2 start

On peut ensuite tester le serveur, pour voir si une page s'affiche ou s'il refuse la connexion :

commande

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

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 :

commande

Pour débugger :

commande
$ tail /var/log/apache2/error.log


Logo

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


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 est disponible sur http://dev.mysql.com/downloads/gui-tools/5.0.html au format :

  1. .msi (Windows)
  2. .dmg (Mac)
  3. .rpm (Linux)
  4. .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 &
$ sudo apt-get install mysql-server mysql_secure_installation

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.


 MySQL a ses propres utilisateurs, avec leurs propres privilèges. Le root MySQL n'est donc pas le root système. Il est conseillé de ne pas mettre les mêmes mots de passes pour les utilisateurs MySQL et les utilisateur du système.

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.

emerge mysql

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/.

 Les extensions PHP nécessitent une autre commande. Ex :
phpenmod mbstring

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 !


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 :

  1. 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.
  2. 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.
  3. 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.
  4. The Uniform Servertéléchargement : en anglais seulement avec Apache2, Perl5, PHP5, MySQL5, phpMyAdmin.

Logo

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.

Logo

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

Logo EasyPHP.

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]

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.

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.

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.

  1. Dans le répertoire de PHP, trouvez la DLL php5ts.dll, et copiez-la dans le répertoire d'Apache.
  2. 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 ?
  3. Dans ce fichier, ajouter cette ligne qui permet à Apache de savoir que l'extension .php concerne l'utilisation du module PHP :
    AddType application/x-httpd-php .php
  4. 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.

Gestionnaire de serveur Windows 2008.

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).

Il existe de nombreuses images de conteneur Docker pouvant fournir PHP. Exemple de Dockerfile[6] :

  • FROM php:8.4-cli
  • FROM php:8.4-fpm

Exemple avec GD[7] :

FROM php:7.4-fpm

RUN apt-get update && apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd

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 dispose de plugins pour programmer en PHP[8].


Composer

Composer est un logiciel de gestion des bibliothèques PHP open source. Celles-ci sont aussi accessibles sur https://packagist.org/.

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

Le programme Composer lit et modifie la liste des bibliothèques du projet dans le fichier composer.json.

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

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.

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

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

Logo

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

Logo

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.

Affiche toutes les bibliothèques installées sur une ligne chacune, ce qui est plus lisible que composer.lock.

Affiche les bibliothèques qui dépendent du package donné en paramètre.

 composer clear-cache

Syntaxe du composer.json

[modifier | modifier le wikicode]

La syntaxe JSON de ce fichier contient quelques extensions[2] :

Le nom de l'application, telle qu'elle sera appelée par composer.

Logo

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 "_".

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

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.

Ensemble de paramètres de composer[4], par exemple le timeout ou les identifiants vers certains dépôts.

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 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

Complément de "scripts".

Liste automatiquement les dépendances en conflit, c'est-à-dire qui ne peuvent pas être installées avec l'application courante dans une tierce.

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.


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...).

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.

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.

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.

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 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".

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.

Logo

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]
Raccourcis clavier indispensables
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

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
  • Des frameworks JS :
    • Node.js
    • Angular
    • Next.js
    • Vue.js
  • Docker
  • 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.



NetBeans

Logo de Apache 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/.

  • 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".
NetBeans

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.

Certains plugins peuvent se révéler utiles :


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.

Eclipse

É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 :

  • ↵ Enter, n'ajoute rien derrière la suggestion ;
  • Espace ou ., ajoute également le caractère produit derrière la suggestion.

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.
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.



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 :

  1. 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.
  2. 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>

Logo

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.

 Dans le cas des fichiers .php qui commencent par <?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');


Commentaires

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 :

  1. La méthode avec les symboles // pour ajouter un commentaire sur une ligne.
  2. La méthode avec le sigle # pour ajouter un commentaire sur une ligne également.
  3. 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 ! */

Logo

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.

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;
    }
}

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().


Premier programme

É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 :

<!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.

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, $_POST et $_COOKIE.
  • Les paramètres (après le nom du script) sont récupérables par la variable globale $argv, et leur nombre par $argc (ou $_SERVER['argc'] si "register_globals = off;" dans php.ini).
 global $argv;
 global $argc;


Variables

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
  <?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" !';

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.

Logo

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].

 Le délimiteur heredoc ou nowdoc permet de forcer une coloration syntaxique dans certains IDE comme PhpStorm ou vscode[3]. Par exemple, echo <<<HTML a un rendu différent de echo <<<SQL.
 Par défaut, on peut accéder aux propriétés et méthodes d'objets en heredoc. Ex :
$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]

Logo

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();

Logo

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']).

[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.

Logo

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'adresse http://www.monsite.com/test.php/foo.bar sera /test.php/foo.bar. La constante __FILE__ contient le chemin complet ainsi que le nom du fichier courant.
'argv'
Tableau des arguments passées au script. Lorsque le script est appelé en ligne de commande, cela donne accès aux arguments, comme en langage C. Lorsque le script est appelé avec la méthode GET, ce tableau contiendra la chaîne de requête.
'argc'
Contient le nombre de paramètres de la ligne de commande passés au script (si le script fonctionne en ligne de commande).
'GATEWAY_INTERFACE'
Numéro de révision de l'interface CGI du serveur : i.e. ' CGI/1.1 '.
'SERVER_NAME'
Le nom du serveur hôte qui exécute le script suivant. Si le script est exécuté sur un hôte virtuel, ce sera la valeur définie pour cet hôte virtuel.
'SERVER_SOFTWARE'
Chaîne d'identification du serveur, qui est donnée dans les en-têtes lors de la réponse aux requêtes.
'SERVER_PROTOCOL'
Nom et révision du protocole de communication : i.e. ' HTTP/1.0 ';
'REQUEST_METHOD'
Méthode de requête utilisée pour accéder à la page; i.e. ' GET ', ' HEAD ', ' POST ', ' PUT '.
'REQUEST_TIME'
Le temps Unix depuis le début de la requête. Disponible depuis PHP 5.1.0.
'QUERY_STRING'
La chaîne de requête, si elle existe, qui est utilisée pour accéder à la page.
'DOCUMENT_ROOT'
La racine sous laquelle le script courant est exécuté, comme défini dans la configuration du serveur.
'HTTP_ACCEPT'
Contenu de l'en-tête Accept: de la requête courante, s'il y en a une.
'HTTP_ACCEPT_CHARSET'
Contenu de l'en-tête Accept-Charset: de la requête courante, si elle existe. Par exemple : ' iso-8859-1,*,utf-8 '.
'HTTP_ACCEPT_ENCODING'
Contenu de l'en-tête Accept-Encoding: de la requête courante, si elle existe. Par exemple : ' gzip '.
'HTTP_ACCEPT_LANGUAGE'
Contenu de l'en-tête Accept-Language: de la requête courante, si elle existe. Par exemple : ' fr '.
'HTTP_CONNECTION'
Contenu de l'en-tête Connection: de la requête courante, si elle existe. Par exemple : ' Keep-Alive '.
'HTTP_HOST'
Contenu de l'en-tête Host: de la requête courante, si elle existe.
'HTTP_REFERER'
L'adresse de la page (si elle existe) qui a conduit le client à la page courante. Cette valeur est affectée par le client, et tous les clients ne le font pas. Certains navigateur permettent même de modifier la valeur de HTTP_REFERER sous forme de fonctionnalité. En bref, ce n'est pas une valeur de confiance.
'HTTP_USER_AGENT'
Contenu de l'en-tête User_Agent: de la requête courante, si elle existe. C'est une chaîne qui décrit le client HTML utilisé pour voir la page courante. Par exemple : Mozilla/4.5 [en] (X11; U; Linux 2.2.9 i586). Entre autres choses, vous pouvez utiliser cette valeur avec get_browser pour optimiser votre page en fonction des capacités du client.
'HTTP_header'
Valeur de l'en-tête HTTP correspondant envoyé dans la requête du client.
'HTTPS'
Définissez à une valeur non-vide si le script nécessite d'utiliser le protocole HTTPS.
'REMOTE_ADDR'
L'adresse IP du client qui demande la page courante.
'REMOTE_HOST'
Le nom de l'hôte qui lit le script courant. La résolution DNS inverse est basée sur la valeur de REMOTE_ADDR
'REMOTE_PORT'
Le port utilisé par la machine cliente pour communiquer avec le serveur web.
'SCRIPT_FILENAME'
Le chemin absolu vers le fichier contenant le script en cours d'exécution.
'SERVER_ADMIN'
La valeur donnée à la directive SERVER_ADMIN (pour Apache), dans le fichier de configuration. Si le script est exécuté par un hôte virtuel, ce sera la valeur définie par l'hôte virtuel.
'SERVER_PORT'
Le port de la machine serveur utilisé pour les communications. Par défaut, c'est '80'. En utilisant SSL, par exemple, il sera remplacé par le numéro de port HTTP sécurisé.
'SERVER_SIGNATURE'
Chaîne contenant le numéro de version du serveur et le nom d'hôte virtuel, qui sont ajoutés aux pages générées par le serveur, si cette option est activée.
'PATH_TRANSLATED'
Chemin dans le système de fichier (pas le document-root) jusqu'au script courant, une fois que le serveur a fait une traduction chemin virtuel -> réel.
'SCRIPT_NAME'
Contient le nom du script courant. Cela sert lorsque les pages doivent s'appeler elles-mêmes. La constante __FILE__ contient le chemin complet ainsi que le nom du fichier (i.e. inclut) courant.
'REQUEST_URI'
L'URI qui a été fourni pour accéder à cette page. Par exemple : ' /index.html '.
'PHP_AUTH_DIGEST'
Lorsque vous utilisez PHP avec Apache en tant que module faisant une identification HTTP Digest cette variable est définie dans l'en-tête 'Authorization' envoyé par le client (que vous devez donc utiliser pour réaliser la validation appropriée).
'PHP_AUTH_USER'
Lorsque vous utilisez PHP avec Apache ou IIS (ISAPI en PHP 5) en tant que module faisant une identification HTTP, cette variable est définie à l'utilisateur fourni par l'utilisateur.
'PHP_AUTH_PW'
Lorsque vous utilisez PHP avec Apache ou IIS (ISAPI en PHP 5) en tant que module faisant une identification HTTP, cette variable est définie au mot de passe fourni par l'utilisateur.
'AUTH_TYPE'
Lorsque vous utilisez PHP avec Apache en tant que module faisant une identification HTTP, cette variable est définie au type d'identification.

Détection du protocole

[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 MaClasse est vrai si l'objet est de type "MaClasse".

En PHP, il n'y a pas de déclaration de variable selon un type figé. Pour le forcer, il faut donc utiliser les fonctions suivantes, ou un cast (vers un type entre parenthèses).

Soit une variable $v, et $s un séparateur :

  • intval($v) ou (int) $v convertit en entier.
  • floatval($v) ou (float) $v convertit en flottant.
  • chr($v) convertit en caractère.
  • strval($v) ou (string) $v convertit un scalaire en chaine de caractères.
  • implode($s, $v) ou join($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

Logo

(bool) "false" renvoie true car toute string non vide est vraie.

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.

  1. http://fr2.php.net/manual/fr/language.types.string.php#language.types.string.syntax.heredoc
  2. https://www.php.net/manual/fr/language.types.string.php#language.types.string.syntax.heredoc
  3. https://github.com/microsoft/vscode/issues/137539
  4. https://www.php.net/manual/en/language.types.float.php
  5. http://php.net/manual/fr/language.variables.variable.php
  6. http://www.php.net/manual/fr/function.setcookie.php
  7. 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'];

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 !


[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...).


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.

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

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]).

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] :

  1. __DIR__ : dossier courant.
  2. __NAMESPACE__ : namespace courant.
  3. __FILE__ : chemin complet du fichier qui est actuellement exécuté par le serveur (exemple : /la/ou/est/le/fichier.php).
  4. __CLASS__ : nom de la classe dans laquelle on se trouve.
  5. __TRAIT__ : trait courant.
  6. __FUNCTION__ : nom de la fonction dans laquelle on se trouve.
  7. __METHOD__ : nom de la méthode dans laquelle on se trouve.
  8. __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.


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

Logo

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
}
 Le reste de la division peut être obtenu avec la fonction 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 false en true et true en false.
  • && : 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 -> 1 et 1 -> 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.

Logo

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]

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
 Il est généralement plus lisible de placer un retour chariot avant les ternaires. Ex :
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
print
and
xor
or


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.

Début d’un principe
Fin du principe


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.

Logo

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].

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).

Logo

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).

Logo

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.

Début d’un principe
Fin du principe


Début d’un principe
Fin du principe


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__;
}

Logo

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();
 Par convention, l'arborescence du système de fichiers correspond à celle des espaces de noms. Dans l'exemple ci-dessus, il faudrait donc placer le fichier dans Projet1\SousProjet2\TestNS.php et faire plutôt 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() (alias echo) : 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

Logo

  • 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, utiliser mb_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 que str_replace[9].
  • strpos($meubleDeFoin, $aiguille)[10] : première position d'une sous-chaine. Attention : ne jamais utiliser comme si elle renvoyait un booléen (if (strpos())) 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 avec 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. Utiliser mb_substr en Unicode.
     Pour accéder à un seul caractère d'une chaine, PHP peut la considérer comme un tableau (ex : $chaine[0]).
    Ex :
echo substr('Hello World', 0, 2); // "He" : les deux premiers caractères
echo substr('Hello World', -2);   // "ld" : les deux derniers caractères
echo substr('Hello World', 0, -1);   // "Hello Worl" : tout sauf le dernier caractère
  • substr_count($chaine, $sous-chaine) : compte le nombre de sous-chaine dans une chaine.
  • str_pad() : complète une chaine avec un caractère pour qu'elle atteigne la taille demandée si ce n'était pas le cas. Ex :
echo str_pad(10, 2, '0');               // 10
echo str_pad(9, 2, '0');                // 90
echo str_pad(9, 2, '0', STR_PAD_LEFT);  // 09
  • eval($chaine) : exécute une chaine comme un script PHP.
  • count_chars($chaine) : renvoie un tableau avec en clés chaque caractère de la chaine, et en valeur son nombre d'occurrences. Pratique pour détecter les anagrammes.
  • strcspn($chaine, $caracteres) : (complementary span) renvoie la taille de la sous-chaine située avant les caractères mentionnés[11].
Pour plus de détails voir : Coder avec Unicode/Conversion#PHP.
  • utf8_encode($chaine) et utf8_decode($chaine) sont dépréciée en PHP 8.2, il faut utiliser à la place mb_convert_encoding($chaine, 'UTF-8') et mb_convert_encoding($chaine, 'ISO-8859-1').
  • 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.

Logo

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.


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);

Logo

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 :

Début d’un principe
Fin du principe


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.
Début d’un principe
Fin du principe


  • 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.


    $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.

  • 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).

Logo

php -r "var_dump(in_array(0, ['test']));" = true

Pour comparer deux tableaux :

    $a1 == $a2;  // compare le contenu et la taille
    $a1 === $a2; // compare le contenu, la taille et l'index

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 remplacer strlen().
  • 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>
   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


Logo

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));


  • 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].
 Les tris croissants définis par défaut sont modifiables par des flags en deuxième paramètre.
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" }


Logo

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.

Début d’un principe
Fin du principe


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.

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 de compact()).


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é.

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 else qui 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 :

Emploi de elseif
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;
}

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.

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:.

Intérêt et emploi de switch
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.

Logo

Le switch compare avec "==" et pas "===".

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; ?>



Boucles

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) :

  1. while() : le programme se répète tant qu'une condition est vraie (ex : tant que x est inférieur à 10).
  2. 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 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);

Idem avec premier passage obligatoire : la condition est vérifié en fin de bloc.

    do {
        instructions(s);
    } while (condition);

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 TRUE l'instruction sera exécutée, si FALSE est renvoyé on sort de la boucle.
  • La dernière expression est exécutée après chaque itération.

Attention la structure dans la parenthèse est for( ; ; )

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. foreach simplifie une tache qui aurait certes été possible avec for, 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

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.
echo date('Y-m-d'); // affiche 2016-07-10

echo date('Y-m-d H:i:s'); // affiche 2016-07-10 20:06:34

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

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

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);.

La méthode DateTime::add() (et son alias date_add()), permet d'ajouter deux dates[6].

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].

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.

Logo

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).

Logo

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.

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].

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().

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');


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.

[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.

Début d’un principe
Fin du principe


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. */


Logo

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.

Voici le code du formulaire en HTML, il va afficher une boîte de texte et un bouton "Connexion".

Début d’un principe
Fin du principe


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.

Début d’un principe
Fin du principe


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.

Début d’un principe
Fin du principe


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 :

Début d’un principe
Fin du principe


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.

Début d’un principe
Fin du principe


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.

Début d’un principe
Fin du principe


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é.

Logo

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

Base de données des cookies Firefox, lue avec SQLite.
Cookie ajouté à Firefox après commande PHP 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] :

  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/).
  2. Chrome C:\Users\%USERNAME%\AppData\Local\Google\Chrome\User Data\Safe Browsing Cookies.
  3. Internet Explorer : C:\Users\%USERNAME%\AppData\Roaming\Microsoft\Windows\Cookies.

Logo

  • 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).
<?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, '/');
<?php
  if (isset($_COOKIE["cookie1"])) {
    echo 'Authentifié';
  } else {
    echo 'Non authentifié';
  }


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

Dans Docker :

    RUN docker-php-ext-install opcache
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
 Sur PHP < 8.0, ajouter après la première ligne : && 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


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].


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

Sur Docker PHP :

 RUN pecl install memcached \
    && docker-php-ext-enable memcache
 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
  • Reset mémoire :
 echo "flush_all" | nc -q 1 localhost 11211

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]

Pour vérifier l'installation de la bibliothèque PHP pour Memcached :

php -i |grep memcached
  • S'il est absent :
sudo pecl install memcached
$memcached = new \Memcached();
$memcached->addServer('127.0.0.1', 11211);
$memcached->set('nom du test', 'valeur du test');
echo $memcached->get('nom du test');


Redis

Redis est comme Memcached, un système de gestion de base de données clef-valeur scalable, très hautes performances. En 2019, il devient plus utilisé que Memcached car il possède plus de fonctionnalités[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].

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
  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.

Pour se loguer au serveur Redis :

telnet nom_du_serveur 6379

Les commandes Redis les plus utiles[3] :

  • 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 nil si elle n'existe pas).
  • MGET : affiche les valeurs des clés en paramètre.
  • QUIT : quitter.
  • 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

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');

Cette bibliothèque permet d'utiliser Redis en clustering, avec des masters et slaves[8].

Dans le framework PHP Symfony.

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.

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

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)%'


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.

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 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.

Début d’un principe
Fin du principe


En gros, ce formulaire enverra sur la page traitement.php la valeur de l'entrée "mdp".

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é.

Début d’un principe
Fin du principe


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.

Début d’un principe
Fin du principe


Types de champ

[modifier | modifier le wikicode]

Pour des <input type="checkbox">, on vérifie si leurs valeurs sont 'on' ou 'off'.

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].


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.

Voici les fonctions usuelles de navigation et manipulation du système de fichier.

Logo

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é").

  • 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.

Logo

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);
  • 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.

Logo

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].

 Pour supprimer un dossier non vide[5] :
array_map('unlink', glob('your/path/*.*'));
rmdir('your/path');

Droits des fichiers

[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.

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."'");

Logo

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]

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

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à :

Début d’un principe
Fin du principe


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.

Début d’un principe
Fin du principe


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]

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]

Pour stocker le fichier entier dans un tableau, on peut utiliser file() qui renvoie un tableau séparant chaque ligne du fichier :

Début d’un principe
Fin du principe

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.

Pour afficher tout le fichier dans la sortie standard[10].

Ligne par ligne

[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 :

Début d’un principe
Fin du principe


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.";
}


La fonction équivalente pour lire caractère par caractère se nomme fgetc :

Début d’un principe
Fin du principe


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 ».

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.

Une fois le fichier ouvert, l'écriture se fait via la fonction fwrite.

Début d’un principe
Fin du principe


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].

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 :

Début d’un principe
Fin du principe

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 :

Début d’un principe
Fin du principe


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]

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].

 fonctionne aussi bien avec HTTPS.

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].

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);

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.

  1. http://php.net/manual/fr/function.file-exists.php
  2. https://www.php.net/manual/en/function.filesize.php
  3. http://php.net/manual/fr/function.glob.php
  4. http://php.net/manual/fr/dir.constants.php
  5. https://stackoverflow.com/questions/1653771/how-do-i-remove-a-directory-that-is-not-empty
  6. http://php.net/manual/fr/ziparchive.addfile.php
  7. https://stackoverflow.com/questions/11265914/how-can-i-extract-or-uncompress-gzip-file-using-php
  8. http://php.net//manual/fr/function.imagecopyresampled.php
  9. https://www.startutorial.com/articles/view/php-generator-reading-file-content
  10. https://www.php.net/manual/en/function.readfile.php
  11. http://php.net/manual/fr/function.fgetcsv.php
  12. http://php.net/manual/fr/function.file-put-contents.php
  13. https://www.php.net/manual/en/function.is-uploaded-file.php
  14. developpez.net
  15. http://php.net/manual/fr/function.get-headers.php
  16. http://php.net/manual/fr/book.ftp.php
  17. http://php.net/manual/fr/function.ftp-nlist.php
  18. http://php.net/manual/fr/function.ftp-set-option.php
  19. http://php.net/manual/fr/function.ssh2-sftp.php
  20. http://php.net/manual/fr/wrappers.php.php



Objets COM

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');

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.


Images

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 :

  1. Chargement en mémoire d'une image, nouvelle ou existante.
  2. Chargement éventuel des couleurs à y apporter.
  3. Modifications éventuelles de ses composants (création de lignes, points, remplissages, ajout de textes...).
  4. Restitution de l'image après avoir posté son type dans l'en-tête.
  5. 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)

$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)
 la première couleur allouée définit le fond de toute l'image.

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_image est l'image source ;
  • dst_image est l'image de destination ;
  • dst_x, dst_y sont les coordonnées de dst_image ;
  • src_x, src_y sont les coordonnées de src_image en partant d'en haut à gauche ;
  • dst_w, dst_h, src_w, src_h sont 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.

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);


Mails

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 :



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.

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]

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.



Programmation orientée objet

Une classe est un format de variable non scalaire, comprenant trois types de composants :

  1. des constantes, accessibles par réflexion avec $maClasse::getConstants().
  2. des variables appelées "propriétés", accessibles avec $maClasse::getProperties().
  3. 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].

Logo

Le mot-réservé static a donc deux sens : un pour les déclarations et un pour les appels.

À 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;

Logo

En PHP, l'inclusion doit précéder les appels du code qui y figure.

 Les classes et fonctions globales peuvent être appelées directement dans le code, ou avec le préfixe "\" (signifiant "namespace global"). Mais il existe aussi use function ma_fonction pour déclarer l'utilisation d'une fonction.
 Avant PHP7.4, on pouvait mettre une URL dans ces fonctions, si la configuration allow_url_include=1[3]. Ex : require_once("http://example.com/");

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->variable pour cibler l'objet instancié (et $this::constante, $this->méthode()).
  • self::variable pour 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();

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]).

Logo

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();

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;

Logo

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.

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]

Logo

Les méthodes abstraites sont obligatoirement à implémenter par les classes filles.

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.

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;
    }
}

Logo

  • 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 extends avant le implements.

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...).

Logo

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 :

  1. __call() : à chaque appel d'une méthode de la classe.
  2. __callStatic() : à chaque appel statique d'une méthode de la classe.
  3. __clone() : lors du clonage de l'objet (via la fonction "clone").
  4. __construct() : à l'instanciation de la classe.
  5. __debugInfo() : modifie les résultats des var_dump().
  6. __destruct() : à la suppression de l'objet instancié.
  7. __get() : à la lecture de propriétés inexistantes ou interdites.
  8. __invoke() : à l'appel de l'objet comme une fonction (ex : echo $object(1)).
  9. __isset() : à l'appel de isset() (ou empty()) sur des propriétés inexistantes ou interdites.
  10. __serialize() : à l'appel de serialize().
  11. __set() : à l'écriture de propriétés inexistantes ou interdites.
  12. __set_state() : modifie les résultats des var_export().
  13. __sleep() : à l'appel de serialize(), pour en modifier le résultat.
  14. __toString() : à l'appel de l'objet comme une chaine de caractères (ex : echo $object).
  15. __unserialize() : à l'appel de serialize().
  16. __unset() : à l'appel de unset() sur des propriétés inexistantes ou interdites.
  17. __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].

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);
    }

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.

Logo

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;
    }
}

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.

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.

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.



Bases de données

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.



Microsoft SQL Server

On distingue plusieurs pilotes PHP pour MS-SQL Server :

  • mssql (désuet en PHP7).
  • sqlsrv
  • PDO

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.dll dans les extensions.
  • Pour PHP 5.4 :
    1. Télécharger les .dll SQL30
    2. Les copier dans C:\PROGRA~2\EasyPHP\binaries\php\php_runningversion\ext
    3. Les ajouter dans C:\PROGRA~2\EasyPHP\binaries\php\php_runningversion\php.ini via les lignes suivantes[2] :
      extension=php_sqlsrv_54_ts.dll
      extension=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).

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).


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].

Début d’un principe
Fin du principe


Alternative désuète

[modifier | modifier le wikicode]

Les fonctions suivantes sont supprimées depuis PHP 7.0[6] :

Début d’un principe
Fin du principe


Alternative Doctrine

[modifier | modifier le wikicode]

Les bibliothèques Doctrine utilisent cette syntaxe[8] :

Début d’un principe
Fin du principe


    // ... 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

Logo

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).



MySQL

PHP offre plusieurs fonctions d’interaction avec MySQL.

mysql_connect()

[modifier | modifier le wikicode]

Logo

La fonction mysql_connect() est obsolète en PHP7, et remplacée par la classe mysqli.

Exemple d'accès à une base de données MySQL
    $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

Exploiter une requête de type SELECT (avec 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_assoc mais retourne un tableau simple indice->valeur.
  • mysql_fetch_object() : identique à mysql_fetch_assoc mais retourne un objet.
  • mysql_fetch_array() : retourne les tableaux mysql_fetch_assoc() et mysql_fetch_row().

Exemple :

Exploiter une requête de type SELECT (avec 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;
    }

Logo

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()

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).



PDO

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] :

  1. 4D
  2. CUBRID
  3. Firebird
  4. IBM DB2
  5. Informix
  6. Microsoft SQL Server
  7. MySQL
  8. Oracle Database
  9. PostgreSQL
  10. SQLite

Et aussi via ODBC.

 Pour PHP 5.0, l'extension est disponible en tant qu'extension PECL, et doit être activée[2] en ajoutant ou décommentant la ligne de "php_pdo.dll" dans php.ini.

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].

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 PDO correspond à une connexion à la base de données.
  • La classe PDOStatement représente d'une part une requête préparée et d'autre part le jeu de résultats de la requête une fois qu'elle est exécutée.

Cette classe offre des méthodes de parcours, de comptage, d'informations.

  • La classe PDOException repré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');

Logo

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_ASSOC retourne 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_NUM retourne 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_BOTH retourne un tableau indexé et un tableau associatif.
  • PDO::FETCH_OBJ retourne le jeu de résultats sous forme d'un objet dont les noms de propriétés correspondent aux noms des colonnes.

Faire une requête préparée avec PDO

[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]


SQLite

SQLite est le moteur de base de données intégré à PHP5.

    // 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');
}

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/.

Expressions rationnelles courantes
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.
[...[...]] union Union des deux ensembles
[...&&[...]] intersection Intersection des deux ensembles
^ 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.
\k<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é.

Quantificateurs
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 :
  • a{2} deux occurrences de "a",
  • a{1,10} (sans espace) entre une et dix,
  • a{,10} jusqu'à 10 fois (de 0 à 10),
  • a{3,} au moins 3 fois (de 3 à l'infini).

Par défaut les quantificateurs ne recherchent pas forcément la plus longue séquence de répétition possible. Il est possible de les suffixer avec un caractère pour modifier leur comportement.

Modificateurs de quantificateurs
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].
Classes de caractères POSIX[3]
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
Expressions rationnelles Unicode[4]
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
\uxxxx Caractère Unicode (4 chiffres hexadécimaux)
\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.
  • ?> : 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
  • ?<! : 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 :
    Chercher une lettre q non suivie d'une lettre u : q(?!u)
    ((?!sous-chaine_exclue).)
    <(?!body).*> : pour avoir toutes les balises HTML sauf "body".
    début((?!mot_exclu).)*fin[5] : pour rechercher tout ce qui ne contient pas un mot entre deux autres.
    (?!000|666) : pour exclure 000 et 666[6].

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.).

Logo

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)

La fonction ereg() qui permettait de rechercher en regex a été remplacée par preg_match() depuis PHP 5.3.

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*'

Logo

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).

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);


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;

Logo

Seul le dernier groupe de capture sera pris en compte.

Idem que preg_replace() mais son résultat ne contient que ce qui est effectivement remplacé.

Décompose une chaine de caractères.


Concevoir du code de haute qualité

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.

Logo

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.

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
 Généralement la production n'affiche pas les 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) Article sur Wikipédia).

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 :

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.

Données XHProf lues par Grafana

XHProf est une extension PHP dédiée au profilage du code[6], développée par Facebook et open source depuis mars 2009.

pecl install -f xhprof

Dans php.ini :

extension=xhprof.so
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

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.

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;
}

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).



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)
);


PHP_CodeSniffer liste ou corrige les violations des normes de codage[1][2].

composer require squizlabs/php_codesniffer --dev
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.

  • 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

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>

Ajoute declare(strict_types=1); dans tous les fichiers, et formate code code et le style.

composer require nunomaduro/phpinsights --dev

Pour la vérification :

vendor/bin/phpinsights analyse src -n

Pour réparer automatiquement, ajouter "--fix".

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,
];

PHP Mess Detector recense les mauvaises pratiques du type "code mort", paramètre inutilisé, mauvais nommage camelCase, présence d'un exit, etc.[3].

composer require  phpmd/phpmd --dev
vendor/bin/phpmd src ansi rulesets.xml

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)

Outil d'analyse fournissant un rapport graphique.

composer require phpmetrics/phpmetrics --dev
vendor/bin/phpmetrics --report-html=reports/phpmetrics-$$(date '+%Y%m%d_%H%M%S').html src

Éviter de versionner les rapports avec l'application. Par exemple ajouter "reports" au .gitignore.

PHP Static Analysis détecte des erreurs potentielles à l'exécution (ex : mauvais type) sans réellement exécuter le code[4].

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
vendor/bin/phpstan analyse --no-progress --memory-limit=256M src

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 cherche à détecter des bugs, par exemple des erreurs de type[6].

composer require --dev vimeo/psalm
vendor/bin/psalm --init
vendor/bin/psalm

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].

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.
 Afin de ne pas versionner le cache du scan, il convient d'ajouter ceci au .gitignore du projet :
/.scannerwork/

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


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() {} }

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.

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].

    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);
 Le bloc finally ajouté après les catch sera exécuté après les instructions du try et des catch.

Logo

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.

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.

Logo

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);

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);

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

Logo

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
    ]);

 C'est une raison pour ne pas inclure de variable dans le message et de ne les mettre que dans le tableau, en plus de pouvoir retrouver tous les logs similaires plus facilement.


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 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
 Le client_port par défaut est passé de 9000 à 9003 entre Xdebug 2 et 3[5].

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

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

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 :

  1. la liste des variables du script et leurs valeurs (modifiables à la volée)
  2. les warnings PHP
  3. 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.

Ajouter le mode au précédent[11] : xdebug.mode=debug,profile

Un ensemble de vidéos explicatives existent en anglais[12].


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.

Liste des mots spécifiques en PHP
Uniquement sous PHP 5
and or xor __FILE__ exception
__LINE__ array as break final
case class const continue php_user_filter
declare default die do public
echo else elseif empty private
enddeclare endfor endforeach endif catch
endswitch endwhile eval exit try
extends for foreach function clone
global if include include_once implements
isset list new print interface
require require_once return static throw
switch unset use var protected
while __FUNCTION__ __CLASS__ __METHOD__ abstract
extends cfunction* old_function* yield[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 :

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;

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) Article sur Wikipédia.



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>
  • 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>

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
  • 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é page change : il vaut 1, 2, 3 ou 4 selon le lien cliqué.
  • Dans la partie « page » de index.php, la valeur du paramètre page est 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]

Le formulaire initial


Exemples/BD 1

Afficher le résultat d'une requête

[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 :


IDNOMPRENOMSALAIRE
1DupondMarcel8000
2MartinXavier4000
3GogolHenri3000
4HugoVictor2000
5GaliDaniel6000
6MartinGeorges9000

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 :

  1. récupérer les informations suivantes :
    1. le nom de la machine qui héberge le serveur de la base de données (ici localhost)
    2. le nom de la base de données (ici toto)
    3. le nom de l'utilisateur de la base de données (ici root)
    4. le mot de passe de cet utilisateur (ici le mot de passe est vide).
  2. se connecter au serveur de base de données
  3. sélectionner la base de données (ici toto) sur ce serveur.
  4. créer la requête dans une chaîne de caractères
  5. envoyer la requête à la base de données et récupérer le résultat
  6. fermer la connexion avec le serveur de base de données
  7. 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.

Voici un forum modulaire, universel, illimité, fonctionnel et très réactif mais à sécuriser avec MySQL en backend et HTML en frontend.

À 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/

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.

 Fichier : config.inc.php
    <?php
    /*  */

    define('OBJ', './Objects/');
    define('LIB', './Libraries/');

    $cnx = array('host'=>'localhost','user'=>'root','db'=>'miniForum','pass'=>'');

    ?>

increment 0 : index.php

 Fichier : 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

 Fichier : 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

 Fichier : 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;

    ?>
 Fichier : frameset.inc.php
    <?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

 Fichier : frameset.inc.php
    <head>
        {$_content['title']}
        {$_content['meta']}
        {$_content['script']}
        {$_content['style']}
    </head>

increment 0 : mainPage.inc.php

 Fichier : 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

 Fichier : 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

 Fichier : 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.
 Fichier : mainPage.inc.php

    ...
    // 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.
 Fichier : mainPage.inc.php
    // 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
 Fichier : mainPage.inc.php
    <?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
 Fichier : pages.inc.php
    <?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
 Fichier : pages.inc.php
    <?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é
 Fichier : 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';
    }

    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 :

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]

 Fichier : Connectivity.cla.php
    <?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");
            }
        }
    }
    ?>


 Fichier : Request.cla.php
    <?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]

 Fichier : utils.fnc.php
    <?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

 Fichier : 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
 Fichier : utils.inc.ohp
    /* get record by id

    */
    function getRecordNfo($cnx, $table, $id)
    {
    return count($res=Request::SQL($cnx,"SELECT * FROM $table WHERE id=$id"))?$res[0]:false;
    }

La modélisation est fonction du métier et des affinités. En voici une fonctionnelle pour notre exemple :

 Fichier : install.sql
    -- 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 :

 Fichier : _install.php
    <?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 ;');
    }

    ?>

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.

À faire...link={{{link}}}


  • une vue
  • un frameset variable
  • des sujets illimités
  • des catégories illimités
  • un système de classement
  • un système d'administration
  • un système d'authentification
  • un espace myAccount


NICE TO HAVE :

  • tout automatiser (censure / délation / ..)
  • mailer
  • BO admin



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].

  • É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 :

  1. si le premier enfant existe,
  2. on test le type de nœud,
  3. 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;
        }//

    }

    ?>

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);

    ?>

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
        )

)

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).

  1. Modélisation (Modèle : Partie métier spécifique à l'application)
  2. Visualisation (Vue : Partie visuelle de l'application)
  3. 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 :

  1. d'un formulaire HTML
  2. d'un module de validation
  3. d'un module de gestion homme-machine
  • Pré-requis conseillés :
  1. html4
  2. 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 :

  1. le frameset de la page
  2. 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 :

  1. check des string alphabétiques
  2. 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 :

  1. validation de saisie
  2. 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 :

  1. charger les containers du plus petit enfant au plus grand parent
  2. 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 :

  1. on rajoute un modele messageList.inc.php
  2. on modifie un peu le contrôleur
  3. 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

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.

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

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].

    <?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>

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;
    }
    ?>
    <?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');

    ?>

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.


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.

  • le champ coderio doit être renseigné avec le code RIO sans blanc.
  • le champ mobile doit être renseigné avec un numéro de téléphone mobile, sans blanc.
$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.

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.

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".



Aller au livre


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

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.

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.

  • Les données : Xmlisation
  • Le code : Php5 Object
  • Dumping & archiving : mysql en soutien pour la recherche indexée et le harvesting
  • 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]

La version suivante a une complétion par points d'ancrages. Cette version va orienter le projet.


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
[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].
  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>
  • 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/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é.

Voici Classes/dataManager.class.php dans sa version finale. Il est orienté données.


Voici le Classes/templateControler.class.php orienté layout.


Classes/utilities.class.php dans sa version finale

Functions/common.inc.php dans sa version finale

Le backend est implémenter suivant quelques règles métier imposées par l'orientation du CMS également.


Objects/Backend/authorize.inc.php

Objects/Backend/responder.inc.php dans sa version finale

Objects/Backend/services.inc.php

Objects/Backend/webservices.inc.php

Deux points d'entrées : Objects/Frontend/admin.inc.php

Objects/Frontend/entry.inc.php dans sa version finale

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".

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

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.

Sur Linux :

 sudo apt-get install php-curl

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

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.

Logo

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 :

  1. on crée le nœud
  2. 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;

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);
    }
}
  1. http://php.net/manual/fr/book.dom.php



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]

apt-get install php5-json

  1. Télécharger le fichier json-1.2.1.tgz sur https://pecl.php.net/package/json.
  2. Décompresser et compiler le code source en json.so.
  3. Le copier dans le dossier des extensions PHP.
  4. Dans le php.ini (ex : C:\Program Files (x86)\EasyPHP\binaries\php\php_runningversion\php.ini), ajouter :

extension=json.so

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"}

Logo

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;
    }

Logo

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.

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.

Logo

Il requiert PHPUnit.phpTélécharger.


La classe Services_JSON de JSON.php peut s'utiliser comme décrit dans Test-JSON.php.



MING

Conceptions d'animations pour pages web

[modifier | modifier le wikicode]
Paquet logiciel

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();

Logo

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]


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


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.

Les fichiers se téléchargent sur http://adodb.sourceforge.net/#download.

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").


Un excellent tutoriel se trouve à l'adresse suivante :

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.

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/.

use FPDF;

class pdfGenerator
{
    public function generate(string $html)
    {
        $pdf = new FPDF();
        $pdf->AddPage();
        $pdf->Cell(10, 10, $html);
        $pdf->Output();
    }
}


PHPExcel

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 :

Terminal

Logo

    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.

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');

Pour ouvrir et lire un fichier existant :

    $objReader = PHPExcel_IOFactory::createReader('Excel2007');
    $objPHPExcel = $objReader->load('./HelloWorld1.xlsx');
    print $objPHPExcel->getActiveSheet()->getCell('A1')->getValue();

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');

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);
À faire...link={{{link}}}

phpoffice/phpexcel is abandoned, you should avoid using it. Use phpoffice/phpspreadsheet


PHPMailer

PHPMailer est une bibliothèque open source[1] pour envoyer des emails plus rapidement qu'à partir de la commande mail().

Télécharger sur GitHub ou bien ajouter à composer.json : "phpmailer/phpmailer": "~5.2".

// Pour la v5.0.0 (2009)
require_once('PHPMailer/class.phpmailer.php');

// Pour la v5.2.14 (2016)
require('PHPMailer/PHPMailerAutoload.php');

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...


PHPWord

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.

Terminal

Logo

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].

    composer require php-amqplib/php-amqplib

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

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
    $connection = new AMQPStreamConnection($host, $port, $login, $password);
    ...
    $connection->close();
 Sur le framework Symfony, on peut utiliser le composant Messenger à la place.

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);
icône image Image externe
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');

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);

Le mode DLX (Dead Letter Exchanges) permet de transférer un message d'une queue dans une autre après un certain temps[7].

    $amqpMessage = new AMQPMessage(json_encode('Hello World!'),
    ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
    );
    $this->rabbitMqConnection->getChannel()->basic_publish($amqpMessage, 'Bus', 'Wikibooks.Queue1');

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.


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.


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].

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.

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]
  1. http://manual.cakephp.org/chapter/installing


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 :

  1. Télécharger les packages un par un sur http://pear.php.net/packages.php.
  2. 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".
  3. La commande pear install Nom_du_package.
  4. La commande php pyrus.phar install pear/Nom_du_package.

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";
    }

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;

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.

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 (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

Logo

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


Symfony

Pour plus de détails voir : Programmation PHP avec Symfony.


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]).

Plusieurs autres projets notables utilisent Symfony, parmi lesquels :

Différences entre les versions

[modifier | modifier le wikicode]

Depuis la version 4, des pages récapitulant les nouvelles fonctionnalités sont mises à disposition :

Créer un projet

[modifier | modifier le wikicode]
Page d'accueil par défaut de Symfony 2.

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 :

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.

Logo

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.

 En YAML, on préfèrera déclarer les services avec des simples quotes car les doubles nécessitent d'échapper les antislashs.

Logo

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 :


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();

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/* }

Logo

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"

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)%'

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.

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')

Chaque service doit donc être déclaré avec un paramètre "class", puis peut ensuite facultativement contenir les paramètres suivants :

Paramètres des services en YAML
Nom Rôle
class Nom de la classe instanciée par le service.
arguments Tableau des arguments du constructeur de la classe, services ou variables.
calls Tableau des méthodes de la classe à lancer après l'instanciation, généralement des setters.
factory Instancie la classe depuis une autre classe donnée. Méthode statique de la classe qui sera renvoyée par le service[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' ] ]


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")
     */

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 (ajouter UrlGeneratorInterface::ABSOLUTE_URL en paramètre 3 pour l'absolue, car il est à UrlGeneratorInterface::ABSOLUTE_PATH par 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.
 On peut changer les options d'encodage en JSON ainsi :
    $response = new JsonResponse();
    $response->setEncodingOptions(JSON_UNESCAPED_UNICODE);
    $response->setData($data);

    return $response;

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);
}

Logo

Avant Symfony 6.2 cela fonctionne avec un composer require sensio/framework-extra-bundle.

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.

Logo

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.

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).*"}
 On peut placer plusieurs attributs ou annotations "route" sur une même méthode. Il est alors possible de changer son comportement selon la route avec if ($request->get('_route') === 'test').

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!".

Logo

  • 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

Logo

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.

 security.yaml utilise les voteurs : des classes qui écoutent des évènements pour vérifier les permissions de l'utilisateur logué[5].

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

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>

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.


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

Logo

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 FrameworkBundle
  • php 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] :

  1. Les arguments : non nommés
  2. 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

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);
    }
 L'option "dry-run" est recommandée pour éviter que la commande modifie des ressources externes, comme une base de données ou une API. Il faut pour cela que la commande en tienne compte au moment d'appeler ces ressources.

Logo

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));
        }


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 :


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].

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

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'

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.

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.

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.

Construit des formulaires HTML.


Pour plus de détails voir : Programmation PHP avec Symfony/Formulaire.

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].

  • 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()],
            ])

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.

 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');
 le composant Symfony Form appelle automatiquement Translations pour ses "labels".

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') }}
  • YAML : la variable est entre accolades (selon la norme de l'ICU[11])
 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.

Permet de lancer des sous-processus en parallèle[14]. Exemple qui lance une commande shell :

    $process = new Process(['ls']);
    $process->run();

Logo

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();

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.

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).

 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.

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.

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;

Patrons de conception "État".

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

Simule un navigateur pour les tests d'intégration.

Permet de manipuler des fichiers de configurations.

Pour la programmation par contrat.

Pour utiliser XPath.

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].

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

Méthodes de lecture et écriture dans les dossiers et fichiers.

Recherche dans les dossiers et fichiers.

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().

Extension de sécurité pour des authentifications complexes.

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

Permet d'utiliser des évènements lors des transformations des requêtes HTTP en réponses.

Deprecated depuis Symfony 5.

Accorde les mots anglais au pluriel à partir de leurs singuliers.

Internationalisation, comme par exemple la classe "Locale" pour gérer une langue.

Connexion aux serveur LDAP.

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;
    }

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.

Pour envoyer des emails.

Manipulation des messages MIME.

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.

Pour lire les métadonnées des attributs de classe.

Chronomètre pour mesurer des temps d'exécution.

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

Extension de construction de templates.


Pour plus de détails voir : Programmation PHP avec Symfony/Templating.

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().

Permet d'instancier une classe sans utiliser son constructeur.

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].

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]

Pour générer des UUID[28].

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]

Utilise Chart.js via Stimulus pour afficher des graphiques, via la fonction Twig render_chart()[33].

Ajoute le framework React.js.


Pour plus de détails voir : Programmation PHP avec Symfony/Stimulus.

Ajoute le framework Vue.js.

Ajoutés en 2023

[modifier | modifier le wikicode]

Webhook et RemoteEvent (>= v6.3)

[modifier | modifier le wikicode]

[34]

AssetMapper (>= v6.3)

[modifier | modifier le wikicode]

[35]

Scheduler (>= v6.3)

[modifier | modifier le wikicode]

[36]

Composants non listés comme tels

[modifier | modifier le wikicode]

Pour faire tourner le site sans passer par le serveur symfony server:start.

  1. https://symfony.com/doc/current/service_container/factories.html
  2. https://symfony.com/components
  3. https://symfony.com/doc/current/reference/configuration/framework.html
  4. https://symfony.com/doc/current/components/console.html
  5. https://symfony.com/doc/current/components/dotenv.html
  6. https://symfony.com/doc/current/configuration/env_var_processors.html
  7. https://symfony.com/doc/current/components/yaml.html
  8. https://symfony.com/doc/current/components/serializer.html
  9. https://symfony.com/doc/current/validation/sequence_provider.html
  10. https://symfony.com/doc/current/translation.html
  11. https://symfony.com/doc/current/reference/formats/message_format.html
  12. http://www.jpsymfony.com/design_patterns/le-design-pattern-observer-avec-symfony2
  13. https://github.com/certificationy/symfony-pack/blob/babd3fee68a7e793767f67c6df140630f52e7f8d/data/architecture.yml#L13
  14. https://symfony.com/doc/current/components/process.html
  15. https://gist.github.com/appaydin/42eaf953172fc7ea6a8b193694645324
  16. https://symfony.com/doc/current/components/asset.html
  17. https://symfonycasts.com/screencast/stimulus/encore
  18. https://symfony.com/doc/current/frontend/encore/simple-example.html
  19. https://vria.eu/delve_into_the_heart_of_the_symfony_messenger/
  20. https://symfony.com/doc/current/workflow.html
  21. https://symfony.com/doc/current/components/dependency_injection/compilation.html
  22. https://symfony.com/doc/current/components/expression_language.html
  23. https://symfony.com/doc/current/components/lock.html
  24. https://symfony.com/bundles/SymfonyMakerBundle/current/index.html
  25. https://symfony.com/components/Locale
  26. https://symfony.com/components/Icu
  27. https://symfony.com/components/ClassLoader
  28. https://symfony.com/doc/current/components/uid.html
  29. https://symfony.com/doc/current/rate_limiter.html
  30. https://symfony.com/doc/current/components/semaphore.html
  31. https://symfony.com/blog/new-in-symfony-5-3-passwordhasher-component
  32. https://symfony.com/blog/new-in-symfony-5-3-runtime-component
  33. https://symfony.com/bundles/ux-chartjs/current/index.html
  34. https://symfony.com/blog/new-in-symfony-6-3-webhook-and-remoteevent-components
  35. https://symfony.com/blog/new-in-symfony-6-3-assetmapper-component
  36. https://symfony.com/blog/new-in-symfony-6-3-scheduler-component


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] :

Terminal
Logo
composer require symfony/http-client


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.

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'));
    }

Logo

  • 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]]);

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);

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 une Symfony\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.

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).

Logo

Pour un test de bundle, il faut créer une classe Kernel qui charge les routes en plus[4].


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.


Terminal
Logo
composer require symfony/event-dispatcher


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
    ------- ----------------------------------------------------------------------------- ----------


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));
 Le setter permet de transférer un résultat issu des observateurs de l’évènement, à l'endroit qui les dispatch (qu'il peut récupérer via le getter de l'évènement). Une alternative serait d'injecter à la place de EventDispatcher, un tableau des services des modules concernés, définis par un tag.
 Certains évènements de Symfony possèdent une méthode "setResponse" pour renvoyer directement quelque chose à l'utilisateur.

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!";
    }
    }

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');
    }
    }
    }

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');


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.

Terminal

Logo composer require symfony/form


Les formulaires présents sont ensuite listables avec :

Terminal

Logo bin/console debug:form


Et vérifiables individuellement :

Terminal

Logo bin/console debug:form "App\Service\Form\MyForm"


Avec le composant maker, on peut créer un formulaire pour chaque entité Doctrine à modifier :

Terminal

Logo composer require symfony/maker-bundle bin/console make:form


Pour ajouter des contrôles sur les champs, il existe un deuxième composant Symfony[2] :

Terminal

Logo composer require symfony/validator


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).

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());

Logo

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.

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',
                ],
        ]);

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.

Logo

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) {
                ...
            })

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]);

Logo

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 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.

Logo

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) }}


Depuis Symfony 4.3, un composant Symfony Mailer a été ajouté.

Pour l'installer[1] :

Terminal
Logo
composer require symfony/mailer


Ajouter ensuite le SMTP dans le .env : MAILER_DSN=smtp://mon_utilisateur:mon_mot_de_passe@smtp.example.com

    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);
    }


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.

Terminal
Logo
composer require symfony/swiftmailer-bundle


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);
 Il existe des applications comme Mailcatcher[3] pour intercepter les emails envoyés en environnement de développement, et les lire dans une interface graphique.

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 %} ...


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]

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.

Logo

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].

<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);
}
 Pour appeler directement un composant React depuis le Twig, il existe aussi react_component().


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].

FriendsOfSymfony[5] propose plusieurs bundles intéressants, parmi lesquels :

  • FOSUserBundle : pour gérer des utilisateurs.
  • FOSRestBundle : pour les API REST.

KNP Labs offre également plusieurs bundles connus, dont un paginateur[6].

Ce bundle permet de créer rapidement un back-office pour lire ou modifier une base de données[7].

Fonctionnalité filtre de EasyAdminBundle.

Mêmes principales fonctions que SonataAdmin mais plus léger[8].

 composer require easycorp/easyadmin-bundle

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.


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}"

Logo

Les apostrophes ne fonctionnent pas avec l'interpolation.

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
]

Logo

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"]
]

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ètre relative=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')) }}.

Logo

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.

 render_esi() peut remplacer render() (en PHP aussi), pour inclure un Twig avec le cache Edge Side Includes[8][9].
  • 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).

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 PHP ucfirst(), met une majuscule à la première lettre d'une chaine de caractères, et passe les autres en minuscules.
  • upper : équivaut au PHP strtoupper(), 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 PHP sizeof(), renvoie la taille de la variable (chaine ou tableau).
  • format : équivaut au PHP printf().
  • date : équivaut au PHP date() 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 PHP str_replace(). Ex : {{ 'Mon titre %tag%.'|replace({'%tag%': '1'}) }}.
  • join : équivaut au PHP implode() : convertit un tableau en chaine avec un séparateur en paramètre.
  • split : équivaut au PHP explode() : convertit une chaine en tableau avec un séparateur en paramètre.
  • slice(début, fin) : équivaut au PHP array_slice() + substr() : découpe un tableau ou une chaine selon deux positions[13].
  • trim : équivaut au PHP trim().
  • 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]
  • loop contient les informations de la boucle dans laquelle elle se trouve. Par exemple loop.index donne le nombre d'itérations déjà survenue (commence par 1 et pas par 0).
  • Les variables globales commencent par des underscores, par exemple[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]

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]

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() }}
 D'ailleurs il est aussi possible de récupérer n'importe quel paramètre de l'URL avec :
{{ 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

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') }}

Logo

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)}) }}

Logo

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]

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" %}

Logo

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 %}

À 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'} %}

Logo

On trouvait en Twig 1 la syntaxe {{ include() }}[20] au lieu de {% include() %}[21] en Twig 2.

Enfin, embed combine les deux précédentes fonctions :

    {% embed "footer.html.twig" %}
    ...
    {% endembed %}

import récupère certaines fonctions d'un fichier en contenant plusieurs :

    {% from 'mes_macros.html' import format_price as price, format_date %}

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 %}

Logo

Lors de l'appel, les paramètres nommés ne fonctionnent que si 100 % des paramètres appelés le sont.

    {% 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].


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

 symfony/orm-pack équivaut aux paquets suivants, qui peuvent bien sûr être installés séparément à la place :
  • doctrine/doctrine-bundle
  • doctrine/doctrine-migrations-bundle
  • doctrine/orm
  • symfony/proxy-manager-bridge

Commandes Doctrine

[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

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

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;

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'])]

Logo

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).

Logo

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".

Logo

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);

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].

Logo

Les types des attributs peuvent être quelque peu différents du SGBD[5].

Logo

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]).

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)]

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);

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.

 D'un point de vue architectural, avant d'instancier une nouvelle entité, on utilise généralement le repository pour savoir si son enregistrement existe en base ou si on doit le créer. Dans ce deuxième cas, la bonne pratique en DDD est d'utiliser une Factory pour faire le new de l'entité, mais aussi pour les new de son agrégat si elle est le nœud racine. Par exemple une CarFactory fera un new Car() mais aussi créera et lui associera ses composants : new Motor()...
 Il est possible de préciser le nom du repository d'une entité dans cette dernière :
#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]

Depuis Doctrine

[modifier | modifier le wikicode]
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();

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);

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 clause WHERE).
  • $repo->findById($id) : engendre automatiquement un SELECT * WHERE id = $id dans la table associée au repo.
  • $repo->findBy(['lastname' => $lastname, 'firstname' => $firstname]) engendre automatiquement un SELECT * WHERE lastname = $lastname AND firstname = $firstname.
  • $repo->findOneById($id) : engendre automatiquement un SELECT * WHERE id = $id LIMIT 1.
  • $repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname]) : engendre automatiquement un SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1.

Logo

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
);

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()".

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')

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() ou getScalarResult() : un tableau de tableaux (entité normalisée) ;
  • getSingleColumnResult() : un tableau unidimensionnel.
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);

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.

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);
            });


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) :

Se produit avant la persistance d'une entité (paramètre : PrePersistEventArgs $args).

Se produit après la persistance d'une entité (PostPersistEventArgs $args).

Se produit avant l'update d'une entité (PreUpdateEventArgs $args).

Se produit après l'update d'une entité (PostUpdateEventArgs $args).

Se produit avant l'update d'une entité (PreRemoveEventArgs $args).

Se produit après l'update d'une entité (PostRemoveEventArgs $args).

Se produit avant la sauvegarde d'une entité (PreFlushEventArgs $args).

Logo

  • 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].

 On peut aussi appeler computeChangeSet() depuis ailleurs pour savoir si une entité va occasionner une requête SQL lors de son flush[12].

Ex :

$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
    //...
}
 On peut aussi utiliser le paramètre LifecycleEventArgs $args dans ces fonctions.

Logo

Parfois le $object::class peut renvoyer Proxies\__CG__\App\Entity\MyEntity au lieu de App\Entity\MyEntity, selon le cache utilisé.

Se produit après la sauvegarde d'une entité (PostFlushEventArgs $args).

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

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).

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');
    }
}
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');
    }
}

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].

Logo

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.

Logo

$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).

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 les entités
[modifier | modifier le wikicode]
 php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity

Logo

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

À 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

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)%'
  1. Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
  2. Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le lazy loading est contourné par au choix :
    1. Avant l'appel null, un ObjetJoint->get().
    2. Dans l'entité, un @ManyToOne(…, fetch="EAGER").
    3. 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.
  3. Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
  4. Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
  5. 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 du LIMIT SQL). La solution consiste à ajouter un paginateur[17].
  6. L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
  7. Pas de hints.
  8. Bug des UNION ALL quand on joint deux entités non liées dans le repo.
À faire...link={{{link}}}


  • Ajouter la connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL (patrons à copier-coller ?)


  1. https://symfony.com/doc/current/doctrine.html
  2. https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string
  3. https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations
  4. https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html
  5. https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix
  6. https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing
  7. https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html
  8. https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6
  9. https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class
  10. https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly
  11. https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener
  12. https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2
  13. https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies
  14. https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
  15. https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23
  16. https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35
  17. 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.
 composer require api

Logo

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.

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
{ ... }

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'],
   ]
)]

Par exemple pour masquer un champ d'entité sur la route d'API :

#[ApiProperty(readable: false, writable: false, required: false, fetchable: false)]

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.

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]

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 ] }

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.

Logo

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$)

Logo

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: []

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
                        ),
                    ]
                )
            )
        ]
    )]

Enfin pour la sérialisation, on distingue plusieurs solutions :

  • symfony/serializer, qui donne des contrôleurs extends AbstractFOSRestController et des méthodes aux annotations @Rest\Post()[11].
  • jms/serializer-bundle, avec des contrôleurs extends RestController et 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"

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');
    }
}


Maintenant dans /api/doc, cliquer sur /api/test, puis "Ty it out" pour exécuter la méthode de test.

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);
    }

Pour tester en shell :

TOKEN=123
curl -H 'Accept: application/json' -H "Authorization: Bearer ${TOKEN}" http://localhost


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].

    composer require --dev phpunit/phpunit ^8

Une fois le .phar téléchargé depuis le site officiel[2], le copier dans le dossier où il sera toujours exécuté. Exemple :

    wget https://phar.phpunit.de/phpunit-8.phar
    mv phpunit.phar /usr/local/bin/phpunit
    chmod +x phpunit.phar
  1. Ajouter à la variable d'environnement PATH, le dossier où se trouve le fichier (ex : ;C:\bin).
  2. 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 de l'installation :

    phpunit --version

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

Logo

Pour exclure un seul fichier ou une seule méthode des tests, lui mettre $this->markTestIncomplete('This test has to be fixed.');

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

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

Logo

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.

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());
    }

Les mocks sont des objets PhpUnit qui permettent de simuler des résultats de classes existantes[5].

 certaines classes n'ont pas besoin de mock PhpUnit, comme Psr\Log\NullLogger qui peut être instancié depuis les tests des classes utilisant Psr\Log\LoggerInterface.

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;
    }

Logo

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());

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
        )
    );

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);

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.

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.

En PHP, Selenium peut s'interfacer avec PHPUnit[9] pour tester du JavaScript.

Avec Symfony, il existe aussi Panther[10].

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');
    }

Logo

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));
    }


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].

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".

Logo

HTTPS n'est pas supporté par le framework : Error reading socket. Unable to find the socket transport "tls". SimpleTest ne semble plus maintenu depuis le 28 mai 2013, mais il existe un dépôt sur GitHub[3].


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.

http://behat.org/en/latest/

Lancer les tests avec en ligne de commande.

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.

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

Mink[1] est une bibliothèque PHP permettant de simuler un navigateur Web, ce qui permet à Behat de tester du JavaScript avec Selenium[2].

  1. http://mink.behat.org/en/latest/
  2. (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.

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.

Parmi les solutions les plus éprouvées on peut utiliser Wordpress avec le plugin WooCommerce, Prestashop, osCommerce ou encore Magento.

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);

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;

[] 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 :

  1. Télécharger sur https://www.microsoft.com/en-us/download/details.aspx?id=20098.
  2. Copier dans le dossier PHP (ex : C:\Program Files (x86)\EasyPHP\binaries\php\php_runningversion\ext).
  3. Ajouter au fichier php.ini.
  4. 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


É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.

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.

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);

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

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...).


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] :

  1. $HOME/.composer/auth.json
  2. 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.

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 use n'est probablement pas exacte.
  • Sinon c'est la portée de la méthode mockée qui n'a pas publique. Si un dump du mock montre les attributs mais pas les méthodes, remplacer ->getMockForAbstractClass() par ->getMock().
  • S'il s'agit d'une méthode finale, il faut la définir lors de l'instanciation du mock. Ex :
    $this->serializerMock = $this
    ->getMockBuilder(SerializerInterface::class)
    ->setMethods(['serialize', 'deserialize', 'decode'])
    ->getMock()
    ;

    $this->serializerMock
    ->method('decode')
    ->willReturn('')
    ;

Trying to @cover or @use not existing method

[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.

Profiler sous Symfony 2.8.

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.

Logo

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.

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).
  • 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é de EntityRepository).

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 donner Error: Class 'pn' is not defined quand il n'y a pas de jointure) :
$this->createQueryBuilder('me')
    ->update()
    ->set('me.isDeleted', 1)

Property \"metadata\" on resource \"App\\MonEntite\" is declared as a subresource, but its type could not be determined in

[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-sync pour 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).

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().

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")

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.

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é.

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".

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 => ...]].

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\
  1. http://windows.php.net/download/
  2. https://stackoverflow.com/questions/9973555/setting-max-input-vars-php-ini-directive-using-ini-set
  3. https://stackoverflow.com/questions/804571/how-to-subtract-two-dates-ignoring-daylight-savings-time-in-php
  4. https://stackoverflow.com/questions/31115982/malformed-utf-8-characters-possibly-incorrectly-encoded-in-laravel
  5. https://whynhow.info/17522/How-to-get-rid-of-SIGBUS-when-running-php-fpm?
  6. https://www.kinamo.fr/fr/support/faq/determiner-le-nombre-de-processes-valide-pour-php-fpm-sur-nginx
  7. https://www.php.net/manual/fr/function.fopen.php
  8. http://stackoverflow.com/questions/4636166/only-variables-should-be-passed-by-reference
  9. https://stackoverflow.com/questions/24443318/getting-error-no-driver-found-to-handle-vcs-repository-on-composer-and-svn?answertab=votes#tab-top
  10. https://getcomposer.org/doc/articles/http-basic-authentication.md
  11. https://github.com/magento/magento2/issues/2523#issuecomment-159884152
  12. https://symfony.com/doc/current/console/verbosity.html
  13. https://silex.symfony.com/doc/2.0/cookbook/json_request_body.html
  14. https://symfony.com/doc/current/bundles/configuration.html
  15. https://symfony.com/doc/current/service_container/autowiring.html
  16. https://symfony.com/doc/5.4/components/http_foundation.html
  17. https://www.it-swarm.dev/fr/php/service-autowire-impossible-largument-fait-reference-la-classe-mais-ce-service-nexiste-pas/836794307/
  18. https://cilefen.github.io/symfony/2016/12/29/doctrine-orm-nulls.html
  19. https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/inheritance-mapping.html
  20. https://openclassrooms.com/forum/sujet/symfony2-collection-erreur-de-namespace
  21. https://www.kerstner.at/2014/09/doctrine-2-exception-entitymanager-closed/
  22. https://openclassrooms.com/forum/sujet/doctrine-schema-update-impossible
  23. https://github.com/api-platform/core/issues/3501
  24. https://stackoverflow.com/questions/40470895/phpstorm-saving-with-linux-line-ending-on-windows
  25. https://www.h3xed.com/web-development/php-and-apache-504-gateway-timeout-troubleshooting-and-solutions
  26. http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_read_timeout
  27. https://www.leighton.com/blog/php-debugging-in-phpstorm-6-0-with-xdebug/
  28. https://support.plesk.com/hc/en-us/articles/115000064929-Website-is-not-accessible-The-timeout-specified-has-expired-Error-dispatching-request-to
  29. https://www.reddit.com/r/drupal/comments/ase67i/for_issue_reference_service_unavailable_error/
  30. https://www.jetbrains.com/help/phpstorm/troubleshooting-php-debugging.html
  31. https://ubuntuforums.org/showthread.php?t=1381516
  32. https://stackoverflow.com/questions/17715128/xdebug-phpstorm-waiting-for-incoming-connection-with-ide-key


GFDL GFDL 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.