Mowa w dzisiejszym wpisie o Symfony Scheduler. Jakże długo czekałem w świecie PHP na takie narzędzie, jakże długo zazdrościłem programistom Javy używającym w Java EE adnotacji @Schedule(dayOfWeek="Sun", hour="0")
, dzięki której ich funkcja uruchamiała się w odpowiednim czasie. Tymczasem my musieliśmy kombinować z konfiguracją i całym tym boilerplate code w CRON-ie, który nigdy nie chciał współpracować po dobroci i ładnie integrować się z naszą aplikacją. Na szczęście to już przeszłość, ponieważ od Symfony 6.4 mamy do dyspozycji Symfony Scheduler. A co to takiego? To analogiczny do Javy planista, który również obsługuje adnotacje (atrybuty), dzięki czemu nasza klasa (handler) może być uruchamiana w sposób podobny do wykonywania jej przez unixowy CRON.
Jak zaimplementować to we własnym projekcie? Po pierwsze, musimy dodać tę bibliotekę:
composer require symfony/messenger symfony/scheduler
Widać tutaj, że potrzebujemy również Symfony Messenger, a to dlatego, że Scheduler jest właściwie nakładką na Messenger, która dostarcza własny interfejs. Zadania są w rzeczywistości wykonywane co sekundę przez worker Messenger, ale to planista decyduje, czy konkretne zadanie ma się rzeczywiście rozpocząć. Dlatego po instalacji należy skonfigurować uruchamianie workera, wydając polecenie:
bin/console messenger:consume -v scheduler_default
(default to nazwa domyślnej grupy zadań). Potrzebujemy jeszcze wiadomości, handlera i samej klasy definiującej częstotliwość uruchamiania się danej wiadomości. Może to wyglądać w ten sposób:
#[AsSchedule(name: 'default')] class MySchedulerProvider implements ScheduleProviderInterface { public function getSchedule(): Schedule { return (new Schedule())->add( RecurringMessage::every('3 days', new MyMessage()) ); } }
Zatem nasza wiadomość MyMessage
będzie wysyłana do swojego handlera co trzy dni. Można oczywiście nadawać wiele interwałów, a także korzystać z różnych kombinacji w określaniu odpowiedniego czasu (wspierana jest również składnia CRON-a). Polecam zapoznanie się z dokumentacją. Co jednak w przypadku, gdy dane zadanie musi zostać wykonane dwa lub więcej razy w tym samym momencie (bo wynika to z harmonogramu)? Jeśli mamy tylko jednego workera, sprawa jest prosta – zadania są uruchamiane jedno po drugim. W innym przypadku trzeba sięgnąć po mechanizm cache i Symfony Lock. Przykładowo:
(…) public function getSchedule(): Schedule { return (new Schedule()) ->add(RecurringMessage::every('3 days', new MyMessage())) ->stateful($this->cache) ->lock($this->lockFactory->createLock('scheduler-default')); }
Wcześniej oczywiście należy wstrzyknąć właściwości cache
i lockFactory
. Dzięki temu będziemy mieć pewność, że dane zadanie będzie wykonywane jedno po drugim.
Co z zadaniami, które mogą trwać naprawdę długo? Dobrą praktyką jest trzymanie się zasady, by zadania były krótkie w realizacji. Jeśli jednak jest to niemożliwe, można skorzystać z opcji przeniesienia (redispatch) zadania do kolejki asynchronicznej:
(new Schedule())->add( RecurringMessage::cron('15 4 */3 * *', new RedispatchMessage(new MyMessage(), 'async'))) );
Na koniec warto wspomnieć o jeszcze dwóch przydatnych opcjach: atrybucie AsPeriodicTask
i poleceniu debug:scheduler
. Atrybut AsPeriodicTask
pozwala nam użyć polecenia lub dowolnej innej klasy jako zadania wykonywanego w zdefiniowanym przez ten atrybut interwale. Natomiast polecenie bin/console debug:scheduler
dostarcza cennych wskazówek do debugowania, takich jak informacje o tym, kiedy i jakie zadania mają się wykonać. …Tak więc teraz pozostaje już tylko zdefiniować datę publikacji tego wpisu!