Étape 24: Exécuter des crons

5.0 version
Maintained

Exécuter des crons

Les crons sont utiles pour les tâches de maintenance. Contrairement aux workers, ils travaillent selon un horaire établi pour une courte période de temps.

Nettoyer les commentaires

Les commentaires marqués comme spam ou refusés par l’admin sont conservés dans la base de données, car l’admin peut vouloir les inspecter pendant un certain temps. Mais ils devraient probablement être supprimés au bout d’un moment. Les garder pendant une semaine après leur création devrait être suffisant.

Créez des méthodes utilitaires dans le repository des commentaires pour trouver les commentaires rejetés, les compter et les supprimer :

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
--- a/src/Repository/CommentRepository.php
+++ b/src/Repository/CommentRepository.php
@@ -6,6 +6,7 @@ use App\Entity\Comment;
 use App\Entity\Conference;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Persistence\ManagerRegistry;
+use Doctrine\ORM\QueryBuilder;
 use Doctrine\ORM\Tools\Pagination\Paginator;

 /**
@@ -16,12 +17,37 @@ use Doctrine\ORM\Tools\Pagination\Paginator;
  */
 class CommentRepository extends ServiceEntityRepository
 {
+    private const DAYS_BEFORE_REJECTED_REMOVAL = 7;
+
     public const PAGINATOR_PER_PAGE = 2;

     public function __construct(ManagerRegistry $registry)
     {
         parent::__construct($registry, Comment::class);
     }
+
+    public function countOldRejected(): int
+    {
+        return $this->getOldRejectedQueryBuilder()->select('COUNT(c.id)')->getQuery()->getSingleScalarResult();
+    }
+
+    public function deleteOldRejected(): int
+    {
+        return $this->getOldRejectedQueryBuilder()->delete()->getQuery()->execute();
+    }
+
+    private function getOldRejectedQueryBuilder(): QueryBuilder
+    {
+        return $this->createQueryBuilder('c')
+            ->andWhere('c.state = :state_rejected or c.state = :state_spam')
+            ->andWhere('c.createdAt < :date')
+            ->setParameters([
+                'state_rejected' => 'rejected',
+                'state_spam' => 'spam',
+                'date' => new \DateTime(-self::DAYS_BEFORE_REJECTED_REMOVAL.' days'),
+            ])
+        ;
+    }

     public function getCommentPaginator(Conference $conference, int $offset): Paginator
     {

Astuce

Pour les requêtes plus complexes, il est parfois utile de jeter un coup d’œil aux requêtes SQL générées (elles se trouvent dans les logs et dans le profileur de requêtes web).

Utiliser des constantes de classe, des paramètres de conteneur et des variables d’environnement

7 jours ? Nous aurions pu choisir un autre chiffre, pourquoi pas 10 ou 20 ? Ce nombre pourrait évoluer avec le temps. Nous avons décidé de le stocker en tant que constante dans la classe, mais nous aurions peut-être pu le stocker en tant que paramètre dans le conteneur, ou même le définir en tant que variable d’environnement.

Voici quelques règles de base pour décider quelle abstraction utiliser :

  • Si la valeur est sensible (mots de passe, jetons API, etc.), utilisez le stockage de chaîne secrète de Symfony ou un Vault ;
  • Si la valeur est dynamique et que vous devriez pouvoir la modifier sans redéployer, utilisez une variable d’environnement ;
  • Si la valeur peut être différente d’un environnement à l’autre, utilisez un paramètre de conteneur ;
  • Pour tout le reste, stockez la valeur dans le code, comme dans une constante de classe.

Créer une commande de console

Supprimer les anciens commentaires est une tâche idéale pour un cron job. Il faut le faire de façon régulière, et un petit retard n’a pas d’impact majeur.

Créez une commande nommée app:comment:cleanup en créant un fichier src/Command/CommentCleanupCommand.php :

src/Command/CommentCleanupCommand.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace App\Command;

use App\Repository\CommentRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class CommentCleanupCommand extends Command
{
    private $commentRepository;

    protected static $defaultName = 'app:comment:cleanup';

    public function __construct(CommentRepository $commentRepository)
    {
        $this->commentRepository = $commentRepository;

        parent::__construct();
    }

    protected function configure()
    {
        $this
            ->setDescription('Deletes rejected and spam comments from the database')
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        if ($input->getOption('dry-run')) {
            $io->note('Dry mode enabled');

            $count = $this->commentRepository->countOldRejected();
        } else {
            $count = $this->commentRepository->deleteOldRejected();
        }

        $io->success(sprintf('Deleted "%d" old rejected/spam comments.', $count));

        return 0;
    }
}

Toutes les commandes de l’application sont enregistrées avec les commandes par défaut de Symfony, et sont toutes accessibles avec symfony console. Comme le nombre de commandes disponibles peut être important, vous devez les mettre dans le bon namespace. Par convention, les commandes spécifiques à l’application devraient être stockées sous le namespace app. Ajoutez autant de sous-namespaces que vous le souhaitez en les séparant par deux points (:).

Une commande reçoit l”entrée (les arguments et les options passés à la commande) et vous pouvez utiliser la sortie pour écrire dans la console.

Nettoyez la base de données en exécutant la commande :

1
$ symfony console app:comment:cleanup

Configurer un cron sur SymfonyCloud

L’un des avantages de SymfonyCloud est qu’une bonne partie de la configuration est stockée dans un seul fichier : .symfony.cloud.yaml. Le conteneur web, les workers et les cron jobs sont décrits au même endroit pour faciliter la maintenance :

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
--- a/.symfony.cloud.yaml
+++ b/.symfony.cloud.yaml
@@ -43,6 +43,15 @@ hooks:

         (>&2 symfony-deploy)

+crons:
+    comment_cleanup:
+        # Cleanup every night at 11.50 pm (UTC).
+        spec: '50 23 * * *'
+        cmd: |
+            if [ "$SYMFONY_BRANCH" = "master" ]; then
+                croncape symfony console app:comment:cleanup
+            fi
+
 workers:
     messages:
         commands:

La section crons définit tous les cron jobs. Chaque cron fonctionne selon un planning spécifique (spec).

L’utilitaire croncape surveille l’exécution de la commande et envoie un email aux adresses définies dans la variable d’environnement MAILTO si la commande retourne un code de sortie différent de 0.

Configurez la variable d’environnement MAILTO :

1
$ symfony var:set MAILTO=[email protected]

Vous pouvez forcer un cron à s’exécuter depuis votre machine locale :

1
$ symfony cron comment_cleanup

Notez que les crons sont installés sur toutes les branches de SymfonyCloud. Si vous ne voulez pas en exécuter sur des environnements hors production, vérifiez la variable d’environnement $SYMFONY_BRANCH :

1
2
3
if [ "$SYMFONY_BRANCH" = "master" ]; then
    croncape symfony app:invoices:send
fi

  • « Previous Étape 23: Redimensionner des images
  • Next » Étape 25: Notifier à tout prix

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.