├── 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 | 
15 | 
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 |
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 |
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 | [](https://github.com/codeigniter4/tasks/actions/workflows/phpunit.yml)
6 | [](https://github.com/codeigniter4/tasks/actions/workflows/phpstan.yml)
7 | [](https://github.com/codeigniter4/tasks/actions/workflows/deptrac.yml)
8 | [](https://coveralls.io/github/codeigniter4/tasks?branch=develop)
9 |
10 | 
11 | 
12 | 
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 |
--------------------------------------------------------------------------------