Etap 24: Harmonogram zadań (ang. cron)

5.0 version
Maintained

Harmonogram zadań (ang. cron)

Harmonogram zadań jest bardzo przydatny do wykonywania cyklicznych operacji, np. zadań konserwacyjnych, ponieważ uruchamiane są one według harmonogramu i pracują przez krótki okres czasu, inaczej niż w przypadku wywoływania robotników (ang. workers).

Czyszczenie komentarzy

Komentarze oznaczone jako spam lub odrzucone w panelu administracyjnym są przechowywane w bazie danych, ponieważ w przyszłości może zajść potrzeba ich ponownej weryfikacji. Po pewnym czasie powinny raczej zostać usunięte. Wystarczające wydaje się przechowywanie ich przez tydzień od momentu utworzenia.

W repozytorium komentarzy utwórz kilka przydatnych metod, które pozwolą nam na pobranie listy odrzuconych komentarzy, policzenie ich oraz ich całkowite usunięcie:

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
     {

Wskazówka

W przypadku bardziej złożonych zapytań, czasami przydatne jest zapoznanie się z wygenerowanymi instrukcjami SQL (można je znaleźć w logach oraz w profilerze, w zakładce Doctrine).

Korzystanie ze stałych zawartych w klasach, parametrów kontenera i zmiennych środowiskowych.

Siedem dni? Mogliśmy wybrać inną liczbę, może 10 albo 20. Ta liczba może zmieniać się z upływem czasu. Zdecydowaliśmy się przechowywać ją jako stałą w klasie, ale moglibyśmy przechowywać ją również jako parametr w kontenerze a nawet zdefiniować jako zmienną środowiskową.

Oto kilka zasad decydujących o tym, jakiej abstrakcji użyć:

  • Jeśli wartość zaliczana jest do tzw. danych wrażliwych, takich jak hasła czy klucze API, użyj magazynu danych poufnych (ang. secret storage) Symfony lub sejfu (ang. vault);
  • Jeśli wartość jest dynamiczna i chcesz ją zmienić bez ponownego wdrażania aplikacji, użyj zmiennej środowiskowej;
  • Jeśli wartość może być różna w różnych środowiskach, należy użyć parametru kontenera;
  • W pozostałych przypadkach przechowuj tę wartość w kodzie, np. jako stałą w klasie.

Tworzenie komendy wiersza poleceń

Usuwanie starych komentarzy to działanie idealne do wykorzystania harmonogramu zadań. Powinno się to odbywać regularnie, a niewielkie opóźnienie nie ma większego znaczenia.

Utwórz komendę linii poleceń o nazwie app:comment:cleanup przez utworzenie pliku 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;
    }
}

Wszystkie polecenia naszej aplikacji są zarejestrowane wraz z tymi wbudowanymi w Symfony i są dostępne poprzez symfony console. Ponieważ liczba dostępnych poleceń może być duża, grupuj je w przestrzeniach nazw. Zgodnie z konwencją, polecenia aplikacji powinny być przechowywane w przestrzeni nazw app. Możesz również tworzyć dowolną liczbę „podprzestrzeni” poprzez rozdzielenie ich dwukropkiem (:).

Komenda linii poleceń dostaje wejście (argumenty i opcje przekazywane do komendy) oraz możesz użyć wyjścia w celu wyświetlenia danych w konsoli.

Wyczyść bazę danych uruchamiając polecenie:

1
$ symfony console app:comment:cleanup

Konfigurowanie harmonogramu zadań w usłudze SymfonyCloud

Jedną z zalet SymfonyCloud jest to, że większość konfiguracji jest przechowywana w jednym pliku: .symfony.cloud.yaml. Kontenery serwisów, robotnicy czy zadania z harmonogramu są opisane razem, aby ułatwić zarządzanie nimi:

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:

Sekcja crons definiuje wszystkie zadania z harmonogramu. Każde zadanie działa zgodnie z planem spec.

Narzędzie croncape monitoruje wykonanie polecenia i wysyła wiadomość e-mail na adresy zdefiniowane w zmiennej środowiskowej MAILTO, jeśli polecenie zwróci kod inny niż 0.

Skonfiguruj zmienną środowiskową MAILTO:

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

Możesz wymusić uruchomienie procesu harmonogramu zadań ze swojego lokalnego środowiska:

1
$ symfony cron comment_cleanup

Zauważ, że harmonogramy zadań są ustawione na wszystkich gałęziach SymfonyCloud. Jeśli nie chcesz uruchamiać niektórych z nich na środowiskach nieprodukcyjnych, sprawdź zmienną środowiskową $SYMFONY_BRANCH:

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

  • « Previous Etap 23: Zmienianie rozmiaru obrazów
  • Next » Etap 25: Powiadamianie na wszystkie możliwe sposoby

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