HTTP, serveurs web, et pages dynamiques

Alexandre Niveau
GREYC — Université de Caen
TODO: séparer en cours sur HTTP (avec plus de détails ?) et détails sur PHP sur le web. TODO: peut-être trop de choses d'un coup sur PHP sur le web…
Cours sur HTTP

* url : https://en.wikipedia.org/wiki/Uniform_Resource_Identifier  avec des exemples. query, fragment. chemin : pas forcément un vrai chemin.

* header fields https://en.wikipedia.org/wiki/List_of_HTTP_header_fields

* stateless
* safe methods
* idempotent methods
* cacheable


* status code & reason phrase   https://en.wikipedia.org/wiki/List_of_HTTP_status_codes

* cache : forward/reverse ; browser/machine/proxy/ISP   https://en.wikipedia.org/wiki/Web_cache
* compression https://en.wikipedia.org/wiki/HTTP_compression
* auth
* HTTPS https://en.wikipedia.org/wiki/HTTPS

serveurs web : 
un programme qui sait répondre à une requête HTTP par une réponse HTTP.
exemples : apache, nginx.




Application client-serveur

Illustration d'une application client-serveur
Exemple d'architecture client–serveur : deux clients font leurs requêtes à un serveur via Internet.
Source : Wikimedia (LGPL 2.1).
Application client-serveur :
  • application en réseau
  • le programme « principal », appelé serveur, tourne en continu sur une machine (généralement distante)
  • pour accéder à l'application depuis sa machine, il faut lancer un programme client, qui va se connecter au serveur et échanger des messages avec lui suivant un protocole bien défini

Le web, une application client-serveur

Dans le cadre du web:
  • Le protocole d'échange de messages s'appelle HTTP
  • Toute machine connectée à internet peut faire tourner un serveur web
    • elle écoute sur son port 80
    • elle interprète les requêtes HTTP reçues
    • elle renvoie les réponses HTTP (qui contiennent typiquement du HTML) au demandeur
  • Il y a de nombreux programmes clients, les plus courants étant les navigateurs web visuels (comme Firefox ou Chrome) :
    • ils envoient des requêtes HTTP aux serveurs
    • ils interprètent la réponse HTTP (typiquement, ils mettent en forme le HTML)

HTTP

HyperText Transfer Protocol ; c'est le langage que parlent le serveur web et le navigateur web pour se communiquer les pages

Élément le plus fondamental du web, et aussi le plus caché pour le grand public

Pas complètement cependant :
  • son nom apparaît au début des URL (mais les navigateurs actuels ne l'affichent plus)
  • les célèbres cookies sont un élément du protocole
  • certains codes de statut sont bien connus… (erreur 404)

HTTPS est la version sécurisée de HTTP : les messages sont chiffrés, et donc illisibles pour quiconque les intercepterait entre le client et le serveur ex: wifi public

URL

Le web est un ensemble de ressources (documents — ou autre !), liées entre elles

Chaque ressource a une adresse (ce qui permet de la récupérer, et de la lier à d'autres ressources)

Les adresses sont appelées URL (ou plus généralement URI)

La syntaxe des URL a été créée pour le web, mais est utilisée dans de nombreux autres contextes

            userinfo          host        port
        ┌───────┴───────┐ ┌────┴────────┐ ┌┴┐
 http://john.doe:password@www.example.com:123/forum/questions/?tag=networking&order=newest#top
 └─┬─┘  └───────────┬───────────────────────┘└─┬─────────────┘└────────┬─────────────────┘└┬─┘
 scheme         authority                      path                  query              fragment

La partie « scheme » indique le protocole à utiliser pour récupérer la ressource

La partie « authority » indique à qui il faut s'adresser pour récupérer la ressource

La partie « path » indique le chemin vers la ressource sur le serveur

La partie « query » permet d'indiquer des paramètres (pour filtrer ou mettre en forme la ressource)

La partie « fragment » identifie une sous-partie de la ressource (par exemple une section d'une page HTML)

Autres exemples de la syntaxe URI

Remarque : il y a de nombreuses variations pour les noms des différentes parties d'une URL

Requête HTTP

Structure d'une requête HTTP :
  • Une ligne de requête avec une commande (request method), un chemin et la version du protocole
  • Plusieurs lignes de champs d'en-tête
  • Une ligne vide
  • Le corps du message (optionnel)

Commandes : GET, POST, HEAD, PUT, DELETE…

Champs d'en-tête : Host, User-Agent, Accept… seul Host est obligatoire

Exemple, le navigateur (ou autre client web) veut accéder à la ressource
http://www.toto.fr/blog/posts/243.html?order=newest#top
Il la découpe en morceaux pour identifier
  • le protocole, ici HTTP
  • l'hôte, ici www.toto.fr
  • le chemin et les éventuels paramètres, ici /blog/posts/243.html?order=newest
  • le fragment, ici #top
Il va ensuite faire une requête HTTP vers la machine nommée www.toto.fr (après avoir récupéré son adresse IP via un serveur DNS) avec comme contenu
GET /blog/posts/243.html?order=newest HTTP/1.1
Host: www.toto.fr
Accept: text/html
Accept-Charset: utf-8
Connection: keep-alive
(et sans doute d'autres champs d'en-tête).
NB:
  • la cible de la requête GET est l'ensemble chemin+paramètres
  • le fragment n'apparaît pas dans la requête — le client demande toujours la ressource entière, et ira chercher tout seul le fragment dans la réponse reçue.

Réponse HTTP

Structure d'une réponse HTTP :
  • Une ligne de statut avec le status code et un petit message explicatif
  • Plusieurs lignes de champs de réponse
  • Une ligne vide
  • Le corps du message (optionnel), typiquement le code HTML de la page

Codes : 200 (OK), 404 (not found), 500 (internal server error)…

Champs de réponse : Content-type, Last-Modified, Location…

HTTP/1.1 200 OK
Date: Mon, 05 Jan 2015 12:12:12 GMT
Last-Modified: Wed, 02 Jan 2013 18:18:18 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 323
Connection: close

<html>
  <head> <title>Le blog de Toto</title> </head>
  <body> <p>Bienvenue sur mon blog !</p> </body>
</html>
faire une démo avec netcat : netcat example.com 80 <demo/requete.txt ça devrait donner qqch comme demo/reponse.txt

Serveurs web

Il existe de nombreux serveurs web (au sens logiciel), les plus utilisés étant Apache et nginx

Rôle du programme serveur : reçoit des requêtes HTTP et renvoie des réponses HTTP (qui peuvent contenir n'importe quoi)

En pratique, on a souvent besoin que les URL correspondent à des fichiers

un des rôles principaux du programme serveur est d'interpréter le chemin demandé dans la requête comme un chemin dans le système de fichiers

Interprétation pas directe ! On ne veut pas que tout le système de fichiers de la machine soit accessible sur le web…

le chemin demandé est « traduit »

Traduction d'un chemin par le serveur web

Typiquement, le serveur considère que la racine de l'URL (chemin /) correspond à un répertoire fixé de son système de fichiers, appelé « web root » ou « document root »
  • souvent /var/www/html sous Linux
  • quelque chose comme C:\Program Files\Apache\htdocs sous Windows

La « traduction » est plus ou moins une simple concaténation des chemins : la requête HTTP

GET /bidule/truc/machin.html?blabla=blibli
sera interprétée comme correspondant au fichier suivant sur le serveur :
/var/www/html/bidule/truc/machin.html
(les paramètres ne font pas partie du chemin : ce qu'en fait le serveur dépend de sa configuration)

On dit que le serveur web « sert » le contenu du répertoire /var/www/html à la racine du site

Exemple : le serveur web de la fac sert le contenu du répertoire /users/durand222/www-dev à la racine du site https://dev-durand222.users.info.unicaen.fr/

Servir un fichier / un répertoire

Si le chemin traduit de l'URL correspond à un fichier (HTML, ou image, PDF, audio…), c'est lui que le serveur envoie en réponse.

Si le chemin traduit ne correspond à rien, le serveur enverra une erreur 404 (on peut configurer ce comportement)

Si le chemin correspond à un répertoire le serveur regarde s'il contient un fichier index.html

si oui, c'est ce fichier qu'il sert

sinon, ça dépend de la configuration : il peut renvoyer une 404, une 403, ou générer une page pour montrer le contenu du répertoire

Pages dynamiques

Si besoin que le contenu d'un site soit dynamique, par exemple dépende d'une base de données, le serveur ne peut pas se contenter de renvoyer le contenu de fichiers écrits à l'avance

il faut que le serveur génère du HTML

Il va déléguer cette tâche à d'autres programmes — on parle de « programmation côté serveur »

Les programmes en question :
  • prennent en entrée la requête HTTP qui leur est fournie par le serveur ;
  • en fonction des différents éléments de la requête (méthode, chemin, paramètres, en-têtes, corps), produisent une réponse HTTP (avec statut, en-têtes, corps) ;
  • la réponse est envoyée par le serveur au client.

CGI

Très rapidement, aux débuts du web, est apparu le besoin d'une interface standard entre les serveurs web et les programmes de génération de HTML

En effet, il y avait plusieurs serveurs logiciels, et les programmes de génération de HTML étaient écrits dans des langages divers

il était souhaitable que tout cela soit interopérable, c'est-à-dire qu'on voulait pouvoir faire exécuter n'importe quel programme de génération par n'importe quel serveur logiciel

il fallait donc se mettre d'accord sur la façon dont le serveur fournit la requête au programme, et recupère la réponse

Le premier standard de la sorte est le Common Gateway Interface (CGI, 1993), qui rend très simple l'écriture de scripts (shell ou Perl à l'époque) pour générer des pages web :
  • la méthode HTTP, la cible de la requête (chemin + paramètres), et les en-têtes HTTP sont passés via des variables d'environnement (REQUEST_METHOD, REQUEST_URI, HTTP_HOST, HTTP_COOKIE…)
  • le corps de la requête (pour une requête POST par exemple) est fourni via l'entrée standard (stdin)
  • pour donner la réponse HTTP, le script doit tout simplement l'écrire sur la sortie standard
    • d'abord les champs d'en-tête, un par ligne ;
    • (le statut est donné par un pseudo en-tête Status)
    • puis une ligne vide ;
    • puis le corps de la réponse.
directory index https://en.wikipedia.org/wiki/Webserver_directory_index
.htaccess ? https://en.wikipedia.org/wiki/.htaccess

CGI https://en.wikipedia.org/wiki/Common_Gateway_Interface

/cgi-bin/
lancer script shell avec env|sort et date (ou java !)
historiquement ça marchait comme ça
autre technique : serveur exécute en fonction de l'extension du fichier
-& php

PHP

PHP est un langage conçu au départ pour écrire des scripts CGI

L'idée était de pouvoir mettre des bouts de programme au milieu d'une page HTML

<!DOCTYPE html>
<html>
<head><title>Ma page</title><meta charset="UTF-8" /></head>
<body>
    <h2>Les nombres</h2>
    <p>Voici les nombres de 1 à 10000 :

        <?php
            
for ($i 1$i <= 10000$i++) {
                echo 
"$i, ";
            }
        
?>

    </p>
</body>
</html>
Résultat

C'est une page HTML avec des blocs d'instructions encadrées par <?php et ?>

  • <? ?> permettent de définir des PI (« processing instruction ») ;
  • php indique que ces instructions sont du PHP.
  • (Cette syntaxe permet qu'un fichier contenant du HTML et du PHP puisse être du XML valide.)
  • on note qu'il y a des points-virgules à la fin de chaque ligne : ils sont obligatoires

Ce qui est en dehors de ces blocs est affiché tel quel

Les blocs sont remplacés par le résultat de l'exécution des instructions

Regarder le code source de la page : noter bien qu'il ne subsiste aucune trace du code PHP !

Que s'est-il passé ?

Déroulement :
  • Le navigateur demande au serveur la page nombres.php
  • Le serveur voit qu'il s'agit d'un script PHP : il appelle l'interpréteur PHP sur le script nombres.php
  • L'interpréteur exécute le script, ce qui génère une page HTML
    • Ce qui est en dehors des blocs PHP est reproduit tel quel
    • Les blocs sont remplacés par le résultat de l'exécution des instructions
  • La page HTML générée est renvoyée au navigateur

Le navigateur ne voit jamais le code PHP : il ne récupère que de l'HTML !

Les navigateurs ne savent d'ailleurs pas interpréter un fichier PHP. Si vous essayez d'en ouvrir un en local avec Firefox, il devrait vous proposer de le télécharger, comme pour tout type de fichier qu'il ne connaît pas.

Et les en-têtes, alors ?

Contrairement à un script CGI normal, si on exécute le script PHP en ligne de commande, il n'y a aucun champ d'en-tête

PHP a plusieurs modes de fonctionnement, appelés SAPI (server API)

Quand on lance l'interpréteur en ligne de commande, le SAPI est CLI (command line interface) : l'interpréteur comprend qu'il n'est pas en train d'interagir avec un serveur web et n'écrit ni ligne de statut ni en-têtes

Avec les autres SAPI, l'interpréteur fournit bien statut et en-têtes ; mais pour les modifier, on ne peut pas simplement les afficher, il faut faire appel à des fonctions dédiées.

  • Pour modifier le statut : http_response_code(404)
  • Pour ajouter ou modifier un en-tête : header("Content-Type: text/plain");

Attention : il ne doit pas y avoir d'espace entre le nom de l'en-tête et le deux-points qui suit. Le serveur ne comprend pas et renvoie une erreur 500 au client !

Buffers already sent

Bien qu'il faille utiliser des fonctions dédiées pour modifier les en-têtes HTTP, PHP continue à se comporter comme s'il fallait avoir écrit tous les en-têtes avant de commencer à écrire le corps de la réponse

Comme http_response_code et header modifient les en-têtes HTTP, elles n'ont aucun effet si elles sont utilisées après que le programme a commencé à écrire le contenu

Cela génère un warning « Headers already sent » : comprendre « j'ai déjà envoyé les en-têtes au serveur, je ne peux plus les modifier »

Attention, même un simple saut de ligne ou une espace avant l'ouverture de la balise <?php suffisent à « envoyer » les en-têtes ! démo et explications

Certaines installations bufferisent la sortie pour éviter ça, mais toutes ne le font pas : c'est donc une fausse sécurité (un peu comme désactiver les erreurs d'un compilateur…)

Redirection client

Il est parfois utile de rediriger le client vers une autre URL

C'est prévu dans le protocole HTTP via les codes de statut 3xx, en particulier
  • 301 Moved Permanently, quand une ressource a définitivement changé d'URL (le client enregistre la redirection et ne fera plus jamais de requête à l'ancienne adresse)
  • 302 Found, pour une redirection temporaire ; à ne pas utiliser en général car la sémantique n'est pas claire (la plupart des navigateurs ne respectent pas la spéc)
  • 303 See Other, quand le résultat de la requête doit être récupéré en GET à une autre URL
  • 307 Temporary Redirect, quand une ressource a changé d'URL temporairement

Quand il reçoit une réponse avec un de ces statuts, le navigateur va chercher la nouvelle URL dans le champ d'en-tête Location et immédiatement faire une nouvelle requête à cette URL, sans même afficher le contenu de la réponse.

Pour faire une redirection client en PHP, il suffit de donner l'en-tête Location, par exemple header("Location: http://example.com"); (PHP se charge alors de passer un code 302)

Récupérer les informations de la requête HTTP

En CGI, les informations provenant de la requête HTTP sont données dans l'entrée standard (corps de le requête) et dans des variables d'environnement (tout le reste).

En PHP :
  • on peut théoriquement récupérer les variables d'environnement dans le tableau superglobal $_ENV… mais il est désactivé sur les serveurs de l'université
  • heureusement toutes les variables d'environnement standard des CGI, accompagnées d'autres informations, sont accessibles dans un autre tableau superglobal, $_SERVER. On y trouve notamment :
    • l'URL locale de la page courante ($_SERVER['REQUEST_URI'])
    • et la « partie virtuelle » de l'URL courante ($_SERVER['PATH_INFO']).

    C'est suffisant pour récupérer les paramètres d'URL, mais ce n'est pas très pratique ; PHP nous fournit donc un autre tableau superglobal, $_GET, qui contient les couples clefs-valeurs des paramètres d'URL (et ce, quelle que soit la méthode HTTP utilisée, même POST !) Démo GET

  • on peut récupérer le corps de la requête avec file_get_contents('php://input'). C'est utile s'il contient par exemple du JSON, mais si le format des données est le format standard renvoyé par les formulaires HTML (application/x-www-form-urlencoded), alors PHP fournit également les couples clefs-valeurs dans un autre tableau superglobal, $_POST.
Démo des superglobales

Rappel : phpinfo() affiche la configuration PHP de votre serveur et la valeur de toutes les superglobales (Voir)

Envoi de données avec HTTP

Sur le web moderne les internautes ont généralement de nombreuses possibilités d'interaction avec les sites web

Iels peuvent faire des actions, notamment des modifications de contenu

Cela implique notamment d'envoyer des données du client au serveur

On va maintenant voir comment cela se passe au niveau HTTP

Retour sur la méthode GET

Pour l'instant, on a principalement mentionné la méthode GET, qui permet de récupérer une ressource

Caractéristique importante : c'est une « safe method  ⇒  elle est définie comme devant n'avoir aucun effet de bord

elle n'est pas censée modifier l'état de l'application (hormis pour des détails secondaires, comme un compteur de visites)

elle est « sûre », au sens où on peut l'utiliser sans crainte ⇒ important par exemple pour les robots indexeurs !

On peut donner des paramètres à l'URL demandée avec GET, qui vont affecter la façon dont la ressource est récupérée, ou le type de contenu

C'est le standard (RFC 7231) qui impose que GET soit une safe method, mais en pratique il n'y a aucune contrainte technique qui oblige à respecter cette contrainte

il est possible d'utiliser les paramètres d'URL pour faire des actions (qui modifient l'état) sur un site web… mais c'est une très mauvaise idée !

Pour faire des actions, il est préférable d'utiliser d'autres méthodes

Les méthodes non safe

Il y a trois méthodes HTTP qui permettent de modifier l'état de l'application :
  • POST permet d'envoyer des données
  • PUT permet d'enregistrer une ressource à un chemin donné
  • DELETE permet de supprimer la ressource d'un chemin donné

En pratique les deux dernières sont rarement utilisées (voir plus loin), c'est en général POST qui est utilisée pour tous les cas de figure non couverts par GET

Anatomie d'un POST

Pour envoyer des données avec la méthode POST de HTTP, on les met dans le corps de la requête

Le format des données est libre, mais en général il suit la même syntaxe que les paramètres d'URL (à quelques détails près)

formellement, il s'agit du format MIME application/x-www-form-urlencoded

POST /chemin/pagederecup HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 21

nom=Toto&couleur=vert+clair

Couples clef-valeur séparés par des &

Espaces remplacées par +

Encodage particulier des caractères spéciaux et non-ASCII

Formulaires HTML

HTML fournit une interface permettant aux internautes de construire « interactivement » ces couples clef-valeur : les formulaires

Ils sont principalement utiles pour envoyer des données (méthode POST) : poster un commentaire, s'authentifier sur un site…

Mais ils permettent aussi de construire une URL paramétrée : champ de recherche, contrôle de l'affichage du contenu (ordre, filtrage)…

Ils fonctionnent exactement de la même manière pour les deux usages ; dans les deux cas le navigateur utilise le contenu du formulaire pour construire une chaîne de couples clef-valeur

Il faut juste préciser une URL et la méthode à utiliser :
  • si on met POST, le navigateur met la chaîne dans une requête POST qu'il envoie à l'URL fournie
  • si on met GET, le navigateur met la chaîne à la fin de l'URL fournie après un ?, et il envoie une requête GET à l'URL paramétrée ainsi obtenue

GET vs POST

Dans les deux cas, cliquer sur le bouton submit construit les couples clef-valeur et les communique au serveur

N'importe quel programme peut être utilisé pour les récupérer : il suffit qu'il comprenne le protocole HTTP, ou qu'il puisse être lancé par le serveur lorsqu'un client demande l'URL en question (ex. : script PHP)

Caractéristiques des deux techniques :
  • paramètres d'URL (method="GET") : limités en pratique à environ 2000 caractères (voire 255) ; ne devraient être utilisés que pour filtrer et rechercher
  • corps de la requête HTTP (method="POST") : pas de limite de taille, et pas visible par l'internaute

NB : il est parfaitement possible d'envoyer des données avec POST à une URL paramétrée !

Si hésitation entre paramètres d'URL et méthode POST, se demander si l'URL paramétrée aurait un sens en tant que lien. Il n'est envisageable d'utiliser la méthode GET que si la réponse est oui.

Attention : les données de la requête POST ne sont pas affichées par les navigateurs, mais elles ne sont pas cachées !
  • Il est possible de les voir (par ex. avec l'outil « network monitor » de Firefox)
  • Elles circulent en clair sur le réseau

Ne pas penser que POST est plus sécurisé que GET

Remarque sur les paramètres

Les paramètres d'URL sont souvent utilisés abusivement pour le routage (mauvaises pratiques qui se sont répandues notamment par le biais de PHP), par ex. http://toto.fr/forum?post=754

Ce n'est pas non plus leur rôle !

L'URL du post 754 devrait être quelque chose comme http://toto.fr/forum/posts/754

Les paramètres, comme leur nom l'indique, servent à paramétrer une page

ils ne devraient être utilisés que pour filtrer ou rechercher :
http://toto.fr/forum?date=2015-02-23&sort=title
http://toto.fr/forum?q=glace+vanille

Petits détails de PHP sur lesquels je voulais revenir

  1. Inclusion de fichiers
  2. Output buffering
  3. isset et empty

Inclusion de fichiers

On a très souvent besoin d'inclure du code d'un autre fichier PHP (ou HTML)

2 possibilités
  • include : génère simplement un warning en cas d'échec
  • require : arrête le programme avec une erreur fatale en cas d'échec, donc rien ne s'affiche

Instructions include_once et require_once pour éviter une inclusion multiple

La gestion des chemins (quels accès sont autorisés ou non, quelle est la référence des chemins relatifs dans un script inclus depuis un autre script…) est relativement infernale : ne pas essayer de faire des choses subtiles, ou prévoir un stock d'aspirine

Output buffering

  • L'inclusion d'un script PHP provoque son exécution : tout ce qu'il écrit est directement affiché par le script appelant.
  • Il est parfois utile de ne pas afficher tout de suite le résultat de l'exécution d'une série d'instructions PHP, notamment pour ce cas-là
  • Il faut utiliser l'output buffering (OB) :
    <pre>
    <?php
        
    echo "One\n";

        
    ob_start();  /* on active l'output buffering :
                      * tout ce qui suit sera enregistré
                      * dans un buffer au lieu d'être affiché
                      */

        
    echo "Two\n";
        include 
    "demo/fichier.txt";

        
    $var ob_get_clean();  /* on désactive l'OB, on
                                 * récupère le contenu du
                                 * buffer, et on le vide.
                                 */

        
    echo "Three\n";
        echo 
    "Contenu du buffer :\n « " $var " »\n";
    ?>
    </pre>
    One
    Three
    Contenu du buffer :
     « Two
    Coucou,
    je suis un fichier !
    
    Au revoir
    
     »
    
  • Il y a d'autres fonctions pour manipuler l'OB, mais dans 95% des cas ces deux-là suffisent.

Existence de variables

Si on utilise une variable qui n'existe pas, un avertissement (notice) est lancé

il n'y a pas d'erreur, PHP considère que la variable a la valeur null

Ne pas s'appuyer là-dessus : s'il y a un avertissement, c'est pour une bonne raison (code pas propre, plus difficile à maintenir)

Il y a une pseudo-fonction pour tester si une variable existe ou non : isset
if (isset($toto))
	echo "La variable \$toto existe, voilà son contenu : $toto";
else
	echo "La variable \$toto n'existe pas";
Le code précédent ne génère aucun avertissement.

A priori, dans du code propre et bien organisé, on n'a jamais besoin de isset() !

Pourtant, en pratique, isset est très souvent utilisée pour vérifier que les tableaux $_GET ou $_POST contiennent un paramètre donné
utiliser plutôt key_exists, qui risque moins de cacher une erreur dans le code :
if (key_exists('toto', $_GET))
	$toto = $_GET['toto'];
else
	$toto = '';
array_key_exists est un peu plus lent (genre 2 fois) à cause de l'appel de fonction. c'est pas bien méchant. ça reste O(1) normalement.

empty()

La pseudo-fonction empty permet de vérifier qu'une variable est « vide », c'est-à-dire que
  • soit elle n'existe pas
  • soit elle existe, mais est (faiblement) égale à false

Attention, dans la liste des valeurs égales à false, il y a notamment la chaîne '0' !

même si vous savez que votre variable contient une chaîne, empty() ne vous permet pas de vérifier qu'elle est vraiment vide !

si vraiment besoin de manipuler des variables qui n'existent pas forcément, il est conseillé d'utiliser plutôt isset et un test explicite (hors cas très particuliers).

Il est également très fortement déconseillé d'utiliser empty($maVariable) si la variable est censée exister : cela peut cacher des erreurs dans le code (erreur dans le nom de variable, par exemple).

si besoin de tester qu'une variable est « vide », utiliser plutôt !$maVariable ou $maVariable == false (comparaison volontairement faible), ou mieux, comparer explicitement à la valeur vide qui vous intéresse : $maChaine === '' ou $monTableau === [].

Pour résumer, il est encore plus déconseillé d'utiliser empty que isset.

Détails sur isset et empty, leur relation avec null, et quand les utiliser : the definitive guide to PHP’s isset and empty