├── .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 | [![Latest Stable Version](https://poser.pugx.org/peppeocchi/php-cron-scheduler/v/stable)](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [![License](https://poser.pugx.org/peppeocchi/php-cron-scheduler/license)](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [![Build Status](https://travis-ci.org/peppeocchi/php-cron-scheduler.svg)](https://travis-ci.org/peppeocchi/php-cron-scheduler) [![Coverage Status](https://coveralls.io/repos/github/peppeocchi/php-cron-scheduler/badge.svg?branch=master)](https://coveralls.io/github/peppeocchi/php-cron-scheduler?branch=master) [![StyleCI](https://styleci.io/repos/38302733/shield)](https://styleci.io/repos/38302733) [![Total Downloads](https://poser.pugx.org/peppeocchi/php-cron-scheduler/downloads)](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 |