├── .codecov.yml
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── doc
├── cli-commands.md
├── define-schedule.md
├── define-tasks.md
├── extending.md
├── images
│ ├── schedule-list-with-issues.png
│ ├── schedule-list.png
│ ├── schedule-run-error.png
│ └── schedule-run.png
└── run-schedule.md
├── phpstan-baseline.neon
├── phpstan.neon
├── phpunit.xml.dist
└── src
├── Attribute
└── AsScheduledTask.php
├── Command
├── ScheduleListCommand.php
└── ScheduleRunCommand.php
├── DependencyInjection
├── Compiler
│ ├── ScheduleBuilderKernelPass.php
│ └── ScheduledServiceBuilderPass.php
├── Configuration.php
└── ZenstruckScheduleExtension.php
├── Event
├── AfterScheduleEvent.php
├── AfterTaskEvent.php
├── BeforeScheduleEvent.php
├── BeforeTaskEvent.php
├── BuildScheduleEvent.php
├── ScheduleEvent.php
└── TaskEvent.php
├── EventListener
├── ScheduleBuilderSubscriber.php
├── ScheduleConsoleOutputSubscriber.php
├── ScheduleExtensionSubscriber.php
├── ScheduleLoggerSubscriber.php
├── ScheduleTimezoneSubscriber.php
├── SelfSchedulingCommandSubscriber.php
└── TaskConfigurationSubscriber.php
├── Resources
└── config
│ ├── http.xml
│ ├── mailer.xml
│ ├── messenger.xml
│ ├── notifier.xml
│ ├── process.xml
│ ├── services.xml
│ ├── single_server.xml
│ ├── timezone.xml
│ └── without_overlapping.xml
├── Schedule.php
├── Schedule
├── Builder
│ └── ScheduledServiceBuilder.php
├── CronExpression.php
├── Exception
│ ├── MissingDependency.php
│ ├── SkipSchedule.php
│ └── SkipTask.php
├── Extension
│ ├── BetweenTimeExtension.php
│ ├── CallbackExtension.php
│ ├── EmailExtension.php
│ ├── EnvironmentExtension.php
│ ├── ExtensionHandler.php
│ ├── ExtensionHandlerRegistry.php
│ ├── Handler
│ │ ├── BetweenTimeHandler.php
│ │ ├── CallbackHandler.php
│ │ ├── EmailHandler.php
│ │ ├── EnvironmentHandler.php
│ │ ├── NotifierHandler.php
│ │ ├── PingHandler.php
│ │ ├── SingleServerHandler.php
│ │ └── WithoutOverlappingHandler.php
│ ├── LockingExtension.php
│ ├── NotifierExtension.php
│ ├── PingExtension.php
│ ├── SingleServerExtension.php
│ └── WithoutOverlappingExtension.php
├── HasExtensions.php
├── HasMissingDependencyMessage.php
├── RunContext.php
├── ScheduleBuilder.php
├── ScheduleRunContext.php
├── ScheduleRunner.php
├── SelfSchedulingCommand.php
├── Task.php
├── Task
│ ├── CallbackTask.php
│ ├── CommandTask.php
│ ├── CompoundTask.php
│ ├── MessageTask.php
│ ├── PingTask.php
│ ├── ProcessTask.php
│ ├── Result.php
│ ├── Runner
│ │ ├── CallbackTaskRunner.php
│ │ ├── CommandTaskRunner.php
│ │ ├── MessageTaskRunner.php
│ │ ├── PingTaskRunner.php
│ │ ├── ProcessTaskRunner.php
│ │ └── ShellVerbosityResetter.php
│ ├── TaskRunContext.php
│ └── TaskRunner.php
└── TaskOutput.php
└── ZenstruckScheduleBundle.php
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: auto
6 | threshold: 1%
7 | patch:
8 | default:
9 | target: auto
10 | threshold: 50%
11 |
12 | comment: false
13 | github_checks:
14 | annotations: false
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.yml]
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /.github export-ignore
2 | /tests export-ignore
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /composer.lock
2 | /phpunit.xml
3 | /vendor/
4 | /build/
5 | /.php-cs-fixer.cache
6 | /.phpunit.result.cache
7 | /var/
8 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 |
70 |
71 | ## [v1.4.0](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.4.0)
72 |
73 | June 8th, 2022 - [v1.3.0...v1.4.0](https://github.com/zenstruck/schedule-bundle/compare/v1.3.0...v1.4.0)
74 |
75 | * 191e113 [minor] support Symfony 6.1 (#63) by @kbond
76 |
77 | ## [v1.3.0](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.3.0)
78 |
79 | April 14th, 2022 - [v1.2.1...v1.3.0](https://github.com/zenstruck/schedule-bundle/compare/v1.2.1...v1.3.0)
80 |
81 | * 9c224b6 [minor] improve converting callbacks to strings (#62) by @kbond
82 | * 0273756 [feature] add `AsScheduledTask` for self-schedule commands/services (#62) by @kbond
83 | * 613bbec [minor] dep upgrade (#61) by @kbond
84 | * 00b7d8f [minor] remove scrutinizer (#61) by @kbond
85 | * 2f7cd2f [feature] add additional cron hash aliases (#61) by @kbond
86 | * c910a93 [doc] Update define-schedule.md (#56) by @Lenny4
87 | * 0dfcf62 [minor] add static code analysis with phpstan (#55) by @kbond
88 | * e021707 [minor] run php-cs-fixer on lowest supported php version (#54) by @kbond
89 |
90 | ## [v1.2.1](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.2.1)
91 |
92 | November 13th, 2021 - [v1.2.0...v1.2.1](https://github.com/zenstruck/schedule-bundle/compare/v1.2.0...v1.2.1)
93 |
94 | * c04c992 [minor] add php 8.1 support by @kbond
95 | * 33b432b [ci] use reusable actions (#53) by @kbond
96 | * a0d76a8 [minor] add symfony 6 to ci matrix (#52) by @kbond
97 |
98 | ## [v1.2.0](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.2.0)
99 |
100 | September 13th, 2021 - [v1.1.2...v1.2.0](https://github.com/zenstruck/schedule-bundle/compare/v1.1.2...v1.2.0)
101 |
102 | * a65343c [minor] allow Symfony 6 (#51) by @kbond
103 | * 7649b18 [minor][SMALL BC BREAK] schedule:run now has no output for no tasks (#49) by @kbond
104 | * 49ade72 [minor] disable codecov pr annotations (#50) by @kbond
105 | * 219878d [minor] add Symfony 5.3 to CI matrix (#46) by @kbond
106 | * d4d9fe9 [minor] update php-cs-fixer to v3 by @kbond
107 | * 87120a4 [minor] set deprectation fail threshold by @kbond
108 |
109 | ## [v1.1.2](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.1.2)
110 |
111 | April 14th, 2021 - [v1.1.1...v1.1.2](https://github.com/zenstruck/schedule-bundle/compare/v1.1.1...v1.1.2)
112 |
113 | * 07fbffd [minor] capture stdout output in failed process task result (#44) by @kbond
114 | * 70a8160 [minor] lock php-cs-fixer version in ci (bug) by @kbond
115 | * 90bfea4 [bug] in CommandTaskRunner, reset SHELL_VERBOSITY to pre-run state (#42) by @kbond
116 |
117 | ## [v1.1.1](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.1.1)
118 |
119 | February 26th, 2021 - [v1.1.0...v1.1.1](https://github.com/zenstruck/schedule-bundle/compare/v1.1.0...v1.1.1)
120 |
121 | * 8d0500f [minor] adjust deps to make it clear it isn't usable in Symfony 3.4 (#38) by @kbond
122 | * 7d9cb91 [doc] Add missing namespace in the README's example (#35) by @justRau
123 | * ae849c7 [minor] fix cs by @kbond
124 | * 60bee6c [minor] replace removed phpunit method (#34) by @kbond
125 | * e5e2e04 [minor] add codecov badge (#33) by @kbond
126 | * 6fa2eea [minor] switch to codecov for code coverage (#32) by @kbond
127 | * 4b3b027 [minor] further streamline gh actions (#31) by @kbond
128 | * b775669 [minor] Streamline GitHub CI by using ramsey/composer-install (#30) by @kbond
129 | * 40677dd [minor] add Symfony 5.2 to ci matrix by @kbond
130 |
131 | ## [v1.1.0](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.1.0)
132 |
133 | November 15th, 2020 - [v1.0.1...v1.1.0](https://github.com/zenstruck/schedule-bundle/compare/v1.0.1...v1.1.0)
134 |
135 | * ff3dcd2 [minor] support php8 (#25) by @kbond
136 | * 68826f7 [minor] ci adjustments (#29) by @kbond
137 | * a862b00 [minor] ci adjustments (#29) by @kbond
138 | * 59fbaeb [feature][experimental] add MessageTask to schedule messenger messages (#28) by @kbond
139 | * cd6bba4 [minor] cs fix (#27) by @kbond
140 | * 2f788a6 [minor] Schedule::addPing()/CompoundTask::addPing() convenience methods (#27) by @kbond
141 | * 6002b61 [minor] test on Symfony 5.1 (#26) by @kbond
142 | * d54f71f [minor] switch flex branch to main (#26) by @kbond
143 | * 6445da4 [doc] replace Envoyer with "Oh Dear" as example cron monitoring service by @kbond
144 |
145 | ## [v1.0.1](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.0.1)
146 |
147 | September 27th, 2020 - [v1.0.0...v1.0.1](https://github.com/zenstruck/schedule-bundle/compare/v1.0.0...v1.0.1)
148 |
149 | * f77865d [bug] Ensure kernel definition class is not null (#22) by @encreinformatique
150 | * 4646d22 [bug] fix test checking for changed exception message (#23) by @kbond
151 | * f7b3d21 [bug] remove minimum-stability from composer.json (#23) by @kbond
152 | * f260289 [minor] self-update php-cs-fixer in action by @kbond
153 | * 073385e [minor] adjust PingTaskRunnerTest tests (#20) by @kbond
154 | * 7e47d8e [minor] add MockLogger to just check the message (not level) (#20) by @kbond
155 | * 926e98f [doc] document "disable on deployment" strategy (fixes #10) (#17) by @kbond
156 | * bc6bccd [doc] prefix "bin/console" with php (fixes #14) (#16) by @kbond
157 | * 05a9c99 [doc] add additional readme badges by @kbond
158 | * 0c06d89 [minor] switch to `Symfony\Component\Mime\Address::create()` (#11) by @kbond
159 | * 21b252b [minor] adjust CI badge by @kbond
160 | * a7cce5e [minor] fixcs by @kbond
161 |
162 | ## [v1.0.0](https://github.com/zenstruck/schedule-bundle/releases/tag/v1.0.0)
163 |
164 | May 13th, 2020 - _[Initial Release](https://github.com/zenstruck/schedule-bundle/commits/v1.0.0)_
165 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Kevin Bond
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zenstruck/schedule-bundle",
3 | "description": "Schedule Cron jobs (commands/callbacks/bash scripts) within your Symfony application.",
4 | "homepage": "https://github.com/zenstruck/schedule-bundle",
5 | "type": "symfony-bundle",
6 | "license": "MIT",
7 | "keywords": ["schedule", "cron"],
8 | "authors": [
9 | {
10 | "name": "Kevin Bond",
11 | "email": "kevinbond@gmail.com"
12 | }
13 | ],
14 | "require": {
15 | "php": ">=8.0",
16 | "dragonmantank/cron-expression": "^2.3|^3.0",
17 | "symfony/console": "^5.4|^6.0|^7.0",
18 | "symfony/dependency-injection": "^5.4|^6.0|^7.0",
19 | "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
20 | "symfony/http-kernel": "^5.4|^6.0|^7.0"
21 | },
22 | "require-dev": {
23 | "lorisleiva/cron-translator": "^0.1.0|^0.3.1|^0.4.0",
24 | "matthiasnoback/symfony-dependency-injection-test": "^4.1|^5.0",
25 | "phpstan/phpstan": "^1.4",
26 | "phpunit/phpunit": "^9.5",
27 | "psr/log": "^1.1",
28 | "symfony/framework-bundle": "^5.4|^6.0|^7.0",
29 | "symfony/http-client": "^5.4|^6.0|^7.0",
30 | "symfony/lock": "^5.4|^6.0|^7.0",
31 | "symfony/mailer": "^5.4|^6.0|^7.0",
32 | "symfony/messenger": "^5.4|^6.0|^7.0",
33 | "symfony/notifier": "^5.4|^6.0|^7.0",
34 | "symfony/phpunit-bridge": "^6.2|^7.0",
35 | "symfony/process": "^5.4|^6.0|^7.0"
36 | },
37 | "suggest": {
38 | "lorisleiva/cron-translator": "Displays human readable cron expression in schedule:list",
39 | "symfony/http-client": "Allows usage of ping* extensions",
40 | "symfony/lock": "Allows usage of withoutOverlapping and onSingleServer extensions",
41 | "symfony/mailer": "Allows usage of email* extensions",
42 | "symfony/notifier": "Allows usage of notify* extensions",
43 | "symfony/process": "Allows usage of ProcessTask"
44 | },
45 | "config": {
46 | "preferred-install": "dist",
47 | "sort-packages": true
48 | },
49 | "autoload": {
50 | "psr-4": { "Zenstruck\\ScheduleBundle\\": "src/" }
51 | },
52 | "autoload-dev": {
53 | "psr-4": { "Zenstruck\\ScheduleBundle\\Tests\\": "tests/" }
54 | },
55 | "minimum-stability": "dev",
56 | "prefer-stable": true
57 | }
58 |
--------------------------------------------------------------------------------
/doc/extending.md:
--------------------------------------------------------------------------------
1 | # Extending
2 |
3 | ## Custom Tasks
4 |
5 | You can define your own task types. Tasks consist of a *task* object that extends
6 | [`Task`](../src/Schedule/Task.php) and a *runner* that implements
7 | [`TaskRunner`](../src/Schedule/Task/TaskRunner.php). The runner is responsible
8 | for running the command and returning a [`Result`](../src/Schedule/Task/Result.php).
9 |
10 | The runner must be a service with the `schedule.task_runner` tag (this is *autoconfigurable*).
11 | Runners must implement the `supports()` method which should return true when passed the task
12 | it handles.
13 |
14 | As an example, let's create a Task that sends a *Message* to your *MessageBus* (`symfony/messenger`
15 | required).
16 |
17 | **NOTE**: There is now a [MessageTask in core](define-tasks.md#messagetask).
18 |
19 | First, let's create the task:
20 |
21 | ```php
22 | // src/Schedule/MessageTask.php
23 |
24 | use Zenstruck\ScheduleBundle\Schedule\Task;
25 |
26 | class MessageTask extends Task
27 | {
28 | private $message;
29 |
30 | public function __construct(object $message)
31 | {
32 | $this->message = $message;
33 |
34 | // be sure to call the parent constructor with a default description
35 | parent::__construct(get_class($message));
36 | }
37 |
38 | public function getMessage(): object
39 | {
40 | return $this->message;
41 | }
42 | }
43 | ```
44 |
45 | Next, let's create the *runner*:
46 |
47 | ```php
48 | // src/Schedule/MessageTaskRunner.php
49 |
50 | use Symfony\Component\Messenger\MessageBusInterface;
51 | use Zenstruck\ScheduleBundle\Schedule\Task;
52 | use Zenstruck\ScheduleBundle\Schedule\Task\Result;
53 | use Zenstruck\ScheduleBundle\Schedule\Task\TaskRunner;
54 |
55 | class MessageTaskRunner implements TaskRunner
56 | {
57 | private $bus;
58 |
59 | public function __construct(MessageBusInterface $bus)
60 | {
61 | $this->bus = $bus;
62 | }
63 |
64 | /**
65 | * @param MessageTask $task
66 | */
67 | public function __invoke(Task $task): Result
68 | {
69 | $this->bus->dispatch($task->getMessage());
70 |
71 | return Result::successful($task);
72 | }
73 |
74 | public function supports(Task $task) : bool
75 | {
76 | return $task instanceof MessageTask;
77 | }
78 | }
79 | ```
80 |
81 | Finally, use this task in your schedule:
82 |
83 | ```php
84 | use App\Message\DoSomething;
85 | use App\Schedule\MessageTask;
86 |
87 | /* @var $schedule \Zenstruck\ScheduleBundle\Schedule */
88 |
89 | $schedule->add(new MessageTask(new DoSomething()))
90 | ->daily()
91 | ->at('13:30')
92 | ;
93 | ```
94 |
95 | ## Custom Extensions
96 |
97 | The primary way of hooking into schedule/task events is with extensions. Extensions
98 | can be added to both tasks and the schedule as a whole. Extensions are plain objects
99 | and require a *handler* that extends
100 | [`ExtensionHandler`](../src/Schedule/Extension/ExtensionHandler.php).
101 |
102 | The handler must be a service with the `schedule.extension_handler` tag (this is
103 | *autoconfigurable*). Extension handlers must implement the `supports()` method which
104 | should return true when passed the extension it handles.
105 |
106 | If your extension is applicable to the schedule, you can auto-add it by registering
107 | it as a service and adding the `schedule.extension` tag (*autoconfiguration* is **not**
108 | available).
109 |
110 | Making your extension stringable by implementing `__toString` shows this value in the
111 | [`schedule:list`](cli-commands.md#schedulelist) command.
112 |
113 | Below are some examples of custom extensions:
114 |
115 | ### Example: Skip Schedule if in maintenance mode
116 |
117 | Say your application has the concept of maintenance mode. You want to prevent the
118 | schedule from running in maintenance mode.
119 |
120 | This example assumes your `Kernel` has an `isInMaintenanceMode()` method.
121 |
122 | The *extension*:
123 |
124 | ```php
125 | // src/Schedule/Extension/NotInMaintenanceMode.php
126 |
127 | class NotInMaintenanceMode
128 | {
129 | public function __toString(): string
130 | {
131 | return 'Do not run in maintenance mode.';
132 | }
133 | }
134 | ```
135 |
136 | The *handler* service:
137 |
138 | ```php
139 | // src/Schedule/Extension/NotInMaintenanceModeHandler.php
140 |
141 | use App\Kernel;
142 | use Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext;
143 | use Zenstruck\ScheduleBundle\Schedule\Exception\SkipSchedule;
144 | use Zenstruck\ScheduleBundle\Schedule\Extension\ExtensionHandler;
145 |
146 | class NotInMaintenanceModeHandler extends ExtensionHandler
147 | {
148 | private $kernel;
149 |
150 | public function __construct(Kernel $kernel)
151 | {
152 | $this->kernel = $kernel;
153 | }
154 |
155 | public function supports(object $extension) : bool
156 | {
157 | return $extension instanceof NotInMaintenanceMode;
158 | }
159 |
160 | /**
161 | * @param NotInMaintenanceMode $extension
162 | */
163 | public function filterSchedule(ScheduleRunContext $context, object $extension): void
164 | {
165 | if ($this->kernel->isInMaintenanceMode()) {
166 | throw new SkipSchedule('Does not run in maintenance mode.');
167 | }
168 | }
169 | }
170 | ```
171 |
172 | The easiest way to add this extension to your schedule is to register the *extension*
173 | (`App\Schedule\Extension\NotInMaintenanceMode`) as a service and tag it with
174 | `schedule.extension`.
175 |
176 | Alternatively, you can add it to the schedule in PHP:
177 |
178 | ```php
179 | use App\Schedule\Extension\NotInMaintenanceMode;
180 |
181 | /* @var $schedule \Zenstruck\ScheduleBundle\Schedule */
182 |
183 | $schedule->addExtension(new NotInMaintenanceMode());
184 | ```
185 |
186 | **NOTE:** This is an example to show creating/registering a custom extension. In
187 | a real world application, all that would be needed to accomplish the above
188 | example would be the following in your `Kernel`:
189 |
190 | ```php
191 | // src/Kernel.php
192 |
193 | use Symfony\Component\HttpKernel\Kernel as BaseKernel;
194 | use Zenstruck\ScheduleBundle\Schedule;
195 | use Zenstruck\ScheduleBundle\Schedule\ScheduleBuilder;
196 |
197 | class Kernel extends BaseKernel implements ScheduleBuilder
198 | {
199 | public function isInMaintenanceMode(): bool
200 | {
201 | // return true if in maintenance mode...
202 | }
203 |
204 | public function buildSchedule(Schedule $schedule): void
205 | {
206 | $schedule->skip('Does not run in maintenance mode.', $this->isInMaintenanceMode());
207 | }
208 |
209 | // ...
210 | }
211 | ```
212 |
213 | ## Events
214 |
215 | The following Symfony events are available:
216 |
217 | | Event | Description |
218 | | ------------------------------------------------------------- | -------------------------------- |
219 | | [`BeforeScheduleEvent`](../src/Event/BeforeScheduleEvent.php) | Runs before the schedule runs |
220 | | [`AfterScheduleEvent`](../src/Event/AfterScheduleEvent.php) | Runs after the schedule runs |
221 | | [`BeforeTaskEvent`](../src/Event/BeforeTaskEvent.php) | Runs before a due task runs |
222 | | [`AfterTaskEvent`](../src/Event/AfterTaskEvent.php) | Runs after a due task runs |
223 | | [`BuildScheduleEvent`](../src/Event/BuildScheduleEvent.php) | Define/manipulate tasks/schedule |
224 |
225 | ### Example: Add "withoutOverlapping" to all defined tasks
226 |
227 | Let's configure all our tasks to have the [withoutOverlapping](define-tasks.md#prevent-overlap)
228 | extension added.
229 |
230 | The *subscriber* service:
231 |
232 | ```php
233 | // src/EventSubscriber/ScheduleWithoutOverlappingSubscriber.php
234 |
235 | namespace App\EventSubscriber;
236 |
237 | use Symfony\Component\EventDispatcher\EventSubscriberInterface;
238 | use Zenstruck\ScheduleBundle\Event\BuildScheduleEvent;
239 |
240 | class ScheduleWithoutOverlappingSubscriber implements EventSubscriberInterface
241 | {
242 | public static function getSubscribedEvents(): array
243 | {
244 | return [
245 | BuildScheduleEvent::class => [
246 | 'onBuildSchedule',
247 | /*
248 | * The actual building of the schedule happens at priority "0".
249 | * We set to a lower priority to ensure all tasks have been defined.
250 | */
251 | -100,
252 | ],
253 | ];
254 | }
255 |
256 | public function onBuildSchedule(BuildScheduleEvent $event): void
257 | {
258 | foreach ($event->getSchedule()->all() as $task) {
259 | $task->withoutOverlapping();
260 | }
261 | }
262 | }
263 | ```
264 |
265 | **NOTE:** If *autoconfiguration* is not enabled, add the `kernel.event_subscriber`
266 | tag to the service.
267 |
--------------------------------------------------------------------------------
/doc/images/schedule-list-with-issues.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstruck/schedule-bundle/c82f4d3e96566260796975a90a8d8075fca681c9/doc/images/schedule-list-with-issues.png
--------------------------------------------------------------------------------
/doc/images/schedule-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstruck/schedule-bundle/c82f4d3e96566260796975a90a8d8075fca681c9/doc/images/schedule-list.png
--------------------------------------------------------------------------------
/doc/images/schedule-run-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstruck/schedule-bundle/c82f4d3e96566260796975a90a8d8075fca681c9/doc/images/schedule-run-error.png
--------------------------------------------------------------------------------
/doc/images/schedule-run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstruck/schedule-bundle/c82f4d3e96566260796975a90a8d8075fca681c9/doc/images/schedule-run.png
--------------------------------------------------------------------------------
/doc/run-schedule.md:
--------------------------------------------------------------------------------
1 | # Running the Schedule
2 |
3 | To run tasks when they are due, the schedule should be *run* **every minute**
4 | on your production server(s) indefinitely.
5 |
6 | *The schedule doesn't have to be run every minute but if it isn't, jobs
7 | scheduled in between the frequency you choose will never run. If you are
8 | careful when choosing task frequencies, this might not be an issue. If not
9 | running every minute, it must be run at predictable times like every hour,
10 | exactly on the hour (ie 08:00, not 08:01).*
11 |
12 | If multiple tasks are due at the same time, they are run synchronously in the
13 | order they were defined. If you define tasks in multiple places
14 | ([Configuration](define-schedule.md#bundle-configuration),
15 | [Builder Service](define-schedule.md#schedulebuilder-service),
16 | [Kernel](define-schedule.md#your-kernel),
17 | [Self-Scheduling Commands](define-schedule.md#self-scheduling-commands)) only
18 | the order of tasks defined in each place is guaranteed.
19 |
20 | Shipped with this bundle is a [`schedule:run`](cli-commands.md#schedulerun)
21 | console command. Running this command determines the due tasks (if any) for
22 | the current time and runs them.
23 |
24 | ## Cron Job on Server
25 |
26 | The most common way to run the schedule is a Cron job that runs the
27 | [`schedule:run`](cli-commands.md#schedulerun) every minute. The following
28 | should be added to your production server's
29 | [crontab](http://man7.org/linux/man-pages/man5/crontab.5.html):
30 |
31 | ```
32 | * * * * * cd /path-to-your-project && php bin/console schedule:run >> /dev/null 2>&1
33 | ```
34 |
35 | ## Symfony Cloud
36 |
37 | The [Symfony Cloud](https://symfony.com/cloud/) platform has the ability to
38 | configure Cron jobs. Add the following configuration to run your schedule every
39 | minute:
40 |
41 | ```yaml
42 | # .symfony.cloud.yaml
43 |
44 | cron:
45 | spec: * * * * *
46 | cmd: bin/console schedule:run
47 |
48 | # ...
49 | ```
50 |
51 | *[View the full Cron Jobs Documentation](https://symfony.com/doc/master/cloud/cookbooks/crons.html)*
52 |
53 | ## Alternatives
54 |
55 | If you don't have the ability to run Cron jobs on your server there may be
56 | other ways to run the schedule.
57 |
58 | The schedule can alternatively be run in your code. Behind the scenes, the
59 | `schedule:run` command invokes the [`ScheduleRunner`](../src/Schedule/ScheduleRunner.php)
60 | service which does all the work. The return value of `ScheduleRunner::__invoke()` is a
61 | [`ScheduleRunContext`](../src/Schedule/ScheduleRunContext.php) object.
62 |
63 | The following is a list of alternative scheduling options (*please add your own solutions
64 | via PR*):
65 |
66 | ### Webhook
67 |
68 | Perhaps you have a service that can *ping* an endpoint (`/run-schedule`) defined in
69 | your app every minute (AWS Lamda can be configured to do this). This endpoint
70 | can run the schedule:
71 |
72 | ```php
73 | // src/Controller/RunScheduleController.php
74 |
75 | use Symfony\Component\HttpFoundation\Response;
76 | use Symfony\Component\Routing\Annotation\Route;
77 | use Zenstruck\ScheduleBundle\Schedule\ScheduleRunner;
78 |
79 | /**
80 | * @Route("/run-schedule")
81 | */
82 | class RunScheduleController
83 | {
84 | public function __invoke(ScheduleRunner $scheduleRunner): Response
85 | {
86 | $result = $scheduleRunner();
87 |
88 | return new Response('', $result->isSuccessful() ? 200 : 500);
89 | }
90 | }
91 | ```
92 |
93 | ## Force Run
94 |
95 | The [`schedule:run`](cli-commands.md#schedulerun) command optionally takes
96 | a list of [Task ID's](define-tasks.md#task-id). This will force run these
97 | tasks (and no others) even if they are not currently due. This can be useful
98 | for re-running tasks that [fail](#dealing-with-failures). The task ID is show
99 | in emails/logs and listed in [`schedule:list --detail`](cli-commands.md#schedulelist).
100 |
101 | ## Dealing with Failures
102 |
103 | It is probable that at some point, a scheduled task will fail. Because the
104 | schedule runs in the background, administrators need to be made aware failures.
105 |
106 | *If multiple tasks are due at the same time, one failure will not prevent the
107 | other due tasks from running.*
108 |
109 | A failing task may or may not be the result of an exception. For instance, a
110 | [CommandTask](define-tasks.md#commandtask) that ran with an exit code of `1`
111 | is considered failed but may not be from the result of an exception (the
112 | command could have returned `1`).
113 |
114 | The following are different methods of being alerted to failures:
115 |
116 | ### Logs
117 |
118 | All schedule/task events are logged (if using monolog, on the `schedule` channel).
119 | Errors and Exceptions are logged with the `ERROR` and `CRITICAL` levels respectively.
120 | The log's context contains useful information like duration, memory usage, task output
121 | and the exception (if failed).
122 |
123 | The following is an example log file (some context excluded):
124 |
125 | ```
126 | [2020-01-20 13:17:13] schedule.INFO: Running 4 due tasks. {"total":22,"due":4}
127 | [2020-01-20 13:17:13] schedule.INFO: Running "CommandTask": my:command
128 | [2020-01-20 13:17:13] schedule.INFO: Successfully ran "CommandTask": my:command
129 | [2020-01-20 13:17:13] schedule.INFO: Running "ProcessTask": fdere -dsdfsd
130 | [2020-01-20 13:17:13] schedule.ERROR: Failure when running "ProcessTask": fdere -dsdfsd
131 | [2020-01-20 13:17:13] schedule.INFO: Running "CallbackTask": some callback
132 | [2020-01-20 13:17:13] schedule.CRITICAL: Exception thrown when running "CallbackTask": some callback
133 | [2020-01-20 13:24:11] schedule.INFO: Running "CommandTask": another:command
134 | [2020-01-20 13:24:11] schedule.INFO: Skipped "CommandTask": another:command {"reason":"the reason for skip..."}
135 | [2020-01-20 13:24:11] schedule.ERROR: 3/4 tasks ran {"total":4,"successful":1,"skipped":1,"failures":2,"duration":"< 1 sec","memory":"10.0 MiB"}
136 | ```
137 |
138 | Services like [Papertrail](https://papertrailapp.com) can be [configured to alert
139 | administrators](https://help.papertrailapp.com/kb/how-it-works/alerts/) when a filter
140 | (ie `schedule.ERROR OR schedule.CRITICAL`) is matched.
141 |
142 | ### Email on Schedule Failure
143 |
144 | Administrators can be notified via email when tasks fail. This can be configured
145 | [per task](define-tasks.md#email-output) or
146 | [for the entire schedule](define-schedule.md#email-on-failure).
147 |
148 | ### `schedule:run` exit code/output
149 |
150 | The [`schedule:run`](cli-commands.md#schedulerun) command will have an exit code of
151 | `1` if one or more tasks fail. The command's output also contains detailed output.
152 | The crontab entry [shown above](#cron-job-on-server) ignores the exit code and
153 | dumps the command's output to `/dev/null` but this could be changed to log the
154 | output and/or alert an administrator.
155 |
156 | ### Alert with Symfony Cloud
157 |
158 | When defining the `schedule:run` cron job with [Symfony Cloud](#symfony-cloud), you can
159 | [prefix the command with `croncape` to be alerted via email](https://symfony.com/doc/master/cloud/cookbooks/crons.html#command-to-run)
160 | when something goes wrong:
161 |
162 | ```yaml
163 | # .symfony.cloud.yaml
164 |
165 | cron:
166 | spec: * * * * *
167 | cmd: croncape bin/console schedule:run
168 |
169 | # ...
170 | ```
171 |
172 | ### Custom Schedule Extension
173 |
174 | You can create a [custom schedule extension](extending.md#custom-extensions) with a
175 | `onScheduleFailure` hook to add your own failure logic.
176 |
177 | ### AfterSchedule Event
178 |
179 | You can [create an event subscriber](extending.md#events) that listens to the
180 | [`AfterScheduleEvent`](../src/Event/AfterScheduleEvent.php), check if the schedule
181 | failed, and run your own failure logic.
182 |
183 | ## Ensuring the Schedule is Running
184 |
185 | It is important to be assured your schedule is always running. The best method
186 | is to use a Cron health monitoring tool like [Oh Dear](https://ohdear.app/),
187 | [Cronitor](https://cronitor.io/) or [Healthchecks](https://healthchecks.io/).
188 | These services give you a unique URL endpoint to *ping*. If the endpoint doesn't
189 | receive a ping after a specified amount of time, an administrator is notified.
190 |
191 | You can [configure your schedule to ping](define-schedule.md#ping-webhook) after
192 | running (assumes your endpoint is `https://my-health-monitor.com/endpoint`):
193 |
194 | ```yaml
195 | # config/packages/zenstruck_schedule.yaml
196 |
197 | zenstruck_schedule:
198 | schedule_extensions:
199 | ping_after: https://my-health-monitor.com/endpoint
200 | ```
201 |
202 | This will ping the endpoint after the schedule runs (every minute). If this is too
203 | frequent, you can configure a *[PingTask](define-tasks.md#pingtask)* to ping the
204 | endpoint at a different frequency:
205 |
206 | ```yaml
207 | zenstruck_schedule:
208 | tasks:
209 | - task: ping:https://my-health-monitor/endpoint
210 | description: Health check
211 | frequency: '@hourly'
212 | ```
213 |
214 | In this case, a notification from one of these services means your schedule isn't
215 | running.
216 |
217 | ## Disable Schedule during Deploy
218 |
219 | Depending on your deployment strategy, it might be desirable to ensure the schedule does not
220 | run when deploying. One way to do this is to have your deployment script create a `.deploying`
221 | file somewhere on the webserver at the start of a deploy, then remove when complete. You can
222 | check for this file when building your schedule and skip if it exists:
223 |
224 | ```php
225 | // src/Kernel.php
226 |
227 | use Symfony\Component\HttpKernel\Kernel as BaseKernel;
228 | use Zenstruck\ScheduleBundle\Schedule;
229 | use Zenstruck\ScheduleBundle\Schedule\ScheduleBuilder;
230 |
231 | class Kernel extends BaseKernel implements ScheduleBuilder
232 | {
233 | public function isDeploying(): bool
234 | {
235 | return file_exists('/path/to/file/.deploying');
236 | }
237 |
238 | public function buildSchedule(Schedule $schedule): void
239 | {
240 | $schedule->skip('App is deploying...', $this->isDeploying());
241 | }
242 |
243 | // ...
244 | }
245 | ```
246 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - phpstan-baseline.neon
3 |
4 | parameters:
5 | level: 8
6 | paths:
7 | - src
8 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ./tests/
21 |
22 |
23 |
24 |
25 |
26 | src
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Attribute/AsScheduledTask.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Attribute;
13 |
14 | /**
15 | * Schedule an invokable service or console command.
16 | *
17 | * @author Kevin Bond
18 | */
19 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
20 | final class AsScheduledTask
21 | {
22 | public function __construct(
23 | /**
24 | * Cron expression or alias.
25 | */
26 | public string $frequency,
27 |
28 | /**
29 | * Task description.
30 | */
31 | public ?string $description = null,
32 |
33 | /**
34 | * The invokable service method to be called when run (must
35 | * have no required parameters).
36 | *
37 | * Only applicable to "invokable services".
38 | */
39 | public string $method = '__invoke',
40 |
41 | /**
42 | * The command arguments (ie "-v --no-interaction").
43 | *
44 | * Only applicable to "console commands".
45 | */
46 | public ?string $arguments = null,
47 | ) {
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Command/ScheduleRunCommand.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Command;
13 |
14 | use Symfony\Component\Console\Attribute\AsCommand;
15 | use Symfony\Component\Console\Command\Command;
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputInterface;
18 | use Symfony\Component\Console\Output\OutputInterface;
19 | use Symfony\Component\Console\Style\SymfonyStyle;
20 | use Symfony\Component\EventDispatcher\EventDispatcherInterface;
21 | use Zenstruck\ScheduleBundle\EventListener\ScheduleConsoleOutputSubscriber;
22 | use Zenstruck\ScheduleBundle\Schedule\ScheduleRunner;
23 |
24 | /**
25 | * @author Kevin Bond
26 | */
27 | #[AsCommand(
28 | name: 'schedule:run',
29 | description: 'Runs scheduled tasks that are due',
30 | )]
31 | final class ScheduleRunCommand extends Command
32 | {
33 | /** @var ScheduleRunner */
34 | private $scheduleRunner;
35 |
36 | /** @var EventDispatcherInterface */
37 | private $dispatcher;
38 |
39 | public function __construct(ScheduleRunner $scheduleRunner, EventDispatcherInterface $dispatcher)
40 | {
41 | $this->scheduleRunner = $scheduleRunner;
42 | $this->dispatcher = $dispatcher;
43 |
44 | parent::__construct();
45 | }
46 |
47 | protected function configure(): void
48 | {
49 | $this
50 | ->addArgument('id', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, '(optional) Task ID\'s to "force" run')
51 | ->setHelp(<<> /dev/null 2>&1
62 | EOF
63 | )
64 | ;
65 | }
66 |
67 | protected function execute(InputInterface $input, OutputInterface $output): int
68 | {
69 | $io = new SymfonyStyle($input, $output);
70 |
71 | $this->dispatcher->addSubscriber(new ScheduleConsoleOutputSubscriber($io));
72 |
73 | return ($this->scheduleRunner)(...$input->getArgument('id'))->isSuccessful() ? 0 : 1;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/ScheduleBuilderKernelPass.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\DependencyInjection\Compiler;
13 |
14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15 | use Symfony\Component\DependencyInjection\ContainerBuilder;
16 | use Zenstruck\ScheduleBundle\Schedule\ScheduleBuilder;
17 |
18 | /**
19 | * @author Kevin Bond
20 | */
21 | final class ScheduleBuilderKernelPass implements CompilerPassInterface
22 | {
23 | public function process(ContainerBuilder $container): void
24 | {
25 | if (!$container->hasDefinition('kernel')) {
26 | return;
27 | }
28 |
29 | $kernel = $container->getDefinition('kernel');
30 |
31 | if (null === $class = $kernel->getClass()) {
32 | return;
33 | }
34 |
35 | /** @var class-string $class */
36 | if ((new \ReflectionClass($class))->implementsInterface(ScheduleBuilder::class)) {
37 | $kernel->addTag('schedule.builder');
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/ScheduledServiceBuilderPass.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\DependencyInjection\Compiler;
13 |
14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15 | use Symfony\Component\DependencyInjection\ContainerBuilder;
16 | use Symfony\Component\DependencyInjection\Reference;
17 | use Zenstruck\ScheduleBundle\Schedule\Builder\ScheduledServiceBuilder;
18 |
19 | /**
20 | * @author Kevin Bond
21 | *
22 | * @internal
23 | */
24 | final class ScheduledServiceBuilderPass implements CompilerPassInterface
25 | {
26 | public function process(ContainerBuilder $container): void
27 | {
28 | $builder = $container->getDefinition('zenstruck_schedule.service_builder');
29 |
30 | foreach ($container->findTaggedServiceIds('schedule.service') as $id => $tags) {
31 | foreach ($tags as $attributes) {
32 | ScheduledServiceBuilder::validate($container->getDefinition($id)->getClass(), $attributes);
33 |
34 | $builder->addMethodCall('add', [new Reference($id), $attributes]);
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/DependencyInjection/ZenstruckScheduleExtension.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\DependencyInjection;
13 |
14 | use Symfony\Component\Config\FileLocator;
15 | use Symfony\Component\DependencyInjection\ContainerBuilder;
16 | use Symfony\Component\DependencyInjection\Definition;
17 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
18 | use Symfony\Component\DependencyInjection\Reference;
19 | use Symfony\Component\HttpClient\HttpClient;
20 | use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
21 | use Symfony\Component\Lock\LockFactory;
22 | use Symfony\Component\Process\Process;
23 | use Zenstruck\ScheduleBundle\Attribute\AsScheduledTask;
24 | use Zenstruck\ScheduleBundle\EventListener\ScheduleTimezoneSubscriber;
25 | use Zenstruck\ScheduleBundle\EventListener\TaskConfigurationSubscriber;
26 | use Zenstruck\ScheduleBundle\Schedule\Extension\EmailExtension;
27 | use Zenstruck\ScheduleBundle\Schedule\Extension\EnvironmentExtension;
28 | use Zenstruck\ScheduleBundle\Schedule\Extension\ExtensionHandler;
29 | use Zenstruck\ScheduleBundle\Schedule\Extension\Handler\EmailHandler;
30 | use Zenstruck\ScheduleBundle\Schedule\Extension\Handler\NotifierHandler;
31 | use Zenstruck\ScheduleBundle\Schedule\Extension\Handler\PingHandler;
32 | use Zenstruck\ScheduleBundle\Schedule\Extension\Handler\SingleServerHandler;
33 | use Zenstruck\ScheduleBundle\Schedule\Extension\Handler\WithoutOverlappingHandler;
34 | use Zenstruck\ScheduleBundle\Schedule\Extension\NotifierExtension;
35 | use Zenstruck\ScheduleBundle\Schedule\Extension\PingExtension;
36 | use Zenstruck\ScheduleBundle\Schedule\Extension\SingleServerExtension;
37 | use Zenstruck\ScheduleBundle\Schedule\ScheduleBuilder;
38 | use Zenstruck\ScheduleBundle\Schedule\SelfSchedulingCommand;
39 | use Zenstruck\ScheduleBundle\Schedule\Task\Runner\MessageTaskRunner;
40 | use Zenstruck\ScheduleBundle\Schedule\Task\Runner\PingTaskRunner;
41 | use Zenstruck\ScheduleBundle\Schedule\Task\TaskRunner;
42 |
43 | /**
44 | * @author Kevin Bond
45 | */
46 | final class ZenstruckScheduleExtension extends ConfigurableExtension
47 | {
48 | protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void
49 | {
50 | $container->registerForAutoconfiguration(ScheduleBuilder::class)
51 | ->addTag('schedule.builder')
52 | ;
53 |
54 | $container->registerForAutoconfiguration(TaskRunner::class)
55 | ->addTag('schedule.task_runner')
56 | ;
57 |
58 | $container->registerForAutoconfiguration(SelfSchedulingCommand::class)
59 | ->addTag('schedule.self_scheduling_command')
60 | ;
61 |
62 | $container->registerForAutoconfiguration(ExtensionHandler::class)
63 | ->addTag('schedule.extension_handler')
64 | ;
65 |
66 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
67 | $loader->load('services.xml');
68 |
69 | if (\class_exists(Process::class)) {
70 | $loader->load('process.xml');
71 | }
72 |
73 | $container
74 | ->getDefinition(TaskConfigurationSubscriber::class)
75 | ->setArgument(0, $mergedConfig['tasks'])
76 | ;
77 |
78 | if ($mergedConfig['without_overlapping_lock_factory'] || \class_exists(LockFactory::class)) {
79 | $loader->load('without_overlapping.xml');
80 | }
81 |
82 | if ($mergedConfig['without_overlapping_lock_factory']) {
83 | $container
84 | ->getDefinition(WithoutOverlappingHandler::class)
85 | ->setArgument(0, new Reference($mergedConfig['without_overlapping_lock_factory']))
86 | ;
87 | }
88 |
89 | if ($mergedConfig['single_server_lock_factory']) {
90 | $loader->load('single_server.xml');
91 |
92 | $container
93 | ->getDefinition(SingleServerHandler::class)
94 | ->setArgument(0, new Reference($mergedConfig['single_server_lock_factory']))
95 | ;
96 | }
97 |
98 | if ($mergedConfig['http_client'] || \class_exists(HttpClient::class)) {
99 | $loader->load('http.xml');
100 | }
101 |
102 | if ($mergedConfig['http_client']) {
103 | $container
104 | ->getDefinition(PingHandler::class)
105 | ->setArgument(0, new Reference($mergedConfig['http_client']))
106 | ;
107 |
108 | $container
109 | ->getDefinition(PingTaskRunner::class)
110 | ->setArgument(0, new Reference($mergedConfig['http_client']))
111 | ;
112 | }
113 |
114 | if ($mergedConfig['timezone']) {
115 | $loader->load('timezone.xml');
116 | $container
117 | ->getDefinition(ScheduleTimezoneSubscriber::class)
118 | ->setArgument(0, $mergedConfig['timezone'])
119 | ;
120 | }
121 |
122 | if ($mergedConfig['messenger']['enabled']) {
123 | $loader->load('messenger.xml');
124 |
125 | $container
126 | ->getDefinition(MessageTaskRunner::class)
127 | ->setArgument(0, new Reference($mergedConfig['messenger']['message_bus']))
128 | ;
129 | }
130 |
131 | if ($mergedConfig['mailer']['enabled']) {
132 | $loader->load('mailer.xml');
133 |
134 | $container
135 | ->getDefinition(EmailHandler::class)
136 | ->setArguments([
137 | new Reference($mergedConfig['mailer']['service']),
138 | $mergedConfig['mailer']['default_from'],
139 | $mergedConfig['mailer']['default_to'],
140 | $mergedConfig['mailer']['subject_prefix'],
141 | ])
142 | ;
143 | }
144 |
145 | if ($mergedConfig['notifier']['enabled']) {
146 | $loader->load('notifier.xml');
147 |
148 | $container
149 | ->getDefinition(NotifierHandler::class)
150 | ->setArguments([
151 | new Reference($mergedConfig['notifier']['service']),
152 | $mergedConfig['notifier']['default_channel'],
153 | $mergedConfig['notifier']['default_email'],
154 | $mergedConfig['notifier']['default_phone'],
155 | $mergedConfig['notifier']['subject_prefix'],
156 | ])
157 | ;
158 | }
159 |
160 | $this->registerScheduleExtensions($mergedConfig, $container);
161 |
162 | if (\method_exists($container, 'registerAttributeForAutoconfiguration')) {
163 | $container->registerAttributeForAutoconfiguration(
164 | AsScheduledTask::class,
165 | static function(Definition $definition, AsScheduledTask $attribute) {
166 | $definition->addTag('schedule.service', \get_object_vars($attribute));
167 | },
168 | );
169 | }
170 | }
171 |
172 | private function registerScheduleExtensions(array $config, ContainerBuilder $container): void
173 | {
174 | /** @var Definition[] $definitions */
175 | $definitions = [];
176 | $idPrefix = 'zenstruck_schedule.extension.';
177 |
178 | if (!empty($config['schedule_extensions']['environments'])) {
179 | $definitions[$idPrefix.'environments'] = new Definition(
180 | EnvironmentExtension::class,
181 | [$config['schedule_extensions']['environments']],
182 | );
183 | }
184 |
185 | if ($config['schedule_extensions']['on_single_server']['enabled']) {
186 | $definitions[$idPrefix.'on_single_server'] = new Definition(
187 | SingleServerExtension::class,
188 | [$config['schedule_extensions']['on_single_server']['ttl']],
189 | );
190 | }
191 |
192 | if ($config['schedule_extensions']['email_on_failure']['enabled']) {
193 | $definition = new Definition(EmailExtension::class);
194 | $definition->setFactory([EmailExtension::class, 'scheduleFailure']);
195 | $definition->setArguments([
196 | $config['schedule_extensions']['email_on_failure']['to'],
197 | $config['schedule_extensions']['email_on_failure']['subject'],
198 | ]);
199 |
200 | $definitions[$idPrefix.'email_on_failure'] = $definition;
201 | }
202 |
203 | if ($config['schedule_extensions']['notify_on_failure']['enabled']) {
204 | $definition = new Definition(NotifierExtension::class);
205 | $definition->setFactory([NotifierExtension::class, 'scheduleFailure']);
206 | $definition->setArguments([
207 | $config['schedule_extensions']['notify_on_failure']['channel'],
208 | $config['schedule_extensions']['notify_on_failure']['email'],
209 | $config['schedule_extensions']['notify_on_failure']['phone'],
210 | $config['schedule_extensions']['notify_on_failure']['subject'],
211 | ]);
212 |
213 | $definitions[$idPrefix.'notify_on_failure'] = $definition;
214 | }
215 |
216 | $pingMap = [
217 | 'ping_before' => 'scheduleBefore',
218 | 'ping_after' => 'scheduleAfter',
219 | 'ping_on_success' => 'scheduleSuccess',
220 | 'ping_on_failure' => 'scheduleFailure',
221 | ];
222 |
223 | foreach ($pingMap as $key => $method) {
224 | if ($config['schedule_extensions'][$key]['enabled']) {
225 | $definition = new Definition(PingExtension::class);
226 | $definition->setFactory([PingExtension::class, $method]);
227 | $definition->setArguments([
228 | $config['schedule_extensions'][$key]['url'],
229 | $config['schedule_extensions'][$key]['method'],
230 | $config['schedule_extensions'][$key]['options'],
231 | ]);
232 |
233 | $definitions[$idPrefix.$key] = $definition;
234 | }
235 | }
236 |
237 | foreach ($definitions as $definition) {
238 | $definition->addTag('schedule.extension');
239 | }
240 |
241 | $container->addDefinitions($definitions);
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/src/Event/AfterScheduleEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Event;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class AfterScheduleEvent extends ScheduleEvent
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/Event/AfterTaskEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Event;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class AfterTaskEvent extends TaskEvent
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/Event/BeforeScheduleEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Event;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class BeforeScheduleEvent extends ScheduleEvent
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/Event/BeforeTaskEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Event;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class BeforeTaskEvent extends TaskEvent
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/Event/BuildScheduleEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Event;
13 |
14 | use Symfony\Contracts\EventDispatcher\Event;
15 | use Zenstruck\ScheduleBundle\Schedule;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | final class BuildScheduleEvent extends Event
21 | {
22 | /** @var Schedule */
23 | private $schedule;
24 |
25 | public function __construct(Schedule $schedule)
26 | {
27 | $this->schedule = $schedule;
28 | }
29 |
30 | public function getSchedule(): Schedule
31 | {
32 | return $this->schedule;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Event/ScheduleEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Event;
13 |
14 | use Symfony\Contracts\EventDispatcher\Event;
15 | use Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | abstract class ScheduleEvent extends Event
21 | {
22 | /** @var ScheduleRunContext */
23 | private $runContext;
24 |
25 | final public function __construct(ScheduleRunContext $runContext)
26 | {
27 | $this->runContext = $runContext;
28 | }
29 |
30 | final public function runContext(): ScheduleRunContext
31 | {
32 | return $this->runContext;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Event/TaskEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\Event;
13 |
14 | use Symfony\Contracts\EventDispatcher\Event;
15 | use Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | abstract class TaskEvent extends Event
21 | {
22 | /** @var TaskRunContext */
23 | private $runContext;
24 |
25 | final public function __construct(TaskRunContext $runContext)
26 | {
27 | $this->runContext = $runContext;
28 | }
29 |
30 | final public function runContext(): TaskRunContext
31 | {
32 | return $this->runContext;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/EventListener/ScheduleBuilderSubscriber.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\EventListener;
13 |
14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15 | use Zenstruck\ScheduleBundle\Event\BuildScheduleEvent;
16 | use Zenstruck\ScheduleBundle\Schedule\ScheduleBuilder;
17 |
18 | /**
19 | * @author Kevin Bond
20 | */
21 | final class ScheduleBuilderSubscriber implements EventSubscriberInterface
22 | {
23 | /** @var iterable */
24 | private $builders;
25 |
26 | /**
27 | * @param iterable $builders
28 | */
29 | public function __construct(iterable $builders)
30 | {
31 | $this->builders = $builders;
32 | }
33 |
34 | public static function getSubscribedEvents(): array
35 | {
36 | return [BuildScheduleEvent::class => 'build'];
37 | }
38 |
39 | public function build(BuildScheduleEvent $event): void
40 | {
41 | foreach ($this->builders as $builder) {
42 | $builder->buildSchedule($event->getSchedule());
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/EventListener/ScheduleConsoleOutputSubscriber.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\EventListener;
13 |
14 | use Symfony\Component\Console\Style\OutputStyle;
15 | use Symfony\Component\Console\Style\SymfonyStyle;
16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17 | use Zenstruck\ScheduleBundle\Event\AfterScheduleEvent;
18 | use Zenstruck\ScheduleBundle\Event\AfterTaskEvent;
19 | use Zenstruck\ScheduleBundle\Event\BeforeScheduleEvent;
20 | use Zenstruck\ScheduleBundle\Event\BeforeTaskEvent;
21 | use Zenstruck\ScheduleBundle\Schedule\Task\Result;
22 |
23 | /**
24 | * @author Kevin Bond
25 | */
26 | final class ScheduleConsoleOutputSubscriber implements EventSubscriberInterface
27 | {
28 | /** @var OutputStyle */
29 | private $io;
30 |
31 | public function __construct(OutputStyle $io)
32 | {
33 | $this->io = $io;
34 | }
35 |
36 | public static function getSubscribedEvents(): array
37 | {
38 | return [
39 | BeforeScheduleEvent::class => 'beforeSchedule',
40 | AfterScheduleEvent::class => 'afterSchedule',
41 | BeforeTaskEvent::class => 'beforeTask',
42 | AfterTaskEvent::class => 'afterTask',
43 | ];
44 | }
45 |
46 | public function afterSchedule(AfterScheduleEvent $event): void
47 | {
48 | $context = $event->runContext();
49 |
50 | if ($context->isSkipped()) {
51 | $this->io->note($context->getSkipReason());
52 |
53 | return;
54 | }
55 |
56 | $total = \count($context->getResults());
57 | $successful = \count($context->getSuccessful());
58 | $failures = \count($context->getFailures());
59 | $skipped = \count($context->getSkipped());
60 | $run = \count($context->getRun());
61 | $messages = ["{$run}/{$total} tasks ran"];
62 |
63 | if (0 === $total) {
64 | return;
65 | }
66 |
67 | if ($successful > 0) {
68 | $messages[] = "{$successful} succeeded";
69 | }
70 |
71 | if ($skipped > 0) {
72 | $messages[] = "{$skipped} skipped";
73 | }
74 |
75 | if ($failures > 0) {
76 | $messages[] = "{$failures} failed";
77 | }
78 |
79 | $messages = \implode(', ', $messages).'.';
80 | $messages .= " (Duration: {$context->getFormattedDuration()}, Memory: {$context->getFormattedMemory()})";
81 |
82 | $this->io->{$context->isSuccessful() ? 'success' : 'error'}($messages);
83 | }
84 |
85 | public function beforeSchedule(BeforeScheduleEvent $event): void
86 | {
87 | $context = $event->runContext();
88 |
89 | $allTaskCount = \count($context->getSchedule()->all());
90 | $dueTaskCount = \count($context->dueTasks());
91 |
92 | if ($dueTaskCount > 0) {
93 | $this->io->{$this->io instanceof SymfonyStyle ? 'comment' : 'text'}(\sprintf(
94 | '%sRunning %s %stask%s. (%s total tasks)',
95 | $context->isForceRun() ? 'Force ' : '',
96 | $dueTaskCount,
97 | $context->isForceRun() ? '' : 'due ',
98 | $dueTaskCount > 1 ? 's' : '',
99 | $allTaskCount,
100 | ));
101 | }
102 |
103 | if ($this->io->isDebug()) {
104 | $this->io->note(\sprintf('No tasks due to run. (%s total tasks)', $allTaskCount));
105 | }
106 | }
107 |
108 | public function beforeTask(BeforeTaskEvent $event): void
109 | {
110 | $context = $event->runContext();
111 | $task = $context->getTask();
112 |
113 | $this->io->text(\sprintf(
114 | '%sRunning %s: %s',
115 | $context->getScheduleRunContext()->isForceRun() ? 'Force ' : '',
116 | $task->getType(),
117 | $task->getDescription(),
118 | ));
119 | }
120 |
121 | public function afterTask(AfterTaskEvent $event): void
122 | {
123 | $context = $event->runContext();
124 |
125 | if ($this->io->isVerbose() && $output = $context->getResult()->getOutput()) {
126 | $this->io->text('---begin output---');
127 | $this->io->write($output);
128 | $this->io->text('---end output---');
129 | }
130 |
131 | $this->io->text(\sprintf('%s (Duration: %s, Memory: %s)',
132 | $this->afterTaskMessage($context->getResult()),
133 | $context->getFormattedDuration(),
134 | $context->getFormattedMemory(),
135 | ));
136 | $this->io->newLine();
137 | }
138 |
139 | private function afterTaskMessage(Result $result): string
140 | {
141 | if ($result->isException()) {
142 | return "Exception: {$result->getDescription()}";
143 | }
144 |
145 | if ($result->isFailure()) {
146 | return "Failure: {$result->getDescription()}";
147 | }
148 |
149 | if ($result->isSkipped()) {
150 | return "Skipped: {$result->getDescription()}";
151 | }
152 |
153 | return 'Success.';
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/EventListener/ScheduleExtensionSubscriber.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\ScheduleBundle\EventListener;
13 |
14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15 | use Zenstruck\ScheduleBundle\Event\BuildScheduleEvent;
16 |
17 | /**
18 | * @author Kevin Bond
19 | */
20 | final class ScheduleExtensionSubscriber implements EventSubscriberInterface
21 | {
22 | /** @var iterable