Skip to content

Cron-Jobs ausführen

Cron-Jobs sind nützlich, um Wartungsarbeiten durchzuführen. Im Gegensatz zu Workern laufen sie gemäß einem Zeitplan für einen kurzen Zeitraum.

Kommentare bereinigen

Kommentare, die als Spam markiert oder von Administratorinnen abgelehnt werden, werden in der Datenbank gespeichert, damit Administratorinnen sie noch eine Weile begutachten können. Sie sollten jedoch nach einiger Zeit entfernt werden. Es reicht vermutlich aus, sie für eine Woche zu behalten.

Erstelle ein paar Hilfsmethoden im Kommentar-Repository, um abgelehnte Kommentare zu finden, zu zählen und zu löschen:

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
49
--- a/src/Repository/CommentRepository.php
+++ b/src/Repository/CommentRepository.php
@@ -5,7 +5,9 @@ namespace App\Repository;
 use App\Entity\Comment;
 use App\Entity\Conference;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Persistence\ManagerRegistry;
+use Doctrine\ORM\QueryBuilder;
 use Doctrine\ORM\Tools\Pagination\Paginator;

 /**
@@ -18,6 +20,8 @@ use Doctrine\ORM\Tools\Pagination\Paginator;
  */
 class CommentRepository extends ServiceEntityRepository
 {
+    private const DAYS_BEFORE_REJECTED_REMOVAL = 7;
+
     public const COMMENTS_PER_PAGE = 2;

     public function __construct(ManagerRegistry $registry)
@@ -25,6 +29,27 @@ class CommentRepository extends ServiceEntityRepository
         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')
+            ->setParameter('state_rejected', 'rejected')
+            ->setParameter('state_spam', 'spam')
+            ->setParameter('date', new \DateTimeImmutable(-self::DAYS_BEFORE_REJECTED_REMOVAL.' days'))
+        ;
+    }
+
     public function getCommentPaginator(Conference $conference, int $offset): Paginator
     {
         $query = $this->createQueryBuilder('c')

Tip

Bei komplexeren Abfragen ist es manchmal sinnvoll, sich die erzeugten SQL-Anweisungen anzusehen (sie befinden sich in den Logs und im Profiler für Web-Anfragen).

Klassen-Konstanten, Container-Parameter und Environment-Variablen verwenden

7 Tage? Wir hätten eine andere Zahl wählen können, vielleicht 10 oder 20. Diese Zahl kann sich im Laufe der Zeit ändern. Wir haben beschlossen, sie als Konstante in der Klasse zu speichern, aber wir könnten sie auch als Parameter im Container speichern, oder sogar als Environment-Variable definieren.

Hier sind einige Faustregeln, um zu entscheiden, welche Abstraktion verwendet werden soll:

  • Wenn der Wert sensibel ist (Passwörter, API-Token,...), verwende den Symfony Secret Storage oder einen Vault;
  • Wenn der Wert dynamisch ist und Du ihn ändern können musst, ohne erneut zu deployen, verwende eine Environment-Variable;
  • Wenn der Wert zwischen den Environments unterschiedlich sein kann, verwende einen Container-Parameter;
  • Für alles andere setzt Du den Wert im Code, zum Beispiel in einer Klassenkonstanten.

Ein CLI-Befehl erstellen

Das Entfernen der alten Kommentare ist die perfekte Aufgabe für einen Cron-Job. Es sollte regelmäßig durchgeführt werden, und eine kleine Verzögerung hat keine größeren Auswirkungen.

Erstelle einen CLI-Befehl app:comment:cleanup, indem Du eine src/Command/CommentCleanupCommand.php-Datei anlegst:

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
namespace App\Command;

use App\Repository\CommentRepository;
use Symfony\Component\Console\Attribute\AsCommand;
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;

#[AsCommand('app:comment:cleanup', 'Deletes rejected and spam comments from the database')]
class CommentCleanupCommand extends Command
{
    public function __construct(
        private CommentRepository $commentRepository,
    ) {
        parent::__construct();
    }

    protected function configure()
    {
        $this
            ->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 Command::SUCCESS;
    }
}

Alle Anwendungsbefehle werden parallel zu den in Symfony eingebauten Befehlen registriert und sind über symfony console erreichbar. Da die Anzahl der verfügbaren Befehle groß sein kann, solltest Du ihnen einen Namespace geben. Nach Konvention sollten die Anwendungsbefehle unter dem app-Namespace abgelegt werden. Du kannst beliebig viele Sub-Namespaces hinzufügen, indem Du diese durch einen Doppelpunkt (:) trennst.

Ein Befehl erhält die Eingabe (Input; Argumente und Optionen, die an den Befehl übergeben wurden) und Du kannst die Ausgabe (Output) verwenden, um Information in der Konsole auszugeben.

Bereinige die Datenbank, indem Du diesen Befehl ausführst:

1
$ symfony console app:comment:cleanup

Einen Cron-Job bei Platform.sh einrichten

Das Schöne an Platform.sh ist, dass der Großteil der Konfiguration in einer Datei gespeichert ist: .platform.app.yaml. Der Webcontainer, die Worker und die Cron-Jobs werden gemeinsam definiert, um die Wartung zu erleichtern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--- a/.platform.app.yaml
+++ b/.platform.app.yaml
@@ -61,6 +61,14 @@ crons:
         spec: '50 23 * * *'
         cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape php-security-checker; fi

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

Der crons-Abschnitt definiert alle Cron-Jobs. Jeder Cron-Job läuft nach einem spec-Zeitplan.

Das croncape-Dienstprogramm überwacht die Ausführung des Befehls und sendet eine E-Mail an die in der Environment-Variable MAILTO definierten Adressen, wenn der Befehl einen anderen Exit-Code als 0 hat.

Konfiguriere die Environment-Variable MAILTO:

1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:MAILTO --value=ops@example.com

Beachte, dass Cron-Jobs auf allen Platform.sh-Branches eingerichtet sind. Überprüfe die Environment-Variable $PLATFORM_ENVIRONMENT_TYPE wenn Du keine Cron-Jobs in Nicht-Production-Environments ausführen möchtest:

1
2
3
if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then
    croncape symfony app:invoices:send
fi
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version