├── .coveralls.yml
├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml
├── src
└── GO
│ ├── FailedJob.php
│ ├── Job.php
│ ├── Scheduler.php
│ └── Traits
│ ├── Interval.php
│ └── Mailer.php
└── tests
├── GO
├── IntervalTest.php
├── JobOutputFilesTest.php
├── JobTest.php
├── MailerTest.php
└── SchedulerTest.php
├── async_job.php
├── test_job.php
└── tmp
└── .gitignore
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 | coverage_clover: clover.xml
3 | json_path: coveralls-upload.json
4 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - 'master'
5 | pull_request:
6 | branches:
7 | - 'master'
8 |
9 | jobs:
10 | run:
11 | runs-on: ${{ matrix.operating-system }}
12 | strategy:
13 | matrix:
14 | operating-system: [ubuntu-latest]
15 | php-versions: ['7.3', '7.4', '8.0', '8.1']
16 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | - name: Install PHP
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | php-version: ${{ matrix.php-versions }}
24 | - name: Check PHP Version
25 | run: php -v
26 | - name: Validate composer.json and composer.lock
27 | run: composer validate
28 | - name: Install composer dependencies
29 | run: composer install --prefer-dist --no-progress --no-suggest
30 | - name: Run tests
31 | run: XDEBUG_MODE=coverage php vendor/bin/phpunit -c phpunit.xml --coverage-clover clover.xml
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | /vendor
4 | composer.lock
5 |
6 | /examples
7 | *.old
8 |
9 | # PHPUnit coverage file
10 | clover.xml
11 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at peppeocchi@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Giuseppe Occhipinti
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PHP Cron Scheduler
2 | ==
3 |
4 | [](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [](https://travis-ci.org/peppeocchi/php-cron-scheduler) [](https://coveralls.io/github/peppeocchi/php-cron-scheduler?branch=master) [](https://styleci.io/repos/38302733) [](https://packagist.org/packages/peppeocchi/php-cron-scheduler)
5 |
6 | This is a framework agnostic cron jobs scheduler that can be easily integrated with your project or run as a standalone command scheduler.
7 | The idea was originally inspired by the [Laravel Task Scheduling](http://laravel.com/docs/5.1/scheduling).
8 |
9 | ## Installing via Composer
10 | The recommended way is to install the php-cron-scheduler is through [Composer](https://getcomposer.org/).
11 | Please refer to [Getting Started](https://getcomposer.org/doc/00-intro.md) on how to download and install Composer.
12 |
13 | After you have downloaded/installed Composer, run
14 |
15 | `php composer.phar require peppeocchi/php-cron-scheduler`
16 |
17 | or add the package to your `composer.json`
18 | ```json
19 | {
20 | "require": {
21 | "peppeocchi/php-cron-scheduler": "4.*"
22 | }
23 | }
24 | ```
25 |
26 | Scheduler V4 requires php >= 7.3, please use the [v3 branch](https://github.com/peppeocchi/php-cron-scheduler/tree/v3.x) for php versions < 7.3 and > 7.1 or the [v2 branch](https://github.com/peppeocchi/php-cron-scheduler/tree/v2.x) for php versions < 7.1.
27 |
28 | ## How it works
29 |
30 | Create a `scheduler.php` file in the root your project with the following content.
31 | ```php
32 | run();
43 | ```
44 |
45 | Then add a new entry to your crontab to run `scheduler.php` every minute.
46 |
47 | ````
48 | * * * * * path/to/phpbin path/to/scheduler.php 1>> /dev/null 2>&1
49 | ````
50 |
51 | That's it! Your scheduler is up and running, now you can add your jobs without worring anymore about the crontab.
52 |
53 | ## Scheduling jobs
54 |
55 | By default all your jobs will try to run in background.
56 | PHP scripts and raw commands will run in background by default, while functions will always run in foreground.
57 | You can force a command to run in foreground by calling the `inForeground()` method.
58 | **Jobs that have to send the output to email, will run foreground**.
59 |
60 | ### Schedule a php script
61 |
62 | ```php
63 | $scheduler->php('path/to/my/script.php');
64 | ```
65 | The `php` method accepts 4 arguments:
66 | - The path to your php script
67 | - The PHP binary to use
68 | - Arguments to be passed to the script (**NOTE**: You need to have **register_argc_argv** enable in your php.ini for this to work ([ref](https://github.com/peppeocchi/php-cron-scheduler/issues/88)). Don't worry it's enabled by default, so unless you've intentionally disabled it or your host has it disabled by default, you can ignore it.)
69 | - Identifier
70 | ```php
71 | $scheduler->php(
72 | 'path/to/my/script.php', // The script to execute
73 | 'path/to/my/custom/bin/php', // The PHP bin
74 | [
75 | '-c' => 'ignore',
76 | '--merge' => null,
77 | ],
78 | 'myCustomIdentifier'
79 | );
80 | ```
81 |
82 | ### Schedule a raw command
83 |
84 | ```php
85 | $scheduler->raw('ps aux | grep httpd');
86 | ```
87 | The `raw` method accepts 3 arguments:
88 | - Your command
89 | - Arguments to be passed to the command
90 | - Identifier
91 | ```php
92 | $scheduler->raw(
93 | 'mycommand | myOtherCommand',
94 | [
95 | '-v' => '6',
96 | '--silent' => null,
97 | ],
98 | 'myCustomIdentifier'
99 | );
100 | ```
101 |
102 | ### Schedule a function
103 |
104 | ```php
105 | $scheduler->call(function () {
106 | return true;
107 | });
108 | ```
109 | The `call` method accepts 3 arguments:
110 | - Your function
111 | - Arguments to be passed to the function
112 | - Identifier
113 | ```php
114 | $scheduler->call(
115 | function ($args) {
116 | return $args['user'];
117 | },
118 | [
119 | ['user' => $user],
120 | ],
121 | 'myCustomIdentifier'
122 | );
123 | ```
124 |
125 | All of the arguments you pass in the array will be injected to your function.
126 | For example
127 |
128 | ```php
129 | $scheduler->call(
130 | function ($firstName, $lastName) {
131 | return implode(' ', [$firstName, $lastName]);
132 | },
133 | [
134 | 'John',
135 | 'last_name' => 'Doe', // The keys are being ignored
136 | ],
137 | 'myCustomIdentifier'
138 | );
139 | ```
140 |
141 | If you want to pass a key => value pair, please pass an array within the arguments array
142 |
143 | ```php
144 | $scheduler->call(
145 | function ($user, $role) {
146 | return implode(' ', [$user['first_name'], $user['last_name']]) . " has role: '{$role}'";
147 | },
148 | [
149 | [
150 | 'first_name' => 'John',
151 | 'last_name' => 'Doe',
152 | ],
153 | 'Admin'
154 | ],
155 | 'myCustomIdentifier'
156 | );
157 | ```
158 |
159 | ### Schedules execution time
160 |
161 | There are a few methods to help you set the execution time of your schedules.
162 | If you don't call any of this method, the job will run every minute (* * * * *).
163 |
164 | - `at` - This method accepts any expression supported by [dragonmantank/cron-expression](https://github.com/dragonmantank/cron-expression)
165 | ```php
166 | $scheduler->php('script.php')->at('* * * * *');
167 | ```
168 | - `everyMinute` - Run every minute. You can optionally pass a `$minute` to specify the job runs every `$minute` minutes.
169 | ```php
170 | $scheduler->php('script.php')->everyMinute();
171 | $scheduler->php('script.php')->everyMinute(5);
172 | ```
173 | - `hourly` - Run once per hour. You can optionally pass the `$minute` you want to run, by default it will run every hour at minute '00'.
174 | ```php
175 | $scheduler->php('script.php')->hourly();
176 | $scheduler->php('script.php')->hourly(53);
177 | ```
178 | - `daily` - Run once per day. You can optionally pass `$hour` and `$minute` to have more granular control (or a string `hour:minute`)
179 | ```php
180 | $scheduler->php('script.php')->daily();
181 | $scheduler->php('script.php')->daily(22, 03);
182 | $scheduler->php('script.php')->daily('22:03');
183 | ```
184 |
185 | There are additional helpers for weekdays (all accepting optionals hour and minute - defaulted at 00:00)
186 | - `sunday`
187 | - `monday`
188 | - `tuesday`
189 | - `wednesday`
190 | - `thursday`
191 | - `friday`
192 | - `saturday`
193 |
194 | ```php
195 | $scheduler->php('script.php')->saturday();
196 | $scheduler->php('script.php')->friday(18);
197 | $scheduler->php('script.php')->sunday(12, 30);
198 | ```
199 |
200 | And additional helpers for months (all accepting optionals day, hour and minute - defaulted to the 1st of the month at 00:00)
201 | - `january`
202 | - `february`
203 | - `march`
204 | - `april`
205 | - `may`
206 | - `june`
207 | - `july`
208 | - `august`
209 | - `september`
210 | - `october`
211 | - `november`
212 | - `december`
213 |
214 | ```php
215 | $scheduler->php('script.php')->january();
216 | $scheduler->php('script.php')->december(25);
217 | $scheduler->php('script.php')->august(15, 20, 30);
218 | ```
219 |
220 | You can also specify a `date` for when the job should run.
221 | The date can be specified as string or as instance of `DateTime`. In both cases you can specify the date only (e.g. 2018-01-01) or the time as well (e.g. 2018-01-01 10:30), if you don't specify the time it will run at 00:00 on that date.
222 | If you're providing a date in a "non standard" format, it is strongly adviced to pass an instance of `DateTime`. If you're using `createFromFormat` without specifying a time, and you want to default it to 00:00, just make sure to add a `!` to the date format, otherwise the time would be the current time. [Read more](http://php.net/manual/en/datetime.createfromformat.php)
223 |
224 | ```php
225 | $scheduler->php('script.php')->date('2018-01-01 12:20');
226 | $scheduler->php('script.php')->date(new DateTime('2018-01-01'));
227 | $scheduler->php('script.php')->date(DateTime::createFromFormat('!d/m Y', '01/01 2018'));
228 | ```
229 |
230 | ### Send output to file/s
231 |
232 | You can define one or multiple files where you want the output of your script/command/function execution to be sent to.
233 |
234 | ```php
235 | $scheduler->php('script.php')->output([
236 | 'my_file1.log', 'my_file2.log'
237 | ]);
238 |
239 | // The scheduler catches both stdout and function return and send
240 | // those values to the output file
241 | $scheduler->call(function () {
242 | echo "Hello";
243 |
244 | return " world!";
245 | })->output('my_file.log');
246 | ```
247 |
248 | ### Send output to email/s
249 |
250 | You can define one or multiple email addresses where you want the output of your script/command/function execution to be sent to.
251 | In order for the email to be sent, the output of the job needs to be sent first to a file.
252 | In fact, the files will be attached to your email address.
253 | In order for this to work, you need to install [swiftmailer/swiftmailer](https://github.com/swiftmailer/swiftmailer)
254 |
255 | ```php
256 | $scheduler->php('script.php')->output([
257 | // If you specify multiple files, both will be attached to the email
258 | 'my_file1.log', 'my_file2.log'
259 | ])->email([
260 | 'someemail@mail.com' => 'My custom name',
261 | 'someotheremail@mail.com'
262 | ]);
263 | ```
264 |
265 | You can optionally customize the `Swift_Mailer` instance with a custom `Swift_Transport`.
266 | You can configure:
267 | - `subject` - The subject of the email sent
268 | - `from` - The email address set as sender
269 | - `body` - The body of the email
270 | - `transport` - The transport to use. For example if you want to use your gmail account or any other SMTP account. The value should be an instance of `Swift_Tranport`
271 | - `ignore_empty_output` - If this is set to `true`, jobs that return no output won't fire any email.
272 |
273 | The configuration can be set "globally" for all the scheduler commands, when creating the scheduler.
274 |
275 | ```php
276 | $scheduler = new Scheduler([
277 | 'email' => [
278 | 'subject' => 'Visitors count',
279 | 'from' => 'cron@email.com',
280 | 'body' => 'This is the daily visitors count',
281 | 'transport' => Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, 'ssl')
282 | ->setUsername('username')
283 | ->setPassword('password'),
284 | 'ignore_empty_output' => false,
285 | ]
286 | ]);
287 | ```
288 |
289 | Or can be set on a job per job basis.
290 |
291 | ```php
292 | $scheduler = new Scheduler();
293 |
294 | $scheduler->php('myscript.php')->configure([
295 | 'email' => [
296 | 'subject' => 'Visitors count',
297 | ]
298 | ]);
299 |
300 | $scheduler->php('my_other_script.php')->configure([
301 | 'email' => [
302 | 'subject' => 'Page views count',
303 | ]
304 | ]);
305 | ```
306 |
307 | ### Schedule conditional execution
308 |
309 | Sometimes you might want to execute a schedule not only when the execution is due, but also depending on some other condition.
310 |
311 | You can delegate the execution of a cronjob to a truthful test with the method `when`.
312 |
313 | ```php
314 | $scheduler->php('script.php')->when(function () {
315 | // The job will run (if due) only when
316 | // this function returns true
317 | return true;
318 | });
319 | ```
320 |
321 | ### Schedules execution order
322 |
323 | The jobs that are due to run are being ordered by their execution: jobs that can run in **background** will be executed **first**.
324 |
325 | ### Schedules overlapping
326 |
327 | To prevent the execution of a schedule while the previous execution is still in progress, use the method `onlyOne`. To avoid overlapping, the Scheduler needs to create **lock files**.
328 | By default it will be used the directory path used for temporary files.
329 |
330 | You can specify a custom directory path globally, when creating a new Scheduler instance.
331 |
332 | ```php
333 | $scheduler = new Scheduler([
334 | 'tempDir' => 'path/to/my/tmp/dir'
335 | ]);
336 |
337 | $scheduler->php('script.php')->onlyOne();
338 | ```
339 |
340 | Or you can define the directory path on a job per job basis.
341 |
342 | ```php
343 | $scheduler = new Scheduler();
344 |
345 | // This will use the default directory path
346 | $scheduler->php('script.php')->onlyOne();
347 |
348 | $scheduler->php('script.php')->onlyOne('path/to/my/tmp/dir');
349 | $scheduler->php('other_script.php')->onlyOne('path/to/my/other/tmp/dir');
350 | ```
351 |
352 | In some cases you might want to run the job also if it's overlapping.
353 | For example if the last execution was more that 5 minutes ago.
354 | You can pass a function as a second parameter, the last execution time will be injected.
355 | The job will not run until this function returns `false`. If it returns `true`, the job will run if overlapping.
356 |
357 | ```php
358 | $scheduler->php('script.php')->onlyOne(null, function ($lastExecutionTime) {
359 | return (time() - $lastExecutionTime) > (60 * 5);
360 | });
361 | ```
362 |
363 | ### Before job execution
364 |
365 | In some cases you might want to run some code, if the job is due to run, before it's being executed.
366 | For example you might want to add a log entry, ping a url or anything else.
367 | To do so, you can call the `before` like the example below.
368 |
369 | ```php
370 | // $logger here is your own implementation
371 | $scheduler->php('script.php')->before(function () use ($logger) {
372 | $logger->info("script.php started at " . time());
373 | });
374 | ```
375 |
376 | ### After job execution
377 |
378 | Sometime you might wish to do something after a job runs. The `then` methods provides you the flexibility to do anything you want after the job execution. The output of the job will be injected to this function.
379 | For example you might want to add an entry to you logs, ping a url etc...
380 | By default, the job will be forced to run in foreground (because the output is injected to the function), if you don't need the output, you can pass `true` as a second parameter to allow the execution in background (in this case `$output` will be empty).
381 |
382 | ```php
383 | // $logger and $messenger here are your own implementation
384 | $scheduler->php('script.php')->then(function ($output) use ($logger, $messenger) {
385 | $logger->info($output);
386 |
387 | $messenger->ping('myurl.com', $output);
388 | });
389 |
390 | $scheduler->php('script.php')->then(function ($output) use ($logger) {
391 | $logger->info('Job executed!');
392 | }, true);
393 | ```
394 |
395 | #### Using "before" and "then" together
396 |
397 | ```php
398 | // $logger here is your own implementation
399 | $scheduler->php('script.php')
400 | ->before(function () use ($logger) {
401 | $logger->info("script.php started at " . time());
402 | })
403 | ->then(function ($output) use ($logger) {
404 | $logger->info("script.php completed at " . time(), [
405 | 'output' => $output,
406 | ]);
407 | });
408 | ```
409 |
410 | ### Multiple scheduler runs
411 | In some cases you might need to run the scheduler multiple times in the same script.
412 | Although this is not a common case, the following methods will allow you to re-use the same instance of the scheduler.
413 | ```php
414 | # some code
415 | $scheduler->run();
416 | # ...
417 |
418 | // Reset the scheduler after a previous run
419 | $scheduler->resetRun()
420 | ->run(); // now we can run it again
421 | ```
422 |
423 | Another handy method if you are re-using the same instance of the scheduler with different jobs (e.g. job coming from an external source - db, file ...) on every run, is to clear the current scheduled jobs.
424 | ```php
425 | $scheduler->clearJobs();
426 |
427 | $jobsFromDb = $db->query(/*...*/);
428 | foreach ($jobsFromDb as $job) {
429 | $scheduler->php($job->script)->at($job->schedule);
430 | }
431 |
432 | $scheduler->resetRun()
433 | ->run();
434 | ```
435 |
436 | ### Faking scheduler run time
437 | When running the scheduler you might pass an `DateTime` to fake the scheduler run time.
438 | The resons for this feature are described [here](https://github.com/peppeocchi/php-cron-scheduler/pull/28);
439 |
440 | ```
441 | // ...
442 | $fakeRunTime = new DateTime('2017-09-13 00:00:00');
443 | $scheduler->run($fakeRunTime);
444 | ```
445 |
446 | ### Job failures
447 | If some job fails, you can access list of failed jobs and reasons for failures.
448 |
449 | ```php
450 | // get all failed jobs and select first
451 | $failedJob = $scheduler->getFailedJobs()[0];
452 |
453 | // exception that occurred during job
454 | $exception = $failedJob->getException();
455 |
456 | // job that failed
457 | $job = $failedJob->getJob();
458 | ```
459 |
460 | ### Worker
461 | You can simulate a cronjob by starting a worker. Let's see a simple example
462 | ```php
463 | $scheduler = new Scheduler();
464 | $scheduler->php('some/script.php');
465 | $scheduler->work();
466 | ```
467 | The above code starts a worker that will run your job/s every minute.
468 | This is meant to be a testing/debugging tool, but you're free to use it however you like.
469 | You can optionally pass an array of "seconds" of when you want the worker to run your jobs, for example by passing `[0, 30]`, the worker will run your jobs at second **0** and at second **30** of the minute.
470 | ```php
471 | $scheduler->work([0, 10, 25, 50, 55]);
472 | ```
473 |
474 | It is highly advisable that you run your worker separately from your scheduler, although you can run the worker within your scheduler. The problem comes when your scheduler has one or more synchronous job, and the worker will have to wait for your job to complete before continuing the loop. For example
475 | ```php
476 | $scheduler->call(function () {
477 | sleep(120);
478 | });
479 | $scheduler->work();
480 | ```
481 | The above will skip more than one execution, so it won't run anymore every minute but it will run probably every 2 or 3 minutes.
482 | Instead the preferred approach would be to separate the worker from your scheduler.
483 | ```php
484 | // File scheduler.php
485 | $scheduler = new Scheduler();
486 | $scheduler->call(function () {
487 | sleep(120);
488 | });
489 | $scheduler->run();
490 | ```
491 | ```php
492 | // File worker.php
493 | $scheduler = new Scheduler();
494 | $scheduler->php('scheduler.php');
495 | $scheduler->work();
496 | ```
497 | Then in your command line run `php worker.php`. This will start a foreground process that you can kill by simply exiting the command.
498 |
499 | The worker is not meant to collect any data about your runs, and as already said it is meant to be a testing/debugging tool.
500 |
501 | ## License
502 | [The MIT License (MIT)](LICENSE)
503 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "peppeocchi/php-cron-scheduler",
3 | "description": "PHP Cron Job Scheduler",
4 | "license": "MIT",
5 | "keywords": ["cron job", "scheduler"],
6 | "authors": [
7 | {
8 | "name": "Giuseppe Occhipinti",
9 | "email": "peppeocchi@gmail.com"
10 | },
11 | {
12 | "name": "Carsten Windler",
13 | "email": "carsten@carstenwindler.de",
14 | "homepage": "http://carstenwindler.de",
15 | "role": "Contributor"
16 | }
17 | ],
18 | "minimum-stability": "dev",
19 | "require": {
20 | "php": "^7.3 || ^8.0",
21 | "dragonmantank/cron-expression": "^3.0"
22 | },
23 | "require-dev": {
24 | "phpunit/phpunit": "~9.5",
25 | "php-coveralls/php-coveralls": "^2.4",
26 | "swiftmailer/swiftmailer": "~5.4 || ^6.0"
27 | },
28 | "suggest": {
29 | "swiftmailer/swiftmailer": "Required to send the output of a job to email address/es (~5.4 || ^6.0)."
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "GO\\": "src/GO/"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "Tests\\": "tests/GO/"
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | src/GO
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ./tests/
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/GO/FailedJob.php:
--------------------------------------------------------------------------------
1 | job = $job;
20 | $this->exception = $exception;
21 | }
22 |
23 | public function getJob(): Job
24 | {
25 | return $this->job;
26 | }
27 |
28 | public function getException(): Exception
29 | {
30 | return $this->exception;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/GO/Job.php:
--------------------------------------------------------------------------------
1 | id = $id;
157 | } else {
158 | if (is_string($command)) {
159 | $this->id = md5($command);
160 | } elseif (is_array($command)) {
161 | $this->id = md5(serialize($command));
162 | } else {
163 | /* @var object $command */
164 | $this->id = spl_object_hash($command);
165 | }
166 | }
167 |
168 | $this->creationTime = new DateTime('now');
169 |
170 | // initialize the directory path for lock files
171 | $this->tempDir = sys_get_temp_dir();
172 |
173 | $this->command = $command;
174 | $this->args = $args;
175 | }
176 |
177 | /**
178 | * Get the Job id.
179 | *
180 | * @return string
181 | */
182 | public function getId()
183 | {
184 | return $this->id;
185 | }
186 |
187 | /**
188 | * Check if the Job is due to run.
189 | * It accepts as input a DateTime used to check if
190 | * the job is due. Defaults to job creation time.
191 | * It also defaults the execution time if not previously defined.
192 | *
193 | * @param DateTime $date
194 | * @return bool
195 | */
196 | public function isDue(DateTime $date = null)
197 | {
198 | // The execution time is being defaulted if not defined
199 | if (! $this->executionTime) {
200 | $this->at('* * * * *');
201 | }
202 |
203 | $date = $date !== null ? $date : $this->creationTime;
204 |
205 | if ($this->executionYear && $this->executionYear !== $date->format('Y')) {
206 | return false;
207 | }
208 |
209 | return $this->executionTime->isDue($date);
210 | }
211 |
212 | /**
213 | * Check if the Job is overlapping.
214 | *
215 | * @return bool
216 | */
217 | public function isOverlapping()
218 | {
219 | return $this->lockFile &&
220 | file_exists($this->lockFile) &&
221 | call_user_func($this->whenOverlapping, filemtime($this->lockFile)) === false;
222 | }
223 |
224 | /**
225 | * Force the Job to run in foreground.
226 | *
227 | * @return self
228 | */
229 | public function inForeground()
230 | {
231 | $this->runInBackground = false;
232 |
233 | return $this;
234 | }
235 |
236 | /**
237 | * Check if the Job can run in background.
238 | *
239 | * @return bool
240 | */
241 | public function canRunInBackground()
242 | {
243 | if (is_callable($this->command) || $this->runInBackground === false) {
244 | return false;
245 | }
246 |
247 | return true;
248 | }
249 |
250 | /**
251 | * This will prevent the Job from overlapping.
252 | * It prevents another instance of the same Job of
253 | * being executed if the previous is still running.
254 | * The job id is used as a filename for the lock file.
255 | *
256 | * @param string $tempDir The directory path for the lock files
257 | * @param callable $whenOverlapping A callback to ignore job overlapping
258 | * @return self
259 | */
260 | public function onlyOne($tempDir = null, callable $whenOverlapping = null)
261 | {
262 | if ($tempDir === null || ! is_dir($tempDir)) {
263 | $tempDir = $this->tempDir;
264 | }
265 |
266 | $this->lockFile = implode('/', [
267 | trim($tempDir),
268 | trim($this->id) . '.lock',
269 | ]);
270 |
271 | if ($whenOverlapping) {
272 | $this->whenOverlapping = $whenOverlapping;
273 | } else {
274 | $this->whenOverlapping = function () {
275 | return false;
276 | };
277 | }
278 |
279 | return $this;
280 | }
281 |
282 | /**
283 | * Compile the Job command.
284 | *
285 | * @return mixed
286 | */
287 | public function compile()
288 | {
289 | $compiled = $this->command;
290 |
291 | // If callable, return the function itself
292 | if (is_callable($compiled)) {
293 | return $compiled;
294 | }
295 |
296 | // Augment with any supplied arguments
297 | foreach ($this->args as $key => $value) {
298 | $compiled .= ' ' . escapeshellarg($key);
299 | if ($value !== null) {
300 | $compiled .= ' ' . escapeshellarg($value);
301 | }
302 | }
303 |
304 | // Add the boilerplate to redirect the output to file/s
305 | if (count($this->outputTo) > 0) {
306 | $compiled .= ' | tee ';
307 | $compiled .= $this->outputMode === 'a' ? '-a ' : '';
308 | foreach ($this->outputTo as $file) {
309 | $compiled .= $file . ' ';
310 | }
311 |
312 | $compiled = trim($compiled);
313 | }
314 |
315 | // Add boilerplate to remove lockfile after execution
316 | if ($this->lockFile) {
317 | $compiled .= '; rm ' . $this->lockFile;
318 | }
319 |
320 | // Add boilerplate to run in background
321 | if ($this->canRunInBackground()) {
322 | // Parentheses are need execute the chain of commands in a subshell
323 | // that can then run in background
324 | $compiled = '(' . $compiled . ') > /dev/null 2>&1 &';
325 | }
326 |
327 | return trim($compiled);
328 | }
329 |
330 | /**
331 | * Configure the job.
332 | *
333 | * @param array $config
334 | * @return self
335 | */
336 | public function configure(array $config = [])
337 | {
338 | if (isset($config['email'])) {
339 | if (! is_array($config['email'])) {
340 | throw new InvalidArgumentException('Email configuration should be an array.');
341 | }
342 | $this->emailConfig = $config['email'];
343 | }
344 |
345 | // Check if config has defined a tempDir
346 | if (isset($config['tempDir']) && is_dir($config['tempDir'])) {
347 | $this->tempDir = $config['tempDir'];
348 | }
349 |
350 | return $this;
351 | }
352 |
353 | /**
354 | * Truth test to define if the job should run if due.
355 | *
356 | * @param callable $fn
357 | * @return self
358 | */
359 | public function when(callable $fn)
360 | {
361 | $this->truthTest = $fn();
362 |
363 | return $this;
364 | }
365 |
366 | /**
367 | * Run the job.
368 | *
369 | * @return bool
370 | */
371 | public function run()
372 | {
373 | // If the truthTest failed, don't run
374 | if ($this->truthTest !== true) {
375 | return false;
376 | }
377 |
378 | // If overlapping, don't run
379 | if ($this->isOverlapping()) {
380 | return false;
381 | }
382 |
383 | $compiled = $this->compile();
384 |
385 | // Write lock file if necessary
386 | $this->createLockFile();
387 |
388 | if (is_callable($this->before)) {
389 | call_user_func($this->before);
390 | }
391 |
392 | if (is_callable($compiled)) {
393 | $this->output = $this->exec($compiled);
394 | } else {
395 | exec($compiled, $this->output, $this->returnCode);
396 | }
397 |
398 | $this->finalise();
399 |
400 | return true;
401 | }
402 |
403 | /**
404 | * Create the job lock file.
405 | *
406 | * @param mixed $content
407 | * @return void
408 | */
409 | private function createLockFile($content = null)
410 | {
411 | if ($this->lockFile) {
412 | if ($content === null || ! is_string($content)) {
413 | $content = $this->getId();
414 | }
415 |
416 | file_put_contents($this->lockFile, $content);
417 | }
418 | }
419 |
420 | /**
421 | * Remove the job lock file.
422 | *
423 | * @return void
424 | */
425 | private function removeLockFile()
426 | {
427 | if ($this->lockFile && file_exists($this->lockFile)) {
428 | unlink($this->lockFile);
429 | }
430 | }
431 |
432 | /**
433 | * Execute a callable job.
434 | *
435 | * @param callable $fn
436 | * @throws Exception
437 | * @return string
438 | */
439 | private function exec(callable $fn)
440 | {
441 | ob_start();
442 |
443 | try {
444 | $returnData = call_user_func_array($fn, $this->args);
445 | } catch (Exception $e) {
446 | ob_end_clean();
447 | throw $e;
448 | }
449 |
450 | $outputBuffer = ob_get_clean();
451 |
452 | foreach ($this->outputTo as $filename) {
453 | if ($outputBuffer) {
454 | file_put_contents($filename, $outputBuffer, $this->outputMode === 'a' ? FILE_APPEND : 0);
455 | }
456 |
457 | if ($returnData) {
458 | file_put_contents($filename, $returnData, FILE_APPEND);
459 | }
460 | }
461 |
462 | $this->removeLockFile();
463 |
464 | return $outputBuffer . (is_string($returnData) ? $returnData : '');
465 | }
466 |
467 | /**
468 | * Set the file/s where to write the output of the job.
469 | *
470 | * @param string|array $filename
471 | * @param bool $append
472 | * @return self
473 | */
474 | public function output($filename, $append = false)
475 | {
476 | $this->outputTo = is_array($filename) ? $filename : [$filename];
477 | $this->outputMode = $append === false ? 'w' : 'a';
478 |
479 | return $this;
480 | }
481 |
482 | /**
483 | * Get the job output.
484 | *
485 | * @return mixed
486 | */
487 | public function getOutput()
488 | {
489 | return $this->output;
490 | }
491 |
492 | /**
493 | * Set the emails where the output should be sent to.
494 | * The Job should be set to write output to a file
495 | * for this to work.
496 | *
497 | * @param string|array $email
498 | * @return self
499 | */
500 | public function email($email)
501 | {
502 | if (! is_string($email) && ! is_array($email)) {
503 | throw new InvalidArgumentException('The email can be only string or array');
504 | }
505 |
506 | $this->emailTo = is_array($email) ? $email : [$email];
507 |
508 | // Force the job to run in foreground
509 | $this->inForeground();
510 |
511 | return $this;
512 | }
513 |
514 | /**
515 | * Finilise the job after execution.
516 | *
517 | * @return void
518 | */
519 | private function finalise()
520 | {
521 | // Send output to email
522 | $this->emailOutput();
523 |
524 | // Call any callback defined
525 | if (is_callable($this->after)) {
526 | call_user_func($this->after, $this->output, $this->returnCode);
527 | }
528 | }
529 |
530 | /**
531 | * Email the output of the job, if any.
532 | *
533 | * @return bool
534 | */
535 | private function emailOutput()
536 | {
537 | if (! count($this->outputTo) || ! count($this->emailTo)) {
538 | return false;
539 | }
540 |
541 | if (isset($this->emailConfig['ignore_empty_output']) &&
542 | $this->emailConfig['ignore_empty_output'] === true &&
543 | empty($this->output)
544 | ) {
545 | return false;
546 | }
547 |
548 | $this->sendToEmails($this->outputTo);
549 |
550 | return true;
551 | }
552 |
553 | /**
554 | * Set function to be called before job execution
555 | * Job object is injected as a parameter to callable function.
556 | *
557 | * @param callable $fn
558 | * @return self
559 | */
560 | public function before(callable $fn)
561 | {
562 | $this->before = $fn;
563 |
564 | return $this;
565 | }
566 |
567 | /**
568 | * Set a function to be called after job execution.
569 | * By default this will force the job to run in foreground
570 | * because the output is injected as a parameter of this
571 | * function, but it could be avoided by passing true as a
572 | * second parameter. The job will run in background if it
573 | * meets all the other criteria.
574 | *
575 | * @param callable $fn
576 | * @param bool $runInBackground
577 | * @return self
578 | */
579 | public function then(callable $fn, $runInBackground = false)
580 | {
581 | $this->after = $fn;
582 |
583 | // Force the job to run in foreground
584 | if ($runInBackground === false) {
585 | $this->inForeground();
586 | }
587 |
588 | return $this;
589 | }
590 |
591 | /**
592 | * @return Cron\CronExpression
593 | */
594 | public function getExecutionTime()
595 | {
596 | if (! $this->executionTime) {
597 | return Cron\CronExpression::factory('* * * * *');
598 | }
599 |
600 | return $this->executionTime;
601 | }
602 | }
603 |
--------------------------------------------------------------------------------
/src/GO/Scheduler.php:
--------------------------------------------------------------------------------
1 | config = $config;
50 | }
51 |
52 | /**
53 | * Queue a job for execution in the correct queue.
54 | *
55 | * @param Job $job
56 | * @return void
57 | */
58 | private function queueJob(Job $job)
59 | {
60 | $this->jobs[] = $job;
61 | }
62 |
63 | /**
64 | * Prioritise jobs in background.
65 | *
66 | * @return array
67 | */
68 | private function prioritiseJobs()
69 | {
70 | $background = [];
71 | $foreground = [];
72 |
73 | foreach ($this->jobs as $job) {
74 | if ($job->canRunInBackground()) {
75 | $background[] = $job;
76 | } else {
77 | $foreground[] = $job;
78 | }
79 | }
80 |
81 | return array_merge($background, $foreground);
82 | }
83 |
84 | /**
85 | * Get the queued jobs.
86 | *
87 | * @return array
88 | */
89 | public function getQueuedJobs()
90 | {
91 | return $this->prioritiseJobs();
92 | }
93 |
94 | /**
95 | * Queues a function execution.
96 | *
97 | * @param callable $fn The function to execute
98 | * @param array $args Optional arguments to pass to the php script
99 | * @param string $id Optional custom identifier
100 | * @return Job
101 | */
102 | public function call(callable $fn, $args = [], $id = null)
103 | {
104 | $job = new Job($fn, $args, $id);
105 |
106 | $this->queueJob($job->configure($this->config));
107 |
108 | return $job;
109 | }
110 |
111 | /**
112 | * Queues a php script execution.
113 | *
114 | * @param string $script The path to the php script to execute
115 | * @param string $bin Optional path to the php binary
116 | * @param array $args Optional arguments to pass to the php script
117 | * @param string $id Optional custom identifier
118 | * @return Job
119 | */
120 | public function php($script, $bin = null, $args = [], $id = null)
121 | {
122 | if (! is_string($script)) {
123 | throw new InvalidArgumentException('The script should be a valid path to a file.');
124 | }
125 |
126 | $bin = $bin !== null && is_string($bin) && file_exists($bin) ?
127 | $bin : (PHP_BINARY === '' ? '/usr/bin/php' : PHP_BINARY);
128 |
129 | $job = new Job($bin . ' ' . $script, $args, $id);
130 |
131 | if (! file_exists($script)) {
132 | $this->pushFailedJob(
133 | $job,
134 | new InvalidArgumentException('The script should be a valid path to a file.')
135 | );
136 | }
137 |
138 | $this->queueJob($job->configure($this->config));
139 |
140 | return $job;
141 | }
142 |
143 | /**
144 | * Queue a raw shell command.
145 | *
146 | * @param string $command The command to execute
147 | * @param array $args Optional arguments to pass to the command
148 | * @param string $id Optional custom identifier
149 | * @return Job
150 | */
151 | public function raw($command, $args = [], $id = null)
152 | {
153 | $job = new Job($command, $args, $id);
154 |
155 | $this->queueJob($job->configure($this->config));
156 |
157 | return $job;
158 | }
159 |
160 | /**
161 | * Run the scheduler.
162 | *
163 | * @param DateTime $runTime Optional, run at specific moment
164 | * @return array Executed jobs
165 | */
166 | public function run(Datetime $runTime = null)
167 | {
168 | $jobs = $this->getQueuedJobs();
169 |
170 | if (is_null($runTime)) {
171 | $runTime = new DateTime('now');
172 | }
173 |
174 | foreach ($jobs as $job) {
175 | if ($job->isDue($runTime)) {
176 | try {
177 | $job->run();
178 | $this->pushExecutedJob($job);
179 | } catch (\Exception $e) {
180 | $this->pushFailedJob($job, $e);
181 | }
182 | }
183 | }
184 |
185 | return $this->getExecutedJobs();
186 | }
187 |
188 | /**
189 | * Reset all collected data of last run.
190 | *
191 | * Call before run() if you call run() multiple times.
192 | */
193 | public function resetRun()
194 | {
195 | // Reset collected data of last run
196 | $this->executedJobs = [];
197 | $this->failedJobs = [];
198 | $this->outputSchedule = [];
199 |
200 | return $this;
201 | }
202 |
203 | /**
204 | * Add an entry to the scheduler verbose output array.
205 | *
206 | * @param string $string
207 | * @return void
208 | */
209 | private function addSchedulerVerboseOutput($string)
210 | {
211 | $now = '[' . (new DateTime('now'))->format('c') . '] ';
212 | $this->outputSchedule[] = $now . $string;
213 |
214 | // Print to stdoutput in light gray
215 | // echo "\033[37m{$string}\033[0m\n";
216 | }
217 |
218 | /**
219 | * Push a succesfully executed job.
220 | *
221 | * @param Job $job
222 | * @return Job
223 | */
224 | private function pushExecutedJob(Job $job)
225 | {
226 | $this->executedJobs[] = $job;
227 |
228 | $compiled = $job->compile();
229 |
230 | // If callable, log the string Closure
231 | if (is_callable($compiled)) {
232 | $compiled = 'Closure';
233 | }
234 |
235 | $this->addSchedulerVerboseOutput("Executing {$compiled}");
236 |
237 | return $job;
238 | }
239 |
240 | /**
241 | * Get the executed jobs.
242 | *
243 | * @return array
244 | */
245 | public function getExecutedJobs()
246 | {
247 | return $this->executedJobs;
248 | }
249 |
250 | /**
251 | * Push a failed job.
252 | *
253 | * @param Job $job
254 | * @param Exception $e
255 | * @return Job
256 | */
257 | private function pushFailedJob(Job $job, Exception $e)
258 | {
259 | $this->failedJobs[] = new FailedJob($job, $e);
260 |
261 | $compiled = $job->compile();
262 |
263 | // If callable, log the string Closure
264 | if (is_callable($compiled)) {
265 | $reflectionClosure = new \ReflectionFunction($compiled);
266 |
267 | $compiled = 'Closure ' . $reflectionClosure->getClosureScopeClass()->getName();
268 | }
269 |
270 | $this->addSchedulerVerboseOutput("{$e->getMessage()}: {$compiled}");
271 |
272 | return $job;
273 | }
274 |
275 | /**
276 | * Get the failed jobs.
277 | *
278 | * @return FailedJob[]
279 | */
280 | public function getFailedJobs()
281 | {
282 | return $this->failedJobs;
283 | }
284 |
285 | /**
286 | * Get the scheduler verbose output.
287 | *
288 | * @param string $type Allowed: text, html, array
289 | * @return mixed The return depends on the requested $type
290 | */
291 | public function getVerboseOutput($type = 'text')
292 | {
293 | switch ($type) {
294 | case 'text':
295 | return implode("\n", $this->outputSchedule);
296 | case 'html':
297 | return implode('
', $this->outputSchedule);
298 | case 'array':
299 | return $this->outputSchedule;
300 | default:
301 | throw new InvalidArgumentException('Invalid output type');
302 | }
303 | }
304 |
305 | /**
306 | * Remove all queued Jobs.
307 | */
308 | public function clearJobs()
309 | {
310 | $this->jobs = [];
311 |
312 | return $this;
313 | }
314 |
315 | /**
316 | * Start a worker.
317 | *
318 | * @param array $seconds - When the scheduler should run
319 | */
320 | public function work(array $seconds = [0])
321 | {
322 | while (true) {
323 | if (in_array((int) date('s'), $seconds)) {
324 | $this->run();
325 | sleep(1);
326 | }
327 | }
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/src/GO/Traits/Interval.php:
--------------------------------------------------------------------------------
1 | executionTime = CronExpression::factory($expression);
18 |
19 | return $this;
20 | }
21 |
22 | /**
23 | * Run the Job at a specific date.
24 | *
25 | * @param string|DateTime $date
26 | * @return self
27 | */
28 | public function date($date)
29 | {
30 | if (! $date instanceof DateTime) {
31 | $date = new DateTime($date);
32 | }
33 |
34 | $this->executionYear = $date->format('Y');
35 |
36 | return $this->at("{$date->format('i')} {$date->format('H')} {$date->format('d')} {$date->format('m')} *");
37 | }
38 |
39 | /**
40 | * Set the execution time to every minute.
41 | *
42 | * @param int|string|null When set, specifies that the job will be run every $minute minutes
43 | *
44 | * @return self
45 | */
46 | public function everyMinute($minute = null)
47 | {
48 | $minuteExpression = '*';
49 | if ($minute !== null) {
50 | $c = $this->validateCronSequence($minute);
51 | $minuteExpression = '*/' . $c['minute'];
52 | }
53 |
54 | return $this->at($minuteExpression . ' * * * *');
55 | }
56 |
57 | /**
58 | * Set the execution time to every hour.
59 | *
60 | * @param int|string $minute
61 | * @return self
62 | */
63 | public function hourly($minute = 0)
64 | {
65 | $c = $this->validateCronSequence($minute);
66 |
67 | return $this->at("{$c['minute']} * * * *");
68 | }
69 |
70 | /**
71 | * Set the execution time to once a day.
72 | *
73 | * @param int|string $hour
74 | * @param int|string $minute
75 | * @return self
76 | */
77 | public function daily($hour = 0, $minute = 0)
78 | {
79 | if (is_string($hour)) {
80 | $parts = explode(':', $hour);
81 | $hour = $parts[0];
82 | $minute = isset($parts[1]) ? $parts[1] : '0';
83 | }
84 |
85 | $c = $this->validateCronSequence($minute, $hour);
86 |
87 | return $this->at("{$c['minute']} {$c['hour']} * * *");
88 | }
89 |
90 | /**
91 | * Set the execution time to once a week.
92 | *
93 | * @param int|string $weekday
94 | * @param int|string $hour
95 | * @param int|string $minute
96 | * @return self
97 | */
98 | public function weekly($weekday = 0, $hour = 0, $minute = 0)
99 | {
100 | if (is_string($hour)) {
101 | $parts = explode(':', $hour);
102 | $hour = $parts[0];
103 | $minute = isset($parts[1]) ? $parts[1] : '0';
104 | }
105 |
106 | $c = $this->validateCronSequence($minute, $hour, null, null, $weekday);
107 |
108 | return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}");
109 | }
110 |
111 | /**
112 | * Set the execution time to once a month.
113 | *
114 | * @param int|string $month
115 | * @param int|string $day
116 | * @param int|string $hour
117 | * @param int|string $minute
118 | * @return self
119 | */
120 | public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0)
121 | {
122 | if (is_string($hour)) {
123 | $parts = explode(':', $hour);
124 | $hour = $parts[0];
125 | $minute = isset($parts[1]) ? $parts[1] : '0';
126 | }
127 |
128 | $c = $this->validateCronSequence($minute, $hour, $day, $month);
129 |
130 | return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *");
131 | }
132 |
133 | /**
134 | * Set the execution time to every Sunday.
135 | *
136 | * @param int|string $hour
137 | * @param int|string $minute
138 | * @return self
139 | */
140 | public function sunday($hour = 0, $minute = 0)
141 | {
142 | return $this->weekly(0, $hour, $minute);
143 | }
144 |
145 | /**
146 | * Set the execution time to every Monday.
147 | *
148 | * @param int|string $hour
149 | * @param int|string $minute
150 | * @return self
151 | */
152 | public function monday($hour = 0, $minute = 0)
153 | {
154 | return $this->weekly(1, $hour, $minute);
155 | }
156 |
157 | /**
158 | * Set the execution time to every Tuesday.
159 | *
160 | * @param int|string $hour
161 | * @param int|string $minute
162 | * @return self
163 | */
164 | public function tuesday($hour = 0, $minute = 0)
165 | {
166 | return $this->weekly(2, $hour, $minute);
167 | }
168 |
169 | /**
170 | * Set the execution time to every Wednesday.
171 | *
172 | * @param int|string $hour
173 | * @param int|string $minute
174 | * @return self
175 | */
176 | public function wednesday($hour = 0, $minute = 0)
177 | {
178 | return $this->weekly(3, $hour, $minute);
179 | }
180 |
181 | /**
182 | * Set the execution time to every Thursday.
183 | *
184 | * @param int|string $hour
185 | * @param int|string $minute
186 | * @return self
187 | */
188 | public function thursday($hour = 0, $minute = 0)
189 | {
190 | return $this->weekly(4, $hour, $minute);
191 | }
192 |
193 | /**
194 | * Set the execution time to every Friday.
195 | *
196 | * @param int|string $hour
197 | * @param int|string $minute
198 | * @return self
199 | */
200 | public function friday($hour = 0, $minute = 0)
201 | {
202 | return $this->weekly(5, $hour, $minute);
203 | }
204 |
205 | /**
206 | * Set the execution time to every Saturday.
207 | *
208 | * @param int|string $hour
209 | * @param int|string $minute
210 | * @return self
211 | */
212 | public function saturday($hour = 0, $minute = 0)
213 | {
214 | return $this->weekly(6, $hour, $minute);
215 | }
216 |
217 | /**
218 | * Set the execution time to every January.
219 | *
220 | * @param int|string $day
221 | * @param int|string $hour
222 | * @param int|string $minute
223 | * @return self
224 | */
225 | public function january($day = 1, $hour = 0, $minute = 0)
226 | {
227 | return $this->monthly(1, $day, $hour, $minute);
228 | }
229 |
230 | /**
231 | * Set the execution time to every February.
232 | *
233 | * @param int|string $day
234 | * @param int|string $hour
235 | * @param int|string $minute
236 | * @return self
237 | */
238 | public function february($day = 1, $hour = 0, $minute = 0)
239 | {
240 | return $this->monthly(2, $day, $hour, $minute);
241 | }
242 |
243 | /**
244 | * Set the execution time to every March.
245 | *
246 | * @param int|string $day
247 | * @param int|string $hour
248 | * @param int|string $minute
249 | * @return self
250 | */
251 | public function march($day = 1, $hour = 0, $minute = 0)
252 | {
253 | return $this->monthly(3, $day, $hour, $minute);
254 | }
255 |
256 | /**
257 | * Set the execution time to every April.
258 | *
259 | * @param int|string $day
260 | * @param int|string $hour
261 | * @param int|string $minute
262 | * @return self
263 | */
264 | public function april($day = 1, $hour = 0, $minute = 0)
265 | {
266 | return $this->monthly(4, $day, $hour, $minute);
267 | }
268 |
269 | /**
270 | * Set the execution time to every May.
271 | *
272 | * @param int|string $day
273 | * @param int|string $hour
274 | * @param int|string $minute
275 | * @return self
276 | */
277 | public function may($day = 1, $hour = 0, $minute = 0)
278 | {
279 | return $this->monthly(5, $day, $hour, $minute);
280 | }
281 |
282 | /**
283 | * Set the execution time to every June.
284 | *
285 | * @param int|string $day
286 | * @param int|string $hour
287 | * @param int|string $minute
288 | * @return self
289 | */
290 | public function june($day = 1, $hour = 0, $minute = 0)
291 | {
292 | return $this->monthly(6, $day, $hour, $minute);
293 | }
294 |
295 | /**
296 | * Set the execution time to every July.
297 | *
298 | * @param int|string $day
299 | * @param int|string $hour
300 | * @param int|string $minute
301 | * @return self
302 | */
303 | public function july($day = 1, $hour = 0, $minute = 0)
304 | {
305 | return $this->monthly(7, $day, $hour, $minute);
306 | }
307 |
308 | /**
309 | * Set the execution time to every August.
310 | *
311 | * @param int|string $day
312 | * @param int|string $hour
313 | * @param int|string $minute
314 | * @return self
315 | */
316 | public function august($day = 1, $hour = 0, $minute = 0)
317 | {
318 | return $this->monthly(8, $day, $hour, $minute);
319 | }
320 |
321 | /**
322 | * Set the execution time to every September.
323 | *
324 | * @param int|string $day
325 | * @param int|string $hour
326 | * @param int|string $minute
327 | * @return self
328 | */
329 | public function september($day = 1, $hour = 0, $minute = 0)
330 | {
331 | return $this->monthly(9, $day, $hour, $minute);
332 | }
333 |
334 | /**
335 | * Set the execution time to every October.
336 | *
337 | * @param int|string $day
338 | * @param int|string $hour
339 | * @param int|string $minute
340 | * @return self
341 | */
342 | public function october($day = 1, $hour = 0, $minute = 0)
343 | {
344 | return $this->monthly(10, $day, $hour, $minute);
345 | }
346 |
347 | /**
348 | * Set the execution time to every November.
349 | *
350 | * @param int|string $day
351 | * @param int|string $hour
352 | * @param int|string $minute
353 | * @return self
354 | */
355 | public function november($day = 1, $hour = 0, $minute = 0)
356 | {
357 | return $this->monthly(11, $day, $hour, $minute);
358 | }
359 |
360 | /**
361 | * Set the execution time to every December.
362 | *
363 | * @param int|string $day
364 | * @param int|string $hour
365 | * @param int|string $minute
366 | * @return self
367 | */
368 | public function december($day = 1, $hour = 0, $minute = 0)
369 | {
370 | return $this->monthly(12, $day, $hour, $minute);
371 | }
372 |
373 | /**
374 | * Validate sequence of cron expression.
375 | *
376 | * @param int|string $minute
377 | * @param int|string $hour
378 | * @param int|string $day
379 | * @param int|string $month
380 | * @param int|string $weekday
381 | * @return array
382 | */
383 | private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null)
384 | {
385 | return [
386 | 'minute' => $this->validateCronRange($minute, 0, 59),
387 | 'hour' => $this->validateCronRange($hour, 0, 23),
388 | 'day' => $this->validateCronRange($day, 1, 31),
389 | 'month' => $this->validateCronRange($month, 1, 12),
390 | 'weekday' => $this->validateCronRange($weekday, 0, 6),
391 | ];
392 | }
393 |
394 | /**
395 | * Validate sequence of cron expression.
396 | *
397 | * @param int|string $value
398 | * @param int $min
399 | * @param int $max
400 | * @return mixed
401 | */
402 | private function validateCronRange($value, $min, $max)
403 | {
404 | if ($value === null || $value === '*') {
405 | return '*';
406 | }
407 |
408 | if (! is_numeric($value) ||
409 | ! ($value >= $min && $value <= $max)
410 | ) {
411 | throw new InvalidArgumentException(
412 | "Invalid value: it should be '*' or between {$min} and {$max}."
413 | );
414 | }
415 |
416 | return (int) $value;
417 | }
418 | }
419 |
--------------------------------------------------------------------------------
/src/GO/Traits/Mailer.php:
--------------------------------------------------------------------------------
1 | emailConfig['subject']) ||
13 | ! is_string($this->emailConfig['subject'])
14 | ) {
15 | $this->emailConfig['subject'] = 'Cronjob execution';
16 | }
17 |
18 | if (! isset($this->emailConfig['from'])) {
19 | $this->emailConfig['from'] = ['cronjob@server.my' => 'My Email Server'];
20 | }
21 |
22 | if (! isset($this->emailConfig['body']) ||
23 | ! is_string($this->emailConfig['body'])
24 | ) {
25 | $this->emailConfig['body'] = 'Cronjob output attached';
26 | }
27 |
28 | if (! isset($this->emailConfig['transport']) ||
29 | ! ($this->emailConfig['transport'] instanceof \Swift_Transport)
30 | ) {
31 | $this->emailConfig['transport'] = new \Swift_SendmailTransport();
32 | }
33 |
34 | return $this->emailConfig;
35 | }
36 |
37 | /**
38 | * Send files to emails.
39 | *
40 | * @param array $files
41 | * @return void
42 | */
43 | private function sendToEmails(array $files)
44 | {
45 | $config = $this->getEmailConfig();
46 |
47 | $mailer = new \Swift_Mailer($config['transport']);
48 |
49 | $message = (new \Swift_Message())
50 | ->setSubject($config['subject'])
51 | ->setFrom($config['from'])
52 | ->setTo($this->emailTo)
53 | ->setBody($config['body'])
54 | ->addPart('Cronjob output attached
', 'text/html');
55 |
56 | foreach ($files as $filename) {
57 | $message->attach(\Swift_Attachment::fromPath($filename));
58 | }
59 |
60 | $mailer->send($message);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/GO/IntervalTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($job->everyMinute()->isDue(\DateTime::createFromFormat('H:i', '00:00')));
13 | }
14 |
15 | public function testShouldRunHourly()
16 | {
17 | $job = new Job('ls');
18 |
19 | // Default run is at minute 00 every hour
20 | $this->assertTrue($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '10:00')));
21 | $this->assertFalse($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '10:01')));
22 | $this->assertTrue($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '11:00')));
23 | }
24 |
25 | public function testShouldRunHourlyWithCustomInput()
26 | {
27 | $job = new Job('ls');
28 |
29 | $this->assertTrue($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '10:19')));
30 | $this->assertTrue($job->hourly('07')->isDue(\DateTime::createFromFormat('H:i', '10:07')));
31 | $this->assertFalse($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '10:01')));
32 | $this->assertTrue($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '11:19')));
33 | }
34 |
35 | public function testShouldThrowExceptionWithInvalidHourlyMinuteInput()
36 | {
37 | $this->expectException(\InvalidArgumentException::class);
38 |
39 | $job = new Job('ls');
40 | $job->hourly('abc');
41 | }
42 |
43 | public function testShouldRunDaily()
44 | {
45 | $job = new Job('ls');
46 |
47 | // Default run is at 00:00 every day
48 | $this->assertTrue($job->daily()->isDue(\DateTime::createFromFormat('H:i', '00:00')));
49 | }
50 |
51 | public function testShouldRunDailyWithCustomInput()
52 | {
53 | $job = new Job('ls');
54 |
55 | $this->assertTrue($job->daily(19)->isDue(\DateTime::createFromFormat('H:i', '19:00')));
56 | $this->assertTrue($job->daily(19, 53)->isDue(\DateTime::createFromFormat('H:i', '19:53')));
57 | $this->assertFalse($job->daily(19)->isDue(\DateTime::createFromFormat('H:i', '18:00')));
58 | $this->assertFalse($job->daily(19, 53)->isDue(\DateTime::createFromFormat('H:i', '19:52')));
59 |
60 | // A string is also acceptable
61 | $this->assertTrue($job->daily('19')->isDue(\DateTime::createFromFormat('H:i', '19:00')));
62 | $this->assertTrue($job->daily('19:53')->isDue(\DateTime::createFromFormat('H:i', '19:53')));
63 | }
64 |
65 | public function testShouldThrowExceptionWithInvalidDailyHourInput()
66 | {
67 | $this->expectException(\InvalidArgumentException::class);
68 |
69 | $job = new Job('ls');
70 | $job->daily('abc');
71 | }
72 |
73 | public function testShouldThrowExceptionWithInvalidDailyMinuteInput()
74 | {
75 | $this->expectException(\InvalidArgumentException::class);
76 |
77 | $job = new Job('ls');
78 | $job->daily(2, 'abc');
79 | }
80 |
81 | public function testShouldRunWeekly()
82 | {
83 | $job = new Job('ls');
84 |
85 | // Default run is every Sunday at 00:00
86 | $this->assertTrue($job->weekly()->isDue(
87 | new \DateTime('Sunday'))
88 | );
89 |
90 | $this->assertFalse($job->weekly()->isDue(
91 | new \DateTime('Tuesday'))
92 | );
93 | }
94 |
95 | public function testShouldRunWeeklyOnCustomDay()
96 | {
97 | $job = new Job('ls');
98 |
99 | $this->assertTrue($job->weekly(6)->isDue(
100 | new \DateTime('Saturday'))
101 | );
102 |
103 | // Testing also the helpers to run weekly on custom day
104 | $this->assertTrue($job->monday()->isDue(
105 | new \DateTime('Monday'))
106 | );
107 | $this->assertFalse($job->monday()->isDue(
108 | new \DateTime('Saturday'))
109 | );
110 |
111 | $this->assertTrue($job->tuesday()->isDue(
112 | new \DateTime('Tuesday'))
113 | );
114 | $this->assertTrue($job->wednesday()->isDue(
115 | new \DateTime('Wednesday'))
116 | );
117 | $this->assertTrue($job->thursday()->isDue(
118 | new \DateTime('Thursday'))
119 | );
120 | $this->assertTrue($job->friday()->isDue(
121 | new \DateTime('Friday'))
122 | );
123 | $this->assertTrue($job->saturday()->isDue(
124 | new \DateTime('Saturday'))
125 | );
126 | $this->assertTrue($job->sunday()->isDue(
127 | new \DateTime('Sunday'))
128 | );
129 | }
130 |
131 | public function testShouldRunWeeklyOnCustomDayAndTime()
132 | {
133 | $job = new Job('ls');
134 |
135 | $date1 = new \DateTime('Saturday 03:45');
136 | $date2 = new \DateTime('Saturday 03:46');
137 |
138 | $this->assertTrue($job->weekly(6, 3, 45)->isDue($date1));
139 | $this->assertTrue($job->weekly(6, '03:45')->isDue($date1));
140 | $this->assertFalse($job->weekly(6, '03:45')->isDue($date2));
141 | }
142 |
143 | public function testShouldRunMonthly()
144 | {
145 | $job = new Job('ls');
146 |
147 | // Default run is every 1st of the month at 00:00
148 | $this->assertTrue($job->monthly()->isDue(
149 | new \DateTime('01 January'))
150 | );
151 | $this->assertTrue($job->monthly()->isDue(
152 | new \DateTime('01 December'))
153 | );
154 |
155 | $this->assertFalse($job->monthly()->isDue(
156 | new \DateTime('02 January'))
157 | );
158 | }
159 |
160 | public function testShouldRunMonthlyOnCustomMonth()
161 | {
162 | $job = new Job('ls');
163 |
164 | $this->assertTrue($job->monthly()->isDue(
165 | new \DateTime('01 January'))
166 | );
167 |
168 | // Testing also the helpers to run weekly on custom day
169 | $this->assertTrue($job->january()->isDue(
170 | new \DateTime('01 January'))
171 | );
172 | $this->assertFalse($job->january()->isDue(
173 | new \DateTime('01 February'))
174 | );
175 |
176 | $this->assertTrue($job->february()->isDue(
177 | new \DateTime('01 February'))
178 | );
179 |
180 | $this->assertTrue($job->march()->isDue(
181 | new \DateTime('01 March'))
182 | );
183 | $this->assertTrue($job->april()->isDue(
184 | new \DateTime('01 April'))
185 | );
186 | $this->assertTrue($job->may()->isDue(
187 | new \DateTime('01 May'))
188 | );
189 | $this->assertTrue($job->june()->isDue(
190 | new \DateTime('01 June'))
191 | );
192 | $this->assertTrue($job->july()->isDue(
193 | new \DateTime('01 July'))
194 | );
195 | $this->assertTrue($job->august()->isDue(
196 | new \DateTime('01 August'))
197 | );
198 | $this->assertTrue($job->september()->isDue(
199 | new \DateTime('01 September'))
200 | );
201 | $this->assertTrue($job->october()->isDue(
202 | new \DateTime('01 October'))
203 | );
204 | $this->assertTrue($job->november()->isDue(
205 | new \DateTime('01 November'))
206 | );
207 | $this->assertTrue($job->december()->isDue(
208 | new \DateTime('01 December'))
209 | );
210 | }
211 |
212 | public function testShouldRunMonthlyOnCustomDayAndTime()
213 | {
214 | $job = new Job('ls');
215 |
216 | $date1 = new \DateTime('May 15 12:21');
217 | $date2 = new \DateTime('February 15 12:21');
218 | $date3 = new \DateTime('February 16 12:21');
219 |
220 | $this->assertTrue($job->monthly(5, 15, 12, 21)->isDue($date1));
221 | $this->assertTrue($job->monthly(5, 15, '12:21')->isDue($date1));
222 | $this->assertFalse($job->monthly(5, 15, '12:21')->isDue($date2));
223 | // Every 15th at 12:21
224 | $this->assertTrue($job->monthly(null, 15, '12:21')->isDue($date1));
225 | $this->assertTrue($job->monthly(null, 15, '12:21')->isDue($date2));
226 | $this->assertFalse($job->monthly(null, 15, '12:21')->isDue($date3));
227 | }
228 |
229 | public function testShouldRunAtSpecificDate()
230 | {
231 | $job = new Job('ls');
232 |
233 | $date = '2018-01-01';
234 |
235 | // As instance of datetime
236 | $this->assertTrue($job->date(new \DateTime($date))->isDue(new \DateTime($date)));
237 | // As date string
238 | $this->assertTrue($job->date($date)->isDue(new \DateTime($date)));
239 | // Fail for different day
240 | $this->assertFalse($job->date($date)->isDue(new \DateTime('2018-01-02')));
241 | }
242 |
243 | public function testShouldRunAtSpecificDateTime()
244 | {
245 | $job = new Job('ls');
246 |
247 | $date = '2018-01-01 12:20';
248 |
249 | // As instance of datetime
250 | $this->assertTrue($job->date(new \DateTime($date))->isDue(new \DateTime($date)));
251 | // As date string
252 | $this->assertTrue($job->date($date)->isDue(new \DateTime($date)));
253 | // Fail for different time
254 | $this->assertFalse($job->date($date)->isDue(new \DateTime('2018-01-01 12:21')));
255 | }
256 |
257 | public function testShouldFailIfDifferentYear()
258 | {
259 | $job = new Job('ls');
260 |
261 | // As instance of datetime
262 | $this->assertFalse($job->date('2018-01-01')->isDue(new \DateTime('2019-01-01')));
263 | }
264 |
265 | public function testEveryMinuteWithParameter()
266 | {
267 | $job = new Job('ls');
268 |
269 | // Job should run at 10:00, 10:05, 10:10 etc., but not at 10:02
270 | $this->assertTrue($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:00')));
271 | $this->assertFalse($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:02')));
272 | $this->assertTrue($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:05')));
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/tests/GO/JobOutputFilesTest.php:
--------------------------------------------------------------------------------
1 | assertFalse(file_exists($outputFile));
18 | $job->output($outputFile)->run();
19 |
20 | sleep(2);
21 | $this->assertTrue(file_exists($outputFile));
22 |
23 | // Content should be 'hi'
24 | $this->assertEquals('hi', file_get_contents($outputFile));
25 |
26 | unlink($outputFile);
27 | }
28 |
29 | public function testShouldWriteCommandOutputToMultipleFiles()
30 | {
31 | $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
32 | $job = new Job($command);
33 | $outputFile1 = __DIR__ . '/../tmp/output1.log';
34 | $outputFile2 = __DIR__ . '/../tmp/output2.log';
35 | $outputFile3 = __DIR__ . '/../tmp/output3.log';
36 |
37 | @unlink($outputFile1);
38 | @unlink($outputFile2);
39 | @unlink($outputFile3);
40 |
41 | // Test fist that the file doesn't exist yet
42 | $this->assertFalse(file_exists($outputFile1));
43 | $this->assertFalse(file_exists($outputFile2));
44 | $this->assertFalse(file_exists($outputFile3));
45 | $job->output([
46 | $outputFile1,
47 | $outputFile2,
48 | $outputFile3,
49 | ])->run();
50 |
51 | sleep(2);
52 | $this->assertTrue(file_exists($outputFile1));
53 | $this->assertTrue(file_exists($outputFile2));
54 | $this->assertTrue(file_exists($outputFile3));
55 |
56 | $this->assertEquals('hi', file_get_contents($outputFile1));
57 | $this->assertEquals('hi', file_get_contents($outputFile2));
58 | $this->assertEquals('hi', file_get_contents($outputFile3));
59 |
60 | unlink($outputFile1);
61 | unlink($outputFile2);
62 | unlink($outputFile3);
63 | }
64 |
65 | public function testShouldWriteFunctionOutputToSingleFile()
66 | {
67 | $job = new Job(function () {
68 | echo 'Hello ';
69 |
70 | return 'World!';
71 | });
72 | $outputFile = __DIR__ . '/../tmp/output.log';
73 |
74 | @unlink($outputFile);
75 |
76 | // Test fist that the file doesn't exist yet
77 | $this->assertFalse(file_exists($outputFile));
78 | $job->output($outputFile)->run();
79 |
80 | sleep(2);
81 | $this->assertTrue(file_exists($outputFile));
82 |
83 | $this->assertEquals('Hello World!', file_get_contents($outputFile));
84 |
85 | unlink($outputFile);
86 | }
87 |
88 | public function testShouldWriteFunctionOutputToMultipleFiles()
89 | {
90 | $job = new Job(function () {
91 | echo 'Hello';
92 | });
93 | $outputFile1 = __DIR__ . '/../tmp/output1.log';
94 | $outputFile2 = __DIR__ . '/../tmp/output2.log';
95 | $outputFile3 = __DIR__ . '/../tmp/output3.log';
96 |
97 | @unlink($outputFile1);
98 | @unlink($outputFile2);
99 | @unlink($outputFile3);
100 |
101 | // Test fist that the file doesn't exist yet
102 | $this->assertFalse(file_exists($outputFile1));
103 | $this->assertFalse(file_exists($outputFile2));
104 | $this->assertFalse(file_exists($outputFile3));
105 | $job->output([
106 | $outputFile1,
107 | $outputFile2,
108 | $outputFile3,
109 | ])->run();
110 |
111 | sleep(2);
112 | $this->assertTrue(file_exists($outputFile1));
113 | $this->assertTrue(file_exists($outputFile2));
114 | $this->assertTrue(file_exists($outputFile3));
115 |
116 | $this->assertEquals('Hello', file_get_contents($outputFile1));
117 | $this->assertEquals('Hello', file_get_contents($outputFile2));
118 | $this->assertEquals('Hello', file_get_contents($outputFile3));
119 |
120 | unlink($outputFile1);
121 | unlink($outputFile2);
122 | unlink($outputFile3);
123 | }
124 |
125 | public function testShouldWriteFunctionReturnToSingleFile()
126 | {
127 | $job = new Job(function () {
128 | return 'Hello World!';
129 | });
130 | $outputFile = __DIR__ . '/../tmp/output1.log';
131 |
132 | // Test fist that the file doesn't exist yet
133 | $this->assertFalse(file_exists($outputFile));
134 | $job->output($outputFile)->run();
135 |
136 | sleep(2);
137 | $this->assertTrue(file_exists($outputFile));
138 |
139 | $this->assertEquals('Hello World!', file_get_contents($outputFile));
140 |
141 | unlink($outputFile);
142 | }
143 |
144 | public function testShouldWriteFunctionReturnToMultipleFiles()
145 | {
146 | $job = new Job(function () {
147 | return ['Hello ', 'World!'];
148 | });
149 | $outputFile1 = __DIR__ . '/../tmp/output1.log';
150 | $outputFile2 = __DIR__ . '/../tmp/output2.log';
151 | $outputFile3 = __DIR__ . '/../tmp/output3.log';
152 |
153 | @unlink($outputFile1);
154 | @unlink($outputFile2);
155 | @unlink($outputFile3);
156 |
157 | // Test fist that the file doesn't exist yet
158 | $this->assertFalse(file_exists($outputFile1));
159 | $this->assertFalse(file_exists($outputFile2));
160 | $this->assertFalse(file_exists($outputFile3));
161 | $job->output([
162 | $outputFile1,
163 | $outputFile2,
164 | $outputFile3,
165 | ])->run();
166 |
167 | sleep(2);
168 | $this->assertTrue(file_exists($outputFile1));
169 | $this->assertTrue(file_exists($outputFile2));
170 | $this->assertTrue(file_exists($outputFile3));
171 |
172 | $this->assertEquals('Hello World!', file_get_contents($outputFile1));
173 | $this->assertEquals('Hello World!', file_get_contents($outputFile2));
174 | $this->assertEquals('Hello World!', file_get_contents($outputFile3));
175 |
176 | unlink($outputFile1);
177 | unlink($outputFile2);
178 | unlink($outputFile3);
179 | }
180 |
181 | public function testShouldWriteFunctionOutputAndReturnToFile()
182 | {
183 | $job = new Job(function () {
184 | echo 'Hello ';
185 |
186 | return 'World!';
187 | });
188 | $outputFile = __DIR__ . '/../tmp/output1.log';
189 |
190 | // Test fist that the file doesn't exist yet
191 | $this->assertFalse(file_exists($outputFile));
192 | $job->output($outputFile)->run();
193 |
194 | sleep(2);
195 | $this->assertTrue(file_exists($outputFile));
196 |
197 | $this->assertEquals('Hello World!', file_get_contents($outputFile));
198 |
199 | unlink($outputFile);
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/tests/GO/JobTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(is_string($job1->getId()));
12 |
13 | $job2 = new Job(function () {
14 | return true;
15 | });
16 | $this->assertTrue(is_string($job2->getId()));
17 |
18 | $job3 = new Job(['MyClass', 'myMethod']);
19 | $this->assertTrue(is_string($job3->getId()));
20 | }
21 |
22 | public function testShouldGenerateIdFromSignature()
23 | {
24 | $job1 = new Job('ls');
25 | $this->assertEquals(md5('ls'), $job1->getId());
26 |
27 | $job2 = new Job('whoami');
28 | $this->assertNotEquals($job1->getId(), $job2->getId());
29 |
30 | $job3 = new Job(['MyClass', 'myMethod']);
31 | $this->assertNotEquals($job1->getId(), $job3->getId());
32 | }
33 |
34 | public function testShouldAllowCustomId()
35 | {
36 | $job = new Job('ls', [], 'aCustomId');
37 |
38 | $this->assertNotEquals(md5('ls'), $job->getId());
39 | $this->assertEquals('aCustomId', $job->getId());
40 |
41 | $job2 = new Job(['MyClass', 'myMethod'], null, 'myCustomId');
42 | $this->assertEquals('myCustomId', $job2->getId());
43 | }
44 |
45 | public function testShouldKnowIfDue()
46 | {
47 | $job1 = new Job('ls');
48 | $this->assertTrue($job1->isDue());
49 |
50 | $job2 = new Job('ls');
51 | $job2->at('* * * * *');
52 | $this->assertTrue($job2->isDue());
53 |
54 | $job3 = new Job('ls');
55 | $job3->at('10 * * * *');
56 | $this->assertTrue($job3->isDue(\DateTime::createFromFormat('i', '10')));
57 | $this->assertFalse($job3->isDue(\DateTime::createFromFormat('i', '12')));
58 | }
59 |
60 | public function testShouldKnowIfCanRunInBackground()
61 | {
62 | $job = new Job('ls');
63 | $this->assertTrue($job->canRunInBackground());
64 |
65 | $job2 = new Job(function () {
66 | return "I can't run in background";
67 | });
68 | $this->assertFalse($job2->canRunInBackground());
69 | }
70 |
71 | public function testShouldForceTheJobToRunInForeground()
72 | {
73 | $job = new Job('ls');
74 |
75 | $this->assertTrue($job->canRunInBackground());
76 | $this->assertFalse($job->inForeground()->canRunInBackground());
77 | }
78 |
79 | public function testShouldReturnCompiledJobCommand()
80 | {
81 | $job1 = new Job('ls');
82 | $this->assertEquals('ls', $job1->inForeground()->compile());
83 |
84 | $fn = function () {
85 | return true;
86 | };
87 | $job2 = new Job($fn);
88 | $this->assertEquals($fn, $job2->compile());
89 | }
90 |
91 | public function testShouldCompileWithArguments()
92 | {
93 | $job = new Job('ls', [
94 | '-l' => null,
95 | '-arg' => 'value',
96 | ]);
97 |
98 | $this->assertEquals("ls '-l' '-arg' 'value'", $job->inForeground()->compile());
99 | }
100 |
101 | public function testShouldCompileCommandInBackground()
102 | {
103 | $job1 = new Job('ls');
104 | $job1->at('* * * * *');
105 |
106 | $this->assertEquals('(ls) > /dev/null 2>&1 &', $job1->compile());
107 | }
108 |
109 | public function testShouldRunInBackground()
110 | {
111 | // This script has a 5 seconds sleep
112 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
113 | $job = new Job($command);
114 |
115 | $startTime = microtime(true);
116 | $job->at('* * * * *')->run();
117 | $endTime = microtime(true);
118 |
119 | $this->assertTrue(5 > ($endTime - $startTime));
120 |
121 | $startTime = microtime(true);
122 | $job->at('* * * * *')->inForeground()->run();
123 | $endTime = microtime(true);
124 |
125 | $this->assertTrue(($endTime - $startTime) >= 5);
126 | }
127 |
128 | public function testShouldRunInForegroundIfSendsEmails()
129 | {
130 | $job = new Job('ls');
131 | $job->email('test@mail.com');
132 |
133 | $this->assertFalse($job->canRunInBackground());
134 | }
135 |
136 | public function testShouldAcceptSingleOrMultipleEmails()
137 | {
138 | $job = new Job('ls');
139 |
140 | $this->assertInstanceOf(Job::class, $job->email('test@mail.com'));
141 | $this->assertInstanceOf(Job::class, $job->email(['test@mail.com', 'other@mail.com']));
142 | }
143 |
144 | public function testShouldFailIfEmailInputIsNotStringOrArray()
145 | {
146 | $this->expectException(\InvalidArgumentException::class);
147 |
148 | $job = new Job('ls');
149 |
150 | $job->email(1);
151 | }
152 |
153 | public function testShouldAcceptEmailConfigurationAndItShouldBeChainable()
154 | {
155 | $job = new Job('ls');
156 | $this->assertInstanceOf(Job::class, $job->configure([
157 | 'email' => [],
158 | ]));
159 | }
160 |
161 | public function testShouldFailIfEmailConfigurationIsNotArray()
162 | {
163 | $this->expectException(\InvalidArgumentException::class);
164 |
165 | $job = new Job('ls');
166 | $job->configure([
167 | 'email' => 123,
168 | ]);
169 | }
170 |
171 | public function testShouldCreateLockFileIfOnlyOne()
172 | {
173 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
174 | $job = new Job($command);
175 |
176 | // Default temp dir
177 | $tmpDir = sys_get_temp_dir();
178 | $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
179 |
180 | @unlink($lockFile);
181 |
182 | $this->assertFalse(file_exists($lockFile));
183 |
184 | $job->onlyOne()->run();
185 |
186 | $this->assertTrue(file_exists($lockFile));
187 | }
188 |
189 | public function testShouldCreateLockFilesInCustomPath()
190 | {
191 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
192 | $job = new Job($command);
193 |
194 | // Default temp dir
195 | $tmpDir = __DIR__ . '/../tmp';
196 | $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
197 |
198 | @unlink($lockFile);
199 |
200 | $this->assertFalse(file_exists($lockFile));
201 |
202 | $job->onlyOne($tmpDir)->run();
203 |
204 | $this->assertTrue(file_exists($lockFile));
205 | }
206 |
207 | public function testShouldRemoveLockFileAfterRunningClosures()
208 | {
209 | $job = new Job(function () {
210 | sleep(3);
211 | });
212 |
213 | // Default temp dir
214 | $tmpDir = __DIR__ . '/../tmp';
215 | $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
216 |
217 | $job->onlyOne($tmpDir)->run();
218 |
219 | $this->assertFalse(file_exists($lockFile));
220 | }
221 |
222 | public function testShouldRemoveLockFileAfterRunningCommands()
223 | {
224 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
225 | $job = new Job($command);
226 |
227 | // Default temp dir
228 | $tmpDir = __DIR__ . '/../tmp';
229 | $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
230 |
231 | $job->onlyOne($tmpDir)->run();
232 |
233 | sleep(1);
234 |
235 | $this->assertTrue(file_exists($lockFile));
236 |
237 | sleep(5);
238 |
239 | $this->assertFalse(file_exists($lockFile));
240 | }
241 |
242 | public function testShouldKnowIfOverlapping()
243 | {
244 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
245 | $job = new Job($command);
246 |
247 | $this->assertFalse($job->isOverlapping());
248 |
249 | $tmpDir = __DIR__ . '/../tmp';
250 |
251 | $job->onlyOne($tmpDir)->run();
252 |
253 | sleep(1);
254 |
255 | $this->assertTrue($job->isOverlapping());
256 |
257 | sleep(5);
258 |
259 | $this->assertFalse($job->isOverlapping());
260 | }
261 |
262 | public function testShouldNotRunIfOverlapping()
263 | {
264 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
265 | $job = new Job($command);
266 |
267 | $this->assertFalse($job->isOverlapping());
268 |
269 | $tmpDir = __DIR__ . '/../tmp';
270 |
271 | $job->onlyOne($tmpDir);
272 |
273 | sleep(1);
274 |
275 | $this->assertTrue($job->run());
276 | $this->assertFalse($job->run());
277 |
278 | sleep(6);
279 | $this->assertTrue($job->run());
280 | }
281 |
282 | public function testShouldRunIfOverlappingCallbackReturnsTrue()
283 | {
284 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
285 | $job = new Job($command);
286 |
287 | $this->assertFalse($job->isOverlapping());
288 |
289 | $tmpDir = __DIR__ . '/../tmp';
290 |
291 | $job->onlyOne($tmpDir, function ($lastExecution) {
292 | return time() - $lastExecution > 2;
293 | })->run();
294 |
295 | // The job should not run as it is overlapping
296 | $this->assertFalse($job->run());
297 | sleep(3);
298 | // The job should run now as the function should now return true,
299 | // while it's still being executed
300 | $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
301 | $this->assertTrue(file_exists($lockFile));
302 | $this->assertTrue($job->run());
303 | }
304 |
305 | public function testShouldAcceptTempDirInConfiguration()
306 | {
307 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
308 | $job = new Job($command);
309 |
310 | $tmpDir = __DIR__ . '/../tmp';
311 |
312 | $job->configure([
313 | 'tempDir' => $tmpDir,
314 | ])->onlyOne()->run();
315 |
316 | sleep(1);
317 |
318 | $this->assertTrue(file_exists($tmpDir . '/' . $job->getId() . '.lock'));
319 | }
320 |
321 | public function testWhenMethodShouldBeChainable()
322 | {
323 | $job = new Job('ls');
324 |
325 | $this->assertInstanceOf(Job::class, $job->when(function () {
326 | return true;
327 | }));
328 | }
329 |
330 | public function testShouldNotRunIfTruthTestFails()
331 | {
332 | $job = new Job('ls');
333 |
334 | $this->assertFalse($job->when(function () {
335 | return false;
336 | })->run());
337 |
338 | $this->assertTrue($job->when(function () {
339 | return true;
340 | })->run());
341 | }
342 |
343 | public function testShouldReturnOutputOfJobExecution()
344 | {
345 | $job1 = new Job(function () {
346 | echo 'hi';
347 | });
348 | $job1->run();
349 | $this->assertEquals('hi', $job1->getOutput());
350 |
351 | $job2 = new Job(function () {
352 | return 'hello';
353 | });
354 | $job2->run();
355 | $this->assertEquals('hello', $job2->getOutput());
356 |
357 | $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
358 | $job3 = new Job($command);
359 | $job3->inForeground()->run();
360 | $this->assertEquals(['hi'], $job3->getOutput());
361 | }
362 |
363 | public function testShouldRunCallbackBeforeJobExecution()
364 | {
365 | $job = new Job(function () {
366 | return 'Job for testing before function';
367 | });
368 |
369 | $callbackWasExecuted = false;
370 | $outputWasSet = false;
371 |
372 | $job->before(function () use ($job, &$callbackWasExecuted, &$outputWasSet) {
373 | $callbackWasExecuted = true;
374 | $outputWasSet = ! is_null($job->getOutput());
375 | })->run();
376 |
377 | $this->assertTrue($callbackWasExecuted);
378 | $this->assertFalse($outputWasSet);
379 | }
380 |
381 | public function testShouldRunCallbackAfterJobExecution()
382 | {
383 | $job = new Job(function () {
384 | $visitors = 1000;
385 |
386 | return 'Daily visitors: ' . $visitors;
387 | });
388 |
389 | $jobResult = null;
390 |
391 | $job->then(function ($output) use (&$jobResult) {
392 | $jobResult = $output;
393 | })->run();
394 |
395 | $this->assertEquals($jobResult, $job->getOutput());
396 |
397 | $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
398 | $job2 = new Job($command);
399 |
400 | $job2Result = null;
401 |
402 | $job2->then(function ($output) use (&$job2Result) {
403 | $job2Result = $output;
404 | }, true)->run();
405 |
406 | // Commands in background should return an empty string
407 | $this->assertTrue(empty($job2Result));
408 |
409 | $job2Result = null;
410 | $job2->then(function ($output) use (&$job2Result) {
411 | $job2Result = $output;
412 | })->inForeground()->run();
413 | $this->assertTrue(! empty($job2Result) &&
414 | $job2Result === $job2->getOutput());
415 | }
416 |
417 | public function testThenMethodShouldPassReturnCode()
418 | {
419 | $command_success = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
420 | $command_fail = $command_success . ' fail';
421 |
422 | $run = function ($command) {
423 | $job = new Job($command);
424 | $testReturnCode = null;
425 |
426 | $job->then(function ($output, $returnCode) use (&$testReturnCode, &$testOutput) {
427 | $testReturnCode = $returnCode;
428 | })->run();
429 |
430 | return $testReturnCode;
431 | };
432 |
433 | $this->assertEquals(0, $run($command_success));
434 | $this->assertNotEquals(0, $run($command_fail));
435 | }
436 |
437 | public function testThenMethodShouldBeChainable()
438 | {
439 | $job = new Job('ls');
440 |
441 | $this->assertInstanceOf(Job::class, $job->then(function () {
442 | return true;
443 | }));
444 | }
445 |
446 | public function testShouldDefaultExecutionInForegroundIfMethodThenIsDefined()
447 | {
448 | $job = new Job('ls');
449 |
450 | $job->then(function () {
451 | return true;
452 | });
453 |
454 | $this->assertFalse($job->canRunInBackground());
455 | }
456 |
457 | public function testShouldAllowForcingTheJobToRunInBackgroundIfMethodThenIsDefined()
458 | {
459 | // This is a use case when you want to execute a callback every time your
460 | // job is executed, but you don't care about the output of the job
461 |
462 | $job = new Job('ls');
463 |
464 | $job->then(function () {
465 | return true;
466 | }, true);
467 |
468 | $this->assertTrue($job->canRunInBackground());
469 | }
470 | }
471 |
--------------------------------------------------------------------------------
/tests/GO/MailerTest.php:
--------------------------------------------------------------------------------
1 | getEmailConfig();
12 |
13 | $this->assertTrue(isset($config['subject']));
14 | $this->assertTrue(isset($config['from']));
15 | $this->assertTrue(isset($config['body']));
16 | $this->assertTrue(isset($config['transport']));
17 | }
18 |
19 | public function testShouldAllowCustomTransportWhenSendingEmails()
20 | {
21 | $job = new Job(function () {
22 | return 'hi';
23 | });
24 |
25 | $job->configure([
26 | 'email' => [
27 | 'transport' => new \Swift_NullTransport(),
28 | ],
29 | ]);
30 |
31 | $this->assertInstanceOf(\Swift_NullTransport::class, $job->getEmailConfig()['transport']);
32 | }
33 |
34 | public function testEmailTransportShouldAlwaysBeInstanceOfSwift_Transport()
35 | {
36 | $job = new Job(function () {
37 | return 'hi';
38 | });
39 |
40 | $job->configure([
41 | 'email' => [
42 | 'transport' => 'Something not allowed',
43 | ],
44 | ]);
45 |
46 | $this->assertInstanceOf(\Swift_Transport::class, $job->getEmailConfig()['transport']);
47 | }
48 |
49 | public function testShouldSendJobOutputToEmail()
50 | {
51 | $emailAddress = 'local@localhost.com';
52 | $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
53 | $job1 = new Job($command);
54 |
55 | $job2 = new Job(function () {
56 | return 'Hello World!';
57 | });
58 |
59 | $nullTransportConfig = [
60 | 'email' => [
61 | 'transport' => new \Swift_NullTransport(),
62 | ],
63 | ];
64 | $job1->configure($nullTransportConfig);
65 | $job2->configure($nullTransportConfig);
66 |
67 | $outputFile1 = __DIR__ . '/../tmp/output001.log';
68 | $this->assertTrue($job1->output($outputFile1)->email($emailAddress)->run());
69 | $outputFile2 = __DIR__ . '/../tmp/output002.log';
70 | $this->assertTrue($job2->output($outputFile2)->email($emailAddress)->run());
71 |
72 | unlink($outputFile1);
73 | unlink($outputFile2);
74 | }
75 |
76 | public function testShouldSendMultipleFilesToEmail()
77 | {
78 | $emailAddress = 'local@localhost.com';
79 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
80 | $job = new Job($command);
81 |
82 | $outputFile1 = __DIR__ . '/../tmp/output003.log';
83 | $outputFile2 = __DIR__ . '/../tmp/output004.log';
84 |
85 | $nullTransportConfig = [
86 | 'email' => [
87 | 'transport' => new \Swift_NullTransport(),
88 | ],
89 | ];
90 | $job->configure($nullTransportConfig);
91 |
92 | $this->assertTrue($job->output([
93 | $outputFile1, $outputFile2,
94 | ])->email([$emailAddress])->run());
95 |
96 | unlink($outputFile1);
97 | unlink($outputFile2);
98 | }
99 |
100 | public function testShouldSendToMultipleEmails()
101 | {
102 | $emailAddress1 = 'local@localhost.com';
103 | $emailAddress2 = 'local1@localhost.com';
104 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
105 | $job = new Job($command);
106 |
107 | $outputFile = __DIR__ . '/../tmp/output005.log';
108 |
109 | $nullTransportConfig = [
110 | 'email' => [
111 | 'transport' => new \Swift_NullTransport(),
112 | ],
113 | ];
114 | $job->configure($nullTransportConfig);
115 |
116 | $this->assertTrue($job->output($outputFile)->email([
117 | $emailAddress1, $emailAddress2,
118 | ])->run());
119 |
120 | unlink($outputFile);
121 | }
122 |
123 | public function testShouldAcceptCustomEmailConfig()
124 | {
125 | $emailAddress = 'local@localhost.com';
126 | $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
127 | $job = new Job($command);
128 |
129 | $outputFile = __DIR__ . '/../tmp/output6.log';
130 |
131 | $this->assertTrue(
132 | $job->output($outputFile)->email($emailAddress)
133 | ->configure([
134 | 'email' => [
135 | 'subject' => 'My custom subject',
136 | 'from' => 'my@custom.from',
137 | 'body' => 'My custom body',
138 | 'transport' => new \Swift_NullTransport(),
139 | ],
140 | ])->run()
141 | );
142 |
143 | unlink($outputFile);
144 | }
145 |
146 | public function testShouldIgnoreEmailIfSpecifiedInConfig()
147 | {
148 | $job = new Job(function () {
149 | $tot = 1 + 2;
150 | // Return nothing....
151 | });
152 |
153 | $nullTransportConfig = [
154 | 'email' => [
155 | 'transport' => new \Swift_NullTransport(),
156 | 'ignore_empty_output' => true,
157 | ],
158 | ];
159 | $job->configure($nullTransportConfig);
160 |
161 | $outputFile = __DIR__ . '/../tmp/output.log';
162 | $this->assertTrue($job->output($outputFile)->email('local@localhost.com')->run());
163 |
164 | @unlink($outputFile);
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/tests/GO/SchedulerTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(count($scheduler->getQueuedJobs()), 0);
16 |
17 | $scheduler->raw('ls');
18 |
19 | $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
20 | }
21 |
22 | public function testShouldQueueAPhpScript()
23 | {
24 | $scheduler = new Scheduler();
25 |
26 | $script = __DIR__ . '/../test_job.php';
27 |
28 | $this->assertEquals(count($scheduler->getQueuedJobs()), 0);
29 |
30 | $scheduler->php($script);
31 |
32 | $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
33 | }
34 |
35 | public function testShouldAllowCustomPhpBin()
36 | {
37 | $scheduler = new Scheduler();
38 | $script = __DIR__ . '/../test_job.php';
39 |
40 | // Create fake bin
41 | $bin = __DIR__ . '/../custom_bin';
42 | touch($bin);
43 |
44 | $job = $scheduler->php($script, $bin)->inForeground();
45 |
46 | unlink($bin);
47 |
48 | $this->assertEquals($bin . ' ' . $script, $job->compile());
49 | }
50 |
51 | public function testShouldUseSystemPhpBinIfCustomBinDoesNotExist()
52 | {
53 | $scheduler = new Scheduler();
54 | $script = __DIR__ . '/../test_job.php';
55 |
56 | // Create fake bin
57 | $bin = '/my/custom/php/bin';
58 |
59 | $job = $scheduler->php($script, $bin)->inForeground();
60 |
61 | $this->assertNotEquals($bin . ' ' . $script, $job->compile());
62 | $this->assertEquals(PHP_BINARY . ' ' . $script, $job->compile());
63 | }
64 |
65 | public function testShouldThrowExceptionIfScriptIsNotAString()
66 | {
67 | $this->expectException(\InvalidArgumentException::class);
68 |
69 | $scheduler = new Scheduler();
70 | $scheduler->php(function () {
71 | return false;
72 | });
73 |
74 | $scheduler->run();
75 | }
76 |
77 | public function testShouldMarkJobAsFailedIfScriptPathIsInvalid()
78 | {
79 | $scheduler = new Scheduler();
80 | $scheduler->php('someInvalidPathToAScript');
81 |
82 | $scheduler->run();
83 | $fail = $scheduler->getFailedJobs();
84 | $this->assertCount(1, $fail);
85 | $this->assertContainsOnlyInstancesOf(FailedJob::class, $fail);
86 | }
87 |
88 | public function testShouldQueueAShellCommand()
89 | {
90 | $scheduler = new Scheduler();
91 |
92 | $this->assertEquals(count($scheduler->getQueuedJobs()), 0);
93 |
94 | $scheduler->raw('ls');
95 |
96 | $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
97 | }
98 |
99 | public function testShouldQueueAFunction()
100 | {
101 | $scheduler = new Scheduler();
102 |
103 | $this->assertEquals(count($scheduler->getQueuedJobs()), 0);
104 |
105 | $scheduler->call(function () {
106 | return true;
107 | });
108 |
109 | $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
110 | }
111 |
112 | public function testShouldKeepTrackOfExecutedJobs()
113 | {
114 | $scheduler = new Scheduler();
115 |
116 | $scheduler->call(function () {
117 | return true;
118 | });
119 |
120 | $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
121 | $this->assertEquals(count($scheduler->getExecutedJobs()), 0);
122 |
123 | $scheduler->run();
124 |
125 | $this->assertEquals(count($scheduler->getExecutedJobs()), 1);
126 | }
127 |
128 | public function testShouldPassParametersToAFunction()
129 | {
130 | $scheduler = new Scheduler();
131 |
132 | $outputFile = __DIR__ . '/../tmp/output.txt';
133 | $scheduler->call(function ($phrase) {
134 | return $phrase;
135 | }, [
136 | 'Hello World!',
137 | ])->output($outputFile);
138 |
139 | @unlink($outputFile);
140 |
141 | $this->assertFalse(file_exists($outputFile));
142 |
143 | $scheduler->run();
144 |
145 | $this->assertNotEquals('Hello', file_get_contents($outputFile));
146 | $this->assertEquals('Hello World!', file_get_contents($outputFile));
147 |
148 | @unlink($outputFile);
149 | }
150 |
151 | public function testShouldKeepTrackOfFailedJobs()
152 | {
153 | $scheduler = new Scheduler();
154 |
155 | $exception = new \Exception('Something failed');
156 | $scheduler->call(function () use ($exception) {
157 | throw $exception;
158 | });
159 |
160 | $this->assertEquals(count($scheduler->getFailedJobs()), 0);
161 |
162 | $scheduler->run();
163 |
164 | $this->assertEquals(count($scheduler->getExecutedJobs()), 0);
165 | $this->assertEquals(count($scheduler->getFailedJobs()), 1);
166 | $failedJob = $scheduler->getFailedJobs()[0];
167 | $this->assertInstanceOf(FailedJob::class, $failedJob);
168 | $this->assertSame($exception, $failedJob->getException());
169 | $this->assertInstanceOf(Job::class, $failedJob->getJob());
170 | }
171 |
172 | public function testShouldKeepExecutingJobsIfOneFails()
173 | {
174 | $scheduler = new Scheduler();
175 |
176 | $scheduler->call(function () {
177 | throw new \Exception('Something failed');
178 | });
179 |
180 | $scheduler->call(function () {
181 | return true;
182 | });
183 |
184 | $scheduler->run();
185 |
186 | $this->assertEquals(count($scheduler->getExecutedJobs()), 1);
187 | $this->assertEquals(count($scheduler->getFailedJobs()), 1);
188 | }
189 |
190 | public function testShouldInjectConfigToTheJobs()
191 | {
192 | $schedulerConfig = [
193 | 'email' => [
194 | 'subject' => 'My custom subject',
195 | ],
196 | ];
197 | $scheduler = new Scheduler($schedulerConfig);
198 |
199 | $job = $scheduler->raw('ls');
200 |
201 | $this->assertEquals($job->getEmailConfig()['subject'], $schedulerConfig['email']['subject']);
202 | }
203 |
204 | public function testShouldPrioritizeJobConfigOverSchedulerConfig()
205 | {
206 | $schedulerConfig = [
207 | 'email' => [
208 | 'subject' => 'My custom subject',
209 | ],
210 | ];
211 | $scheduler = new Scheduler($schedulerConfig);
212 |
213 | $jobConfig = [
214 | 'email' => [
215 | 'subject' => 'My job subject',
216 | ],
217 | ];
218 | $job = $scheduler->raw('ls')->configure($jobConfig);
219 |
220 | $this->assertNotEquals($job->getEmailConfig()['subject'], $schedulerConfig['email']['subject']);
221 | $this->assertEquals($job->getEmailConfig()['subject'], $jobConfig['email']['subject']);
222 | }
223 |
224 | public function testShouldShowClosuresVerboseOutputAsText()
225 | {
226 | $scheduler = new Scheduler();
227 |
228 | $scheduler->call(function ($phrase) {
229 | return $phrase;
230 | }, [
231 | 'Hello World!',
232 | ]);
233 |
234 | $scheduler->run();
235 |
236 | $this->assertMatchesRegularExpression('/ Executing Closure$/', $scheduler->getVerboseOutput());
237 | $this->assertMatchesRegularExpression('/ Executing Closure$/', $scheduler->getVerboseOutput('text'));
238 | }
239 |
240 | public function testShouldShowClosuresVerboseOutputAsHtml()
241 | {
242 | $scheduler = new Scheduler();
243 |
244 | $scheduler->call(function ($phrase) {
245 | return $phrase;
246 | }, [
247 | 'Hello World!',
248 | ]);
249 |
250 | $scheduler->call(function () {
251 | return true;
252 | });
253 |
254 | $scheduler->run();
255 |
256 | $this->assertMatchesRegularExpression('/
/', $scheduler->getVerboseOutput('html'));
257 | }
258 |
259 | public function testShouldShowClosuresVerboseOutputAsArray()
260 | {
261 | $scheduler = new Scheduler();
262 |
263 | $scheduler->call(function ($phrase) {
264 | return $phrase;
265 | }, [
266 | 'Hello World!',
267 | ]);
268 |
269 | $scheduler->call(function () {
270 | return true;
271 | });
272 |
273 | $scheduler->run();
274 |
275 | $this->assertTrue(is_array($scheduler->getVerboseOutput('array')));
276 | $this->assertEquals(count($scheduler->getVerboseOutput('array')), 2);
277 | }
278 |
279 | public function testShouldThrowExceptionWithInvalidOutputType()
280 | {
281 | $this->expectException(\InvalidArgumentException::class);
282 |
283 | $scheduler = new Scheduler();
284 |
285 | $scheduler->call(function ($phrase) {
286 | return $phrase;
287 | }, [
288 | 'Hello World!',
289 | ]);
290 |
291 | $scheduler->call(function () {
292 | return true;
293 | });
294 |
295 | $scheduler->run();
296 |
297 | $scheduler->getVerboseOutput('multiline');
298 | }
299 |
300 | public function testShouldPrioritizeJobsInBackround()
301 | {
302 | $scheduler = new Scheduler();
303 |
304 | $scheduler->php(__DIR__ . '/../async_job.php', null, null, 'async_foreground')->then(function () {
305 | return true;
306 | });
307 |
308 | $scheduler->php(__DIR__ . '/../async_job.php', null, null, 'async_background');
309 |
310 | $jobs = $scheduler->getQueuedJobs();
311 |
312 | $this->assertEquals('async_background', $jobs[0]->getId());
313 | $this->assertEquals('async_foreground', $jobs[1]->getId());
314 | }
315 |
316 | public function testCouldRunTwice()
317 | {
318 | $scheduler = new Scheduler();
319 |
320 | $scheduler->call(function () {
321 | return true;
322 | });
323 |
324 | $scheduler->run();
325 |
326 | $this->assertCount(1, $scheduler->getExecutedJobs(), 'Number of executed jobs');
327 |
328 | $scheduler->resetRun();
329 | $scheduler->run();
330 |
331 | $this->assertCount(1, $scheduler->getExecutedJobs(), 'Number of executed jobs');
332 | }
333 |
334 | public function testClearJobs()
335 | {
336 | $scheduler = new Scheduler();
337 |
338 | $scheduler->call(function () {
339 | return true;
340 | });
341 |
342 | $this->assertCount(1, $scheduler->getQueuedJobs(), 'Number of queued jobs');
343 |
344 | $scheduler->clearJobs();
345 |
346 | $this->assertCount(0, $scheduler->getQueuedJobs(), 'Number of queued jobs');
347 | }
348 |
349 | public function testShouldRunDelayedJobsIfDueWhenCreated()
350 | {
351 | $scheduler = new Scheduler();
352 | $currentTime = date('H:i');
353 |
354 | $scheduler->call(function () {
355 | $s = (int) date('s');
356 | sleep(60 - $s + 1);
357 | })->daily($currentTime);
358 |
359 | $scheduler->call(function () {
360 | // do nothing
361 | })->daily($currentTime);
362 |
363 | $executed = $scheduler->run();
364 |
365 | $this->assertEquals(2, count($executed));
366 | }
367 |
368 | public function testShouldRunAtSpecificTime()
369 | {
370 | $scheduler = new Scheduler();
371 | $runTime = new DateTime('2017-09-13 00:00:00');
372 |
373 | $scheduler->call(function () {
374 | // do nothing
375 | })->daily('00:00');
376 |
377 | $executed = $scheduler->run($runTime);
378 |
379 | $this->assertEquals(1, count($executed));
380 | }
381 | }
382 |
--------------------------------------------------------------------------------
/tests/async_job.php:
--------------------------------------------------------------------------------
1 |