├── docs ├── CNAME ├── assets │ ├── js │ │ └── hljs.js │ ├── favicon.ico │ ├── css │ │ ├── codeigniter.css │ │ └── codeigniter_dark_mode.css │ └── flame.svg ├── index.md ├── configuration.md ├── installation.md ├── cli-commands.md ├── vision.md └── basic-usage.md ├── roave-bc-check.yaml ├── infection.json.dist ├── composer-unused.php ├── src ├── Language │ └── en │ │ └── Tasks.php ├── Commands │ ├── TaskCommand.php │ ├── Enable.php │ ├── Disable.php │ ├── Publish.php │ ├── Run.php │ └── Lister.php ├── Exceptions │ └── TasksException.php ├── Test │ ├── MockScheduler.php │ └── MockTask.php ├── Config │ ├── Services.php │ └── Tasks.php ├── TaskLog.php ├── Scheduler.php ├── CronExpression.php ├── TaskRunner.php ├── RunResolver.php ├── Task.php └── FrequenciesTrait.php ├── CONTRIBUTING.md ├── psalm_autoload.php ├── psalm.xml ├── .php-cs-fixer.dist.php ├── LICENSE ├── SECURITY.md ├── phpstan-baseline.neon ├── README.md ├── composer.json ├── mkdocs.yml ├── spark ├── deptrac.yaml └── rector.php /docs/CNAME: -------------------------------------------------------------------------------- 1 | tasks.codeigniter.com 2 | -------------------------------------------------------------------------------- /docs/assets/js/hljs.js: -------------------------------------------------------------------------------- 1 | window.document$.subscribe(() => { 2 | hljs.highlightAll(); 3 | }); 4 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeigniter4/tasks/HEAD/docs/assets/favicon.ico -------------------------------------------------------------------------------- /roave-bc-check.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#\[BC\] SKIPPED: .+ could not be found in the located source#' 4 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src/" 5 | ], 6 | "excludes": [ 7 | "Config", 8 | "Database/Migrations", 9 | "Views" 10 | ] 11 | }, 12 | "logs": { 13 | "text": "build/infection.log" 14 | }, 15 | "mutators": { 16 | "@default": true 17 | }, 18 | "bootstrap": "vendor/codeigniter4/framework/system/Test/bootstrap.php" 19 | } 20 | -------------------------------------------------------------------------------- /composer-unused.php: -------------------------------------------------------------------------------- 1 | setAdditionalFilesFor('codeigniter4/settings', [ 10 | __DIR__ . '/vendor/codeigniter4/settings/src/Helpers/setting_helper.php', 11 | ]); 12 | }; 13 | -------------------------------------------------------------------------------- /src/Language/en/Tasks.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | return [ 15 | 'invalidTaskType' => '"{0}" is not a valid type of task.', 16 | 'invalidCronExpression' => '"{0}" is not a valid cron expression.', 17 | ]; 18 | -------------------------------------------------------------------------------- /docs/assets/css/codeigniter.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="codeigniter"] { 2 | --md-primary-fg-color: #dd4814; 3 | --md-primary-fg-color--light: #ECB7B7; 4 | --md-primary-fg-color--dark: #90030C; 5 | 6 | --md-default-bg-color: #fcfcfc; 7 | 8 | --md-typeset-a-color: #e74c3c; 9 | --md-accent-fg-color: #97310e; 10 | 11 | --md-accent-fg-color--transparent: #ECB7B7; 12 | 13 | --md-code-bg-color: #ffffff; 14 | 15 | .md-typeset code { 16 | border: 1px solid #e1e4e5; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CodeIgniter4 2 | 3 | CodeIgniter is a community driven project and accepts contributions of 4 | code and documentation from the community. 5 | 6 | If you'd like to contribute, please read [Contributing to CodeIgniter](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/README.md) 7 | in the [main repository](https://github.com/codeigniter4/CodeIgniter4). 8 | 9 | If you are going to contribute to this repository, please report bugs or send PRs 10 | to this repository instead of the main repository. 11 | -------------------------------------------------------------------------------- /psalm_autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Commands/TaskCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Commands; 15 | 16 | use CodeIgniter\CLI\BaseCommand; 17 | 18 | /** 19 | * Base functionality for enable/disable. 20 | */ 21 | abstract class TaskCommand extends BaseCommand 22 | { 23 | /** 24 | * Command grouping. 25 | */ 26 | protected $group = 'Tasks'; 27 | 28 | /** 29 | * location to save. 30 | */ 31 | protected string $path = WRITEPATH . 'tasks'; 32 | } 33 | -------------------------------------------------------------------------------- /src/Exceptions/TasksException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Exceptions; 15 | 16 | use RuntimeException; 17 | 18 | final class TasksException extends RuntimeException 19 | { 20 | public static function forInvalidTaskType(string $type) 21 | { 22 | return new self(lang('Tasks.invalidTaskType', [$type])); 23 | } 24 | 25 | public static function forInvalidCronExpression(string $string) 26 | { 27 | return new self(lang('Tasks.invalidCronExpression', [$string])); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Test/MockScheduler.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Test; 15 | 16 | use CodeIgniter\Tasks\Scheduler; 17 | use CodeIgniter\Tasks\Task; 18 | 19 | /** 20 | * Mock Scheduler Class 21 | * 22 | * A wrapper class for testing to return 23 | * MockTasks instead of Tasks. 24 | */ 25 | class MockScheduler extends Scheduler 26 | { 27 | /** 28 | * @param mixed $action 29 | * 30 | * @return MockTask 31 | */ 32 | protected function createTask(string $type, $action): Task 33 | { 34 | $task = new MockTask($type, $action); 35 | $this->tasks[] = $task; 36 | 37 | return $task; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Commands/Enable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Commands; 15 | 16 | use CodeIgniter\CLI\CLI; 17 | 18 | /** 19 | * Enables Task Running 20 | */ 21 | class Enable extends TaskCommand 22 | { 23 | /** 24 | * The Command's name 25 | */ 26 | protected $name = 'tasks:enable'; 27 | 28 | /** 29 | * the Command's short description 30 | */ 31 | protected $description = 'Enables the task runner.'; 32 | 33 | /** 34 | * the Command's usage 35 | */ 36 | protected $usage = 'tasks:enable'; 37 | 38 | /** 39 | * Enables task running 40 | */ 41 | public function run(array $params) 42 | { 43 | helper('setting'); 44 | 45 | setting('Tasks.enabled', true); 46 | 47 | CLI::write('Tasks have been enabled.', 'green'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Commands/Disable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Commands; 15 | 16 | use CodeIgniter\CLI\CLI; 17 | 18 | /** 19 | * Disable Task Running. 20 | */ 21 | class Disable extends TaskCommand 22 | { 23 | /** 24 | * The Command's name 25 | */ 26 | protected $name = 'tasks:disable'; 27 | 28 | /** 29 | * the Command's short description 30 | */ 31 | protected $description = 'Disables the task runner.'; 32 | 33 | /** 34 | * the Command's usage 35 | */ 36 | protected $usage = 'tasks:disable'; 37 | 38 | /** 39 | * Disables task running 40 | */ 41 | public function run(array $params) 42 | { 43 | helper('setting'); 44 | 45 | setting('Tasks.enabled', false); 46 | 47 | CLI::write('Tasks have been disabled.', 'red'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | use CodeIgniter\CodingStandard\CodeIgniter4; 15 | use Nexus\CsConfig\Factory; 16 | use PhpCsFixer\Finder; 17 | 18 | $finder = Finder::create() 19 | ->files() 20 | ->in([ 21 | __DIR__ . '/src/', 22 | __DIR__ . '/tests/', 23 | ]) 24 | ->exclude([ 25 | 'build', 26 | 'Views', 27 | ]) 28 | ->append([ 29 | __FILE__, 30 | __DIR__ . '/rector.php', 31 | ]); 32 | 33 | $overrides = [ 34 | 'declare_strict_types' => true, 35 | // 'void_return' => true, 36 | ]; 37 | 38 | $options = [ 39 | 'finder' => $finder, 40 | 'cacheFile' => 'build/.php-cs-fixer.cache', 41 | ]; 42 | 43 | return Factory::create(new CodeIgniter4(), $overrides, $options)->forLibrary( 44 | 'CodeIgniter Tasks', 45 | 'CodeIgniter Foundation', 46 | 'admin@codeigniter.com', 47 | ); 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2021 Lonnie Ezell 4 | Copyright (c) 2021-2023 CodeIgniter Foundation 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Config/Services.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Config; 15 | 16 | use CodeIgniter\Config\BaseService; 17 | use CodeIgniter\Tasks\CronExpression; 18 | use CodeIgniter\Tasks\Scheduler; 19 | 20 | class Services extends BaseService 21 | { 22 | /** 23 | * Returns the Task Scheduler 24 | */ 25 | public static function scheduler(bool $getShared = true): Scheduler 26 | { 27 | if ($getShared) { 28 | return static::getSharedInstance('scheduler'); 29 | } 30 | 31 | return new Scheduler(); 32 | } 33 | 34 | /** 35 | * Returns the CronExpression class. 36 | */ 37 | public static function cronExpression(bool $getShared = true): CronExpression 38 | { 39 | if ($getShared) { 40 | return static::getSharedInstance('cronExpression'); 41 | } 42 | 43 | return new CronExpression(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Test/MockTask.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Test; 15 | 16 | use CodeIgniter\Tasks\Exceptions\TasksException; 17 | use CodeIgniter\Tasks\Task; 18 | 19 | /** 20 | * Mock Task Class 21 | * 22 | * Test class that prevents actions 23 | * from being called. 24 | */ 25 | class MockTask extends Task 26 | { 27 | /** 28 | * Pretends to run this Task's action. 29 | * 30 | * @return array 31 | * 32 | * @throws TasksException 33 | */ 34 | public function run() 35 | { 36 | $method = 'run' . ucfirst($this->type); 37 | if (! method_exists($this, $method)) { 38 | throw TasksException::forInvalidTaskType($this->type); 39 | } 40 | 41 | $_SESSION['tasks_cache'] = [$this->type, $this->action]; 42 | 43 | return [ 44 | 'command' => 'success', 45 | 'shell' => [], 46 | 'closure' => 42, 47 | 'event' => true, 48 | 'url' => 'body', 49 | 'queue' => true, 50 | ][$this->type]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The development team and community take all security issues seriously. **Please do not make public any uncovered flaws.** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Thank you for improving the security of our code! Any assistance in removing security flaws will be acknowledged. 8 | 9 | **Please report security flaws by emailing the development team directly: security@codeigniter.com**. 10 | 11 | The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating 12 | the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the 13 | progress towards a fix and full announcement, and may ask for additional information or guidance. 14 | 15 | ## Disclosure Policy 16 | 17 | When the security team receives a security bug report, they will assign it to a primary handler. 18 | This person will coordinate the fix and release process, involving the following steps: 19 | 20 | - Confirm the problem and determine the affected versions. 21 | - Audit code to find any potential similar problems. 22 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. 23 | 24 | ## Comments on this Policy 25 | 26 | If you have suggestions on how this process could be improved please submit a Pull Request. 27 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter Task Scheduler 2 | 3 | This makes scheduling Cron Jobs in your application simple, flexible, and powerful. Instead of setting up 4 | multiple Cron Jobs on each server your application runs on, you only need to setup a single cronjob to 5 | point to the script, and then all of your tasks are scheduled in your code. 6 | 7 | Besides that, it provides CLI tools to help you manage the tasks that should be ran, and more. 8 | 9 | This library relies on [CodeIgniter\Settings](https://github.com/codeigniter4/settings) library to store 10 | information, which provides a convenient way of storing settings in the database or a config file. 11 | 12 | ### Requirements 13 | 14 | ![PHP](https://img.shields.io/badge/PHP-%5E8.1-red) 15 | ![CodeIgniter](https://img.shields.io/badge/CodeIgniter-%5E4.1-red) 16 | 17 | ### Quickstart 18 | 19 | Install via Composer: 20 | 21 | ```console 22 | composer require codeigniter4/tasks 23 | ``` 24 | 25 | And schedule the task: 26 | 27 | ```php 28 | command('foo')->weekdays()->hourly(); 36 | } 37 | } 38 | ``` 39 | 40 | ### Acknowledgements 41 | 42 | Every open-source project depends on its contributors to be a success. The following users have 43 | contributed in one manner or another in making this project: 44 | 45 | 46 | Contributors 47 | 48 | 49 | Made with [contrib.rocks](https://contrib.rocks). 50 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | - [Publishing the Config file](#publishing-the-config-file) 4 | - [Config file options](#config-file-options) 5 | - [Setting the Cron Job](#setting-the-cron-job) 6 | 7 | ## Publishing the Config file 8 | 9 | To make changes to the config file, we have to have our copy in the `app/Config/Tasks.php`. Luckily, this package comes with handy command that will make this easy. 10 | 11 | When we run: 12 | 13 | php spark tasks:publish 14 | 15 | We will get our copy ready for modifications. 16 | 17 | ## Config file options 18 | 19 | - [$logPerformance](#logperformance) 20 | - [$maxLogsPerTask](#maxlogspertask) 21 | 22 | 23 | ### $logPerformance 24 | 25 | Should performance metrics be logged - `bool`. 26 | 27 | If `true`, performance information and errors will be logged to the database through the Settings library. 28 | A new record is created each time the task is run. 29 | 30 | Default value is `false`. 31 | 32 | ### $maxLogsPerTask 33 | 34 | Maximum performance logs - `int`. 35 | 36 | Specifies the maximum number of log files that should be stored for each defined task. Once the maximum is reached 37 | the oldest one is deleted when creating a new one. 38 | 39 | Default value is `10`. 40 | 41 | ## Setting the Cron Job 42 | 43 | The last thing to do is to set the Cron Job - you only need to add a single line. Usually you can do this via admin panel provided by your hosting provider. 44 | Remember to replace *path-to-your-project* with an actual path to your project. 45 | 46 | ```console 47 | * * * * * cd /path-to-your-project && php spark tasks:run >> /dev/null 2>&1 48 | ``` 49 | 50 | This will call your script every minute. When `tasks:run` is called, Tasks will determine the correct tasks that should be run and execute them. 51 | -------------------------------------------------------------------------------- /docs/assets/css/codeigniter_dark_mode.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="slate"] { 2 | --md-primary-fg-color: #b13a10; 3 | --md-primary-fg-color--light: #8d7474; 4 | --md-primary-fg-color--dark: #6d554d; 5 | 6 | --md-default-bg-color: #1e2129; 7 | 8 | --md-typeset-a-color: #ed6436; 9 | --md-accent-fg-color: #f18a67; 10 | 11 | --md-accent-fg-color--transparent: #625151; 12 | 13 | --md-code-bg-color: #282b2d; 14 | 15 | .hljs-title, 16 | .hljs-title.class_, 17 | .hljs-title.class_.inherited__, 18 | .hljs-title.function_ { 19 | color: #c9a69b; 20 | } 21 | 22 | .hljs-meta .hljs-string, 23 | .hljs-regexp, 24 | .hljs-string { 25 | color: #a3b4c7; 26 | } 27 | 28 | .hljs-attr, 29 | .hljs-attribute, 30 | .hljs-literal, 31 | .hljs-meta, 32 | .hljs-number, 33 | .hljs-operator, 34 | .hljs-selector-attr, 35 | .hljs-selector-class, 36 | .hljs-selector-id, 37 | .hljs-variable { 38 | color: #c1b79f; 39 | } 40 | 41 | .hljs-doctag, 42 | .hljs-keyword, 43 | .hljs-meta .hljs-keyword, 44 | .hljs-template-tag, 45 | .hljs-template-variable, 46 | .hljs-type, 47 | .hljs-variable.language_ { 48 | color: #c97100; 49 | } 50 | 51 | .hljs-subst { 52 | color: #ddba52 53 | } 54 | 55 | .md-typeset code { 56 | border: 1px solid #3f4547; 57 | } 58 | 59 | .md-typeset .admonition.note, 60 | .md-typeset details.note { 61 | border-color: #2c5293; 62 | } 63 | 64 | .md-typeset .note > .admonition-title:before, 65 | .md-typeset .note > summary:before { 66 | background-color: #2c5293; 67 | -webkit-mask-image: var(--md-admonition-icon--note); 68 | mask-image: var(--md-admonition-icon--note); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/Commands/Publish.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Commands; 15 | 16 | use CodeIgniter\CLI\CLI; 17 | use CodeIgniter\Publisher\Publisher; 18 | use Throwable; 19 | 20 | class Publish extends TaskCommand 21 | { 22 | protected $name = 'tasks:publish'; 23 | protected $description = 'Publish Tasks config file into the current application.'; 24 | 25 | /** 26 | * @return void 27 | */ 28 | public function run(array $params) 29 | { 30 | $source = service('autoloader')->getNamespace('CodeIgniter\\Tasks')[0]; 31 | 32 | $publisher = new Publisher($source, APPPATH); 33 | 34 | try { 35 | $publisher->addPaths([ 36 | 'Config/Tasks.php', 37 | ])->merge(false); 38 | } catch (Throwable $e) { 39 | $this->showError($e); 40 | 41 | return; 42 | } 43 | 44 | foreach ($publisher->getPublished() as $file) { 45 | $publisher->replace( 46 | $file, 47 | [ 48 | 'namespace CodeIgniter\\Tasks\\Config' => 'namespace Config', 49 | 'use CodeIgniter\\Config\\BaseConfig' => 'use CodeIgniter\\Tasks\\Config\\Tasks as BaseTasks', 50 | 'class Tasks extends BaseConfig' => 'class Tasks extends BaseTasks', 51 | ], 52 | ); 53 | } 54 | 55 | CLI::write(CLI::color(' Published! ', 'green') . 'You can customize the configuration by editing the "app/Config/Tasks.php" file.'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/assets/flame.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Config/Tasks.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Config; 15 | 16 | use CodeIgniter\Config\BaseConfig; 17 | use CodeIgniter\Tasks\Scheduler; 18 | 19 | class Tasks extends BaseConfig 20 | { 21 | /** 22 | * -------------------------------------------------------------------------- 23 | * Should performance metrics be logged 24 | * -------------------------------------------------------------------------- 25 | * 26 | * If true, will log the time it takes for each task to run. 27 | * Requires the settings table to have been created previously. 28 | */ 29 | public bool $logPerformance = false; 30 | 31 | /** 32 | * -------------------------------------------------------------------------- 33 | * Maximum performance logs 34 | * -------------------------------------------------------------------------- 35 | * 36 | * The maximum number of logs that should be saved per Task. 37 | * Lower numbers reduced the amount of database required to 38 | * store the logs. 39 | */ 40 | public int $maxLogsPerTask = 10; 41 | 42 | /** 43 | * Register any tasks within this method for the application. 44 | * Called by the TaskRunner. 45 | */ 46 | public function init(Scheduler $schedule) 47 | { 48 | $schedule->command('foo:bar')->daily(); 49 | 50 | $schedule->shell('cp foo bar')->daily('11:00 pm'); 51 | 52 | // $schedule->call(static function () { 53 | // // do something.... 54 | // })->mondays()->named('foo'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: '#^Parameter \#1 \$array of function usort contains unresolvable type\.$#' 5 | identifier: argument.unresolvableType 6 | count: 1 7 | path: src/Commands/Lister.php 8 | 9 | - 10 | message: '#^Parameter \#2 \$callback of function usort contains unresolvable type\.$#' 11 | identifier: argument.unresolvableType 12 | count: 1 13 | path: src/Commands/Lister.php 14 | 15 | - 16 | message: '#^Call to an undefined method CodeIgniter\\I18n\\Time\:\:getMonthDay\(\)\.$#' 17 | identifier: method.notFound 18 | count: 1 19 | path: src/RunResolver.php 20 | 21 | - 22 | message: '#^Call to an undefined method CodeIgniter\\I18n\\Time\:\:getWeekDay\(\)\.$#' 23 | identifier: method.notFound 24 | count: 1 25 | path: src/RunResolver.php 26 | 27 | - 28 | message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''CodeIgniter\\\\I18n\\\\Time'' and CodeIgniter\\I18n\\Time will always evaluate to true\.$#' 29 | identifier: method.alreadyNarrowedType 30 | count: 1 31 | path: tests/unit/CronExpressionTest.php 32 | 33 | - 34 | message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Closure'' and Closure\(\)\: ''Hello'' will always evaluate to true\.$#' 35 | identifier: method.alreadyNarrowedType 36 | count: 1 37 | path: tests/unit/SchedulerTest.php 38 | 39 | - 40 | message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''CodeIgniter\\\\Tasks\\\\Task'' and CodeIgniter\\Tasks\\Task will always evaluate to true\.$#' 41 | identifier: method.alreadyNarrowedType 42 | count: 4 43 | path: tests/unit/SchedulerTest.php 44 | 45 | - 46 | message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with ''Hello'' and ''Hello'' will always evaluate to true\.$#' 47 | identifier: method.alreadyNarrowedType 48 | count: 1 49 | path: tests/unit/SchedulerTest.php 50 | -------------------------------------------------------------------------------- /src/Commands/Run.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Commands; 15 | 16 | use CodeIgniter\CLI\CLI; 17 | use CodeIgniter\Tasks\TaskRunner; 18 | 19 | /** 20 | * Runs current tasks. 21 | */ 22 | class Run extends TaskCommand 23 | { 24 | /** 25 | * The Command's name 26 | */ 27 | protected $name = 'tasks:run'; 28 | 29 | /** 30 | * The Command's Options 31 | */ 32 | protected $options = [ 33 | '--task' => 'Run specific task by alias.', 34 | ]; 35 | 36 | /** 37 | * the Command's short description 38 | */ 39 | protected $description = 'Runs tasks based on the schedule, should be configured as a crontask to run every minute.'; 40 | 41 | /** 42 | * the Command's usage 43 | */ 44 | protected $usage = 'tasks:run'; 45 | 46 | /** 47 | * Runs tasks at the proper time. 48 | */ 49 | public function run(array $params) 50 | { 51 | helper('setting'); 52 | 53 | if (setting('Tasks.enabled') === false) { 54 | CLI::write(CLI::color('WARNING: Task running is currently disabled.', 'red')); 55 | CLI::write('To re-enable tasks run: tasks:enable'); 56 | 57 | return EXIT_ERROR; 58 | } 59 | 60 | CLI::write('Running Tasks...'); 61 | 62 | config('Tasks')->init(service('scheduler')); 63 | 64 | $runner = new TaskRunner(); 65 | 66 | if (CLI::getOption('task')) { 67 | $runner->only([CLI::getOption('task')]); 68 | } 69 | 70 | $runner->run(); 71 | 72 | CLI::write(CLI::color('Completed Running Tasks', 'green')); 73 | 74 | return EXIT_SUCCESS; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Composer Installation 4 | 5 | The only thing you have to do is to run this command, and you're ready to go. 6 | 7 | ```console 8 | composer require codeigniter4/tasks 9 | ``` 10 | 11 | #### A composer error occurred? 12 | 13 | If you get the following error: 14 | 15 | ```console 16 | Could not find a version of package codeigniter4/tasks matching your minimum-stability (stable). 17 | Require it with an explicit version constraint allowing its desired stability. 18 | ``` 19 | 20 | 1. Run the following commands to change your [minimum-stability](https://getcomposer.org/doc/articles/versions.md#minimum-stability) in your project `composer.json`: 21 | 22 | ```console 23 | composer config minimum-stability dev 24 | composer config prefer-stable true 25 | ``` 26 | 27 | 2. Or specify an explicit version: 28 | 29 | ```console 30 | composer require codeigniter4/tasks:dev-develop 31 | ``` 32 | 33 | The above specifies `develop` branch. 34 | See 35 | 36 | ## Manual Installation 37 | 38 | In the example below we will assume, that files from this project will be located in `app/ThirdParty/tasks` directory. 39 | 40 | Download this project and then enable it by editing the `app/Config/Autoload.php` file and adding the `CodeIgniter\Tasks` namespace to the `$psr4` array. You also have to add a companion project [Settings](https://github.com/codeigniter4/settings) in the same fashion, like in the below example: 41 | 42 | ```php 43 | APPPATH, // For custom app namespace 49 | 'Config' => APPPATH . 'Config', 50 | 'CodeIgniter\Settings' => APPPATH . 'ThirdParty/settings/src', 51 | 'CodeIgniter\Tasks' => APPPATH . 'ThirdParty/tasks/src', 52 | ]; 53 | 54 | // ... 55 | 56 | ``` 57 | 58 | ## Database Migration 59 | 60 | Regardless of which installation method you chose, we also need to migrate the database to add new tables. 61 | 62 | You can do this with the following command: 63 | 64 | #### for Unix 65 | ```console 66 | php spark migrate -n CodeIgniter\\Settings 67 | ``` 68 | 69 | #### for Windows 70 | ```console 71 | php spark migrate -n CodeIgniter\Settings 72 | ``` 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter Tasks 2 | 3 | A task scheduler for CodeIgniter 4. 4 | 5 | [![PHPUnit](https://github.com/codeigniter4/tasks/actions/workflows/phpunit.yml/badge.svg)](https://github.com/codeigniter4/tasks/actions/workflows/phpunit.yml) 6 | [![PHPStan](https://github.com/codeigniter4/tasks/actions/workflows/phpstan.yml/badge.svg)](https://github.com/codeigniter4/tasks/actions/workflows/phpstan.yml) 7 | [![Deptrac](https://github.com/codeigniter4/tasks/actions/workflows/deptrac.yml/badge.svg)](https://github.com/codeigniter4/tasks/actions/workflows/deptrac.yml) 8 | [![Coverage Status](https://coveralls.io/repos/github/codeigniter4/tasks/badge.svg?branch=develop)](https://coveralls.io/github/codeigniter4/tasks?branch=develop) 9 | 10 | ![PHP](https://img.shields.io/badge/PHP-%5E8.1-blue) 11 | ![CodeIgniter](https://img.shields.io/badge/CodeIgniter-%5E4.1-blue) 12 | ![License](https://img.shields.io/badge/License-MIT-blue) 13 | 14 | ## Installation 15 | 16 | Install via Composer: 17 | 18 | composer require codeigniter4/tasks 19 | 20 | Migrate the database: 21 | 22 | #### for Unix 23 | php spark migrate -n CodeIgniter\\Settings 24 | #### for Windows 25 | php spark migrate -n CodeIgniter\Settings 26 | 27 | ## Configuration 28 | 29 | Publish the config file: 30 | 31 | php spark tasks:publish 32 | 33 | ## Defining tasks 34 | 35 | Define your tasks in the `init()` method: 36 | 37 | ```php 38 | // app/Config/Tasks.php 39 | command('demo:refresh --all')->mondays('11:00 pm'); 56 | } 57 | } 58 | ``` 59 | 60 | ## Docs 61 | 62 | Read the full documentation: https://tasks.codeigniter.com 63 | 64 | ## Contributing 65 | 66 | We accept and encourage contributions from the community in any shape. It doesn't matter 67 | whether you can code, write documentation, or help find bugs, all contributions are welcome. 68 | See the [CONTRIBUTING.md](CONTRIBUTING.md) file for details. 69 | -------------------------------------------------------------------------------- /src/TaskLog.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks; 15 | 16 | use CodeIgniter\I18n\Time; 17 | use Exception; 18 | use Throwable; 19 | 20 | /** 21 | * @property ?Throwable $error 22 | * @property ?string $output 23 | * @property Time $runStart 24 | * @property Task $task 25 | */ 26 | class TaskLog 27 | { 28 | protected Task $task; 29 | protected ?string $output = null; 30 | protected Time $runStart; 31 | protected Time $runEnd; 32 | 33 | /** 34 | * The exception thrown during execution, if any. 35 | */ 36 | protected ?Throwable $error = null; 37 | 38 | /** 39 | * TaskLog constructor. 40 | */ 41 | public function __construct(array $data) 42 | { 43 | foreach ($data as $key => $value) { 44 | if ($key === 'output') { 45 | $this->output = $this->setOutput($value); 46 | } elseif (property_exists($this, $key)) { 47 | $this->{$key} = $value; 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Returns the duration of the task in seconds and fractions of a second. 54 | * 55 | * @return string 56 | * 57 | * @throws Exception 58 | */ 59 | public function duration() 60 | { 61 | return number_format((float) $this->runEnd->format('U.u') - (float) $this->runStart->format('U.u'), 2); 62 | } 63 | 64 | /** 65 | * Magic getter. 66 | */ 67 | public function __get(string $key) 68 | { 69 | if (property_exists($this, $key)) { 70 | return $this->{$key}; 71 | } 72 | } 73 | 74 | /** 75 | * Unify output to string. 76 | * 77 | * @param array|bool|int|string|null $value 78 | */ 79 | private function setOutput($value): ?string 80 | { 81 | if (is_string($value) || $value === null) { 82 | return $value; 83 | } 84 | 85 | if (is_array($value)) { 86 | return implode(PHP_EOL, $value); 87 | } 88 | 89 | return (string) $value; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeigniter4/tasks", 3 | "description": "Task Scheduler for CodeIgniter 4", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "codeigniter", 8 | "codeigniter4", 9 | "task scheduling", 10 | "cron" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Lonnie Ezell", 15 | "email": "lonnieje@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "homepage": "https://github.com/codeigniter4/tasks", 20 | "require": { 21 | "php": "^8.1", 22 | "ext-json": "*", 23 | "codeigniter4/settings": "^2.0", 24 | "codeigniter4/queue": "dev-develop" 25 | }, 26 | "require-dev": { 27 | "codeigniter4/devkit": "^1.3", 28 | "codeigniter4/framework": "^4.1" 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true, 32 | "autoload": { 33 | "psr-4": { 34 | "CodeIgniter\\Tasks\\": "src" 35 | }, 36 | "exclude-from-classmap": [ 37 | "**/Database/Migrations/**" 38 | ] 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Tests\\Support\\": "tests/_support" 43 | } 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "phpstan/extension-installer": true 48 | } 49 | }, 50 | "scripts": { 51 | "post-update-cmd": [ 52 | "bash admin/setup.sh" 53 | ], 54 | "analyze": [ 55 | "Composer\\Config::disableProcessTimeout", 56 | "phpstan analyze", 57 | "psalm", 58 | "rector process --dry-run" 59 | ], 60 | "sa": "@analyze", 61 | "ci": [ 62 | "Composer\\Config::disableProcessTimeout", 63 | "@cs", 64 | "@deduplicate", 65 | "@inspect", 66 | "@analyze", 67 | "@test" 68 | ], 69 | "cs": "php-cs-fixer fix --ansi --verbose --dry-run --diff", 70 | "cs-fix": "php-cs-fixer fix --ansi --verbose --diff", 71 | "style": "@cs-fix", 72 | "deduplicate": "phpcpd src/ tests/", 73 | "inspect": "deptrac analyze --cache-file=build/deptrac.cache", 74 | "mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit", 75 | "retool": "retool", 76 | "test": "phpunit" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Commands/Lister.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks\Commands; 15 | 16 | use CodeIgniter\CLI\CLI; 17 | use CodeIgniter\I18n\Time; 18 | 19 | /** 20 | * Lists currently scheduled tasks. 21 | */ 22 | class Lister extends TaskCommand 23 | { 24 | /** 25 | * The Command's name 26 | */ 27 | protected $name = 'tasks:list'; 28 | 29 | /** 30 | * the Command's short description 31 | */ 32 | protected $description = 'Lists the tasks currently set to run.'; 33 | 34 | /** 35 | * the Command's usage 36 | */ 37 | protected $usage = 'tasks:list'; 38 | 39 | /** 40 | * Lists upcoming tasks 41 | */ 42 | public function run(array $params) 43 | { 44 | helper('setting'); 45 | 46 | if (setting('Tasks.enabled') === false) { 47 | CLI::write('WARNING: Task running is currently disabled.', 'red'); 48 | CLI::write('To re-enable tasks run: tasks:enable'); 49 | CLI::newLine(); 50 | } 51 | 52 | $scheduler = service('scheduler'); 53 | 54 | config('Tasks')->init($scheduler); 55 | 56 | $tasks = []; 57 | 58 | foreach ($scheduler->getTasks() as $task) { 59 | $cron = service('cronExpression'); 60 | 61 | $nextRun = $cron->nextRun($task->getExpression()); 62 | $lastRun = $task->lastRun(); 63 | 64 | $tasks[] = [ 65 | 'name' => $task->name ?: $task->getAction(), 66 | 'type' => $task->getType(), 67 | 'schedule' => $task->getExpression(), 68 | 'last_run' => $lastRun instanceof Time ? $lastRun->toDateTimeString() : $lastRun, 69 | 'next_run' => $nextRun, 70 | 'runs_in' => $nextRun->humanize(), 71 | ]; 72 | } 73 | 74 | usort($tasks, static fn ($a, $b) => ($a['next_run'] < $b['next_run']) ? -1 : 1); 75 | 76 | CLI::table($tasks, [ 77 | 'Name', 78 | 'Type', 79 | 'Schedule', 80 | 'Last Run', 81 | 'Next Run', 82 | 'Runs', 83 | ]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/cli-commands.md: -------------------------------------------------------------------------------- 1 | # CLI Commands 2 | 3 | Included in the package are several commands that can be run from that CLI that provide that bit of emergency 4 | help you might need when something is going wrong with a cron job at 1am on a Saturday. 5 | 6 | ## Available Commands 7 | 8 | All commands are run through CodeIgniter's `spark` cli tool. 9 | 10 | - [tasks:list](#taskslist) 11 | - [tasks:disable](#tasksdisable) 12 | - [tasks:enable](#tasksenable) 13 | - [tasks:run](#tasksrun) 14 | - [tasks:publish](#taskspublish) 15 | 16 | ### tasks:list 17 | 18 | ```console 19 | php spark tasks:list 20 | ``` 21 | 22 | This will list all available tasks that have been defined in the project, along with their type and 23 | the next time they are scheduled to run. 24 | 25 | +---------------+--------------+-------------+----------+---------------------+-------------+ 26 | | Name | Type | Schedule | Last Run | Next Run | Runs | 27 | +---------------+--------------+-------------+----------+---------------------+-------------+ 28 | | emails | command | 0 0 * * * | -- | 2023-03-21-18:30:00 | in 1 hour | 29 | +---------------+--------------+-------------+----------+---------------------+-------------+ 30 | 31 | ### tasks:disable 32 | 33 | ```console 34 | php spark tasks:disable 35 | ``` 36 | 37 | Will disable the task runner manually until you enable it again. Stores the setting in the default 38 | database through the [Settings](https://github.com/codeigniter4/settings) library. 39 | 40 | ### tasks:enable 41 | 42 | ```console 43 | php spark tasks:enable 44 | ``` 45 | 46 | Will enable the task runner if it was previously disabled, allowing all tasks to resume running. 47 | 48 | ### tasks:run 49 | 50 | ```console 51 | php spark tasks:run 52 | ``` 53 | 54 | This is the primary entry point to the Tasks system. It should be called by a cron task on the server 55 | every minute in order to be able to effectively run all the scheduled tasks. You typically will not 56 | run this manually. 57 | 58 | You can run the command and pass the `--task` option to immediately run a single task. This requires 59 | the name of the task. You can either name a task using the `->named('foo')` method when defining the 60 | schedule, or one will be automatically generated. The name can be found using `tasks:list`. 61 | 62 | ```console 63 | php spark tasks:run --task emails 64 | ``` 65 | 66 | ### tasks:publish 67 | 68 | ```console 69 | php spark tasks:publish 70 | ``` 71 | 72 | This will publish Tasks config file into the current application. 73 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: CodeIgniter Tasks 2 | site_description: Documentation for the Tasks library for CodeIgniter 4 framework 3 | 4 | theme: 5 | name: material 6 | logo: assets/flame.svg 7 | favicon: assets/favicon.ico 8 | icon: 9 | repo: fontawesome/brands/github 10 | font: 11 | text: Raleway 12 | palette: 13 | # Palette toggle for light mode 14 | - media: "(prefers-color-scheme: light)" 15 | scheme: codeigniter 16 | primary: custom 17 | accent: custom 18 | toggle: 19 | icon: material/brightness-7 20 | name: Switch to dark mode 21 | # Palette toggle for dark mode 22 | - media: "(prefers-color-scheme: dark)" 23 | scheme: slate 24 | primary: custom 25 | accent: custom 26 | toggle: 27 | icon: material/brightness-4 28 | name: Switch to light mode 29 | features: 30 | - navigation.instant 31 | - content.code.copy 32 | - navigation.footer 33 | - content.action.edit 34 | - navigation.top 35 | - search.suggest 36 | - search.highlight 37 | - search.share 38 | 39 | extra: 40 | homepage: https://codeigniter.com 41 | generator: false 42 | 43 | social: 44 | - icon: material/github 45 | link: https://github.com/codeigniter4/tasks 46 | name: GitHub 47 | - icon: material/twitter 48 | link: https://twitter.com/CodeIgniterPhp 49 | name: X 50 | - icon: material/forum 51 | link: https://forum.codeigniter.com 52 | name: Forum Codeigniter 53 | - icon: material/slack 54 | link: https://join.slack.com/t/codeigniterchat/shared_invite/zt-244xrrslc-l_I69AJSi5y2a2RVN~xIdQ 55 | name: Slack 56 | 57 | site_url: https://tasks.codeigniter.com/ 58 | repo_url: https://github.com/codeigniter4/tasks 59 | edit_uri: edit/develop/docs/ 60 | copyright: Copyright © 2023 CodeIgniter Foundation. 61 | 62 | markdown_extensions: 63 | - pymdownx.superfences 64 | - pymdownx.highlight: 65 | use_pygments: false 66 | - admonition 67 | - pymdownx.details 68 | 69 | extra_css: 70 | - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/styles/github.min.css 71 | - assets/css/codeigniter.css 72 | - assets/css/codeigniter_dark_mode.css 73 | 74 | extra_javascript: 75 | - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/highlight.min.js 76 | - assets/js/hljs.js 77 | 78 | plugins: 79 | - search 80 | 81 | nav: 82 | - Home: index.md 83 | - Installation: installation.md 84 | - Configuration: configuration.md 85 | - CLI Commands: cli-commands.md 86 | - Basic Usage: basic-usage.md 87 | -------------------------------------------------------------------------------- /src/Scheduler.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks; 15 | 16 | use Closure; 17 | use CodeIgniter\Queue\Queue; 18 | use CodeIgniter\Tasks\Exceptions\TasksException; 19 | 20 | class Scheduler 21 | { 22 | protected array $tasks = []; 23 | 24 | /** 25 | * Returns the created Tasks. 26 | * 27 | * @return list 28 | */ 29 | public function getTasks(): array 30 | { 31 | return $this->tasks; 32 | } 33 | 34 | // -------------------------------------------------------------------- 35 | 36 | /** 37 | * Schedules a closure to run. 38 | */ 39 | public function call(Closure $func): Task 40 | { 41 | return $this->createTask('closure', $func); 42 | } 43 | 44 | /** 45 | * Schedules a console command to run. 46 | */ 47 | public function command(string $command): Task 48 | { 49 | return $this->createTask('command', $command); 50 | } 51 | 52 | /** 53 | * Schedules a local function to be exec'd 54 | */ 55 | public function shell(string $command): Task 56 | { 57 | return $this->createTask('shell', $command); 58 | } 59 | 60 | /** 61 | * Schedules an Event to trigger 62 | * 63 | * @param string $name Name of the event to trigger 64 | */ 65 | public function event(string $name): Task 66 | { 67 | return $this->createTask('event', $name); 68 | } 69 | 70 | /** 71 | * Schedules a cURL command to a remote URL 72 | */ 73 | public function url(string $url): Task 74 | { 75 | return $this->createTask('url', $url); 76 | } 77 | 78 | /** 79 | * Schedule a queue job. 80 | * 81 | * @throws TasksException 82 | */ 83 | public function queue(string $queue, string $job, array $data): Task 84 | { 85 | return $this->createTask('queue', [$queue, $job, $data]); 86 | } 87 | 88 | // -------------------------------------------------------------------- 89 | 90 | /** 91 | * @param mixed $action 92 | */ 93 | protected function createTask(string $type, $action): Task 94 | { 95 | $task = new Task($type, $action); 96 | $this->tasks[] = $task; 97 | 98 | return $task; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /spark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; 58 | $app = require realpath($bootstrap) ?: $bootstrap; 59 | 60 | $dotenv = new DotEnv(__DIR__); 61 | $dotenv->load(); 62 | 63 | // Grab our Console 64 | $console = new CodeIgniter\CLI\Console($app); 65 | 66 | // We want errors to be shown when using it from the CLI. 67 | error_reporting(-1); 68 | ini_set('display_errors', '1'); 69 | 70 | // Show basic information before we do anything else. 71 | $console->showHeader(); 72 | 73 | // fire off the command in the main framework. 74 | $response = $console->run(); 75 | if ($response->getStatusCode() >= 300) 76 | { 77 | exit($response->getStatusCode()); 78 | } 79 | -------------------------------------------------------------------------------- /deptrac.yaml: -------------------------------------------------------------------------------- 1 | deptrac: 2 | paths: 3 | - ./src/ 4 | - ./vendor/codeigniter4/framework/system/ 5 | exclude_files: 6 | - '#.*test.*#i' 7 | layers: 8 | - name: Model 9 | collectors: 10 | - type: bool 11 | must: 12 | - type: class 13 | value: .*[A-Za-z]+Model$ 14 | must_not: 15 | - type: directory 16 | value: vendor/.* 17 | - name: Vendor Model 18 | collectors: 19 | - type: bool 20 | must: 21 | - type: class 22 | value: .*[A-Za-z]+Model$ 23 | - type: directory 24 | value: vendor/.* 25 | - name: Controller 26 | collectors: 27 | - type: bool 28 | must: 29 | - type: class 30 | value: .*\/Controllers\/.* 31 | must_not: 32 | - type: directory 33 | value: vendor/.* 34 | - name: Vendor Controller 35 | collectors: 36 | - type: bool 37 | must: 38 | - type: class 39 | value: .*\/Controllers\/.* 40 | - type: directory 41 | value: vendor/.* 42 | - name: Config 43 | collectors: 44 | - type: bool 45 | must: 46 | - type: directory 47 | value: app/Config/.* 48 | must_not: 49 | - type: class 50 | value: .*Services 51 | - type: directory 52 | value: vendor/.* 53 | - name: Vendor Config 54 | collectors: 55 | - type: bool 56 | must: 57 | - type: directory 58 | value: vendor/.*/Config/.* 59 | must_not: 60 | - type: class 61 | value: .*Services 62 | - name: Entity 63 | collectors: 64 | - type: bool 65 | must: 66 | - type: directory 67 | value: app/Entities/.* 68 | must_not: 69 | - type: directory 70 | value: vendor/.* 71 | - name: Vendor Entity 72 | collectors: 73 | - type: bool 74 | must: 75 | - type: directory 76 | value: vendor/.*/Entities/.* 77 | - name: View 78 | collectors: 79 | - type: bool 80 | must: 81 | - type: directory 82 | value: app/Views/.* 83 | must_not: 84 | - type: directory 85 | value: vendor/.* 86 | - name: Vendor View 87 | collectors: 88 | - type: bool 89 | must: 90 | - type: directory 91 | value: vendor/.*/Views/.* 92 | - name: Service 93 | collectors: 94 | - type: class 95 | value: .*Services.* 96 | ruleset: 97 | Entity: 98 | - Config 99 | - Model 100 | - Service 101 | - Vendor Config 102 | - Vendor Entity 103 | - Vendor Model 104 | Config: 105 | - Service 106 | - Vendor Config 107 | Model: 108 | - Config 109 | - Entity 110 | - Service 111 | - Vendor Config 112 | - Vendor Entity 113 | - Vendor Model 114 | Service: 115 | - Config 116 | - Vendor Config 117 | 118 | # Ignore anything in the Vendor layers 119 | Vendor Model: 120 | - Config 121 | - Service 122 | - Vendor Config 123 | - Vendor Controller 124 | - Vendor Entity 125 | - Vendor Model 126 | - Vendor View 127 | Vendor Controller: 128 | - Service 129 | - Vendor Config 130 | - Vendor Controller 131 | - Vendor Entity 132 | - Vendor Model 133 | - Vendor View 134 | Vendor Config: 135 | - Config 136 | - Service 137 | - Vendor Config 138 | - Vendor Controller 139 | - Vendor Entity 140 | - Vendor Model 141 | - Vendor View 142 | Vendor Entity: 143 | - Service 144 | - Vendor Config 145 | - Vendor Controller 146 | - Vendor Entity 147 | - Vendor Model 148 | - Vendor View 149 | Vendor View: 150 | - Service 151 | - Vendor Config 152 | - Vendor Controller 153 | - Vendor Entity 154 | - Vendor Model 155 | - Vendor View 156 | skip_violations: [] 157 | -------------------------------------------------------------------------------- /src/CronExpression.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks; 15 | 16 | use CodeIgniter\I18n\Time; 17 | use CodeIgniter\Tasks\Exceptions\TasksException; 18 | use Exception; 19 | 20 | class CronExpression 21 | { 22 | /** 23 | * The timezone this should be considered under. 24 | */ 25 | protected string $timezone; 26 | 27 | /** 28 | * The current date/time. Used for testing. 29 | */ 30 | protected ?Time $testTime = null; 31 | 32 | /** 33 | * The current Cron expression string to process 34 | */ 35 | private ?string $currentExpression = null; 36 | 37 | /** 38 | * Allows us to set global timezone for all tasks 39 | * on construct 40 | * 41 | * @param string $timezone The global timezone for all tasks 42 | * 43 | * @return void 44 | */ 45 | public function __construct(?string $timezone = null) 46 | { 47 | $this->timezone = $timezone ?? app_timezone(); 48 | } 49 | 50 | /** 51 | * Checks whether cron expression should be run. Allows 52 | * for custom timezone to be used for specific task 53 | * 54 | * @param string $expression The Cron Expression to be evaluated 55 | */ 56 | public function shouldRun(string $expression): bool 57 | { 58 | $this->setTime(); 59 | 60 | $this->currentExpression = $expression; 61 | 62 | // Break the expression into separate parts 63 | [ 64 | $min, 65 | $hour, 66 | $monthDay, 67 | $month, 68 | $weekDay, 69 | ] = explode(' ', $expression); 70 | 71 | return $this->checkMinute($min) 72 | && $this->checkHour($hour) 73 | && $this->checkMonthDay($monthDay) 74 | && $this->checkMonth($month) 75 | && $this->checkWeekDay($weekDay); 76 | } 77 | 78 | /** 79 | * Returns a Time instance representing the next 80 | * date/time this expression would be ran. 81 | */ 82 | public function nextRun(string $expression): Time 83 | { 84 | $this->setTime(); 85 | 86 | return (new RunResolver())->nextRun($expression, clone $this->testTime); 87 | } 88 | 89 | /** 90 | * Returns a Time instance representing the last 91 | * date/time this expression would have ran. 92 | */ 93 | public function lastRun(string $expression): Time 94 | { 95 | return new Time(); 96 | } 97 | 98 | /** 99 | * Sets a date/time that will be used in place 100 | * of the current time to help with testing. 101 | * 102 | * @return $this 103 | * 104 | * @throws Exception 105 | */ 106 | public function testTime(string $dateTime) 107 | { 108 | $this->testTime = Time::parse($dateTime, $this->timezone); 109 | 110 | return $this; 111 | } 112 | 113 | private function checkMinute(string $time): bool 114 | { 115 | return $this->checkTime($time, 'i'); 116 | } 117 | 118 | private function checkHour(string $time): bool 119 | { 120 | return $this->checkTime($time, 'G'); 121 | } 122 | 123 | private function checkMonthDay(string $time): bool 124 | { 125 | return $this->checkTime($time, 'j'); 126 | } 127 | 128 | private function checkMonth(string $time): bool 129 | { 130 | return $this->checkTime($time, 'n'); 131 | } 132 | 133 | private function checkWeekDay(string $time): bool 134 | { 135 | return $this->checkTime($time, 'w'); 136 | } 137 | 138 | private function checkTime(string $time, string $format): bool 139 | { 140 | if ($time === '*') { 141 | return true; 142 | } 143 | 144 | $currentTime = $this->testTime->format($format); 145 | assert(ctype_digit($currentTime)); 146 | 147 | // Handle repeating times (i.e. /5 or */5 for every 5 minutes) 148 | if (str_contains($time, '/')) { 149 | $period = substr($time, strpos($time, '/') + 1) ?: ''; 150 | 151 | if ($period === '' || ! ctype_digit($period)) { 152 | throw TasksException::forInvalidCronExpression($this->currentExpression); 153 | } 154 | 155 | return ($currentTime % $period) === 0; 156 | } 157 | 158 | // Handle ranges (1-5) 159 | if (str_contains($time, '-')) { 160 | $items = []; 161 | [$start, $end] = explode('-', $time); 162 | 163 | for ($i = $start; $i <= $end; $i++) { 164 | $items[] = $i; 165 | } 166 | } 167 | // Handle multiple days 168 | else { 169 | $items = explode(',', $time); 170 | } 171 | 172 | return in_array($currentTime, $items, false); 173 | } 174 | 175 | /** 176 | * Sets the current time if it hasn't already been set. 177 | * 178 | * @throws Exception 179 | */ 180 | private function setTime(): void 181 | { 182 | // Set our current time 183 | if ($this->testTime instanceof Time) { 184 | return; 185 | } 186 | 187 | $this->testTime = Time::now($this->timezone); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/TaskRunner.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks; 15 | 16 | use CodeIgniter\CLI\CLI; 17 | use CodeIgniter\I18n\Time; 18 | use Throwable; 19 | 20 | /** 21 | * Class TaskRunner 22 | */ 23 | class TaskRunner 24 | { 25 | protected Scheduler $scheduler; 26 | protected ?string $testTime = null; 27 | 28 | /** 29 | * Stores aliases of tasks to run 30 | * If empty, All tasks will be executed as per their schedule 31 | */ 32 | protected array $only = []; 33 | 34 | public function __construct() 35 | { 36 | helper('setting'); 37 | $this->scheduler = service('scheduler'); 38 | } 39 | 40 | /** 41 | * The main entry point to run tasks within the system. 42 | * Also handles collecting output and sending out 43 | * notifications as necessary. 44 | */ 45 | public function run() 46 | { 47 | $tasks = $this->scheduler->getTasks(); 48 | 49 | if ($tasks === []) { 50 | return; 51 | } 52 | 53 | foreach ($tasks as $task) { 54 | // If specific tasks were chosen then skip executing remaining tasks 55 | if ($this->only !== [] && ! in_array($task->name, $this->only, true)) { 56 | continue; 57 | } 58 | 59 | if (! $task->shouldRun($this->testTime) && $this->only === []) { 60 | continue; 61 | } 62 | 63 | $error = null; 64 | $start = Time::now(); 65 | $output = null; 66 | 67 | $this->cliWrite('Processing: ' . ($task->name ?: 'Task'), 'green'); 68 | 69 | try { 70 | $output = $task->run(); 71 | 72 | $this->cliWrite('Executed: ' . ($task->name ?: 'Task'), 'cyan'); 73 | } catch (Throwable $e) { 74 | $this->cliWrite('Failed: ' . ($task->name ?: 'Task'), 'red'); 75 | 76 | log_message('error', $e->getMessage(), $e->getTrace()); 77 | $error = $e; 78 | } finally { 79 | // Save performance info 80 | $taskLog = new TaskLog([ 81 | 'task' => $task, 82 | 'output' => $output, 83 | 'runStart' => $start, 84 | 'runEnd' => Time::now(), 85 | 'error' => $error, 86 | ]); 87 | 88 | $this->updateLogs($taskLog); 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * Specify tasks to run 95 | */ 96 | public function only(array $tasks = []): TaskRunner 97 | { 98 | $this->only = $tasks; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Sets a time that will be used. 105 | * Allows setting a specific time to test against. 106 | * Must be in a DateTime-compatible format. 107 | */ 108 | public function withTestTime(string $time): TaskRunner 109 | { 110 | $this->testTime = $time; 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Write a line to command line interface 117 | */ 118 | protected function cliWrite(string $text, ?string $foreground = null) 119 | { 120 | // Skip writing to cli in tests 121 | if (defined('ENVIRONMENT') && ENVIRONMENT === 'testing') { 122 | return; 123 | } 124 | 125 | if (! is_cli()) { 126 | return; 127 | } 128 | 129 | CLI::write('[' . date('Y-m-d H:i:s') . '] ' . $text, $foreground); 130 | } 131 | 132 | /** 133 | * Adds the performance log to the 134 | */ 135 | protected function updateLogs(TaskLog $taskLog) 136 | { 137 | if (setting('Tasks.logPerformance') === false) { 138 | return; 139 | } 140 | 141 | // "unique" name will be returned if one wasn't set 142 | $name = $taskLog->task->name; 143 | $error = null; 144 | 145 | if ($taskLog->error instanceof Throwable) { 146 | $error = "Exception: {$taskLog->error->getCode()} - {$taskLog->error->getMessage()}" . PHP_EOL . 147 | "file: {$taskLog->error->getFile()}:{$taskLog->error->getLine()}"; 148 | } 149 | 150 | $data = [ 151 | 'task' => $name, 152 | 'type' => $taskLog->task->getType(), 153 | 'start' => $taskLog->runStart->format('Y-m-d H:i:s'), 154 | 'duration' => $taskLog->duration(), 155 | 'output' => $taskLog->output ?? null, 156 | 'error' => $error, 157 | ]; 158 | 159 | // Get existing logs 160 | $logs = setting("Tasks.log-{$name}"); 161 | if (empty($logs)) { 162 | $logs = []; 163 | } 164 | 165 | // Make sure we have room for one more 166 | /** @var int $maxLogsPerTask */ 167 | $maxLogsPerTask = setting('Tasks.maxLogsPerTask'); 168 | if ((is_countable($logs) ? count($logs) : 0) > $maxLogsPerTask) { 169 | array_pop($logs); 170 | } 171 | 172 | // Add the log to the top of the array 173 | array_unshift($logs, $data); 174 | 175 | setting("Tasks.log-{$name}", $logs); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /docs/vision.md: -------------------------------------------------------------------------------- 1 | # Project Vision 2 | 3 | **This document is intended for developers to understand the flow of the module and be able to help out with 4 | its development. Features and usage may change before its release.** 5 | 6 | ## Task Types 7 | 8 | Tasks are scheduled within the config file, `Config\Tasks.php`, within the `init()` method: 9 | 10 | ```php 11 | public function init(Scheduler $schedule) 12 | { 13 | $schedule->command('foo:bar')->nightly(); 14 | } 15 | ``` 16 | 17 | There are 5 types of tasks: 18 | 19 | 1. Commands. These are CLI commands that are defined by the name defined within their file, like `migrate:create`. 20 | 21 | ```php 22 | $schedule->command('foo:bar')->nightly(); 23 | ``` 24 | 25 | 2. Shell commands. These are passed to the system through `exec()` or `shell()`. Need to determine what makes 26 | sense here. 27 | 28 | ```php 29 | $schedule->shell('cp foo bar')->daily()->at('11:00 pm'); 30 | ``` 31 | 32 | 3. Closures. Anonymous functions can be used to define the action to take, also. 33 | 34 | ```php 35 | $schedule->call(function() { 36 | // do something.... 37 | })->mondays(); 38 | ``` 39 | 40 | 4. Events. These trigger pre-defined framework [Events](https://codeigniter4.github.io/CodeIgniter4/extending/events.html) 41 | which allows for more dynamic actions to take place from a single Task. 42 | 43 | ```php 44 | $schedule->event('reminders')->weekdays()->at('9:00 am'); 45 | ``` 46 | 47 | 5. URL. These access a remote URL and are handy for interacting with other APIs or coordinating tasks across servers. 48 | 49 | ```php 50 | $schedule->url('https://example.com/api/sync_remote_db')->environments('production')->everyTuesday();); 51 | ``` 52 | 53 | > Note that URL Tasks perform a simple GET request; should you need more involved remote calls (like authentication) 54 | they should be written into a separate command. 55 | 56 | ## Scheduling Tasks 57 | 58 | Tasks are scheduled via fluent commands that allow chaining commands together. Some examples of available methods 59 | would be: 60 | 61 | - `cron('/5 * * * *'')` specifies the exact cron syntax to use. 62 | - `daily()` which runs every day at midnight. Optionally pass a time as the only argument, ie. `04:00 am` 63 | - `weekdays()` runs M-F at midnight. Optionally pass a time as the only argument, ie. `04:00 am` 64 | - `weekends()` runs on Saturday and Sunday at midnight. Optionally pass a time as the only argument, ie. `04:00 am` 65 | - `mondays()` runs every Monday at midnight. Optionally pass a time as the only argument, ie. `04:00 am` 66 | - `everyMinute()`, `everyFiveMinutes()`, allows scheduling some common intervals 67 | - `hourly()` runs the task at the top of every hour 68 | - `environments()` specifies one or more environments the tasks should run in 69 | - `alias()` specifies an alias that the script can be called by, useful when manually running the task. 70 | 71 | ## How CRON interacts with the scheduler 72 | 73 | In order to have the scheduler work, a cronjob needs to be setup on the server to run every minute against a 74 | command we'll need to create: 75 | 76 | ``` 77 | * * * * * cd /path-to-your-project && php spark tasks:run 78 | ``` 79 | 80 | ## Other classes 81 | 82 | The following classes are anticipated to be needed for the main process flow: 83 | 84 | **TaskRunner** is called by the main command and handles actually determining which tasks to run and 85 | running all of the commands. 86 | 87 | **Scheduler** is passed into the `init()` method of the config class and handles generating the crontab formats 88 | for all tasks. It houses the commands that are called when scheduling tasks. 89 | 90 | **Task** represents a single task that should be scheduled. These are generated by the Scheduler and passed 91 | to the TaskRunner when they're ready to be run. 92 | 93 | **CronExpression** understands how to interpret a raw crontab expression (* * * * *) and determine if the task 94 | should run now, as well as provide future/past dates it would run. 95 | 96 | ## Commands 97 | 98 | In addition to the core functionality, the following features will be availble by commands to help developers: 99 | 100 | **tasks:run foo:bar** an optional flag, `--task` can be set to define a single task that should be run right now. 101 | It can take either the name of a command one is associated with, or by the `alias` defined in the setup. 102 | 103 | **tasks:list** generates a table with all tasks and the last and next times it is scheduled to run 104 | 105 | **tasks::disable** can disable a job from running until `tasks:enable` is called on it again. Useful 106 | on production servers when something is going wrong. Likely stores a json file in `/writable` to maintain state 107 | 108 | **tasks::performance** generates a table to display performance information about all of the last runs. 109 | See https://github.com/codestudiohq/laravel-totem for inspiration, though we're doing it on the CLI. 110 | 111 | ## Notifications 112 | 113 | The schedular should also provide a few different ways to return information about the jobs. I envision the 114 | following methods to start: 115 | 116 | **logs** - simply logs the runtime and performance information. Would be nice to do this in a separate log file. 117 | 118 | **email** - can notify one of more people of the performance of a single task, or a daily summary of all tasks 119 | at a specified time of day. This would basically be a provided cron task that could be scheduled. So - would 120 | need a command. Should display list of tasks ran, the time they ran, whether there were errors or not, and 121 | performance information. 122 | -------------------------------------------------------------------------------- /src/RunResolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks; 15 | 16 | use CodeIgniter\I18n\Time; 17 | 18 | class RunResolver 19 | { 20 | /** 21 | * The maximum number of times to loop 22 | * when looking for next run date. 23 | */ 24 | protected int $maxIterations = 1000; 25 | 26 | /** 27 | * Takes a cron expression, i.e. '* * * * 4', and returns 28 | * a Time instance that represents that next time that 29 | * expression would run. 30 | */ 31 | public function nextRun(string $expression, Time $next): Time 32 | { 33 | // Break the expression into separate parts 34 | [ 35 | $minute, 36 | $hour, 37 | $monthDay, 38 | $month, 39 | $weekDay, 40 | ] = explode(' ', $expression); 41 | 42 | $cron = [ 43 | 'minute' => $minute, 44 | 'hour' => $hour, 45 | 'monthDay' => $monthDay, 46 | 'month' => $month, 47 | 'weekDay' => $weekDay, 48 | ]; 49 | 50 | // We don't need to satisfy '*' values, so 51 | // remove them to have less to loop over. 52 | $cron = array_filter($cron, static fn ($item) => $item !== '*'); 53 | 54 | // If there's nothing left then it's every minute 55 | // so set it to one minute from now. 56 | if ($cron === []) { 57 | return $next->addMinutes(1)->setSecond(0); 58 | } 59 | 60 | // Loop over each of the remaining $cron elements 61 | // until we manage to satisfy all of the them 62 | for ($i = 1; $i <= $this->maxIterations; $i++) { 63 | foreach ($cron as $position => $value) { 64 | $satisfied = false; 65 | 66 | // The method to use on the Time instance 67 | $method = 'get' . ucfirst($position); 68 | 69 | // monthDay and weekDay need custom methods 70 | if ($position === 'monthDay') { 71 | $method = 'getDay'; 72 | } 73 | if ($position === 'weekDay') { 74 | $method = 'getDayOfWeek'; 75 | 76 | $value = $this->convertDOWToNumbers($value); 77 | } 78 | $nextValue = $next->{$method}(); 79 | 80 | // If it's a single value 81 | if ($nextValue === $value) { 82 | $satisfied = true; 83 | } 84 | // If the value is a list 85 | elseif (str_contains($value, ',')) { 86 | if ($this->isInList($nextValue, $value)) { 87 | $satisfied = true; 88 | } 89 | } 90 | // If the value is a range 91 | elseif (str_contains($value, '-')) { 92 | if ($this->isInRange($nextValue, $value)) { 93 | $satisfied = true; 94 | } 95 | } 96 | // If the value is an increment 97 | elseif (str_contains($value, '/')) { 98 | if ($this->isInIncrement($nextValue, $value)) { 99 | $satisfied = true; 100 | } 101 | } 102 | 103 | // If we didn't match it, then start the iterations over 104 | if (! $satisfied) { 105 | $next = $this->increment($next, $position); 106 | 107 | continue 2; 108 | } 109 | } 110 | } 111 | 112 | return $next; 113 | } 114 | 115 | /** 116 | * Increments the part of the cron to the next appropriate. 117 | * 118 | * Note: this is a pretty brute-force way to do it. We could 119 | * definitely make it smarter in the future to cut down on the 120 | * amount of iterations needed. 121 | */ 122 | protected function increment(Time $next, string $position): Time 123 | { 124 | return match ($position) { 125 | 'minute' => $next->addMinutes(1), 126 | 'hour' => $next->addHours(1), 127 | 'monthDay', 'weekDay' => $next->addDays(1), 128 | 'month' => $next->addMonths(1), 129 | default => $next, 130 | }; 131 | } 132 | 133 | /** 134 | * Determines if the given value is in the specified range. 135 | * 136 | * @param int|string $value 137 | */ 138 | protected function isInRange($value, string $range): bool 139 | { 140 | [$start, $end] = explode('-', $range); 141 | 142 | return $value >= $start && $value <= $end; 143 | } 144 | 145 | /** 146 | * Determines if the given value is in the specified list of values. 147 | * 148 | * @param int|string $value 149 | */ 150 | protected function isInList($value, string $list): bool 151 | { 152 | $list = explode(',', $list); 153 | 154 | return in_array(trim((string) $value), $list, true); 155 | } 156 | 157 | /** 158 | * Determines if the $value is one of the increments. 159 | * 160 | * @param int|string $value 161 | */ 162 | protected function isInIncrement($value, string $increment): bool 163 | { 164 | [$start, $increment] = explode('/', $increment); 165 | 166 | // Allow for empty start values 167 | if ($start === '' || $start === '*') { 168 | $start = 0; 169 | } 170 | 171 | // The $start interval should be the first one to test against 172 | if ($value === $start) { 173 | return true; 174 | } 175 | 176 | return ($value - $start) > 0 177 | && (($value - $start) % $increment) === 0; 178 | } 179 | 180 | /** 181 | * Given a cron setting for Day of Week, will convert 182 | * settings with text days of week (Mon, Tue, etc) 183 | * into numeric values for easier handling. 184 | */ 185 | protected function convertDOWToNumbers(string $origValue): string 186 | { 187 | $origValue = strtolower(trim($origValue)); 188 | 189 | // If it doesn't contain any letters, just return it. 190 | preg_match('/\w/', $origValue, $matches); 191 | 192 | if ($matches === []) { 193 | return $origValue; 194 | } 195 | 196 | $days = [ 197 | 'sun' => '0', 198 | 'mon' => '1', 199 | 'tue' => '2', 200 | 'wed' => '3', 201 | 'thu' => '4', 202 | 'fri' => '5', 203 | 'sat' => '6', 204 | ]; 205 | 206 | return str_replace(array_keys($days), array_values($days), $origValue); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | use Rector\Caching\ValueObject\Storage\FileCacheStorage; 15 | use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector; 16 | use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector; 17 | use Rector\CodeQuality\Rector\Empty_\SimplifyEmptyCheckOnEmptyArrayRector; 18 | use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector; 19 | use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector; 20 | use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector; 21 | use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector; 22 | use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector; 23 | use Rector\CodeQuality\Rector\FuncCall\SingleInArrayToCompareRector; 24 | use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; 25 | use Rector\CodeQuality\Rector\If_\CombineIfRector; 26 | use Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector; 27 | use Rector\CodeQuality\Rector\If_\ShortenElseIfRector; 28 | use Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector; 29 | use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector; 30 | use Rector\CodeQuality\Rector\Ternary\TernaryEmptyArrayArrayDimFetchToCoalesceRector; 31 | use Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector; 32 | use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector; 33 | use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; 34 | use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; 35 | use Rector\CodingStyle\Rector\FuncCall\VersionCompareFuncCallToConstantRector; 36 | use Rector\Config\RectorConfig; 37 | use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector; 38 | use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; 39 | use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; 40 | use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; 41 | use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector; 42 | use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; 43 | use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector; 44 | use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\AnnotationWithValueToAttributeRector; 45 | use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; 46 | use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector; 47 | use Rector\PHPUnit\Set\PHPUnitSetList; 48 | use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; 49 | use Rector\Set\ValueObject\LevelSetList; 50 | use Rector\Set\ValueObject\SetList; 51 | use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector; 52 | use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector; 53 | use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector; 54 | use Rector\ValueObject\PhpVersion; 55 | 56 | return static function (RectorConfig $rectorConfig): void { 57 | $rectorConfig->sets([ 58 | SetList::DEAD_CODE, 59 | LevelSetList::UP_TO_PHP_81, 60 | PHPUnitSetList::PHPUNIT_CODE_QUALITY, 61 | PHPUnitSetList::PHPUNIT_100, 62 | ]); 63 | 64 | $rectorConfig->parallel(); 65 | 66 | // Github action cache 67 | $rectorConfig->cacheClass(FileCacheStorage::class); 68 | if (is_dir('/tmp')) { 69 | $rectorConfig->cacheDirectory('/tmp/rector'); 70 | } 71 | 72 | // The paths to refactor (can also be supplied with CLI arguments) 73 | $rectorConfig->paths([ 74 | __DIR__ . '/src/', 75 | __DIR__ . '/tests/', 76 | ]); 77 | 78 | // Include Composer's autoload - required for global execution, remove if running locally 79 | $rectorConfig->autoloadPaths([ 80 | __DIR__ . '/vendor/autoload.php', 81 | ]); 82 | 83 | // Do you need to include constants, class aliases, or a custom autoloader? 84 | $rectorConfig->bootstrapFiles([ 85 | realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php', 86 | ]); 87 | 88 | if (is_file(__DIR__ . '/phpstan.neon.dist')) { 89 | $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist'); 90 | } 91 | 92 | // Set the target version for refactoring 93 | $rectorConfig->phpVersion(PhpVersion::PHP_81); 94 | 95 | // Auto-import fully qualified class names 96 | $rectorConfig->importNames(); 97 | 98 | // Are there files or rules you need to skip? 99 | $rectorConfig->skip([ 100 | __DIR__ . '/app/Views', 101 | 102 | StringifyStrNeedlesRector::class, 103 | YieldDataProviderRector::class, 104 | 105 | // Note: requires php 8 106 | RemoveUnusedPromotedPropertyRector::class, 107 | AnnotationWithValueToAttributeRector::class, 108 | 109 | // May load view files directly when detecting classes 110 | StringClassNameToClassConstantRector::class, 111 | 112 | // Because of the BaseCommand 113 | TypedPropertyFromAssignsRector::class => [ 114 | __DIR__ . '/src/Commands/Disable.php', 115 | __DIR__ . '/src/Commands/Enable.php', 116 | __DIR__ . '/src/Commands/Lister.php', 117 | __DIR__ . '/src/Commands/Publish.php', 118 | __DIR__ . '/src/Commands/Run.php', 119 | __DIR__ . '/src/Commands/TaskCommand.php', 120 | __DIR__ . '/tests/_support/Commands/TasksExample.php', 121 | __DIR__ . '/tests/unit/TaskRunnerTest.php', 122 | ], 123 | 124 | // Temporary fix 125 | DeclareStrictTypesTestsRector::class, 126 | ]); 127 | 128 | // auto import fully qualified class names 129 | $rectorConfig->importNames(); 130 | 131 | $rectorConfig->rule(SimplifyUselessVariableRector::class); 132 | $rectorConfig->rule(RemoveAlwaysElseRector::class); 133 | $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); 134 | $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); 135 | $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); 136 | $rectorConfig->rule(SimplifyStrposLowerRector::class); 137 | $rectorConfig->rule(CombineIfRector::class); 138 | $rectorConfig->rule(SimplifyIfReturnBoolRector::class); 139 | $rectorConfig->rule(InlineIfToExplicitIfRector::class); 140 | $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); 141 | $rectorConfig->rule(ShortenElseIfRector::class); 142 | $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); 143 | $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); 144 | $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); 145 | $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); 146 | $rectorConfig->rule(SimplifyRegexPatternRector::class); 147 | $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); 148 | $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); 149 | $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); 150 | $rectorConfig->rule(SimplifyEmptyCheckOnEmptyArrayRector::class); 151 | $rectorConfig->rule(TernaryEmptyArrayArrayDimFetchToCoalesceRector::class); 152 | $rectorConfig->rule(EmptyOnNullableObjectToInstanceOfRector::class); 153 | $rectorConfig->rule(DisallowedEmptyRuleFixerRector::class); 154 | $rectorConfig 155 | ->ruleWithConfiguration(TypedPropertyFromAssignsRector::class, [ 156 | /** 157 | * The INLINE_PUBLIC value is default to false to avoid BC break, 158 | * if you use for libraries and want to preserve BC break, you don't 159 | * need to configure it, as it included in LevelSetList::UP_TO_PHP_74 160 | * Set to true for projects that allow BC break 161 | */ 162 | TypedPropertyFromAssignsRector::INLINE_PUBLIC => true, 163 | ]); 164 | $rectorConfig->rule(StringClassNameToClassConstantRector::class); 165 | $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class); 166 | $rectorConfig->rule(CompleteDynamicPropertiesRector::class); 167 | $rectorConfig->rule(SingleInArrayToCompareRector::class); 168 | $rectorConfig->rule(VersionCompareFuncCallToConstantRector::class); 169 | $rectorConfig->rule(ExplicitBoolCompareRector::class); 170 | }; 171 | -------------------------------------------------------------------------------- /docs/basic-usage.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | Tasks are configured with the `app/Config/Tasks.php` config file, inside the `init()` method. 4 | Let's start with a simple example: 5 | 6 | ```php 7 | call(function() { 24 | DemoContent::refresh(); 25 | })->mondays(); 26 | } 27 | } 28 | ``` 29 | 30 | In this example, we use a closure to refresh demo content at 12:00 am every Monday morning. Closures are 31 | a simple way to handle quick functions like this. You can also execute server commands, execute custom 32 | CLI commands you have written, call a URL, or even fire off an Event of your choosing. Details are covered 33 | below. 34 | 35 | ## Scheduling 36 | 37 | This is how we can schedule our tasks. We have many options. 38 | 39 | ### Scheduling CLI Commands 40 | 41 | If you have written your own [CLI Commands](https://codeigniter.com/user_guide/cli/cli_commands.html), you 42 | can schedule them to run using the `command()` method. 43 | 44 | ```php 45 | $schedule->command('demo:refresh --all'); 46 | ``` 47 | 48 | The only argument is a string that calls the command, complete with an options or arguments. 49 | 50 | ### Scheduling Shell Commands 51 | 52 | You can call out to the server and execute a command using the `shell()` method. 53 | 54 | ```php 55 | $schedule->shell('cp foo bar')->daily()->at('11:00 pm'); 56 | ``` 57 | 58 | Simply provide the command to call and any arguments, and it will be executed using PHP's `exec()` method. 59 | 60 | !!! note 61 | 62 | Many shared servers turn off exec access for security reasons. If you will be running 63 | on a shared server, double-check you can use the `exec` command before using this feature. 64 | 65 | ### Scheduling Events 66 | 67 | If you want to trigger an [Event](https://codeigniter.com/user_guide/extending/events.html) you can 68 | use the `event()` method to do that for you, passing in the name of the event to trigger. 69 | 70 | ```php 71 | $schedule->event('Foo')->hourly(); 72 | ``` 73 | 74 | ### Scheduling URL Calls 75 | 76 | If you need to ping a URL on a regular basis, you can use the `url()` method to perform a simple 77 | GET request using cURL to the URL you pass in. If you need more dynamism than can be provided in 78 | a simple URL string, you can use a closure or command instead. 79 | 80 | ```php 81 | $schedule->url('https://my-status-cloud.com?site=foo.com')->everyFiveMinutes(); 82 | ``` 83 | 84 | ### Scheduling Queue Jobs 85 | 86 | If you want to schedule a Queue Job, you can use the `queue()` method and specify the queue name, job name and data your job needs: 87 | 88 | ```php 89 | $schedule->queue('queue-name', 'jobName', ['data' => 'array'])->hourly(); 90 | ``` 91 | 92 | !!! note 93 | 94 | To learn more about queues, you can visit the [Queue package](https://github.com/codeigniter4/queue). 95 | 96 | 97 | The `singleInstance()` option, described in the next section, works a bit differently than with other scheduling methods. 98 | Since queue jobs are added quickly and processed later in the background, the lock is applied as soon as the job is queued - not when it actually runs. 99 | 100 | ```php 101 | $schedule->queue('queue-name', 'jobName', ['data' => 'array']) 102 | ->hourly() 103 | ->singleInstance(); 104 | ``` 105 | 106 | This means: 107 | 108 | - The lock is created immediately when the job is queued. 109 | - The lock is released only after the job is processed (whether it succeeds or fails). 110 | 111 | We can optionally pass a TTL to `singleInstance()` to limit how long the job lock should last: 112 | 113 | ```php 114 | $schedule->queue('queue-name', 'jobName', ['data' => 'array']) 115 | ->hourly() 116 | ->singleInstance(30 * MINUTE); 117 | ``` 118 | 119 | How it works: 120 | 121 | - The lock is set immediately when the job is queued. 122 | - The job must start processing before the TTL expires (in this case, within 30 minutes). 123 | - Once the job starts, the lock is renewed for the same TTL. 124 | - So, effectively, you have 30 minutes to start, and another 30 minutes to complete the job. 125 | 126 | ## Single Instance Tasks 127 | 128 | Some tasks can run longer than their scheduled interval. To prevent multiple instances of the same task running simultaneously, you can use the `singleInstance()` method: 129 | 130 | ```php 131 | $schedule->command('demo:heavy-task')->everyMinute()->singleInstance(); 132 | ``` 133 | 134 | With this setup, even if the task takes more than one minute to complete, a new instance won't start until the running one finishes. 135 | 136 | ### Setting Lock Duration 137 | 138 | By default, the lock will remain active until the task completes execution. However, you can specify a maximum lock duration by passing a TTL (time-to-live) value in seconds to the `singleInstance()` method: 139 | 140 | ```php 141 | // Lock for a maximum of 30 minutes (1800 seconds) 142 | $schedule->command('demo:heavy-task') 143 | ->everyFifteenMinutes() 144 | ->singleInstance(30 * MINUTE); 145 | ``` 146 | 147 | This is useful in preventing "stuck" locks. If a task crashes unexpectedly, the lock might remain indefinitely. Setting a TTL ensures the lock eventually expires. 148 | 149 | If a task completes before the TTL expires, the lock is released immediately. The TTL only represents the maximum duration the lock can exist. 150 | 151 | ## Frequency Options 152 | 153 | There are a number of ways available to specify how often the task is called. 154 | 155 | 156 | | Method | Description | 157 | |:----------------------------------------------|:--------------------------------------------------------------------| 158 | | `->cron('* * * * *')` | Run on a custom cron schedule. | 159 | | `->daily('4:00 am')` | Runs daily at 12:00am, unless a time string is passed in. | 160 | | `->hourly() / ->hourly(15)` | Runs at the top of every hour or at specified minute. | 161 | | `->everyHour(3, 15)` | Runs every 3 hours at XX:15. | 162 | | `->betweenHours(6,12)` | Runs between hours 6 and 12. | 163 | | `->hours([0,10,16])` | Runs at hours 0, 10 and 16. | 164 | | `->everyMinute(20)` | Runs every 20 minutes. | 165 | | `->betweenMinutes(0,30)` | Runs between minutes 0 and 30. | 166 | | `->minutes([0,20,40])` | Runs at specific minutes 0,20 and 40. | 167 | | `->everyFiveMinutes()` | Runs every 5 minutes (12:00, 12:05, 12:10, etc) | 168 | | `->everyFifteenMinutes()` | Runs every 15 minutes (12:00, 12:15, etc) | 169 | | `->everyThirtyMinutes()` | Runs every 30 minutes (12:00, 12:30, etc) | 170 | | `->days([0,3])` | Runs only on Sunday and Wednesday ( 0 is Sunday , 6 is Saturday ) | 171 | | `->sundays('3:15am')` | Runs every Sunday at midnight, unless time passed in. | 172 | | `->mondays('3:15am')` | Runs every Monday at midnight, unless time passed in. | 173 | | `->tuesdays('3:15am')` | Runs every Tuesday at midnight, unless time passed in. | 174 | | `->wednesdays('3:15am')` | Runs every Wednesday at midnight, unless time passed in. | 175 | | `->thursdays('3:15am')` | Runs every Thursday at midnight, unless time passed in. | 176 | | `->fridays('3:15am')` | Runs every Friday at midnight, unless time passed in. | 177 | | `->saturdays('3:15am')` | Runs every Saturday at midnight, unless time passed in. | 178 | | `->monthly('12:21pm')` | Runs the first day of every month at 12:00am unless time passed in. | 179 | | `->daysOfMonth([1,15])` | Runs only on days 1 and 15. | 180 | | `->everyMonth(4)` | Runs every 4 months. | 181 | | `->betweenMonths(4,7)` | Runs between months 4 and 7. | 182 | | `->months([1,7])` | Runs only on January and July. | 183 | | `->quarterly('5:00am')` | Runs the first day of each quarter (Jan 1, Apr 1, July 1, Oct 1) | 184 | | `->yearly('12:34am')` | Runs the first day of the year. | 185 | | `->weekdays('1:23pm')` | Runs M-F at 12:00 am unless time passed in. | 186 | | `->weekends('2:34am')` | Runs Saturday and Sunday at 12:00 am unless time passed in. | 187 | | `->environments('local', 'prod')` | Restricts the task to run only in the specified environments. | 188 | | `->singleInstance() / ->singleInstance(HOUR)` | Prevents concurrent executions of the same task. | 189 | 190 | 191 | These methods can be combined to create even more nuanced timings: 192 | 193 | ```php 194 | $schedule->command('foo') 195 | ->weekdays() 196 | ->hourly() 197 | ->environments('development'); 198 | ``` 199 | 200 | This would run the task at the top of every hour, Monday - Friday, but only in development environments. 201 | 202 | ## Naming Tasks 203 | 204 | You can name tasks so they can be easily referenced later, such as through the CLI with the `named()` method: 205 | 206 | ```php 207 | $schedule->command('foo')->nightly()->named('foo-task'); 208 | ``` 209 | -------------------------------------------------------------------------------- /src/Task.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks; 15 | 16 | use CodeIgniter\Events\Events; 17 | use CodeIgniter\I18n\Time; 18 | use CodeIgniter\Queue\Payloads\PayloadMetadata; 19 | use CodeIgniter\Tasks\Exceptions\TasksException; 20 | use InvalidArgumentException; 21 | use ReflectionException; 22 | use ReflectionFunction; 23 | use SplFileObject; 24 | 25 | /** 26 | * Class Task 27 | * 28 | * Represents a single task that should be scheduled 29 | * and run periodically. 30 | * 31 | * @property mixed $action 32 | * @property array $environments 33 | * @property string $name 34 | * @property string $type 35 | * @property array $types 36 | */ 37 | class Task 38 | { 39 | use FrequenciesTrait; 40 | 41 | /** 42 | * Supported action types. 43 | * 44 | * @var list 45 | */ 46 | protected array $types = [ 47 | 'command', 48 | 'shell', 49 | 'closure', 50 | 'event', 51 | 'url', 52 | 'queue', 53 | ]; 54 | 55 | /** 56 | * The type of action. 57 | */ 58 | protected string $type; 59 | 60 | /** 61 | * If not empty, lists the allowed environments 62 | * this can run in. 63 | */ 64 | protected array $environments = []; 65 | 66 | /** 67 | * The alias this task can be run by 68 | */ 69 | protected string $name; 70 | 71 | /** 72 | * Whether to prevent concurrent executions of this task. 73 | */ 74 | protected bool $singleInstance = false; 75 | 76 | /** 77 | * Maximum lock duration in seconds for single instance tasks. 78 | */ 79 | protected ?int $singleInstanceTTL = null; 80 | 81 | /** 82 | * @param $action mixed The actual content that should be run. 83 | * 84 | * @throws TasksException 85 | */ 86 | public function __construct(string $type, protected mixed $action) 87 | { 88 | if (! in_array($type, $this->types, true)) { 89 | throw TasksException::forInvalidTaskType($type); 90 | } 91 | 92 | $this->type = $type; 93 | } 94 | 95 | /** 96 | * Set the name to reference this task by 97 | * 98 | * @return $this 99 | */ 100 | public function named(string $name) 101 | { 102 | $this->name = $name; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Returns the type. 109 | */ 110 | public function getType(): string 111 | { 112 | return $this->type; 113 | } 114 | 115 | /** 116 | * Returns the saved action. 117 | * 118 | * @return mixed 119 | */ 120 | public function getAction() 121 | { 122 | return $this->action; 123 | } 124 | 125 | /** 126 | * Runs this Task's action. 127 | * 128 | * @return mixed 129 | * 130 | * @throws TasksException 131 | */ 132 | public function run() 133 | { 134 | if ($this->singleInstance) { 135 | $lockKey = $this->getLockKey(); 136 | cache()->save($lockKey, [], $this->singleInstanceTTL ?? 0); 137 | } 138 | 139 | try { 140 | $method = 'run' . ucfirst($this->type); 141 | if (! method_exists($this, $method)) { 142 | throw TasksException::forInvalidTaskType($this->type); 143 | } 144 | 145 | return $this->{$method}(); 146 | } finally { 147 | if ($this->singleInstance && $this->getType() !== 'queue') { 148 | cache()->delete($lockKey); 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * Determines whether this task should be run now 155 | * according to its schedule and environment. 156 | */ 157 | public function shouldRun(?string $testTime = null): bool 158 | { 159 | $cron = service('cronExpression'); 160 | 161 | // Allow times to be set during testing 162 | if ($testTime !== null && $testTime !== '' && $testTime !== '0') { 163 | $cron->testTime($testTime); 164 | } 165 | 166 | // Are we restricting to environments? 167 | if ($this->environments !== [] && ! $this->runsInEnvironment($_SERVER['CI_ENVIRONMENT'])) { 168 | return false; 169 | } 170 | 171 | // If this is a single instance task and a lock exists, don't run 172 | if ($this->singleInstance && cache()->get($this->getLockKey()) !== null) { 173 | return false; 174 | } 175 | 176 | return $cron->shouldRun($this->getExpression()); 177 | } 178 | 179 | /** 180 | * Set this task to be a single instance 181 | * 182 | * @param int|null $lockTTL Time-to-live for the cache lock in seconds 183 | * 184 | * @return $this 185 | */ 186 | public function singleInstance(?int $lockTTL = null): static 187 | { 188 | $this->singleInstance = true; 189 | $this->singleInstanceTTL = $lockTTL; 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * Restricts this task to run within only 196 | * specified environments. 197 | * 198 | * @param mixed ...$environments 199 | * 200 | * @return $this 201 | */ 202 | public function environments(...$environments) 203 | { 204 | $this->environments = $environments; 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * Returns the date this was last ran. 211 | * 212 | * @return string|Time 213 | */ 214 | public function lastRun() 215 | { 216 | helper('setting'); 217 | if (setting('Tasks.logPerformance') === false) { 218 | return '--'; 219 | } 220 | 221 | // Get the logs 222 | $logs = setting("Tasks.log-{$this->name}"); 223 | 224 | if (empty($logs)) { 225 | return '--'; 226 | } 227 | 228 | $log = array_shift($logs); 229 | 230 | return Time::parse($log['start']); 231 | } 232 | 233 | /** 234 | * Checks if it runs within the specified environment. 235 | */ 236 | protected function runsInEnvironment(string $environment): bool 237 | { 238 | // If nothing is specified then it should run 239 | if ($this->environments === []) { 240 | return true; 241 | } 242 | 243 | return in_array($environment, $this->environments, true); 244 | } 245 | 246 | /** 247 | * Runs a framework Command. 248 | * 249 | * @return string Buffered output from the Command 250 | * 251 | * @throws InvalidArgumentException 252 | */ 253 | protected function runCommand(): string 254 | { 255 | return command($this->getAction()); 256 | } 257 | 258 | /** 259 | * Executes a shell script. 260 | * 261 | * @return array Lines of output from exec 262 | */ 263 | protected function runShell(): array 264 | { 265 | exec($this->getAction(), $output); 266 | 267 | return $output; 268 | } 269 | 270 | /** 271 | * Calls a Closure. 272 | * 273 | * @return mixed The result of the closure 274 | */ 275 | protected function runClosure() 276 | { 277 | return $this->getAction()->__invoke(); 278 | } 279 | 280 | /** 281 | * Triggers an Event. 282 | * 283 | * @return bool Result of the trigger 284 | */ 285 | protected function runEvent(): bool 286 | { 287 | return Events::trigger($this->getAction()); 288 | } 289 | 290 | /** 291 | * Queries a URL. 292 | * 293 | * @return mixed|string Body of the Response 294 | */ 295 | protected function runUrl() 296 | { 297 | $response = service('curlrequest')->request('GET', $this->getAction()); 298 | 299 | return $response->getBody(); 300 | } 301 | 302 | /** 303 | * Sends a job to the queue. 304 | * 305 | * @return bool Status of the queue push 306 | */ 307 | protected function runQueue() 308 | { 309 | $queueAction = $this->getAction(); 310 | 311 | if ($this->singleInstance) { 312 | // Create PayloadMetadata instance with the task lock key 313 | $queueAction[] = new PayloadMetadata([ 314 | 'queue' => $queueAction[0], 315 | 'taskLockTTL' => $this->singleInstanceTTL, 316 | 'taskLockKey' => $this->getLockKey(), 317 | ]); 318 | } 319 | 320 | return service('queue')->push(...$queueAction)->getStatus(); 321 | } 322 | 323 | /** 324 | * Builds a unique name for the task. 325 | * Used when an existing name doesn't exist. 326 | * 327 | * @return string 328 | * 329 | * @throws ReflectionException 330 | */ 331 | protected function buildName() 332 | { 333 | // Get a hash based on the action 334 | // Closures cannot be serialized so do it the hard way 335 | if ($this->getType() === 'closure') { 336 | $ref = new ReflectionFunction($this->getAction()); 337 | $file = new SplFileObject($ref->getFileName()); 338 | $file->seek($ref->getStartLine() - 1); 339 | $content = ''; 340 | 341 | while ($file->key() < $ref->getEndLine()) { 342 | $content .= $file->current(); 343 | $file->next(); 344 | } 345 | $actionString = json_encode([ 346 | $content, 347 | $ref->getStaticVariables(), 348 | ]); 349 | } else { 350 | $actionString = serialize($this->getAction()); 351 | } 352 | 353 | // Get a hash based on the expression 354 | $expHash = $this->getExpression(); 355 | 356 | return $this->getType() . '_' . md5($actionString . '_' . $expHash); 357 | } 358 | 359 | /** 360 | * Magic getter 361 | * 362 | * @return mixed 363 | * 364 | * @throws ReflectionException 365 | */ 366 | public function __get(string $key) 367 | { 368 | if ($key === 'name' && (! isset($this->name) || ($this->name === ''))) { 369 | return $this->buildName(); 370 | } 371 | 372 | if (property_exists($this, $key)) { 373 | return $this->{$key}; 374 | } 375 | } 376 | 377 | /** 378 | * Determine the lock key for the task. 379 | * 380 | * @throws ReflectionException 381 | */ 382 | private function getLockKey(): string 383 | { 384 | $name = $this->name ?? $this->buildName(); 385 | 386 | return sprintf('task_lock_%s', $name); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/FrequenciesTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\Tasks; 15 | 16 | /** 17 | * Trait FrequenciesTrait 18 | * 19 | * Provides the methods to assign frequencies to individual tasks. 20 | */ 21 | trait FrequenciesTrait 22 | { 23 | /** 24 | * The generated cron expression 25 | * 26 | * @var array 27 | */ 28 | protected array $expression = [ 29 | 'min' => '*', 30 | 'hour' => '*', 31 | 'dayOfMonth' => '*', 32 | 'month' => '*', 33 | 'dayOfWeek' => '*', 34 | ]; 35 | 36 | /** 37 | * If listed, will restrict this to running 38 | * within only those environments. 39 | */ 40 | protected $allowedEnvironments; 41 | 42 | /** 43 | * Schedules the task through a raw crontab expression string. 44 | * 45 | * @return $this 46 | */ 47 | public function cron(string $expression) 48 | { 49 | $this->expression = explode(' ', $expression); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Returns the generated expression. 56 | * 57 | * @return string 58 | */ 59 | public function getExpression() 60 | { 61 | return implode(' ', array_values($this->expression)); 62 | } 63 | 64 | /** 65 | * Runs daily at midnight, unless a time string is 66 | * passed in (like 4:08 pm) 67 | * 68 | * @return $this 69 | */ 70 | public function daily(?string $time = null) 71 | { 72 | $min = $hour = 0; 73 | 74 | if ($time !== null && $time !== '' && $time !== '0') { 75 | [$min, $hour] = $this->parseTime($time); 76 | } 77 | 78 | $this->expression['min'] = $min; 79 | $this->expression['hour'] = $hour; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Runs at a specific time of the day 86 | */ 87 | public function time(string $time) 88 | { 89 | [$min, $hour] = $this->parseTime($time); 90 | 91 | $this->expression['min'] = $min; 92 | $this->expression['hour'] = $hour; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Runs at the top of every hour or at a specific minute. 99 | * 100 | * @return $this 101 | */ 102 | public function hourly(?int $minute = null) 103 | { 104 | $this->expression['min'] = $minute ?? '00'; 105 | $this->expression['hour'] = '*'; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Runs at every hour or every x hours 112 | * 113 | * @param int|string|null $minute 114 | * 115 | * @return $this 116 | */ 117 | public function everyHour(int $hour = 1, $minute = null) 118 | { 119 | $this->expression['min'] = $minute ?? '0'; 120 | $this->expression['hour'] = ($hour === 1) ? '*' : '*/' . $hour; 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Runs in a specific range of hours 127 | * 128 | * @return $this 129 | */ 130 | public function betweenHours(int $fromHour, int $toHour) 131 | { 132 | $this->expression['hour'] = $fromHour . '-' . $toHour; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Runs on a specific chosen hours 139 | * 140 | * @param array|int $hours 141 | * 142 | * @return $this 143 | */ 144 | public function hours($hours = []) 145 | { 146 | if (! is_array($hours)) { 147 | $hours = [$hours]; 148 | } 149 | 150 | $this->expression['hour'] = implode(',', $hours); 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * Set the execution time to every minute or every x minutes. 157 | * 158 | * @param int|string|null $minute When set, specifies that the job will be run every $minute minutes 159 | * 160 | * @return $this 161 | */ 162 | public function everyMinute($minute = null) 163 | { 164 | $this->expression['min'] = null === $minute ? '*' : '*/' . $minute; 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Runs every 5 minutes 171 | * 172 | * @return $this 173 | */ 174 | public function everyFiveMinutes() 175 | { 176 | return $this->everyMinute(5); 177 | } 178 | 179 | /** 180 | * Runs every 15 minutes 181 | * 182 | * @return $this 183 | */ 184 | public function everyFifteenMinutes() 185 | { 186 | return $this->everyMinute(15); 187 | } 188 | 189 | /** 190 | * Runs every 30 minutes 191 | * 192 | * @return $this 193 | */ 194 | public function everyThirtyMinutes() 195 | { 196 | return $this->everyMinute(30); 197 | } 198 | 199 | /** 200 | * Runs in a specific range of minutes 201 | * 202 | * @return $this 203 | */ 204 | public function betweenMinutes(int $fromMinute, int $toMinute) 205 | { 206 | $this->expression['min'] = $fromMinute . '-' . $toMinute; 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Runs on a specific chosen minutes 213 | * 214 | * @param array|int $minutes 215 | * 216 | * @return $this 217 | */ 218 | public function minutes($minutes = []) 219 | { 220 | if (! is_array($minutes)) { 221 | $minutes = [$minutes]; 222 | } 223 | 224 | $this->expression['min'] = implode(',', $minutes); 225 | 226 | return $this; 227 | } 228 | 229 | /** 230 | * Runs on specific days 231 | * 232 | * @param array|int $days [0 : Sunday - 6 : Saturday] 233 | * 234 | * @return $this 235 | */ 236 | public function days($days) 237 | { 238 | if (! is_array($days)) { 239 | $days = [$days]; 240 | } 241 | 242 | $this->expression['dayOfWeek'] = implode(',', $days); 243 | 244 | return $this; 245 | } 246 | 247 | /** 248 | * Runs every Sunday at midnight, unless time passed in. 249 | * 250 | * @return $this 251 | */ 252 | public function sundays(?string $time = null) 253 | { 254 | return $this->setDayOfWeek(0, $time); 255 | } 256 | 257 | /** 258 | * Runs every Monday at midnight, unless time passed in. 259 | * 260 | * @return $this 261 | */ 262 | public function mondays(?string $time = null) 263 | { 264 | return $this->setDayOfWeek(1, $time); 265 | } 266 | 267 | /** 268 | * Runs every Tuesday at midnight, unless time passed in. 269 | * 270 | * @return $this 271 | */ 272 | public function tuesdays(?string $time = null) 273 | { 274 | return $this->setDayOfWeek(2, $time); 275 | } 276 | 277 | /** 278 | * Runs every Wednesday at midnight, unless time passed in. 279 | * 280 | * @return $this 281 | */ 282 | public function wednesdays(?string $time = null) 283 | { 284 | return $this->setDayOfWeek(3, $time); 285 | } 286 | 287 | /** 288 | * Runs every Thursday at midnight, unless time passed in. 289 | * 290 | * @return $this 291 | */ 292 | public function thursdays(?string $time = null) 293 | { 294 | return $this->setDayOfWeek(4, $time); 295 | } 296 | 297 | /** 298 | * Runs every Friday at midnight, unless time passed in. 299 | * 300 | * @return $this 301 | */ 302 | public function fridays(?string $time = null) 303 | { 304 | return $this->setDayOfWeek(5, $time); 305 | } 306 | 307 | /** 308 | * Runs every Saturday at midnight, unless time passed in. 309 | * 310 | * @return $this 311 | */ 312 | public function saturdays(?string $time = null) 313 | { 314 | return $this->setDayOfWeek(6, $time); 315 | } 316 | 317 | /** 318 | * Should run the first day of every month. 319 | * 320 | * @return $this 321 | */ 322 | public function monthly(?string $time = null) 323 | { 324 | $min = $hour = 0; 325 | 326 | if ($time !== null && $time !== '' && $time !== '0') { 327 | [$min, $hour] = $this->parseTime($time); 328 | } 329 | 330 | $this->expression['min'] = $min; 331 | $this->expression['hour'] = $hour; 332 | $this->expression['dayOfMonth'] = 1; 333 | 334 | return $this; 335 | } 336 | 337 | /** 338 | * Runs on specific days of the month 339 | * 340 | * @param array|int $days [1-31] 341 | * 342 | * @return $this 343 | */ 344 | public function daysOfMonth($days) 345 | { 346 | if (! is_array($days)) { 347 | $days = [$days]; 348 | } 349 | 350 | $this->expression['dayOfMonth'] = implode(',', $days); 351 | 352 | return $this; 353 | } 354 | 355 | /** 356 | * Set the execution time to every month or every x months. 357 | * 358 | * @param int|string|null $month When set, specifies that the job will be run every $month months 359 | * 360 | * @return $this 361 | */ 362 | public function everyMonth($month = null) 363 | { 364 | $this->expression['month'] = null === $month ? '*' : '*/' . $month; 365 | 366 | return $this; 367 | } 368 | 369 | /** 370 | * Runs on specific range of months 371 | * 372 | * @param int $from Month [1-12] 373 | * @param int $to Month [1-12] 374 | * 375 | * @return $this 376 | */ 377 | public function betweenMonths(int $from, int $to) 378 | { 379 | $this->expression['month'] = $from . '-' . $to; 380 | 381 | return $this; 382 | } 383 | 384 | /** 385 | * Runs on specific months 386 | * 387 | * @return $this 388 | */ 389 | public function months(array $months = []) 390 | { 391 | $this->expression['month'] = implode(',', $months); 392 | 393 | return $this; 394 | } 395 | 396 | /** 397 | * Should run the first day of each quarter, 398 | * i.e. Jan 1, Apr 1, July 1, Oct 1 399 | * 400 | * @return $this 401 | */ 402 | public function quarterly(?string $time = null) 403 | { 404 | $min = $hour = 0; 405 | 406 | if ($time !== null && $time !== '' && $time !== '0') { 407 | [$min, $hour] = $this->parseTime($time); 408 | } 409 | 410 | $this->expression['min'] = $min; 411 | $this->expression['hour'] = $hour; 412 | $this->expression['dayOfMonth'] = 1; 413 | 414 | $this->everyMonth(3); 415 | 416 | return $this; 417 | } 418 | 419 | /** 420 | * Should run the first day of the year. 421 | * 422 | * @return $this 423 | */ 424 | public function yearly(?string $time = null) 425 | { 426 | $min = $hour = 0; 427 | 428 | if ($time !== null && $time !== '' && $time !== '0') { 429 | [$min, $hour] = $this->parseTime($time); 430 | } 431 | 432 | $this->expression['min'] = $min; 433 | $this->expression['hour'] = $hour; 434 | $this->expression['dayOfMonth'] = 1; 435 | $this->expression['month'] = 1; 436 | 437 | return $this; 438 | } 439 | 440 | /** 441 | * Should run M-F. 442 | * 443 | * @return $this 444 | */ 445 | public function weekdays(?string $time = null) 446 | { 447 | $min = $hour = 0; 448 | 449 | if ($time !== null && $time !== '' && $time !== '0') { 450 | [$min, $hour] = $this->parseTime($time); 451 | } 452 | 453 | $this->expression['min'] = $min; 454 | $this->expression['hour'] = $hour; 455 | $this->expression['dayOfWeek'] = '1-5'; 456 | 457 | return $this; 458 | } 459 | 460 | /** 461 | * Should run Saturday and Sunday 462 | * 463 | * @return $this 464 | */ 465 | public function weekends(?string $time = null) 466 | { 467 | $min = $hour = 0; 468 | 469 | if ($time !== null && $time !== '' && $time !== '0') { 470 | [$min, $hour] = $this->parseTime($time); 471 | } 472 | 473 | $this->expression['min'] = $min; 474 | $this->expression['hour'] = $hour; 475 | $this->expression['dayOfWeek'] = '6-7'; 476 | 477 | return $this; 478 | } 479 | 480 | /** 481 | * Internal function used by the mondays(), etc. functions. 482 | * 483 | * @return $this 484 | */ 485 | protected function setDayOfWeek(int $day, ?string $time = null) 486 | { 487 | $min = $hour = '*'; 488 | 489 | if ($time !== null && $time !== '' && $time !== '0') { 490 | [$min, $hour] = $this->parseTime($time); 491 | } 492 | 493 | $this->expression['min'] = $min; 494 | $this->expression['hour'] = $hour; 495 | $this->expression['dayOfWeek'] = $day; 496 | 497 | return $this; 498 | } 499 | 500 | /** 501 | * Parses a time string (like 4:08 pm) into mins and hours 502 | */ 503 | protected function parseTime(string $time): array 504 | { 505 | $time = strtotime($time); 506 | 507 | return [ 508 | date('i', $time), // mins 509 | date('G', $time), 510 | ]; 511 | } 512 | } 513 | --------------------------------------------------------------------------------