Tutoriel : Commandes personnalisées et planification dans BowPHP
La plupart des applications finissent par accumuler quelques tâches récurrentes : purger des données obsolètes, envoyer un récapitulatif nocturne, reconstruire un cache. La manière propre de les gérer consiste à créer une commande de console personnalisée que vous pouvez exécuter à la main, plus le planificateur pour l'exécuter automatiquement. Dans ce tutoriel, nous allons en construire une de bout en bout et la brancher correctement.
À la fin, vous disposerez d'une commande data:prune — avec des options, un mode
de simulation (dry-run) et une sortie lisible — qui s'exécutera chaque nuit en
production, en toute sécurité.
Ce que nous allons construire
Une application SaaS accumule des lignes de réinitialisation de mot de passe expirées que personne ne nettoie jamais. Nous voulons une commande qui supprime les lignes plus anciennes que N jours, que nous pouvons :
- exécuter manuellement lors d'un incident (
php bow data:prune --days=7), - prévisualiser sans rien supprimer (
--dry-run), - et planifier pour s'exécuter automatiquement à 2 h du matin.
Étape 1 — Générer la commande
BowPHP génère la classe à votre place :
php bow add:command PruneExpiredDataCommand
Cela crée app/Commands/PruneExpiredDataCommand.php. Une commande générée étend
AbstractCommand et effectue son travail dans process().
Étape 2 — Écrire la logique de la commande
Une bonne commande est paramétrable, sûre à exécuter en simulation et
claire sur ce qu'elle a fait. À l'intérieur d'une commande sous forme de
classe, les arguments sont accessibles via $this->arg :
getParameter('--name', $default) pour les options et les drapeaux,
getTarget() pour un argument positionnel.
<?php
namespace App\Commands;
use App\Models\PasswordReset;
use Bow\Console\AbstractCommand;
use Bow\Console\Color;
class PruneExpiredDataCommand extends AbstractCommand
{
public function process(): void
{
// `--days=7` (vaut 30 par défaut) ; `--dry-run` est un drapeau (true s'il est présent).
$days = (int) $this->arg->getParameter('--days', 30);
$dryRun = (bool) $this->arg->getParameter('--dry-run', false);
$cutoff = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$count = PasswordReset::where('created_at', '<', $cutoff)->count();
if ($count === 0) {
echo Color::green("Nothing to prune.\n");
return;
}
if ($dryRun) {
echo Color::yellow(
"[dry-run] {$count} rows older than {$days} days would be deleted.\n"
);
return;
}
PasswordReset::where('created_at', '<', $cutoff)->delete();
echo Color::green("Pruned {$count} rows older than {$days} days.\n");
}
}
Une commande est un mécanisme de livraison, pas un foyer pour la logique
métier. Pour tout traitement non trivial, injectez un service et appelez-le
depuis process() — la même logique reste testable et réutilisable en dehors de
la console.
Étape 3 — Enregistrer la commande
Générer la classe ne suffit pas à la rendre connue de php bow — vous devez en
enregistrer le nom. Faites-le dans la méthode run() d'un fournisseur de
configuration afin qu'elle soit chargée avec l'application :
namespace App\Configurations;
use App\Commands\PruneExpiredDataCommand;
use Bow\Configuration\Configuration;
use Bow\Configuration\Loader;
use Bow\Console\Console;
class AppServiceProvider extends Configuration
{
public function create(Loader $config): void
{
//
}
public function run(): void
{
Console::register(
'data:prune', // nom saisi sur la CLI
PruneExpiredDataCommand::class, // la classe à exécuter
'Prune expired password-reset rows',
"\nUsage:\n php bow data:prune [--days=30] [--dry-run]\n"
);
}
}
Assurez-vous que le fournisseur figure bien dans votre kernel :
public function configurations(): array
{
return [
// ...autres fournisseurs
\App\Configurations\AppServiceProvider::class,
];
}
Essayez-la maintenant :
php bow data:prune --dry-run # prévisualiser
php bow data:prune --days=7 # supprimer les lignes plus anciennes que 7 jours
php bow data:prune help # afficher le texte d'usage
Votre commande apparaît également sous la section CUSTOM de php bow help.
Étape 4 — La planifier
Les tâches planifiées vivent dans la méthode schedules() de App\Kernel. Le
planificateur vous offre quatre façons de planifier du travail — command(),
task(), exec() et call() — avec une API de fréquence fluide et lisible :
use Bow\Scheduler\Scheduler;
public function schedules(Scheduler $schedule): void
{
// Notre commande personnalisée, chaque nuit à 02:00.
$schedule->command('data:prune', ['--days' => 30])
->dailyAt('02:00')
->withoutOverlapping()
->description('Prune expired password-reset rows');
// Une commande intégrée, chaque semaine.
$schedule->command('clear:log')
->weeklyOn(7, '03:00')
->description('Rotate application logs');
// Une vérification rapide en ligne toutes les cinq minutes.
$schedule->call(function () {
logger('Heartbeat: scheduler alive');
})->everyFiveMinutes();
}
Étape 5 — Les bonnes pratiques qui vous sauvent à 3 h du matin
Quelques options transforment un cron fragile en un cron fiable :
Empêchez les chevauchements pour tout ce qui pourrait s'exécuter longtemps. Si l'exécution précédente n'est pas terminée, la suivante est ignorée plutôt que de s'accumuler :
$schedule->command('data:prune')
->hourly()
->withoutOverlapping(30); // le verrou expire au bout de 30 minutes
withoutOverlapping() stocke son verrou dans le cache, alors assurez-vous qu'un
magasin de cache est configuré (voir le guide du cache).
Protégez les tâches spécifiques à un environnement afin qu'un serveur de préproduction n'envoie jamais d'e-mails à vos clients :
$schedule->task(\App\Tasks\SendWeeklyReportTask::class)
->weeklyOn(5, '17:00')
->when(fn () => app()->environment('production'))
->description('Send the weekly reports');
Reléguez le travail shell lourd en arrière-plan afin qu'une tâche lente ne retarde pas le reste des tâches de la minute :
$schedule->exec('mysqldump mydb > /backups/daily.sql')
->dailyAt('01:00')
->runInBackground();
Ajoutez toujours une description() — c'est ce que schedule:list vous
affiche.
Inspectez avant de faire confiance
La console vous permet de visualiser la planification sans attendre l'horloge :
php bow schedule:list # toutes les tâches enregistrées
php bow schedule:next # quand chacune s'exécute la prochaine fois
php bow schedule:test "App\\Tasks\\SendWeeklyReportTask" # en exécuter une maintenant
Étape 6 — La brancher en production
Le planificateur ne se déclenche que si quelque chose le pilote. Deux options :
Cron (simple). Une seule entrée, qui exécute le distributeur chaque minute — c'est BowPHP qui décide quelles tâches sont réellement dues :
* * * * * cd /var/www/bow-app && php bow schedule:run >> /var/log/bow-scheduler.log 2>&1
Démon (résilient). Exécutez schedule:work sous un superviseur de processus
afin qu'il redémarre en cas de plantage ou de redémarrage :
[program:bow-scheduler]
directory=/var/www/bow-app
command=php bow schedule:work
autostart=true
autorestart=true
user=www-data
stdout_logfile=/var/log/bow-scheduler.log
sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start bow-scheduler
Récapitulatif
php bow add:commandgénère une commande ;process()est l'endroit où elle s'exécute, et$this->argvous donne accès aux options et aux arguments.Console::register()dans lerun()d'un fournisseur relie le nom à la classe.schedules()dans le kernel planifie des commandes, des tâches, du shell ou des closures avec une API de fréquence lisible.withoutOverlapping(),when(),runInBackground()etdescription()sont les options qui rendent les tâches planifiées sûres en production.- Une seule ligne cron (
schedule:run) ou un démonschedule:worksupervisé pilote l'ensemble.
Voilà toute la boucle : une commande que vous pouvez exécuter à la main aujourd'hui, et qui s'exécutera d'elle-même chaque nuit demain. Plongez dans la documentation du planificateur et de la console pour la liste complète des fréquences et des commandes.
Il manque quelque chose ?
Si vous rencontrez des problèmes avec la documentation ou si vous avez des suggestions pour améliorer la documentation ou le projet en général, veuillez déposer une issue pour nous, ou envoyer un tweet mentionnant le compte Twitter @bowframework ou directement sur le github.