Preskočiť na obsah

Odložená pripomienka ​

Cieľ: poslať e-mail s pripomienkou deň pred rezerváciou. Správa sa vloží pri vytvorení rezervácie s executeAt nastaveným na čas pripomienky.

Súbory ​

Redis, Mailpit a src/Hermes.php ako v návode na uvítací e-mail.

json
{
    "require": {
        "php": ">=8.4",
        "tomaj/hermes": "dev-master",
        "predis/predis": "^2.3 || ^3.0",
        "symfony/mailer": "^7.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}
php
<?php

declare(strict_types=1);

use App\Hermes;
use Tomaj\Hermes\Message;

require __DIR__ . '/../vendor/autoload.php';

// A customer reserved a table. Remind them one day before, or in REMIND_IN seconds while testing.
$reservation = ['id' => 77, 'email' => 'jane@example.com', 'at' => strtotime('+2 days 19:00')];
$remindAt = (float) (getenv('REMIND_IN') ? time() + (int) getenv('REMIND_IN') : $reservation['at'] - 86400);

Hermes::emitter()->emit(new Message(
    'reservation.remind',
    ['reservation_id' => $reservation['id'], 'email' => $reservation['email']],
    executeAt: $remindAt, // the worker will not run it before this time; id and created are generated
));

echo 'Reminder scheduled for ' . date('Y-m-d H:i:s', (int) $remindAt) . "\n";
php
<?php

declare(strict_types=1);

namespace App;

use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Tomaj\Hermes\Handler\RetryableHandlerInterface;
use Tomaj\Hermes\Handler\RetryTrait;
use Tomaj\Hermes\MessageInterface;

final class ReminderHandler implements RetryableHandlerInterface
{
    use RetryTrait;

    public function __construct(private MailerInterface $mailer) {}

    #[\Override]
    public function handle(MessageInterface $message): bool
    {
        $payload = $message->getPayload() ?? [];

        // The message was written a day ago. Check that it still applies.
        if (!$this->stillBooked((int) $payload['reservation_id'])) {
            return true;
        }

        $email = new Email();
        $email
            ->from('bookings@example.com')
            ->to($payload['email'])
            ->subject('See you tomorrow')
            ->text('This is a reminder of your reservation tomorrow.');
        $this->mailer->send($email);

        return true;
    }

    private function stillBooked(int $reservationId): bool
    {
        return true; // replace with a database lookup
    }
}
php
<?php

declare(strict_types=1);

use App\Hermes;
use App\ReminderHandler;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Tomaj\Hermes\Dispatcher;
use Tomaj\Hermes\Shutdown\PredisShutdown;

require __DIR__ . '/../vendor/autoload.php';

$redis = Hermes::redis();
$driver = Hermes::driver($redis);
$driver->setShutdown(new PredisShutdown($redis));

$mailer = new Mailer(Transport::fromDsn(getenv('MAILER_DSN') ?: 'smtp://127.0.0.1:1025'));

$dispatcher = new Dispatcher($driver);
$dispatcher->registerHandler('reservation.remind', new ReminderHandler($mailer));
$dispatcher->handle();

Spustenie ​

REMIND_IN prepíše oneskorenie, aby ste nemuseli čakať deň:

bash
composer install
docker compose up -d
php bin/worker.php                    # terminal 1
REMIND_IN=10 php public/index.php     # terminal 2
docker compose exec redis redis-cli zrange hermes_schedule 0 -1 withscores

Správa čaká v hermes_schedule so skóre rovným času pripomienky. Asi o desať sekúnd ju worker presunie do živej fronty, spracuje ju a v Mailpite na http://localhost:8025 sa objaví „See you tomorrow“.

Úskalia ​

  • Vo verzii 5.x spoľahlivo odkladá len Predis. RedisSetDriver (ext-redis) v 5.x zapisuje do zoradenej množiny, ale splatné správy nikdy nevráti (#95, opravené v 6.0). RabbitMQ, SQS a Kafka ignorujú executeAt a doručia hneď. Pozrite Odložené spracovanie.
  • Stav kontrolujte v čase spustenia správy. Deň je dlhý: rezervácia sa môže zrušiť alebo presunúť. V handleri ju načítajte znova a ak už neplatí, nerobte nič.
  • Musí bežať worker. Splatné správy sa presúvajú len vo wait(). Ak v čase pripomienky žiadny worker nebeží, pripomienka príde neskoro, ale nestratí sa.
  • executeAt je Unix časová značka v sekundách ako float. Počítajte ju v UTC, napríklad cez strtotime() alebo DateTimeImmutable::getTimestamp(), aby ju časové pásma nemohli posunúť.
  • Pripomienku nemeňte úpravou správy. Vložte novú a nech handler staré ignoruje, napríklad uložením id aktuálnej pripomienky k rezervácii.

Celý zdrojový kód ​

Všetky súbory tohto receptu, s README.md, Makefile a kontrolou e2e.sh, sú v examples/reminder na GitHube. Naklonujte repozitár, spustite cd examples/reminder a make install up, potom make e2e.

Časté otázky ​

Môžem na pripomienky použiť ext-redis? ​

Áno, od 6.0, ktorú tieto recepty vyžadujú. Vo verzii 5.x jeho krok presunu necháva splatné správy v zoradenej množine, preto tam použite Predis. Pozrite Odložené spracovanie.

Čo ak sa rezervácia zruší? ​

Handler ju načíta znova a ak už neplatí, nerobí nič.

Čo ak v čase pripomienky nebeží žiadny worker? ​

Pripomienka sa spustí neskôr, hneď ako sa worker spustí. Nestratí sa.

Ako pripomienku presunúť? ​

Vložte novú správu a nech handler starú ignoruje, napríklad uložením id aktuálnej pripomienky.

V akých jednotkách je executeAt? ​

Unix časová značka v sekundách ako float.

Vydané pod licenciou MIT.