I. Introduction

Il est fréquent dans le développement web d'avoir besoin d'un script s'exécutant régulièrement, pour nettoyer une base de données, faire des statistiques… Toutefois, la création d'un tel script varie selon le serveur utilisé, les restrictions, les moyens, et relève souvent du casse-tête. Nous présenterons successivement plusieurs méthodes, avec leurs avantages, leurs limites, et le code PHP les implémentant pour configurer un tel script.
À vous de choisir la plus adaptée à votre système.

I-A. Comment lire cet article ?

Si vous lisez cet article pour résoudre un problème précis, vous devriez peut-être commencer par lire les sections configurations, qui vous permettront de repérer les solutions utilisables sur votre serveur.
Vous pouvez également commencer par lire le tableau récapitulatif en fin d'article qui vous aiguillera selon vos besoins.
Enfin, si vous ne codez pas vous-même en PHP, vous pouvez sauter toutes les sections de code, qui ne sont que des illustrations des principes décrits auparavant. Vous pouvez également passer la section 6 qui est entièrement articulée autour d'un script PHP.

II. Prérequis : comment lancer un script PHP

Suivant votre système, différentes solutions sont possibles pour appeler un script PHP. Dans la suite de cet article, nous utiliserons alternativement l'une ou l'autre de ces méthodes. Elles sont interchangeables.

  • Depuis une invite de commande
    • /chemin/de/php -f /chemin/du/script.php est accessible sur tous les systèmes. Sur UNIX, vous trouvez le chemin vers PHP avec whereis PHP. Sur Windows, reportez-vous à IV.D.1 Notez que selon vos versions vous pouvez vous servir de l'exécutable php, php-cgi, ou php-cli. Consultez la documentation pour plus d'informations.
    • /chemin/du/script.php est utilisable sur les systèmes UNIX. Vous devez placer la ligne #! /chemin/de/php en tête du script à exécuter.
    • Enfin en désespoir de cause, vous pouvez lancer un navigateur : explorer http://localhost/chemin/du/script.php (Windows) ou lynx http://localhost/chemin/du/script.php & (UNIX)
  • Depuis un autre script PHP
    • Vous pouvez exécuter l'une des commandes précédentes via exec(), passthru(), popen(), shell_exec() ou l'opérateur backtick (`)
    • Vous pouvez aussi bien sûr ouvrir le script via fopen("http://localhost/chemin/du/script.php", "r"); ou équivalent

III. Cron sur un serveur UNIX : bienvenue au paradis

III-A. Présentation

La première solution présentée pour l'exécution de notre script est Cron. Cron est un démon (programme tournant en permanence) UNIX se chargeant de lancer des programmes à intervalles réguliers. Chaque utilisateur dispose d'un fichier, dit « cron table » regroupant les noms des programmes à lancer et leurs fréquences d'exécution.

Avantages Inconvénients
  • Très souple pour le paramétrage de la fréquence
  • Très répandu
  • Les administrateurs seront en terrain connu
  • Souvent désactivé chez les hébergeurs de base

III-B. Configuration

Pour vous servir de cron, votre serveur doit être sous UNIX, et vous devez avoir accès à la commande crontab.
Si vous souhaitez configurer cron via PHP vous devez également avoir accès à au moins l'une des fonctions suivantes :

  • exec() ;
  • passthru() ;
  • system() ;
  • popen() et pclose() ;
  • shell_exec() ;
  • l'opérateur guillemets obliques (`).

III-C. Utilisation de cron

Laissons un instant PHP de côté pour étudier rapidement cron.
Crontab peut-être appelé de quatre façons :

  • crontab nomDeFichier va définir nomDeFichier comme cron table de l'utilisateur courant. Nous étudierons le format d'une cron table sous peu ;
  • crontab -l va afficher le contenu de la cron table de l'utilisateur courant ;
  • crontab -r va supprimer la cron table de l'utilisateur courant ;
  • enfin, crontab -e va lancer un éditeur de texte pour modifier la cron table de l'utilisateur courant.

Voyons maintenant le format d'un fichier cron table : chaque ligne correspond à un programme à lancer, les lignes commençant par # sont ignorées.
Chaque ligne est constituée de six champs, séparés par des espaces.
Ces champs représentent dans l'ordre :

  • les minutes où le programme sera exécuté (de 0 à 59) ;
  • les heures où le programme sera exécuté (de 0 à 23) ;
  • les jours du mois où le programme sera exécuté (de 1 à 31) ;
  • les mois où le programme sera exécuté (de 1 à 12) ;
  • les jours de la semaine où le programme sera exécuté (de 0 à 6, 0 = dimanche, 6 = samedi) ;
  • enfin le programme à exécuter.

Chacun des cinq premiers champs peut prendre comme valeur (exemple pour le champ des heures) :

  • une valeur simple ex-: 23 -> le script sera exécuté à 23 heures ;
  • une énumération ex-: 8,14,23 -> exécuté à 8 heures, 14 heures, 23 heures ;
  • un intervalle ex-: 8-23 -> exécuté chaque heure de 8 heures à 23 heures ;
  • * pour représenter le plus grand intervalle possible ex-: * -> exécuté chaque heure ;
  • un intervalle avec une fréquence ex-: 4-11/3 -> exécuté toutes les 3 heures de 4 heures à 11 heures.
Exemple de Cron Table
Sélectionnez
#chaque minute, sans se décourager :

* * * * * * php-cli -f /etc/www/uneMinuteEstPassee.php

#chaque jour de semaine à 8h30 sauf en juillet et août

30 8 * 1,2,3,4,5,6,9,10,11,12 1-5 php -f /etc/www/hell.php

#chaque vendredi à 18h...

0 18 * * 5 /etc/www/paradise.php

#...et tous les 14 jours à 8 heures, mais pas un jour de week-end

0 8 */14 * 1-5 /etc/www/paradise.php

III-D. Let there be code !

Nous allons maintenant écrire le code pour ajouter et supprimer un programme de Cron. Pour clarifier les choses, nous écrirons des Cron Table à ce format :

  • tout d'abord un nombre indéterminé de lignes gérées par l'utilisateur ;
  • le commentaire suivant : #Les lignes suivantes sont gérées automatiquement via un script PHP. - Merci de ne pas éditer manuellement ;
  • puis, pour chaque script à lancer :
    • une première ligne de commentaire la décrivant : # numéro du script : commentaire sur le script,
    • ensuite, les directives cron, comme détaillées ci-dessus ;
  • enfin le commentaire : #Les lignes suivantes ne sont plus gérées automatiquement.

Cette structure a un double intérêt :

  • il est plus simple de retrouver nos scripts pour les effacer ;
  • un utilisateur éditant le fichier est averti de l'utilité des lignes, et ne les effacera pas par erreur.

III-D-1. Ajouter un script

Notre fonction prend comme paramètres les différents champs pour la fréquence, la commande à exécuter (voir II), et une description ou un commentaire sur le programme. Elle retourne le numéro du script enregistré :

  • exécute crontab -l pour récupérer les programmes déjà enregistrés ;
  • modifie ou crée la section avec les scripts gérés automatiquement ;
  • enregistre le tout dans un fichier temporaire ;
  • exécute crontab leFichierTemporaire pour écraser l'ancienne table ;
  • efface le fichier temporaire.

Vous noterez que cette fonction ne comporte aucune gestion d'erreur, et qu'une cron table bidouillée l'induirait facilement en erreur. Autant que possible, nous nous contenterons dans cet article de codes simples, basiques, essentiellement pour illustrer nos propos plus que pour vous fournir des solutions « clé en main ».

ajouteScript()
Sélectionnez
$debut = '#Les lignes suivantes sont gerees automatiquement via un script PHP. - Merci de ne pas editer manuellement';



$fin = '#Les lignes suivantes ne sont plus gerees automatiquement';



function ajouteScript($chpHeure, $chpMinute, $chpJourMois, $chpJourSemaine, $chpMois, $chpCommande, $chpCommentaire)

{

        $oldCrontab = Array();                          /* pour chaque cellule une ligne du crontab actuel */

        $newCrontab = Array();                          /* pour chaque cellule une ligne du nouveau crontab */

        $isSection = false;

        $maxNb = 0;                                     /* le plus grand numéro de script trouvé */

        exec('crontab -l', $oldCrontab);                /* on récupère l'ancienne crontab dans $oldCrontab */

        

        foreach($oldCrontab as $index => $ligne)        /* copie $oldCrontab dans $newCrontab et ajoute le nouveau script */

        {

                if ($isSection == true)                 /* on est dans la section gérée automatiquement */

                {

                        $motsLigne = explode(' ', $ligne);

                        if ($motsLigne[0] == '#' && $motsLigne[1] > $maxNb)     /* si on trouve un numéro plus grand */



                        {

                                        $maxNb = $motsLigne[1];

                        }

                }

                

                if ($ligne == $debut) { $isSection = true; }

                

                if ($ligne == $fin)                     /* on est arrivé à la fin, on rajoute le nouveau script */

                {

                        $id = $maxNb + 1;

                        $newCrontab[] = '# '.$id.' : '.$chpCommentaire;



                        $newCrontab[] = $chpMinute.' '.$chpHeure.' '.$chpJourMois.' '.

                                $chpMois.' '.$chpJourSemaine.' '.$chpCommande;

                }

                

                $newCrontab[] = $ligne;                 /* copie $oldCrontab, ligne après ligne */

        }

        

        if ($isSection == false)                        /* s'il n'y a pas de section gérée par le script */

        {                                               /*  on l'ajoute maintenant */

                $id = 1;

                $newCrontab[] = $debut;

                $newCrontab[] = '# 1 : '.$chpCommentaire;



                $newCrontab[] = $chpMinute.' '.$chpHeure.' '.$chpJourMois.' '.$chpMois.' '.$chpJourSemaine.' '.$chpCommande;

                $newCrontab[] = $fin;

        }

        

        $f = fopen('/tmp', 'w');                        /* on crée le fichier temporaire */

        fwrite($f, implode('\n', $newCrontab); 

        fclose($f);

        

        exec('crontab /tmp');                           /* on le soumet comme crontab */

        

        return  $id;

}                               

III-D-2. Retirer un script

La fonction pour retirer un script est proche de la précédente. Elle prend comme paramètre le numéro du script à effacer :

retireScript()
Sélectionnez
$debut = '#Les lignes suivantes sont gerees automatiquement via un script PHP. - Merci de ne pas editer manuellement';



$fin = '#Les lignes suivantes ne sont plus gerees automatiquement';



function retireScript($id)

{

        $oldCrontab = Array();                          /* pour chaque cellule une ligne du crontab actuel */

        $newCrontab = Array();                          /* pour chaque cellule une ligne du nouveau crontab */

        $isSection = false;

        

        exec('crontab -l', $oldCrontab);                /* on récupère l'ancienne crontab dans $oldCrontab */

        

        foreach($oldCrontab as $ligne)                  /* copie $oldCrontab dans $newCrontab sans le script à effacer */

        {

                if ($isSection == true)                 /* on est dans la section gérée automatiquement */

                {

                        $motsLigne = explode(' ', $ligne);

                        if ($motsLigne[0] != '#' || $motsLigne[1] != $id)       /* ce n est pas le script à effacer */



                        {

                                        $newCrontab[] = $ligne;                 /* copie $oldCrontab, ligne après ligne */

                        }

                }else{

                        $newCrontab[] = $ligne;         /* copie $oldCrontab, ligne après ligne */

                }

                

                if ($ligne == $debut) { $isSection = true; }

        }

        

        $f = fopen('/tmpCronTab', 'w');                 /* on crée le fichier temporaire */

        fwrite($f, implode('\n', $newCrontab); 

        fclose($f);

        

        exec('crontab /tmpCronTab');                    /* on le soumet comme crontab */

        

        return  $id;

}                               

IV. At sur un serveur Windows : bienvenue au paradis de Microsoft

IV-A. Présentation

At est à Windows ce que Cron est à UNIX, attention à ne pas confondre avec la commande at de UNIX. Un peu moins souple que Cron, At est néanmoins présent sur tous les serveurs Windows, et ne doit pas être négligé.

Avantages Inconvénients
  • Les administrateurs seront en terrain connu
  • Présent sur tous les serveurs Windows
  • Souvent désactivé chez les hébergeurs de base
  • Risque de disparaître à moyen terme (voir VI.E)
  • Moins souple que cron
  • Les paramètres à passer dépendent du pays de votre serveur !

IV-B. Configuration

Pour vous servir de at, votre serveur doit être sous Windows, et vous devez avoir accès à la commande at.
Si vous souhaitez configurer at via PHP vous devez également avoir accès à au moins l'une des fonctions suivantes :

  • exec() ;
  • passthru() ;
  • system() ;
  • popen() et pclose() ;
  • shell_exec() ;
  • l'opérateur guillemets obliques (`).

IV-C. Format de la commande at

Pour programmer l'exécution périodique d'un script PHP, at prend ce format :

Ajouter une tâche
Sélectionnez
at hh:mm /Every:jour1,jour2,jour3[...] "chemin\absolu\vers\php.exe -f chemin\vers\le\script"
  • hh:mm est l'heure à laquelle le script sera exécuté les jours spécifiés ;
  • jour désigne un jour d'exécution, les différents jours sont séparés par des virgules. Ce peut-être
    • un jour de semaine (l, ma, me, j, v, s, d pour un système francophone),
    • un jour du mois (de 1 à 31) ;
  • enfin, nous reconnaissons la chaîne de commande avec l'adresse du script
Exemple 1 : tous les jours du week-end à 11h30
Sélectionnez
at 11:30 /Every:s,d "C:\Program Files\PHP\php.exe -f C:\Program Files\www\chocolatEtCroissantsAuLit.php"
Exemple 2 : un mois sur deux à 4 heures
Sélectionnez
at 04:00 /Every:31 "C:\Program Files\PHP\php.exe -f ./sauvegardeBdd.php"
Exemple 3 : tous les jours de semaine à 14h15 ainsi que les 4 premiers jours du mois quels qu'il soit
Sélectionnez
at 14:15 /Every:l,ma,me,j,v,1,2,3,4 "C:\Program Files\PHP\php.exe -f C:\Program Files\www\allezSavoirQuoi.php"

Vous remarquez que les règles accessibles sont assez basiques et ne permettent pas une exécution très fréquente. Pour cela, vous devez créer plusieurs tâches
Notez également que contrairement à cron, at fait l'union, et non pas l'intersection de l'ensemble des jours de semaine et de l'ensemble des jours du mois.

Pour effacer une tâche, at doit être exécuté au format suivant

Supprimer une tâche
Sélectionnez
at id /delete /yes

id est le numéro de la tâche qui est renvoyé au moment de son insertion s'il n'est pas précisé, toutes les tâches sont effacées.
/yes permet de ne pas avoir de message de confirmation.

IV-D. Code

IV-D-1. Trouver le chemin vers php.exe

Comme at demande des chemins absolus vers les commandes à exécuter, et qu'il n'y a pas de variable indiquant où est php.exe, nous allons commencer par une fonction pour le trouver : ce n'est qu'une proposition, qui est très facilement modifiable ou remplaçable, par exemple par une recherche directement en PHP et non via dir :

  • crée un fichier rechercheEnCours qui sera effacé à la fin. Si ce fichier existe au moment de l'appel, c'est que la fonction a été interrompue ;
  • demande à Windows de trouver un fichier php.exe sur les lecteurs C: à Z:;
  • retourne la première réponse.
trouvePHP()
Sélectionnez
function trouvePHP()

{

        if (! file_exists('./rechercheEnCours'))                /* rechercheEnCours est un fichier */

        {                       /*  créé au début de la recherche et effacé à la fin. */

                @set_time_limit(0);                             /* 30 secondes risque de ne pas faire assez */

                $fp = fopen('./rechercheEnCours', 'w');         /* on crée rechercheEnCours */

                fclose($fp);

                $cheminsTrouves = Array();                      /* le tableau ou on stockera les réponses de dir */

                

                for ($lecteur=67; $lecteur < 91; $lecteur++)    /* $lecteur prend les codes ascii des lettres de c à z */

                {

                        /* on se place à la racine du lecteur et on cherche un fichier php.exe */

                        /* les résultats sont placés dans $cheminTrouvés */

                        exec('cd /d '.chr($lecteur).':\\ && dir /s /b php.exe', $cheminsTrouves); 

                }

                

                unlink('./rechercheEnCours');

                

                return $cheminsTrouves[0];                      /* nous nous contenterons du premier résultat */        

        }else{

                echo ("Le script de recherche a été interrompu pendant une recherche précédente<br />");

                echo ("Il n a sans doute pas eu le temps de se terminer");

                echo ("Vous devriez contacter votre administrateur pour trouver l emplacement du fichier php.exe");

                unlink('./rechercheEnCours');   

        }

}

IV-D-2. Ajout d'une tâche

Nous pouvons maintenant enregistrer notre tâche. Nous supposerons que $heure et $minutes contiennent déjà les bonnes valeurs au bon format et que $jours est un tableau avec les différents jours d'exécution. Enfin $fichier est le chemin absolu vers le script PHP à exécuter.

ajouteTache()
Sélectionnez
function ajouteTache($heure, $minutes, $jours, $fichier)

{

        $pathPhp = trouvePHP();                                 /* le chemin vers php.exe */

        $listeJours = implode(',', $jours);                     /* la liste de jours au format attendu */

        

        /*la commande, le résulat est mis dans $res */

        $res = shell_exec('at '.$heure.':'.$minutes.' /Every:'.$listeJours.' "'.$pathPhp.' -f '.$fichier.'"');

        

        $motsRes = explode(' ', $res);                          /* on récupère les différents mots */

        return $motsRes[count($motsRes)-1];                     /* l'id est le dernier mot */

}

IV-D-3. Supprimer une tâche

Pour supprimer une tâche, nous avons uniquement besoin de id. Nous vérifions qu'il n'est pas vide pour ne pas effacer toutes les tâches.

effaceTache()
Sélectionnez
function effaceTache($id)

{

        if (! empty($id))

        {

                shell_exec('at '.$id.' /delete /yes');

        }

}

IV-D-4. Reprenons…

Nous souhaitons programmer l'exécution de sieste.php tous les jours de semaine à 150 h 00 et une fois bonus tous les deux mois. Comme dans un exemple précédent, pour l'exécuter tous les deux mois, nous le programmons le 31 (7 fois par an).

Test
Sélectionnez
$jours = Array();

$jours[] = 'l';

$jours[] = 'ma';

$jours[] = 'me';

$jours[] = 'j';

$jours[] = 'v';

$jours[] = '31';



$heure = '15';

$minutes = '00';



$script = 'C:\\Program Files\\EasyPHP\\www\\sieste.php';



$id = ajouteTache($heure, $minutes, $jours, $script);   /* vous pouvez vérifier l'insertion en lançant at sans argument */



effaceTache($id);       /* le boss est susceptible ? */

IV-E. Le futur : schtasks

At, présent depuis la préhistoire de Windows, s'est maintenant trouvé un successeur : schtasks. Plus puissant et souple que at, il n'est pour le moment présent que sous Windows XP professionnel, et sans doute (non vérifié) sous Windows Server 2003. Bien que sur ces systèmes, il cohabite avec at, pensez que à l'avenir, at risque d'être supprimé au profit de schtasks.

V. Webcron : encore un petit goût de paradis, parfois

V-A. Présentation

Le site webcron.org propose d'appeler lui-même vos scripts, à concurrence d'une fois par heure. Le service est gratuit, le webmaster proposant juste une wishlist sur Amazon… les miracles existeraient-ils ?

Avantages Inconvénients
  • Universel
  • Même souplesse et puissance que Cron
  • Votre serveur doit être connecté à internet et accepter des connexions de l'IP de webcron
  • Vous devenez dépendant du serveur de webcron.org
  • Vous ne pouvez pas exécuter vos scripts plus d'une fois par heure
  • Les scripts reposant sur webcron sont difficilement diffusables
  • L'historique doit être effacé manuellement

V-B. Configuration

Pour vous servir de webcron, votre serveur doit accepter les connexions du serveur webcron (l'IP est affichée sur la page d'accueil).

V-C. Mise en place

L'inscription se fait sans données personnelles sur le site.
Vous êtes limités à six tâches par compte, et devez vider manuellement vos historiques.
Si vous souhaitez plus de tâches, ou vider automatiquement les historiques, vous êtes prié de faire de la pub pour webcron sur votre site, ou encore mieux de faire un cadeau au webmaster…

VI. Poste client : de retour sur terre

VI-A. Présentation

Avant-dernière méthode pour lancer vos scripts, l'utilisation d'un ordinateur client appelant vos pages. Les approches sont multiples, vous pouvez par exemple vous contenter d'enregistrer une commande sur le cron du client, ou réaliser un petit programme tournant en boucle. C'est cette solution que nous retiendrons, à travers une simple page HTML.

Avantages Inconvénients
  • Universel
  • Librement configurable
  • Vous êtes dépendant du client

VI-B. Configuration

Pour utiliser cette méthode, vous devez juste disposer d'un poste client, connecté en permanence à internet.

VI-C. Code

Le code présenté ici est simplifié à l'extrême, vous trouverez des idées pour l'améliorer à la section VII.
Vous devez paramétrer les attributs suivants :

  • meta -> content : mettez ici l'intervalle en secondes entre deux appels du script, un point virgule, et l'URL de la page cliente ;
  • iframe -> src : mettez ici l'URL du script à appeler.
Page Client
Sélectionnez
<html>

<head>

        <meta http-equiv="Refresh" content="60;URL=url/de/cette/page.htm" />

        <title>NE PAS FERMER</title>

</head>

<body>

<iframe name="pageServeur" src="http://www.monserveur.com/le/chemin/du/script.php" />

</body>

</html>

VII. Script PHP : les meilleurs paradis sont ceux qu'on rêve

VII-A. Présentation

La dernière solution que nous présenterons est l'utilisation d'un script PHP tournant perpétuellement en arrière-plan et ne se réveillant que pour lancer le script prévu. Nous détaillerons un code basique, mais très facile à étendre pour une gestion plus fine.

Avantages Inconvénients
  • Universel
  • Librement modifiable et adaptable
  • Deux fonctions nécessaires (set_time_limit() et ignore_user_abort()) sont souvent désactivées
  • Doit être relancé à chaque démarrage du serveur

VII-B. Configuration

Pour exploiter cette méthode, vous devez avoir accès aux deux fonctions suivantes :

  • set_time_limit() ;
  • ignore_user_abort().

De plus, votre serveur ne doit pas être redémarré trop souvent

VII-C. Code

VII-C-1. Exécution permanente

La première chose à faire est de permettre à notre script de s'exécuter de façon continue. Un script peut s'arrêter pour trois raisons :

  • limite de temps dépassée ;
  • l'utilisateur a fermé son navigateur ;
  • le serveur est down.

La limite de temps peut être réglée par un appel à set_time_limit :

Limite de temps
Sélectionnez
set_time_limit(grandNombre);     /* permet au script de s exécuter pendant grandNombre de secondes supplémentaires */

set_time_limit(0);      /*permet au script de s'exécuter indéfiniment */

La fermeture du navigateur peut être ignorée par :

Fermeture du navigateur
Sélectionnez
ignore_user_abort(1);

Quant à l'arrêt du serveur, nous pourrions le traiter en demandant au système de lancer notre script au démarrage, toutefois cela nous rendrait dépendant de la plateforme. Nous allons donc nous contenter de programmer une fonction qui s'exécutera à l'arrêt du script pour par exemple nous prévenir par mail ou tenter de relancer le script.
Là encore, reportez-vous à la documentation, pour savoir quand cette fonction est réellement appelée.

Enregistrer une fonction de fin
Sélectionnez
register_shutdown_function(maFonction);

VII-C-2. Gestion de l'horloge

Le cœur de notre script est en fait une boucle infinie dormant un temps défini entre chaque passage.
Nous avons maintenant deux approches :

- ou nous souhaitons juste exécuter un script à intervalle régulier :

un script à intervalle régulier
Sélectionnez
while(1)

{

fairequelquechose();    /* cette fonction est exécutée tous les intervalles secondes */

sleep(intervalle);

}

- ou nous avons besoin de plus de finesse, par exemple gérer plusieurs scripts, avec des périodes variables entre chaque exécution variable.
Pour cela, nous allons reproduire le système de Cron :

  • $scripts sera un tableau à deux dimensions, dont chaque élément définira un script ;
  • chaque élément de $scripts sera un tableau avec comme éléments : 'minutes', 'heures', 'jour', 'mois', 'jourSemaine', 'URLScript' ;
  • chacun de ces éléments pourra prendre les mêmes valeurs que pour cron (intervalle, énumération, *) à l'exception des pas.
Exemple de $scripts
Sélectionnez
$scripts[0]['minutes'] = '30';

$scripts[0]['heures'] = '8';

$scripts[0]['jour'] = '*';

$scripts[0]['jourSemaine'] = '1-5';

$scripts[0]['mois'] = '1-6,9-12';

$scripts[0]['URLScript'] = 'http://localhost/auBoulot.php';



$scripts[1]['minutes'] = '0, 15, 30, 45';

$scripts[1]['heures'] = '*';

$scripts[1]['jour'] = '*';

$scripts[1]['jourSemaine'] = '1-6';

$scripts[1]['mois'] = '*';

$scripts[1]['URLScript'] = 'http://localhost/coucou.php';

Let's code

buildScriptsNext();

while(1)

{

        $next = getNextExecutionTime();                 /* on récupère l'heure (timestamp) de la prochaine exécution */

        $indexScript = getNextExecutionScript();        /* on récupère le numéro du prochain script à exécuter */

        $dodo = $next - time();                         /* le temps en seconde qu'il faut pour arriver à $next */

        sleep($dodo);                                   /* on dort jusqu'à ce qu'il soit temps d'exécuter le script */

        fopen($scripts[$indexScript]['URLScript'], 'r') /* on lance le script. */

                                                        /* fopen peut être remplacé par une autre méthode, (shell_exec...) */

        $scripts[$indexScript]['prochain'] = setNextExecutionTimeForScript($indexScript); /* prochaine exécution du script */ 

}

Détaillons maintenant chaque fonction, tout d'abord buildScriptsNext() calcule pour chaque script le moment de prochaine exécution, et le résultat est placé dans $scriptsNext, pour gagner du temps. Nous allons tout simplement faire reposer cette fonction setNextExecutionTimeForScript qui fait la même chose pour un script

buildScriptsNext()
Sélectionnez
function buildScriptsNext()

{

        global $scripts;

        

        foreach($scripts as $index => $val)

        {

                $scripts[$index]['prochain'] = setNextExecutionTimeForScript($index);

        }

}

setNextExecutionTimeForScript()

function setNextExecutionTimeForScript($indexScript)

{ 

        global  $scripts, $a, $m, $j, $h, $min;

        

        $aNow = date("Y");

        $mNow = date("m");

        $jNow = date("d");

        $hNow = date("H");

        $minNow = date("i")+1;

        

        $a = $aNow;

        $m = $mNow - 1;



        while(prochainMois($indexScript) != -1)                 /* on parcourt tous les mois de l'intervalle demandé */

        {                                                       /* jusqu'à trouver une réponse convenable */

                if ($m != $mNow || $a != $aNow)                 /*si ce n'est pas ce mois ci */

                {

                        $j = 0;

                        if (prochainJour($indexScript) == -1)   /* le premier jour trouvé sera le bon. */

                        {                                       /*  -1 si l'intersection entre jour de semaine */

                                                                /* et jour du mois est nulle */

                                continue;                       /* ...auquel cas on passe au mois suivant */

                        }else{                                  /* s'il y a un jour */

                                $h=-1;

                                prochainHeure($indexScript);    /* la première heure et la première minute conviendront*/

                                $min = -1;

                                prochainMinute($indexScript);

                                return mktime($h, $min, 0, $m, $j, $a);

                        }

                }else{                                          /* c'est ce mois ci */

                        $j = $jNow-1;                                   

                        while(prochainJour($indexScript) != -1) /* on cherche un jour à partir d'aujourd'hui compris */

                        {

                                if ($j > $jNow)                 /* si ce n'est pas aujourd'hui */

                                {                               /* on prend les premiers résultats */

                                        $h=-1;

                                        prochainHeure($indexScript);

                                        $min = -1;

                                        prochainMinute($indexScript);

                                        return mktime($h, $min, 0, $m, $j, $a);

                                }

                                if ($j == $jNow)                /* même algo pour les heures et les minutes */

                                {

                                        $h = $hNow - 1;

                                        while(prochainHeure($indexScript) != -1)

                                        {

                                                if ($h > $hNow)

                                                {

                                                        $min = -1;

                                                        prochainMinute($indexScript);

                                                        return mktime($h, $min, 0, $m, $j, $a);

                                                }

                                                if ($h == $hNow)

                                                {

                                                        $min = $minNow - 1;

                                                        while(prochainMinute($indexScript) != -1)

                                                        {

                                                                if ($min > $minNow) { return mktime($h, $min, 0, $m, $j, $a); }

                                                                

                                                                /* si c'est maintenant, on l'exécute directement */

                                                                if ($min == $minNow)

                                                                {

                                                                        fopen($scripts[$indexScript]['URLScript'], 'r');

                                                                }

                                                        }

                                                }                                               

                                        }

                                }

                        }

                }

        }

}

Nous aurons sous peu besoin d'une fonction pour analyser les intervalles fournis. parseFormat prend comme paramètres les valeurs min et max de l'intervalle, et la chaîne de format fournie par l'utilisateur. Elle retourne un tableau de la taille de l'intervalle, initialisé avec true pour les éléments appartenant à l'intervalle de l'utilisateur et false pour les autres

parseFormat()
Sélectionnez
function parseFormat($min, $max, $intervalle)

{

        $retour = Array();

        

        if ($intervalle == '*')

        {

                for($i=$min; $i<=$max; $i++) $retour[$i] = TRUE;

                return $retour;

        }else{

                for($i=$min; $i<=$max; $i++) $retour[$i] = FALSE;

        }

        

        $intervalle = explode(',', $intervalle);

        foreach ($intervalle as $val)

        {

                $val = explode('-', $val);

                if (isset($val[0]) && isset($val[1]))

                {

                        if ($val[0] <= $val[1])

                        {

                                for($i=$val[0]; $i<=$val[1]; $i++) $retour[$i] = TRUE;  /* ex : 9-12 = 9, 10, 11, 12 */

                        }else{

                                for($i=$val[0]; $i<=$max; $i++) $retour[$i] = TRUE;     /* ex : 10-4 = 10, 11, 12... */

                                for($i=$min; $i<=$val[1]; $i++) $retour[$i] = TRUE;     /* ...et 1, 2, 3, 4 */

                        }

                }else{

                        $retour[$val[0]] = TRUE;

                }

        }

        return $retour;

}

Passons maintenant aux fonctions prochainMois, prochainJour, prochainHeure, prochainMinute. Le principe est globalement le même, à savoir parcourir le tableau retourné par parseFormat jusqu'à trouver un élément à true, puis retourner cet élément. prochainMois gère aussi les années, et prochainJour tient compte du jour du mois et du jour de semaine

prochainMois
Sélectionnez
function prochainMois($indexScript)

{

        global $a, $m, $scripts;

        $valeurs = parseFormat(1, 12, $scripts[$indexScript]['mois']);

        do

        {

                $m++;

                if ($m == 13) 

                {

                        $m=1;

                        $a++;           /*si on a fait le tour, on réessaye l'année suivante */

                }

        }while($valeurs[$m] != TRUE);

}
prochainJour
Sélectionnez
function prochainJour($indexScript)

{

        global $a, $m, $j, $scripts;

        $valeurs = parseFormat(1, 31, $scripts[$indexScript]['jour']);

        $valeurSemaine = parseFormat(0, 6, $scripts[$indexScript]['jourSemaine']);

        

        do

        {

                $j++

                

                /* si $j est égal au nombre de jours du mois + 1 */

                if ($j == date('t', mktime(0, 0, 0, $m, 1, $a))+1) { return -1; }

                        

                $js = date('w', mktime(0, 0, 0, $m, $j, $a));

        }while($valeurs[$j] != TRUE || $valeurSemaine[$js] != TRUE)

}
prochainHeure
Sélectionnez
function prochainHeure($indexScript)

{

        global $h, $scripts;

        $valeurs = parseFormat(0, 23, $scripts[$indexScript]['heures'];

        

        do

        {

                $h++;

                if ($h == 24) { return -1; }

        }while($valeurs[$h] != TRUE)

}
prochainMinute
Sélectionnez
function prochainMinute($indexScript)

{

        global $min, $scripts;

        $valeurs = parseFormat(0, 59, $scripts[$indexScript]['heures'];

        

        do

        {

                $min++;

                if ($min == 60) { return -1; }

        }while($valeurs[$min] != TRUE)

}

Toujours là ? Allez, encore deux petites fonctions :
getNextExecutionTime et getNextExecutionScript qui se contentent respectivement de renvoyer le plus petit timestamp enregistré dans $scripts et l'index correspondant.

getNextExecutionTime
Sélectionnez
function getNextExecutionTime()

{

        global $scripts;

        

        foreach($scripts as $script)

        {

                if($script['prochain'] < $min || !(isset($min)))

                {

                        $min = $script['prochain'];

                }

        }

        return $min;

}
getNextExecutionScript
Sélectionnez
function getNextExecutionScript()

{

        global $scripts;

        

        foreach($scripts as $index => $script)

        {

                if($script['prochain'] < $min || !(isset($min)))

                {

                        $min = $script['prochain'];

                        $minIndex = $index;

                }

        }

        return $minIndex;

}

VII-C-3. Arrêt

Pour l'arrêt, nous nous contenterons de vérifier la présence d'un fichier STOP avec la fonction file_exists. Selon l'inspiration du moment, vous pouvez créer ce fichier via FTP, un script PHP…

VII-C-4. Code complet

Allez, on assemble le puzzle…

code complet
Sélectionnez
set_time_limit(0);

ignore_user_abort(1);



function fini()

{

        fopen('./ERREUR', 'w);

}



register_shutdown_function(fini());



while(1)

{

        if file_exists('STOP') { die("script arrêté. Effacez le fichier STOP avant de reprendre") }



        $next = getNextExecutionTime();                 /* on récupère l'heure (timestamp) de la prochaine exécution */

        $indexScript = getNextExecutionScript();        /* on récupère le numéro du prochain script à exécuter */

        $dodo = $next - time();                         /* le temps en seconde qu'il faut pour arriver à $next */

        sleep($dodo);                                   /* on dort jusqu'à ce qu'il soit temps d'exécuter le script */

        fopen($scripts[$indexScript]['URLScript'], 'r') /* on lance le script. */

                                                        /* fopen peut être remplacé par une autre méthode, (shell_exec...) */

        $scripts[$indexScript]['prochain'] = setNextExecutionTimeForScript($indexScript); /* prochaine exécution */ 

}

VIII. Quelques autres pistes avant de se damner

Gwargl… arriver ici, sans voir l'ombre d'un espoir pour résoudre son problème, ça doit être un peu frustrant, non ?
Malheureusement, nous avons quasiment fait le tour des solutions disponibles… Si vous en connaissez d'autres, n'hésitez pas à me contacter, je les ajouterai bien volontiers.
Si votre site génère un trafic assez important, vous pouvez placer au début de chaque page un code vérifiant la date, et éventuellement exécutant votre script, mais vous n'obtiendrez pas des résultats très ponctuels.
Et puis sinon, reste le forum : http://www.developpez.net/forums/ !

IX. Conclusion

Pour conclure, voici un tableau récapitulatif des différentes solutions proposées :

  Cron At Webcron Poste Client Script PHP
Serveur UNIX oui non, at existe, mais ce n'est pas la même fonction oui oui oui
Serveur Windows non oui oui oui oui
Intranet oui oui non, nécessite une connexion internet oui oui
Finesse des règles excellente moyenne excellente librement configurable librement configurable
Installation aisée aisée paradisiaque aisée aisée, mais tout le code n'est pas fait
Pérennité pas de problème sans doute remplacé par schtasks à moyen terme pas de raison pour s'arrêter, dépend de la bonne volonté du webmaster tant que le client est au bout du fil… pas de problème

X. Remerciements

Merci à tous les habitués de developpez.com pour leurs participations aux forums qui ont nourri cet article.

(c) Matthieu Pometan al m@ pour developpez.com - juillet 2004
matthieu@grimpentete.org n'hésitez pas à balancer toutes vos critiques, suggestions, ou même remerciements (qui sait) sur ce mail, c'est fait POUR !!