Nuevo Componente: Programador
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeUno de los componentes nuevos más chulos es Scheduler, que viene de Symfony 6.3. Si necesitas activar una tarea recurrente, como generar un informe semanal, enviar algún tipo de latido cada 10 minutos, realizar un mantenimiento rutinario... o incluso algo personalizado y raro, este componente es para ti. ¡Es realmente genial! Merece su propio tutorial, pero nos preocuparemos de eso más adelante. Vamos a probarlo.
Instalación del Programador
En tu línea de comandos, instálalo con:
composer require symfony/scheduler symfony/messenger
Scheduler se basa en Messenger: ¡funcionan juntos! El proceso es el siguiente. Creas una clase de mensaje y un manejador, como harías normalmente con Messenger. Luego le dices a Symfony
¡Eh! Quiero que envíes este mensaje para que se gestione cada siete días, o cada una hora... o algo más raro.
Crear la clase y el manejador de mensajes
Esto significa que el primer paso es generar un mensaje Messenger. Ejecuta:
php bin/console make:message
Llámalo LogHello. ¡Genial! Aquí ha creado la clase mensaje - LogHello
| // ... lines 1 - 2 | |
| namespace App\Message; | |
| final class LogHello | |
| { | |
| public function __construct() | |
| { | |
| } | |
| } |
y su manejador, cuyo método __invoke() será llamado cuando LogHello se envíe a través de Messenger.
| // ... lines 1 - 2 | |
| namespace App\MessageHandler; | |
| use App\Message\LogHello; | |
| use Symfony\Component\Messenger\Attribute\AsMessageHandler; | |
| final class LogHelloHandler | |
| { | |
| public function __construct() | |
| { | |
| } | |
| public function __invoke(LogHello $message) | |
| { | |
| } | |
| } |
En LogHello, dale un constructor con public int $length.
| // ... lines 1 - 2 | |
| namespace App\Message; | |
| final class LogHello | |
| { | |
| public function __construct(public int $length) | |
| { | |
| } | |
| } |
Esto nos ayudará a saber qué mensaje se está gestionando y cuándo. En el manejador, añade también un constructor para que podamos autocablear LoggerInterface $logger.
| // ... lines 1 - 5 | |
| use Psr\Log\LoggerInterface; | |
| // ... lines 7 - 9 | |
| final class LogHelloHandler | |
| { | |
| public function __construct(private LoggerInterface $logger) | |
| { | |
| } | |
| // ... lines 15 - 19 | |
| } |
Abajo en el método, utiliza $this->logger->warning() -sólo para que estas entradas de registro sean fáciles de ver- y luego str_repeat() para registrar un icono de guitarra $message->lengthveces. También registraré ese número al final.
| // ... lines 1 - 5 | |
| use Psr\Log\LoggerInterface; | |
| // ... lines 7 - 9 | |
| final class LogHelloHandler | |
| { | |
| public function __construct(private LoggerInterface $logger) | |
| { | |
| } | |
| public function __invoke(LogHello $message) | |
| { | |
| $this->logger->warning(str_repeat('🎸', $message->length).' '.$message->length); | |
| } | |
| } |
¡Comprobación de mensajes y manejadores!
Crear el programa
Lo siguiente es crear un horario que le diga a Symfony:
Yo, otra vez. Por favor, envía un mensaje
LogHelloa través de Messenger cada 7 días.
O en nuestro caso, ¡cada pocos segundos porque no creo que quieras ver este screencast durante la próxima semana!
En src/, no tengo que hacer esto, pero crearé un directorio Scheduler. Y dentro, una clase PHP llamada, qué tal, MainSchedule. Haz que esto implementeScheduleProviderInterface.
| // ... lines 1 - 2 | |
| namespace App\Scheduler; | |
| // ... lines 4 - 6 | |
| use Symfony\Component\Scheduler\ScheduleProviderInterface; | |
| // ... lines 8 - 9 | |
| class MainSchedule implements ScheduleProviderInterface | |
| { | |
| // ... lines 12 - 14 | |
| } |
Puedes tener varios de estos proveedores de programación en tu sistema... o puedes tener una clase que configure todos tus mensajes recurrentes. Tú decides.
Esta clase también necesita un atributo llamado #[AsSchedule]. Tiene un argumento opcional: el nombre de la programación, que, creativamente, es por defecto default. Pronto veremos por qué es importante ese nombre. Yo utilizaré default.
| // ... lines 1 - 2 | |
| namespace App\Scheduler; | |
| use Symfony\Component\Scheduler\Attribute\AsSchedule; | |
| // ... line 6 | |
| use Symfony\Component\Scheduler\ScheduleProviderInterface; | |
| // ... line 8 | |
| class MainSchedule implements ScheduleProviderInterface | |
| { | |
| // ... lines 12 - 14 | |
| } |
Crear los mensajes recurrentes
Bien, ve a Código -> Generar, o comando+N en un Mac - para implementar el único método que necesitamos: getSchedule().
| // ... lines 1 - 2 | |
| namespace App\Scheduler; | |
| use Symfony\Component\Scheduler\Attribute\AsSchedule; | |
| use Symfony\Component\Scheduler\Schedule; | |
| use Symfony\Component\Scheduler\ScheduleProviderInterface; | |
| class MainSchedule implements ScheduleProviderInterface | |
| { | |
| public function getSchedule(): Schedule | |
| { | |
| } | |
| } |
El código aquí es maravillosamente sencillo y expresivo. Devuelve un new Schedule(), luego añade cosas a éste llamando a ->add(). Dentro, por cada "cosa" que necesites programar, di RecurringMessage::. Hay varias formas de crear estos mensajes recurrentes. La más sencilla es every(), como cada 7 days o cada 5 minutes. También puedes pasar una sintaxis cron, o llamar a trigger(). En ese caso, definirías tu propia lógica para saber exactamente cuándo quieres que se active tu mensaje raro.
Utiliza every() y pasa 4 seconds. Cada 4 segundos, queremos que este nuevo mensaje LogHello se envíe a Messenger. Cópialo y crea otro para cada 3 seconds.
| // ... lines 1 - 4 | |
| use App\Message\LogHello; | |
| // ... line 6 | |
| use Symfony\Component\Scheduler\RecurringMessage; | |
| // ... lines 8 - 11 | |
| class MainSchedule implements ScheduleProviderInterface | |
| { | |
| public function getSchedule(): Schedule | |
| { | |
| return (new Schedule())->add( | |
| RecurringMessage::every('4 seconds', new LogHello(4)), | |
| RecurringMessage::every('3 seconds', new LogHello(3)), | |
| ); | |
| } | |
| } |
¡Ya está!
Consumir el transporte programador
El resultado de crear un proveedor de programación es que se crea un nuevo transporte de Messenger. Para que se procesen tus mensajes recurrentes, necesitas tener un trabajador que esté ejecutando el comando messenger:consume.
En tu terminal, ejecuta bin/console messenger:consume con un -v para que podamos ver los mensajes de registro de nuestro manejador. A continuación, pasa el nombre del nuevo transporte añadido automáticamente: scheduler_default... donde default es el nombre que utilizamos en el atributo#[AsSchedule].
php bin/console messenger:consume -v scheduler_default
Dale, espera unos 3 segundos... ¡ahí está! ¡Cuatro! Luego vuelve a aparecer el 3, y cuatro, y luego tres. Al cabo de 12 segundos, deberían ejecutarse, sí, casi en el mismo momento. Técnicamente, éste se despachó primero, y aquél se despachó inmediatamente después.
Pero, permíteme que deje de flipar y retroceda: ¡funciona! ¡Es precioso!
¿Cómo funciona el Programador?
¿Cómo funciona? Yo me preguntaba lo mismo. Cuando se inicia el comando trabajador, hace un bucle sobre cada RecurringMessage, calcula el próximo tiempo de ejecución de cada uno y lo utiliza para crear una lista -llamada "montón"- de próximos mensajes. A continuación, realiza un bucle sin fin. En cuanto la hora actual coincide -o es posterior- al tiempo de ejecución programado del siguiente mensaje de la pila, toma ese mensaje y lo envía a través de Messenger. A continuación, pide a este mensaje recurrente su siguiente tiempo de ejecución y lo coloca en el montón.
Y este proceso... continúa para siempre.
Haz que tu Programación tenga Estado
Aunque hay un problema que se esconde a plena vista: si reiniciamos el comando, crea la programación desde cero. Eso significa que espera tres segundos y cuatro segundos nuevos antes de enviar los mensajes.
En una aplicación real, esto será un problema. Imagina que tienes un mensaje que se ejecuta cada 7 días. Por alguna razón, al cabo de 5 días, tu comando messenger:consume sale y se reinicia. Debido a esto, tu mensaje recurrente se ejecutará ahora siete días después de este reinicio: así que se ejecutará el día 12. Si se sigue reiniciando, ¡puede que tu mensaje no se ejecute nunca!
Esto no es factible. Por eso, en el mundo real, siempre hacemos que nuestra programación tenga estado. Y esto es fácil. Crea un método __construct y autoconecta unprivate CacheInterface: el de la caché de Symfony.
| // ... lines 1 - 9 | |
| use Symfony\Contracts\Cache\CacheInterface; | |
| // ... lines 11 - 12 | |
| class MainSchedule implements ScheduleProviderInterface | |
| { | |
| public function __construct( | |
| private CacheInterface $cache, | |
| ) | |
| { | |
| } | |
| // ... lines 21 - 29 | |
| } |
A continuación, llama a ->stateful() y pásale $this->cache.
| // ... lines 1 - 9 | |
| use Symfony\Contracts\Cache\CacheInterface; | |
| // ... lines 11 - 12 | |
| class MainSchedule implements ScheduleProviderInterface | |
| { | |
| public function __construct( | |
| private CacheInterface $cache, | |
| ) | |
| { | |
| } | |
| // ... line 21 | |
| public function getSchedule(): Schedule | |
| { | |
| return (new Schedule())->add( | |
| // ... lines 25 - 26 | |
| ) | |
| ->stateful($this->cache); | |
| } | |
| } |
Además, abre services.yaml. En un tutorial anterior, añadí alguna configuración que desactivaba efectivamente la caché en el entorno dev. Elimínalo para que tengamos una caché adecuada.
Bien, detén el trabajador y reinícialo. La primera vez que hagamos esto, tendrá el mismo comportamiento que antes: esperar tres segundos y cuatro segundos, ya está.
Pero ahora, detén esto, espera unos segundos y observa lo que ocurre cuando reinicie. ¡Se pone al día! ¡Esos mensajes ocurrieron inmediatamente!
El estado lleva la cuenta de la última vez que el Programador comprobó si había mensajes. Y así, si tu trabajador se apaga durante un rato, cuando se reinicia, lee esa hora y la utiliza como hora de inicio para ponerse al día con todos los mensajes que se perdió.
Esto significa que puedes tener algunos mensajes que se ejecuten varias veces inmediatamente, pero no se perderá nada.
Múltiples Trabajadores: Bloquea tu Programación
Ah, y si planeas tener múltiples trabajadores para tu transporte programador, también necesitarás añadir un bloqueo a la programación. Esto es fácil y está cubierto en los documentos: autocablea la fábrica de bloqueos, luego llama a ->lock() para pasar un nuevo bloqueo. Esto asegurará que dos trabajadores no cojan el mismo mensaje recurrente al mismo tiempo y ambos lo procesen.
Muy bien equipo, ¡eso es todo lo que tengo! Gracias por esperar. Si tienes alguna pregunta sobre la actualización o te has encontrado con algún problema que no hayamos mencionado, estamos a tu disposición en los comentarios. Y avísanos si consigues una victoria: nos encanta escuchar éxitos.
Muy bien, amigos. ¡Hasta la próxima!
18 Comments
I have a collection of Symfony console commands that I would like to run using the scheduler at different times but I would like to be able to manage these commands and their CRON expression from the database.
So I have created a `ScheduledCommand ' entity with properties such as
app:send-api-create,*/5 * * * *, andI manage these ScheduledCommands objects from the UI.
Then I have the Schedule.php, what would be the best way to update the scheduler in real-time, for instance, if the command is changed (arguments/options updated) or the CRON expression is altered in the database, it also reflects in the scheduler and consumer picks up the new changes in real time?
Currently, if I have the consumer running in the terminal and any command is changed from the UI, it won't be reflected in the consumer.
bin/console messenger:consume scheduler_default -vvHey @GhazanfarMir
I think you can do it by implementing a custom scheduler provider that reads your database and set up the schedule. Something like this
Then, you can start the consumer worker to only dispatch a low amount of messages so it restarts often and it catch up with the changes.
Also, here's info on how to modify scheduled messages in real time: https://symfony.com/doc/current/scheduler.html#modifying-scheduled-messages-in-real-time
Hope it helps. Cheers!
Does anybody know how to deploy a symfony app with scheduler on azure? I have an ASP with a PHP webapp on azure. I think best practice would be to use a docker image instead?
If so, which docker image would you recommend for production?
Hey Mike-Profile,
Well, probably Azure docs will suggest some? Maybe they have their own Docker images, not sure. Can't really suggest a good one as we don't use Docker, but I think if there's no any Azure-specific images, you can always go with the official Docker images for the technologies you're using, I bet that's a good fallback option.
About specific deploy, I haven't used Azure personally, but usually you can deploy everything with Ansible and Ansistrano. Take a look at the Ansistrano course we have on SymfonyCasts: https://symfonycasts.com/screencast/ansistrano
I hope that helps!
Cheers!
Is this correct?
"Previously, Symfony had separate commands for processing scheduled tasks and regular messages:
These processes operated independently, requiring separate execution to handle both scheduled and regular messages. 
In Symfony 7.2, the Scheduler has been fully integrated into the Messenger system. Scheduled tasks are now treated as Messenger messages, eliminating the need for a separate scheduler:consume command. Instead, the messenger:consume command processes both regular and scheduled messages.
Therefore, to process scheduled tasks in Symfony 7.2, you should use the messenger:consume command with the appropriate transport:
php bin/console messenger:consume async -vv
This command will handle both regular and scheduled messages, streamlining the message processing workflow."
Hey @Mike-Profile!
No, I don't believe there was ever a
scheduler:consume. Even when the Scheduler was introduced in 6.3, it wasmessenger:consume.Where did you find that note?
--Kevin
Thank you! It was my mistake, a personal readme note in combination with AI hallucination.
Hah, no problem. I suspected AI hallucination!
I use to clear cache every time I push changes. It affects the scheduler cache? clear cache with cache:clear after pushing changes is not a good practice?
Hey @Adrian_Ntss!
You have configured a stateful schedule? You'll need to use a cache that doesn't get cleared when you deploy (or run
cache:clearon production). This could be something like Redis or even a different filesystem cache in a different directory (that's kept between deploys).In the documentation they use "(new Schedule())->with()", but here you are using "(new Schedule())->add()".
Why?
Hey @Nick-F! Good catch!
These methods are almost the same but have one subtle difference, and this only applies under certain circumstances.
with()can be used at runtime to add additional tasks dynamically (without restarting the schedule).add()ensures the schedule is reset before adding.If using the Scheduler the standard way, and how we use it in this chapter, it doesn't matter which method you use. The schedule is built when it's first started so it's always reset.
If you require adding tasks at runtime, a more advanced usage, you'd use
with().Hope this helps!
Hello,
I don't get one thing...
I've created a scheduler, which executes every 7 seconds (I don't add any lock, as you can see):
This scheduler sometimes runs very hard tasks (> 7 seconds), and it looks like next scheduler tick isn't triggered until previous tick isn't finished. If some scheduler tick executes, let's say, 2 minutes, there is no next scheduler ticks (every 7 seconds) during those 2 minutes.
I expected, that scheduler starts some task and handles it in background, so "7 seconds" works like a clock (like unix cron). But it looks like some blocking process is present, and next scheduler ticks "waiting" for previous task finishing.
Am I right or I miss something?
PS. Just in case - here are my scheduler process settings (in supervisor conf):
Hey @BooleanType!
Yes, when a scheduled message is dispatched, it blocks the process (preventing schedule ticks). As you've noted, this is unlike a standard unix cron job. From what you describe, the schedule likely never catches up.
I think your best bet would be to have another worker running a standard
messenger:consumeon a non-schedule transport (likeasync). Then, when creating the schedule, wrap your message withRedispatchMessage:This will have the schedule only dispatch another message, freeing it up to be available for the next tick.
You can find some documentation on this at the end of this section.
Let me know if you have any luck with this!
Thank you for such a detailed answer, now it’s clearer to me!
For now, I gave preference to the good old cron, leaving the scheduler “until better times” :)
Is it good to use this component for database and/or images upload daily backups?
Hey @Kirill
I've not used it for tasks like that but I'd say yes, it should work, the only issue I can think of is if the backup takes too much time to complete, it may delay other tasks to execute. If that's the case you could configure it to run at midnight (or at your lowest traffic hours), or set up a dedicated worker for this Schedule
Cheers!
Thank you
"Houston: no signs of life"
Start the conversation!