Utiliser des procédures stockées dans un projet PHP / MySQL
Quand on découvre les procédures stockées dans MySQL ou MariaDB, on pense souvent à un quelque chose de réservé aux bases géantes, aux applications critiques ou aux environnements industriels. Pourtant, ce n’est rien de plus qu’un petit bloc de logique SQL qu’on stocke dans la base plutôt que de devoir le réécrire à chaque fois coté PHP.
Au lieu d’envoyer une requête complète, on dit simplement à MySQL : “exécute cette procédure”. Et la base sait quoi faire. L’intérêt devient évident dès qu’on commence à factoriser, à sécuriser un traitement, ou à éviter les copier-coller entre différents modules. C’est simple, puissant, et souvent sous-estimé.
Dans cet article, nous allons explorer ce qu’il en est, à quoi ça sert, comment ça se crée, comment on les appelle depuis PHP, et surtout, dans quels cas cela peut nous être utile. On prendra le temps de distinguer les procédures et les fonctions stockées, car l’une ne remplace pas l’autre — et elles peuvent même très bien travailler ensemble.
Une procédure stockée, c’est quoi ?
On peut le dire simplement : c’est une sorte de fonction, mais côté base de données. Plutôt que d’envoyer une requête SQL depuis PHP, on demande à MySQL ou MariaDB d’exécuter une commande qu’il connaît déjà. Une procédure stockée, c’est exactement ça : un petit bloc de logique qu’on a enregistré une bonne fois pour toutes, et qu’on peut appeler quand on veut, d’où on veut.
Prenons un cas très simple. On veut insérer un utilisateur fictif dans une table. En SQL “classique”, côté PHP, on écrirait quelque chose comme :
$sql = "INSERT INTO tab_users (user_name, user_mail)
VALUES ('Alice', 'alice@example.com');"
$dbh->prepare($sql)->execute();Mais maintenant, imaginons qu’on veuille employer ce mode d’insertion dans une autre partie de notre application. Ou qu’on veuille ajouter une vérification, une date automatique, un log. On commence à dupliquer, modifier le PHP ici et là, puis à l’adapter au fil du temps… et c’est là qu’on peut se dire : et si on encapsulait tout ça dans une procédure ?
Voici exactement la même action, mais écrite côté base de données, sous forme de procédure stockée. Bien sûr, dans un usage réel, on passerait les données en paramètre plutôt que d’écrire les valeurs en dur. Mais ici, on se concentre sur le principe général, pour en saisir la logique.
CREATE PROCEDURE sp_add_user()
BEGIN
INSERT INTO tab_users (user_name, user_mail)
VALUES ('Alice', 'alice@example.com');
END;Et côté PHP, pour l’appeler, c’est presque aussi simple :
$dbh->prepare("CALL sp_add_user()")->execute();En fait, il ne s’agit de rien de plus que ce qu’on ferait avec une requête classique. La seule différence, c’est que le traitement n’est plus écrit coté PHP, mais confié à la base, qui sait quoi faire.
Est-ce que ça change la vie ? Non. Mais ça ouvre une porte. Parce que si cette logique se complexifie un peu, ou si elle doit être réutilisée dans différents modules de l’application, on a déjà un cadre solide, centralisé, et facile à maintenir.
Fonction ou procédure : quelle différence, et pourquoi ça compte
Maintenant qu’on a vu ce qu’était une procédure stockée, il est temps de lever une petite ambiguïté. Si vous ouvrez phpMyAdmin ou la documentation MySQL, vous verrez deux mots apparaître côte à côte : procédures et fonctions stockées. Ce ne sont pas des synonymes.
Les deux se ressemblent, mais elles n’ont pas le même rôle.
- Une procédure stockée sert à manipuler la base, à gérer une action : insérer une ligne, supprimer une entrée, modifier une table, enchaîner plusieurs actions. On l’utilise un peu comme une commande. On l’invoque, et elle enclanche un processus de traitement purement base de données.
- Une fonction stockée, elle, sert à renvoyer une valeur, un peu comme nos fonctions PHP habituelles. Elle encapsule une logique simple : vérifier une condition, formater une chaîne, extraire une information. On peut l’utiliser dans une requête SQL, comme n’importe quelle fonction de la base (
NOW(),CONCAT(), etc.). Contrairement à une procédure, elle ne modifie rien dans la base : elle calcule et retourne un résultat, discrètement.
Et concrètement, on les utilise quand et comment ?
Plaçons nous en situation, et voyons comment employer l’une ou l’autre, ou les deux en même temps.
Lors de nos développement, il arrive parfois que nous préférions présenter nos identifiants sous forme de référence… par exemple au lieu de lire un ID sous sa forme numéraire « 12« , nous souhaitons générer une chaine de type « U-0012« . C’est là que peut intervenir une fonction stockée qui encapsule cette logique. Donc plutôt que d’avoir à écrire ce traitement en PHP à chaque fois, nous pouvons avoir recours à SQL :
CREATE FUNCTION formatte_identifiant(uid INT)
RETURNS VARCHAR(10)
RETURN CONCAT('U-', LPAD(uid, 5, '0'));Que l’on pourra invoquer aussi bien depuis PHP, comme une expression dans une requête SELECT :
$dbh->prepare("SELECT formatte_identifiant(:uid)")->execute([':uid' => 12]);Que directement au sein d’une autre requête SQL
SELECT user_id, formatte_identifiant(user_id) AS ref_user FROM tab_users;C’est propre, clair, et réutilisable dans toute l’application, cela reste disponible et accessible sans avoir à sortir de la base de données. Cependant une fonction ne va pas plus loin. Elle ne modifie pas la base, elle ne déclenche rien. Elle est silencieuse, et c’est exactement ce qu’on lui demande.
À l’inverse, si l’on souhaite insérer un enregistrement, ou agir sur une table, on passera par une procédure :
CREATE PROCEDURE sp_log_action(IN uid INT)
BEGIN
INSERT INTO tab_logs (user_id, reference_id)
VALUES (uid, formatte_identifiant(uid));
END;Ici, la procédure agit : elle écrit quelque chose en base. Et au passage, elle peut très bien appeler une fonction, comme formatte_identifiant(), pour éviter de réécrire une logique déjà existante.
Voilà pourquoi cette distinction a du sens : une fonction ne remplace pas une procédure, une procédure ne remplace pas une fonction. Ce sont deux outils complémentaires, qui ne jouent pas le même rôle, mais qui peuvent très bien communiquer entre elles, pour nous simplifier la vie… et celle de la base.
Examinons la syntaxe d’une procédure stockée
Une procédure stockée encapsule une suite d’instructions exécutées en bloc par le serveur de base de données. Elle peut insérer, modifier, supprimer des données, appeler des fonctions, ou effectuer des traitements conditionnels. La structure de base ressemble à ceci :
CREATE PROCEDURE nom_de_la_procedure (paramètres)
BEGIN
instructions SQL...
END;Les mots-clés BEGIN et END jouent ici le rôle des accolades en PHP : ils encadrent les instructions que la procédure doit exécuter. Cette structure devient indispensable dès que la procédure contient plusieurs lignes, ou qu’elle inclut des instructions conditionnelles. Sans elle, MySQL ne saurait pas où s’arrête la procédure.
Chaque paramètre transmis à la procédure doit être précédé d’une direction, précisant comment il sera utilisé :
- IN : la valeur est transmise à la procédure. C’est le cas le plus courant, utilisé pour adapter l’exécution aux données reçues.
- OUT : la procédure place une valeur dans ce paramètre, que l’appelant pourra ensuite récupérer. Cela permet de retourner un résultat.
- INOUT : la valeur est transmise, modifiée au cours du traitement, puis renvoyée à l’appelant.
Voici une procédure concrète qui ajoute une entrée dans un journal de logs et retourne l’identifiant de cette nouvelle ligne :
CREATE PROCEDURE sp_log_action(IN uid INT, OUT new_log_id INT)
BEGIN
INSERT INTO tab_logs (user_id) VALUES (uid);
SET new_log_id = LAST_INSERT_ID();
UPDATE tab_users SET last_action = NOW() WHERE user_id = uid;
-- D'autres traitements peuvent suivre ici
END;Et voici son appel depuis PHP, avec récupération de la variable de sortie dans un second temps :
$stmt = $dbh->prepare("CALL sp_log_action(:uid, @log_id)");
$stmt->execute([':uid' => $userId]);
$result = $dbh->query("SELECT @log_id AS log_id")->fetch();
$logId = $result['log_id'];La variable @log_id ici est une variable de session MySQL, accessible tant que la connexion reste active. Attention : si vous utilisez un pool de connexions ou un hébergement mutualisé, ce comportement peut ne pas être fiable.
Pour éviter cette étape de récupération via une variable intermédiaire, une autre approche consiste à faire un SELECT directement dans la procédure :
CREATE PROCEDURE sp_log_action(IN uid INT)
BEGIN
INSERT INTO tab_logs (user_id) VALUES (uid);
UPDATE tab_users SET last_action = NOW() WHERE user_id = uid;
SELECT LAST_INSERT_ID() AS log_id;
END;Ce qui permet, côté PHP, de récupérer directement le résultat en une seule opération :
$stmt = $dbh->prepare("CALL sp_log_action(:uid)");
$stmt->execute([':uid' => $userId]);
$logId = $stmt->fetch(PDO::FETCH_ASSOC)['log_id'];Cette variante est souvent plus simple à maintenir dans un environnement multi-utilisateurs. L’essentiel reste de choisir l’approche la plus lisible, stable, et adaptée à votre cas d’usage.
Du côté des fonctions, la structure reste très proche.
Une fonction stockée ressemble beaucoup à une procédure, mais elle a deux particularités majeures : elle doit toujours renvoyer un résultat, et elle ne peut pas modifier directement les données de la base. En clair, on l’utilise pour produire une valeur, pas pour effectuer une action.
Comme pour les procédures, si la fonction ne contient qu’une seule instruction, on peut se passer du bloc BEGIN ... END. Mais dès qu’on prévoit un minimum de logique — comme un test, une transformation ou une variable intermédiaire —, mieux vaut conserver cette structure pour rester lisible et pouvoir évoluer sans tout réécrire.
CREATE FUNCTION nom (paramètres)
RETURNS type_retour
BEGIN
RETURN valeur_retour;
END;Prenons un exemple simple, imaginons qu’on souhaite transformer un identifiant numérique en une référence lisible, du type U-00123, mais adapté à un contexte. Voici une version un peu plus structurée :
CREATE FUNCTION formatte_identifiant(uid INT)
RETURNS VARCHAR(12)
DETERMINISTIC
BEGIN
DECLARE prefixe VARCHAR(3);
DECLARE resultat VARCHAR(12);
-- Choix du préfixe selon la valeur
IF uid < 1000 THEN
SET prefixe = 'U-';
ELSE
SET prefixe = 'UX-';
END IF;
-- Construction de la chaîne finale
SET resultat = CONCAT(prefixe, LPAD(uid, 5, '0'));
RETURN resultat;
END;Cette fonction suit la structure complète attendue par MySQL :
CREATE FUNCTIONdéclare la fonction,RETURNSprécise le type de retour (ici une chaîne de caractères),- et
BEGIN ... ENDencadre le bloc logique, permettant d’introduire- des variables locales (
DECLARE), - des conditions (
IF ... THEN) - et des opérations intermédiaires (
SET).
- des variables locales (
- Le mot-clé
DETERMINISTICsignale que, pour un même paramètre, le résultat sera toujours identique — une information utile pour l’optimisation du moteur.
On notera que le type retourné est ici un VARCHAR(12), mais il pourrait tout aussi bien s’agir d’un INT, d’un DECIMAL, d’un DATE, ou même d’un BOOLEAN. C’est ce champ RETURNS qui définit à la fois la nature et la limite de la fonction : elle ne peut renvoyer qu’un seul résultat. Si l’on souhaite produire plus d’une valeur, il faudra soit créer différentes fonctions dédiées, soit préférer une procédure avec plusieurs paramètres OUT, soit, dans certains cas, concaténer les éléments dans une chaîne unique et les reconstituer ensuite côté PHP. Ce n’est pas une limite bloquante, mais une contrainte à garder en tête.
Créer une fonction ou une procédure dans MySQL ou MariaDB : même logique, même syntaxe
Que l’on souhaite créer une fonction ou une procédure stockée, la logique reste la même : on écrit du SQL. Cela peut se faire dans un terminal MySQL, dans un script .sql, ou via l’onglet SQL de phpMyAdmin. L’essentiel est de structurer correctement le bloc que l’on veut enregistrer dans la base.
DELIMITER //
CREATE PROCEDURE sp_add_user(IN name VARCHAR(100), IN mail VARCHAR(100))
BEGIN
INSERT INTO tab_users (user_name, user_mail)
VALUES (name, mail);
INSERT INTO tab_logs (event_type, user_email, log_time)
VALUES ('NEW_USER', mail, NOW());
UPDATE tab_stats
SET user_count = user_count + 1;
END;
//
DELIMITER ;La commande CREATE PROCEDURE crée une nouvelle procédure, ici nommée sp_add_user. Tout ce qui se trouve entre BEGIN et END est le corps de la procédure : c’est là qu’on écrit les instructions SQL à exécuter. On peut en mettre une seule, ou plusieurs comme ici, dans un ordre précis. Mais attention à un point de syntaxe important : MySQL exécute par défaut chaque instruction dès qu’il voit un point-virgule (;). Or dans notre cas, on veut pouvoir utiliser des points-virgules à l’intérieur même de la procédure, sans que MySQL la coupe en morceaux.
C’est là qu’intervient la ligne DELIMITER //. Elle dit simplement à MySQL : “ne considère plus le ; comme la fin d’une instruction, mais utilise // à la place”. On encadre notre procédure avec ce nouveau délimiteur, puis on revient à la normale en utlsat le point virgule (DELIMITER ;) juste après. C’est une astuce de syntaxe, pas une instruction SQL. Mais elle est très utile, voire indispensable, dès qu’on écrit une procédure contenant plusieurs lignes.
Et via phpMyAdmin, s’il est activée par l’hébergeur
Si on préfère l’approche visuelle, phpMyAdmin propose aussi un outil de création de procédures ou de fonctions stockées. Attention, certains hébergeurs mutualisés n’autorisent pas la création de procédures stockées. Si l’onglet “Procédures stockées” est absent ou si vous avez une erreur “insufficient privileges”, c’est probablement le cas.

Si vous y avez accès, il suffit de cliquer sur l’onglet “Procédures Stockées”, puis sur “Créer une procédure” pour faire apparaître un formulaire complet, où tout peut être défini sans écrire une seule ligne de SQL, ou presque.

On commence par donner un nom à notre routine, et à choisir son type : procédure ou fonction. L’interface s’adapte alors dynamiquement.
Pour une procédure, on peut définir plusieurs paramètres, chacun avec une direction (IN, OUT ou INOUT), un nom, un type (INT, VARCHAR, TEXT…), une longueur ou valeur par défaut, et des options qui varient en fonction du type choisi. Un bouton permet d’ajouter autant de paramètres que nécessaire.
Pour une fonction, la structure est similaire, mais sans notion de direction : on indique simplement nom, type, longueur, et options. Il faut aussi spécifier le type de retour attendu, par exemple VARCHAR(10) ou INT, avec les mêmes options de format.
Dans les deux cas, un champ principal permet de saisir directement le corps SQL de la routine. Inutile d’ajouter DELIMITER ou même BEGIN…END : phpMyAdmin se charge automatiquement de les encadrer si nécessaire, selon le contenu.
En bas de formulaire, différentes options avancées peuvent être précisées :
- Est déterministe : Cette case est à cocher si la routine renvoie toujours le même résultat pour les mêmes entrées. Exemple : une fonction
CONCAT('U-', uid)est déterministe, car si vous lui passez12, elle renverra toujoursU-12. En revanche, une fonction qui utiliseNOW()ouRAND()ne l’est pas, car elle peut produire des résultats différents à chaque appel, même sans changement d’entrée. - Créateur : Ce champ désigne l’utilisateur à l’origine de la routine, sous la forme
utilisateur@hôte. Il est automatiquement rempli par MySQL selon l’utilisateur connecté au moment de la création. Attention si vous travaillez en local (ex.root@localhost) puis migrez en production : ce nom reste inscrit dans la définition. Selon le mode de sécurité choisi (DEFINERouINVOKER), cela peut entraîner des restrictions ou des erreurs si cet utilisateur n’existe pas sur le serveur cible. - Type de sécurité : DEFINER ou INVOKER :
DEFINERsignifie que la routine s’exécutera avec les droits de la personne qui l’a créée, quel que soit l’utilisateur qui l’appelle ensuite. Exemple : même si un utilisateur n’a pas le droit d’écrire dans une tabletab_logs, il pourra exécuter une procéduresp_log_actiondéfinie par un administrateur, si cette dernière est en modeDEFINER.INVOKERsignifie que la routine utilisera les droits de la personne qui l’appelle. Exemple : si l’utilisateur n’a pas les droits nécessaires pour exécuter certaines requêtes à l’intérieur de la routine, celle-ci échouera, même si elle a été créée par un administrateur.
- Accès aux données SQL : Cette option permet d’indiquer à MySQL le type d’opérations que la routine est autorisée à faire. Cela ne limite pas son exécution, mais sert à des vérifications internes et à l’optimisation.
NO SQL: la routine ne contient aucune instruction SQL (rare).CONTAINS SQL: la routine contient du SQL, mais ne lit ni n’écrit dans la base.READS SQL DATA: la routine lit des données (SELECT) mais ne les modifie pas.MODIFIES SQL DATA: la routine peut modifier les données (INSERT, UPDATE, DELETE…).
Exemple : une fonction qui effectue un SELECT sur une table sans rien modifier relèvera deREADS SQL DATA. Une procédure qui insère un log relèvera deMODIFIES SQL DATA.
- Commentaire : Ce champ est libre. Vous pouvez y décrire la finalité de la routine, les prérequis ou toute autre note utile pour vous ou un collègue.
Exemple :Insère une ligne dans tab_logs avec identifiant formattéouRenvoie un identifiant lisible pour affichage dans l’interface admin.
Un clic sur “Exécuter” ou “Enregistrer” suffit alors pour que phpMyAdmin crée la procédure dans la base, et la rende immédiatement disponible dans l’onglet “Procédures stockées”.
Variables, portée et subtilités à ne pas oublier dans les procédures stockées
Dans l’univers des procédures stockées, quelques particularités peuvent surprendre, surtout lorsqu’on vient du monde PHP. Notamment la manière dont on déclare et utilise des variables, la portée qu’elles ont, ou encore les différences entre une variable temporaire, une valeur d’entrée, et un résultat à transmettre. Prenons le temps d’en clarifier quelques unes.
Variables locales : DECLARE et SET
Dans une procédure ou une fonction, on peut déclarer des variables temporaires internes. Cela se fait avec DECLARE, en précisant le nom, le type, et éventuellement la taille. Une fois déclarée, on affecte une valeur avec SET. Ces variables ne sont accessibles que dans la procédure ou la fonction en cours.
DECLARE total INT;
SET total = 0;On doit obligatoirement les déclarer au tout début du bloc BEGIN ... END. Impossible d’intercaler un DECLARE au milieu des instructions. Ces variables locales sont idéales pour stocker des résultats intermédiaires, des indicateurs, ou préparer une sortie.
Paramètres : IN, OUT, INOUT
Les paramètres sont déclarés à la création d’une procédure. On peut les passer en entrée (IN), en sortie (OUT), ou dans les deux sens (INOUT). Une fonction ne peut utiliser que des paramètres IN, puisqu’elle renvoie une seule valeur via RETURN.
IN: la procédure reçoit une valeur mais ne peut pas la modifier à l’extérieur.OUT: la procédure peut renvoyer une valeur, affectée depuis l’intérieur.INOUT: on transmet une variable qui pourra être modifiée et lue après exécution.
CREATE PROCEDURE calcul_double(IN nombre INT, OUT resultat INT)
BEGIN
SET resultat = nombre * 2;
END;Depuis PHP, on peut récupérer ce OUT en effectuant un second SELECT après l’appel.
$dbh->prepare("CALL calcul_double(:nb, @res)")->execute([':nb' => 8]);
$result = $dbh->query("SELECT @res AS resultat")->fetch(PDO::FETCH_ASSOC);
echo $result['resultat']; // Affichera 16Variables utilisateur : le rôle de @variable
En dehors des variables locales, MySQL permet de stocker des valeurs dans des variables utilisateur, identifiables par le préfixe @. Elles peuvent être utilisées dans une procédure ou même entre divers appels, tant que la session reste active.
SET @dernier_id = LAST_INSERT_ID();Ces variables sont pratiques pour tester ou chaîner des appels, mais attention : leur portée est plus large, elles persistent tant que la session est ouverte. Elles ne sont pas idéales pour encapsuler une logique propre ou sécurisée.
Retour de résultats : ce qu’une procédure peut transmettre
Une fonction ne peut renvoyer qu’une seule valeur, avec RETURN. Si plusieurs valeurs doivent être transmises, il faut créer une procédure avec des paramètres OUT, ou concaténer un résultat complexe dans une chaîne à traiter ensuite en PHP.
RETURN CONCAT('Nom:', nom, ' | Âge:', age);Mais dès qu’on a besoin de plusieurs sorties bien séparées, une procédure reste plus adaptée. Chaque OUT devient de ce fait un canal de sortie, et on peut même les combiner avec un SELECT dans la procédure, selon le contexte d’usage. Nous en avons exploré la mise en place dans le chapitre Examinons la syntaxe d’une procédure stockée.
Portée et durée de vie des variables
Les variables locales (DECLARE) existent uniquement pendant l’exécution de la procédure. Elles sont détruites automatiquement dès la fin du traitement.
Les variables utilisateur (@) vivent aussi longtemps que dure la session SQL, et peuvent être visibles d’une requête à l’autre.
Les paramètres sont accessibles uniquement à l’intérieur de la procédure ou de la fonction qui les utilise, mais peuvent interagir avec des variables extérieures s’ils sont en OUT ou INOUT.
| Type de variable | Portée | Durée de vie | Exemple |
|---|---|---|---|
DECLARE total INT; |
Bloc courant | Jusqu’à la fin du bloc | Calculs internes |
@var := 42 |
Session SQL | Jusqu’à déconnexion | Test temporaire |
IN uid INT |
Paramètre entrée | Pendant exécution | Appel de proc |
OUT log_id INT |
Paramètre sortie | Pendant exécution | Retour de valeur |
Conclusion
Les procédures et fonctions stockées ne sont pas réservées aux architectures complexes ou aux géants du web. Elles sont à portée de main dans MySQL et MariaDB, même dans un projet personnel ou une application artisanale. Il suffit de prendre le temps de comprendre leur logique, leur syntaxe, et leurs cas d’usage concrets.
Centraliser un traitement, éviter des répétitions, découpler des responsabilités entre PHP et SQL, sécuriser une action ou simplement gagner en lisibilité : les raisons de s’y intéresser sont nombreuses. Et plus on structure sa base, plus ces outils prennent leur sens.
Bien sûr, il ne s’agit pas de tout déplacer côté base, ni de chercher à tout encapsuler. Mais dès qu’un traitement revient, dès qu’un format se répète, dès qu’une logique devient sensible, c’est peut-être le bon moment pour franchir le pas.
