PHP cron job scheduler

Overview

PHP Cron Scheduler

Latest Stable Version License Build Status Coverage Status StyleCI Total Downloads

This is a framework agnostic cron jobs scheduler that can be easily integrated with your project or run as a standalone command scheduler. The idea was originally inspired by the Laravel Task Scheduling.

Installing via Composer

The recommended way is to install the php-cron-scheduler is through Composer. Please refer to Getting Started on how to download and install Composer.

After you have downloaded/installed Composer, run

php composer.phar require peppeocchi/php-cron-scheduler

or add the package to your composer.json

{
    "require": {
        "peppeocchi/php-cron-scheduler": "3.*"
    }
}

Scheduler V3 requires php >= 7.1, please use the v2 branch for php versions < 7.1.

How it works

Create a scheduler.php file in the root your project with the following content.

<?php require_once __DIR__.'/vendor/autoload.php';

use GO\Scheduler;

// Create a new scheduler
$scheduler = new Scheduler();

// ... configure the scheduled jobs (see below) ...

// Let the scheduler execute jobs which are due.
$scheduler->run();

Then add a new entry to your crontab to run scheduler.php every minute.

* * * * * path/to/phpbin path/to/scheduler.php 1>> /dev/null 2>&1

That's it! Your scheduler is up and running, now you can add your jobs without worring anymore about the crontab.

Scheduling jobs

By default all your jobs will try to run in background. PHP scripts and raw commands will run in background by default, while functions will always run in foreground. You can force a command to run in foreground by calling the inForeground() method. Jobs that have to send the output to email, will run foreground.

Schedule a php script

$scheduler->php('path/to/my/script.php');

The php method accepts 4 arguments:

  • The path to your php script
  • The PHP binary to use
  • Arguments to be passed to the script (NOTE: You need to have register_argc_argv enable in your php.ini for this to work (ref). Don't worry it's enabled by default, so unlessy you've intentionally disabled it or your host has it disabled by default, you can ignore it.)
  • Identifier
$scheduler->php(
    'path/to/my/script.php', // The script to execute
    'path/to/my/custom/bin/php', // The PHP bin
    [
        '-c' => 'ignore',
        '--merge' => null,
    ],
    'myCustomIdentifier'
);

Schedule a raw command

$scheduler->raw('ps aux | grep httpd');

The raw method accepts 3 arguments:

  • Your command
  • Arguments to be passed to the command
  • Identifier
$scheduler->raw(
    'mycommand | myOtherCommand',
    [
        '-v' => '6',
        '--silent' => null,
    ],
    'myCustomIdentifier'
);

Schedule a function

$scheduler->call(function () {
    return true;
});

The call method accepts 3 arguments:

  • Your function
  • Arguments to be passed to the function
  • Identifier
$scheduler->call(
    function ($args) {
        return $args['user'];
    },
    [
        ['user' => $user],
    ],
    'myCustomIdentifier'
);

All of the arguments you pass in the array will be injected to your function. For example

$scheduler->call(
    function ($firstName, $lastName) {
        return implode(' ', [$firstName, $lastName]);
    },
    [
        'John',
        'last_name' => 'Doe', // The keys are being ignored
    ],
    'myCustomIdentifier'
);

If you want to pass a key => value pair, please pass an array within the arguments array

$scheduler->call(
    function ($user, $role) {
        return implode(' ', [$user['first_name'], $user['last_name']]) . " has role: '{$role}'";
    },
    [
        [
            'first_name' => 'John',
            'last_name' => 'Doe',
        ],
        'Admin'
    ],
    'myCustomIdentifier'
);

Schedules execution time

There are a few methods to help you set the execution time of your schedules. If you don't call any of this method, the job will run every minute (* * * * *).

  • at - This method accepts any expression supported by dragonmantank/cron-expression
    $scheduler->php('script.php')->at('* * * * *');
  • everyMinute - Run every minute. You can optionally pass a $minute to specify the job runs every $minute minutes.
    $scheduler->php('script.php')->everyMinute();
    $scheduler->php('script.php')->everyMinute(5);
  • hourly - Run once per hour. You can optionally pass the $minute you want to run, by default it will run every hour at minute '00'.
    $scheduler->php('script.php')->hourly();
    $scheduler->php('script.php')->hourly(53);
  • daily - Run once per day. You can optionally pass $hour and $minute to have more granular control (or a string hour:minute)
    $scheduler->php('script.php')->daily();
    $scheduler->php('script.php')->daily(22, 03);
    $scheduler->php('script.php')->daily('22:03');

There are additional helpers for weekdays (all accepting optionals hour and minute - defaulted at 00:00)

  • sunday
  • monday
  • tuesday
  • wednesday
  • thursday
  • friday
  • saturday
$scheduler->php('script.php')->saturday();
$scheduler->php('script.php')->friday(18);
$scheduler->php('script.php')->sunday(12, 30);

And additional helpers for months (all accepting optionals day, hour and minute - defaulted to the 1st of the month at 00:00)

  • january
  • february
  • march
  • april
  • may
  • june
  • july
  • august
  • september
  • october
  • november
  • december
$scheduler->php('script.php')->january();
$scheduler->php('script.php')->december(25);
$scheduler->php('script.php')->august(15, 20, 30);

You can also specify a date for when the job should run. The date can be specified as string or as instance of DateTime. In both cases you can specify the date only (e.g. 2018-01-01) or the time as well (e.g. 2018-01-01 10:30), if you don't specify the time it will run at 00:00 on that date. If you're providing a date in a "non standard" format, it is strongly adviced to pass an instance of DateTime. If you're using createFromFormat without specifying a time, and you want to default it to 00:00, just make sure to add a ! to the date format, otherwise the time would be the current time. Read more

$scheduler->php('script.php')->date('2018-01-01 12:20');
$scheduler->php('script.php')->date(new DateTime('2018-01-01'));
$scheduler->php('script.php')->date(DateTime::createFromFormat('!d/m Y', '01/01 2018'));

Send output to file/s

You can define one or multiple files where you want the output of your script/command/function execution to be sent to.

$scheduler->php('script.php')->output([
    'my_file1.log', 'my_file2.log'
]);

// The scheduler catches both stdout and function return and send
// those values to the output file
$scheduler->call(function () {
    echo "Hello";

    return " world!";
})->output('my_file.log');

Send output to email/s

You can define one or multiple email addresses where you want the output of your script/command/function execution to be sent to. In order for the email to be sent, the output of the job needs to be sent first to a file. In fact, the files will be attached to your email address. In order for this to work, you need to install swiftmailer/swiftmailer

$scheduler->php('script.php')->output([
    // If you specify multiple files, both will be attached to the email
    'my_file1.log', 'my_file2.log'
])->email([
    '[email protected]' => 'My custom name',
    '[email protected]'
]);

You can optionally customize the Swift_Mailer instance with a custom Swift_Transport. You can configure:

  • subject - The subject of the email sent
  • from - The email address set as sender
  • body - The body of the email
  • transport - The transport to use. For example if you want to use your gmail account or any other SMTP account. The value should be an instance of Swift_Tranport
  • ignore_empty_output - If this is set to true, jobs that return no output won't fire any email.

The configuration can be set "globally" for all the scheduler commands, when creating the scheduler.

$scheduler = new Scheduler([
    'email' => [
        'subject' => 'Visitors count',
        'from' => '[email protected]',
        'body' => 'This is the daily visitors count',
        'transport' => Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, 'ssl')
            ->setUsername('username')
            ->setPassword('password'),
        'ignore_empty_output' => false,
    ]
]);

Or can be set on a job per job basis.

$scheduler = new Scheduler();

$scheduler->php('myscript.php')->configure([
    'email' => [
        'subject' => 'Visitors count',
    ]
]);

$scheduler->php('my_other_script.php')->configure([
    'email' => [
        'subject' => 'Page views count',
    ]
]);

Schedule conditional execution

Sometimes you might want to execute a schedule not only when the execution is due, but also depending on some other condition.

You can delegate the execution of a cronjob to a truthful test with the method when.

$scheduler->php('script.php')->when(function () {
    // The job will run (if due) only when
    // this function returns true
    return true;
});

Schedules execution order

The jobs that are due to run are being ordered by their execution: jobs that can run in background will be executed first.

Schedules overlapping

To prevent the execution of a schedule while the previous execution is still in progress, use the method onlyOne. To avoid overlapping, the Scheduler needs to create lock files. By default it will be used the directory path used for temporary files.

You can specify a custom directory path globally, when creating a new Scheduler instance.

$scheduler = new Scheduler([
    'tempDir' => 'path/to/my/tmp/dir'
]);

$scheduler->php('script.php')->onlyOne();

Or you can define the directory path on a job per job basis.

$scheduler = new Scheduler();

// This will use the default directory path
$scheduler->php('script.php')->onlyOne();

$scheduler->php('script.php')->onlyOne('path/to/my/tmp/dir');
$scheduler->php('other_script.php')->onlyOne('path/to/my/other/tmp/dir');

In some cases you might want to run the job also if it's overlapping. For example if the last execution was more that 5 minutes ago. You can pass a function as a second parameter, the last execution time will be injected. The job will not run until this function returns false. If it returns true, the job will run if overlapping.

$scheduler->php('script.php')->onlyOne(null, function ($lastExecutionTime) {
    return (time() - $lastExecutionTime) > (60 * 5);
});

Before job execution

In some cases you might want to run some code, if the job is due to run, before it's being executed. For example you might want to add a log entry, ping a url or anything else. To do so, you can call the before like the example below.

// $logger here is your own implementation
$scheduler->php('script.php')->before(function () use ($logger) {
    $logger->info("script.php started at " . time());
});

After job execution

Sometime you might wish to do something after a job runs. The then methods provides you the flexibility to do anything you want after the job execution. The output of the job will be injected to this function. For example you might want to add an entry to you logs, ping a url etc... By default, the job will be forced to run in foreground (because the output is injected to the function), if you don't need the output, you can pass true as a second parameter to allow the execution in background (in this case $output will be empty).

// $logger and $messenger here are your own implementation
$scheduler->php('script.php')->then(function ($output) use ($logger, $messenger) {
    $logger->info($output);

    $messenger->ping('myurl.com', $output);
});

$scheduler->php('script.php')->then(function ($output) use ($logger) {
    $logger->info('Job executed!');
}, true);

Using "before" and "then" together

// $logger here is your own implementation
$scheduler->php('script.php')
    ->before(function () use ($logger) {
        $logger->info("script.php started at " . time());
    })
    ->then(function ($output) use ($logger) {
        $logger->info("script.php completed at " . time(), [
            'output' => $output,
        ]);
    });

Multiple scheduler runs

In some cases you might need to run the scheduler multiple times in the same script. Although this is not a common case, the following methods will allow you to re-use the same instance of the scheduler.

# some code
$scheduler->run();
# ...

// Reset the scheduler after a previous run
$scheduler->resetRun()
          ->run(); // now we can run it again

Another handy method if you are re-using the same instance of the scheduler with different jobs (e.g. job coming from an external source - db, file ...) on every run, is to clear the current scheduled jobs.

$scheduler->clearJobs();

$jobsFromDb = $db->query(/*...*/);
foreach ($jobsFromDb as $job) {
    $scheduler->php($job->script)->at($job->schedule);
}

$scheduler->resetRun()
          ->run();

Faking scheduler run time

When running the scheduler you might pass an DateTime to fake the scheduler run time. The resons for this feature are described here;

// ...
$fakeRunTime = new DateTime('2017-09-13 00:00:00');
$scheduler->run($fakeRunTime);

Job failures

If some job fails, you can access list of failed jobs and reasons for failures.

// get all failed jobs and select first
$failedJob = $scheduler->getFailedJobs()[0];

// exception that occurred during job
$exception = $failedJob->getException();

// job that failed
$job = $failedJob->getJob();

Worker

You can simulate a cronjob by starting a worker. Let's see a simple example

$scheduler = new Scheduler();
$scheduler->php('some/script.php');
$scheduler->work();

The above code starts a worker that will run your job/s every minute. This is meant to be a testing/debugging tool, but you're free to use it however you like. You can optionally pass an array of "seconds" of when you want the worker to run your jobs, for example by passing [0, 30], the worker will run your jobs at second 0 and at second 30 of the minute.

$scheduler->work([0, 10, 25, 50, 55]);

It is highly advisable that you run your worker separately from your scheduler, although you can run the worker within your scheduler. The problem comes when your scheduler has one or more synchronous job, and the worker will have to wait for your job to complete before continuing the loop. For example

$scheduler->call(function () {
    sleep(120);
});
$scheduler->work();

The above will skip more than one execution, so it won't run anymore every minute but it will run probably every 2 or 3 minutes. Instead the preferred approach would be to separate the worker from your scheduler.

// File scheduler.php
$scheduler = new Scheduler();
$scheduler->call(function () {
    sleep(120);
});
$scheduler->run();
// File worker.php
$scheduler = new Scheduler();
$scheduler->php('scheduler.php');
$scheduler->work();

Then in your command line run php worker.php. This will start a foreground process that you can kill by simply exiting the command.

The worker is not meant to collect any data about your runs, and as already said it is meant to be a testing/debugging tool.

License

The MIT License (MIT)

Comments
  • Add before run method

    Add before run method

    Hi, I'v added before method and use Job instance as an input parameter of the callable function.

    Hope it fits your needs! :)

    https://github.com/peppeocchi/php-cron-scheduler/issues/32

    opened by emptyhand 11
  •  Allow run() to be executed multiple times

    Allow run() to be executed multiple times

    The current code expects the run() to be called only once. This is fine if you start the scheduler from cron each minute and re-initialize it each time. If you want to manually run the scheduler, or maybe have a lot of jobs you do not want to init each minute, it would be useful to call run() multiple times in the lifetime of the scheduler.

    In this case the collected data of the last run should be reset. the executedJobs, failedJobs and outputSchedule.

    If the scheduler is used to do multiple runs then it would be useful to reset all queued Jobs. Currently the only way to do this is by re-creating the scheduler object. But if the object is injected in the code then this is not practical.

    opened by merijnvdk 10
  • Schedule not running

    Schedule not running

    I am not running the scheduler from cron job.

    I start it manually by running this command

    php index.php

    This is my index.php file :

    require_once '/var/www/html/cron/vendor/autoload.php';
    use GO\Scheduler;
    
    $myfile = fopen("/var/www/html/cron/newfile".date('Y-m-d H:i:s').".txt", "w") or die("Unable to open file!");
    $txt = "John Doe\n";
    fwrite($myfile, $txt);
    $txt = "Jane Doe\n";
    fwrite($myfile, $txt);
    fclose($myfile);
    
    $scheduler = new Scheduler();
    $scheduler->php('/var/www/html/cron/index.php')->everyMinute();
    
    

    I want to run this file again but it is not running not even any error in console. nothing,

    When i run this manually by php index.php command newFIle.text is created successfully. It should be created every minute bcz of this line $scheduler->php('/var/www/html/cron/index.php')->everyMinute(); but it is not.

    Kindly let me know if i am missing something here

    opened by AneebImamdin 7
  • Updating dependency from mtdowling/cron-expression to dragonmantank/cron-expression

    Updating dependency from mtdowling/cron-expression to dragonmantank/cron-expression

    Hello,

    Thanks you for all this work. It's awesome.

    Are you aware that mtdowling/cron-expression is falling ? According to http://ctankersley.com/2017/10/12/cron-expression-update/ , it seems there is a complete rewrite.

    The prototype of CronExpression::factory seems to remain the same in the new project (dragonmantank/cron-expression)

    OLD : https://github.com/mtdowling/cron-expression/blob/master/src/Cron/CronExpression.php NEW : https://github.com/dragonmantank/cron-expression/blob/master/src/Cron/CronExpression.php

    I guess there is no problem to update your composer.json to replace "mtdowling/cron-expression": "~1.0" by "dragonmantank/cron-expression/": "~2.0"

    Best regards

    Vincent

    opened by touchweb-vincent 7
  • Implement

    Implement "before" method

    The scheduler currently supports a then method, a callback executed after the job runs.

    It seems a good idea to have a before method that's being executed before the job runs.

    Hacktoberfest 
    opened by peppeocchi 7
  • ->then is called immediately when a job is run and not after the job ends

    ->then is called immediately when a job is run and not after the job ends

    I have a job that takes 10 secs before it finishes. When I call ->then(function ($output) that function is called immediately and not after the job has finished 10 secs later.

    The first thing I do in the ->then(function ($output) function is write the time and it's always something like 2017-10-03 07:14:00, not 2017-10-03 07:14:10

    Also, my job produces stdout but $output is always null.

    opened by adamwinn 6
  • Added php 8.0 support

    Added php 8.0 support

    Closes #116

    1. Add support of PHP 8.0 in composer.json
    2. Upgrade phpunit from 5.7 to 9.5 (because 5.7 doesn't support php 8.0)
    3. Remove support of php 7.1 and 7.2 unsupported by new version of phpunit
    4. Migrate phpunit config to new configuration format
    5. Upgrade exception expectation system to new phpunit 9 definition
    6. Replace deprecated satooshi/php-coveralls by suggested replacement.
    opened by PedroTroller 5
  • Storing exception on failed job

    Storing exception on failed job

    Hi,

    thanks for this library. It would be nice to store exception on failed jobs for debugging. Now you can only access exception message via outputSchedule, which is not enough to debug complex jobs.

    Should I send PR?

    opened by jfilla 5
  • execution in seconds

    execution in seconds

    Would it be possible to circumvent this scheduling method where it allows only at least every minute and is able to adjust to run for example every 10 seconds?

    opened by gamadoleo 5
  • Invalid script path - mark it as failed job

    Invalid script path - mark it as failed job

    Currently when passing an invalid path for a job to the scheduler (php only), it is throwing an InvalidArgumentException straight away.

    It would be a nice to have to instead of throwing the exception (and preventing other valid jobs to run) it will mark it as failed job and it will keep running the other jobs. This is something already covered when scheduling Closures, so it should be handled only when scheduling php scripts.

    Hacktoberfest 
    opened by peppeocchi 5
  • How does it know when was the last time a job ran?

    How does it know when was the last time a job ran?

    Here is my schedule.php content:

    require_once __DIR__ . '/vendor/autoload.php';
    use GO\Scheduler;
    
    $scheduler = new Scheduler();
    $scheduler->call(function () {
        echo "Hello";
    
        return " world!";
    })->everyMinute()->output('/tmp/my_file.log');
    
    $scheduler->run();
    

    I executed $ php schedule.php. As expected, the file /tmp/my_file.log was created and had Hello World! in it.

    Immediately after that I emptied out /tmp/my_file.log and ran $ php schedule.php again. Since one minute has not passed, the /tmp/my_file.log should NOT get filled with Hello World! but it did. Why did it?

    opened by drupalista-br 5
  • Fixing Scheduler on Windows w/ Spaces in PHP_BINARY Path

    Fixing Scheduler on Windows w/ Spaces in PHP_BINARY Path

    Adding some code to catch if running on Windows and the PHP_BINARY path includes spaces. Adds double quotes around the value of $bin so it can run without issues.

    Tested as working on Windows 10 with PHP 7.4 and 8.0.

    I believe this should also address #129.

    opened by AndyM84 0
  • This library does not work for windows

    This library does not work for windows

    Please the developer of this library should state it on the documentation page that this library doesn't work on windows. I wasted over 5 hours trying debugging what is not lost before I knew this...

    opened by mitmelon 2
  • no way to set timezone for each job

    no way to set timezone for each job

    I have checked throughout the code there is no way to define the time zone of each job. You can easily find that functionality in the Laravel task scheduler. It's quite important to shoot jobs on time precisely.

    enhancement 
    opened by hemant-mudgil 0
  • Task start and end time

    Task start and end time

    It is possible in the method $scheduler->getExecutedJobs returns the time and start the task was started and ended and the amount of processing it consumed.

    because I have a program with more than 200 scheduled tasks that would help to build a graph with heavier tasks and with long execution times so that they can be analyzed

    opened by elvispdosreis 0
  • Minute Error

    Minute Error

    Hello,

    This library is great! but I have a problem.

    this my code;

                    $scheduler->php(__DIR__ . "/" . $taskFile)
                    ->onlyOne()
                    ->output([
                        'mylog.log'
                    ])
                    ->at("*/$taskTime * * * *");
    

    I set the time as 10080 minutes or 569 minutes but the system is doing the task every hour. Why? what should i do?

    opened by suaterkilic 1
Releases(v4.0)
Owner
Giuseppe Occhipinti
Giuseppe Occhipinti
Laravel Cron Scheduling - The ability to run the Laravel task scheduler using different crons

Laravel Cron Scheduling Laravel Task Scheduling is a great way to manage the cron. But the documentation contains the following warning: By default, m

Sergey Zhidkov 4 Sep 9, 2022
A PHP-based job scheduler

Crunz Install a cron job once and for all, manage the rest from the code. Crunz is a framework-agnostic package to schedule periodic tasks (cron jobs)

null 58 Dec 26, 2022
Task Scheduling with Cron Job in Laravel

About Laravel Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experie

Shariful Islam 1 Oct 16, 2021
Cron Job Manager for Magento 2

EthanYehuda_CronJobManager A Cron Job Management and Scheduling tool for Magento 2 Control Your Cron Installation In your Magento2 root directory, you

Ethan Yehuda 265 Nov 24, 2022
:date: Easy!Appointments - Open Source Appointment Scheduler

Easy!Appointments A powerful Open Source Appointment Scheduler that can be installed on your server. About • Features • Setup • Installation • License

Alex Tselegidis 2.5k Jan 8, 2023
Flow Framework Task Scheduler

This package provides a simple to use task scheduler for Neos Flow. Tasks are configured via settings, recurring tasks can be configured using cron syntax. Detailed options configure the first and last executions as well as options for the class handling the task.

Flowpack 11 Dec 21, 2022
Cronlike scheduler running inside a ReactPHP Event Loop

Cronlike scheduler running inside a ReactPHP Event Loop Install To install via Composer, use the command below, it will automatically detect the lates

Cees-Jan Kiewiet 35 Jul 12, 2022
Crunz is a framework-agnostic package to schedule periodic tasks (cron jobs) in PHP using a fluent API.

Crunz Install a cron job once and for all, manage the rest from the code. Crunz is a framework-agnostic package to schedule periodic tasks (cron jobs)

Reza Lavarian 1.4k Dec 26, 2022
Manage all your cron jobs without modifying crontab. Handles locking, logging, error emails, and more.

Jobby, a PHP cron job manager Install the master jobby cron job, and it will manage all your offline tasks. Add jobs without modifying crontab. Jobby

null 1k Dec 25, 2022
Cron expression generator built on php8

The most powerful and extendable tool for Cron expression generation Cron expression generator is a beautiful tool for PHP applications. Of course, th

Pavel Buchnev 46 Nov 27, 2022
Schedule and unschedule eloquent models elegantly without cron jobs

Laravel Schedulable Schedule and Unschedule any eloquent model elegantly without cron job. Salient Features: Turn any Eloquent Model into a schedulabl

Neelkanth Kaushik 103 Dec 7, 2022
A better way to create complex batch job queues in Laravel.

Relay A better way to create complex batch job queues in Laravel. Installation composer require agatanga/relay Usage Example Let's say you have the fo

Agatanga 10 Dec 22, 2022
Modern task runner for PHP

RoboTask Modern and simple PHP task runner inspired by Gulp and Rake aimed to automate common tasks: writing cross-platform scripts processing assets

Consolidation 2.6k Jan 3, 2023
Pure PHP task runner

task/task Got a PHP project? Heard of Grunt and Gulp but don't use NodeJS? Task is a pure PHP task runner. Leverage PHP as a scripting language, and a

null 184 Sep 28, 2022
Modern task runner for PHP

RoboTask Modern and simple PHP task runner inspired by Gulp and Rake aimed to automate common tasks: writing cross-platform scripts processing assets

Consolidation 2.6k Dec 28, 2022
PHP port of resque (Workers and Queueing)

php-resque: PHP Resque Worker (and Enqueue) Resque is a Redis-backed library for creating background jobs, placing those jobs on one or more queues, a

Chris Boulton 3.5k Jan 5, 2023
Elegant SSH tasks for PHP.

Laravel Envoy Introduction Laravel Envoy provides a clean, minimal syntax for defining common tasks you run on your remote servers. Using Blade style

The Laravel Framework 1.5k Jan 1, 2023
🐺 Asynchronous Task Queue Based on Distributed Message Passing for PHP.

?? Asynchronous Task Queue Based on Distributed Message Passing for PHP.

Ahmed 36 Aug 11, 2022
A versatile and lightweight PHP task runner, designed with simplicity in mind.

Blend A versatile and lightweight PHP task runner, designed with simplicity in mind. Table of Contents About Blend Installation Config Examples API Ch

Marwan Al-Soltany 42 Sep 29, 2022