Pianificare le attività
Alcune attività di manutenzione devono essere eseguite secondo una pianificazione. A differenza dei worker, che girano in continuazione, le attività pianificate vengono eseguite periodicamente per un breve periodo di tempo.
Pulire i commenti
I commenti contrassegnati come spam o rifiutati dall'amministratore sono conservati nel database, in quanto l'amministratore potrebbe volerli ispezionare per un po' di tempo. Ma probabilmente dovrebbero essere rimossi in un secondo momento. Probabilmente è sufficiente conservarli per una settimana.
Creare alcuni metodi nel repository dei commenti: per trovare i commenti rifiutati, per contarli e per cancellarli:
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
--- i/src/Repository/CommentRepository.php
+++ w/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;
/**
@@ -13,6 +15,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)
@@ -20,6 +24,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
Per le query più complesse, a volte è utile dare un'occhiata alle istruzioni SQL generate (si possono trovare nei log e nel profiler per le richieste web).
Utilizzo delle costanti di classe, dei parametri del container e delle variabili d'ambiente
Sette giorni? Avremmo potuto scegliere un altro numero, forse dieci o venti. Questo numero potrebbe evolvere nel tempo. Abbiamo deciso di memorizzarlo come costante di classe, ma avremmo potuto memorizzarlo come parametro nel container o ancora come variabile d'ambiente.
Ecco alcune regole empiriche per decidere quale astrazione usare:
- Se il valore deve essere mantenuto segreto (password, token API, ...), usare il portachiavi di Symfony o un sistema esterno di portachiavi;
- Se il valore è dinamico e lo si può cambiare senza dover rifare un deploy, usare una variabile d'ambiente;
- Se il valore può essere diverso da un ambiente all'altro, usare un parametro del container;
- Per tutto il resto, memorizzare il valore nel codice, come costante di classe.
Creazione di un comando CLI
La rimozione dei vecchi commenti è il compito perfetto per un cron. Andrebbe fatta su base regolare e un piccolo ritardo non ha un impatto significativo.
Creare un comando CLI denominato app:comment:cleanup, creando un file 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
namespace App\Command;
use App\Repository\CommentRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand('app:comment:cleanup', 'Deletes rejected and spam comments from the database')]
class CommentCleanupCommand
{
public function __invoke(
SymfonyStyle $io,
CommentRepository $commentRepository,
#[Option(description: 'Dry run')]
bool $dryRun = false,
): int {
if ($dryRun) {
$io->note('Dry mode enabled');
$count = $commentRepository->countOldRejected();
} else {
$count = $commentRepository->deleteOldRejected();
}
$io->success(sprintf('Deleted "%d" old rejected/spam comments.', $count));
return Command::SUCCESS;
}
}
Tutti i comandi dell'applicazione sono registrati insieme a quelli integrati in Symfony e sono tutti accessibili tramite symfony console. Poiché il numero di comandi disponibili potrebbe essere elevato, dovremmo raggrupparli per nome. Per convenzione, i comandi dell'applicazione vengono raggruppati sotto il namespace app. Si possono aggiungere ulteriori namespace, separati con i due punti (:).
Un comando dichiara i suoi parametri e le sue opzioni con gli attributi #[Argument] e #[Option] sui parametri di __invoke() (il parametro $dryRun diventa l'opzione --dry-run). Symfony inietta gli altri parametri in base al loro tipo: SymfonyStyle per scrivere in console un output ben formattato, e qualsiasi servizio, come il repository dei commenti, allo stesso modo degli argomenti dei controller.
Pulire il database eseguendo il comando:
1
$ symfony console app:comment:cleanup
Pianificare il comando
Eseguire il comando a mano funziona, ma dovrebbe girare ogni notte. Il componente Symfony Scheduler genera messaggi secondo una pianificazione; vengono poi consumati da un worker, come qualsiasi altro messaggio Messenger.
Aggiungere il componente Scheduler, insieme alla libreria che analizza le espressioni cron:
1
$ symfony composer req scheduler dragonmantank/cron-expression
Pianificare il comando con l'attributo #[AsCronTask]:
1 2 3 4 5 6 7 8 9 10 11 12 13
--- i/src/Command/CommentCleanupCommand.php
+++ w/src/Command/CommentCleanupCommand.php
@@ -7,8 +7,10 @@ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Scheduler\Attribute\AsCronTask;
#[AsCommand('app:comment:cleanup', 'Deletes rejected and spam comments from the database')]
+#[AsCronTask('50 23 * * *')]
class CommentCleanupCommand
{
public function __invoke(
L'attributo registra il comando sulla pianificazione (schedule) predefinita con un'espressione cron: ogni notte alle 23:50 (UTC). Verificarlo:
1
$ symfony console debug:scheduler
Una pianificazione viene esposta come un normale trasporto Messenger che ne porta il nome; consumarla come qualsiasi altro trasporto:
1
$ symfony run -d symfony console messenger:consume scheduler_default -vv
Distribuire la pianificazione
Su Upsun, il worker consuma solo il trasporto async. Fargli consumare anche la pianificazione:
1 2 3 4 5 6 7 8
--- i/.upsun/config.yaml
+++ w/.upsun/config.yaml
@@ -87,4 +87,4 @@ applications:
messenger:
commands:
# Consume "async" messages (as configured in the routing section of config/packages/messenger.yaml)
- start: symfony console --time-limit=3600 --memory-limit=64M messenger:consume async
+ start: symfony console --time-limit=3600 --memory-limit=64M messenger:consume async scheduler_default
Non serve altro: nessun crontab, nessun processo aggiuntivo; la pianificazione vive nel codice PHP, accanto all'attività che attiva, e viene distribuita e versionata come il resto dell'applicazione.
E i cron di sistema?
Upsun supporta anche i cron a livello di sistema operativo, descritti in .upsun/config.yaml accanto al container web e ai worker; la configurazione predefinita ne definisce già uno che ripulisce le sessioni PHP scadute. I cron di sistema sono adatti alle attività che non sono implementate in PHP.
L'utility croncape usata dal cron predefinito monitora l'esecuzione del comando e invia un'email agli indirizzi definiti nella variabile d'ambiente MAILTO, se il comando restituisce un codice di uscita diverso da 0:
1
$ symfony cloud:variable:create --sensitive=1 --level=project -y --name=env:MAILTO --value=ops@example.com
Si noti che i cron vengono impostati su tutti i rami di Upsun. Se non si vuole eseguirne alcuni in ambienti diversi dalla produzione, verificare la variabile d'ambiente $PLATFORM_ENVIRONMENT_TYPE:
1 2 3
if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then
croncape symfony app:invoices:send
fi