skip to Main Content

I’ve been tasked with setting up a schedule for a command to run on the 2nd Tuesday of each month, at 10am. I haven’t been able to find any real way to test when the command will actually run.

This is what I have at the moment:

$schedule->command('foo')
            ->monthly()
            ->tuesdays()
            ->at('10:00')
            ->when(function () {
                return Carbon::now()->weekOfMonth == 2;
            })
            ->withoutOverlapping()
            ->appendOutputTo(storage_path($logPath));

Am I correct in thinking that the ‘when’ closure is evaluated only when the command is actually scheduled to run? Do I even need the ‘monthly’ condition? And most importantly: will this only run on the 2nd Tuesday of each month, at 10am?

Also, is there a way I could test this schedule without running the command (e.g. by passing in a date and getting a true/false if it’ll run)?

Thanks

2

Answers


  1. The scheduler helper methods just build a cron expression, so we can look at the expression from your example to see if it does what you expect. Running:

    (new IlluminateConsoleSchedulingSchedule())->command('')->monthly()->tuesdays()->at('10:00')->expression
    

    return "0 10 1 * 2" which means run at 10am on the 1st of every month and and Tuesdays. (You can check that, and other expressions, here).

    Your when filter will ensure it does not get run on the 1st of the month, but there is no need for the monthly() call as without it you get the expression "0 10 * * 2" which means at 10am on Tuesdays.

    As for testing if it works you can mock the current time for Carbon. The following code will check if the command will run at the specified time:

    $schedule = new IlluminateConsoleSchedulingSchedule();
    $event = $schedule->command('foo')
                ->tuesdays()
                ->at('10:00')
                ->when(function () {
                    return Carbon::now()->weekOfMonth == 2;
                })
                ->withoutOverlapping()
                ->appendOutputTo(storage_path($logPath));
    
    Carbon::setTestNow('2023-01-10 10:00:00');
    
    $event->isDue(app()) && $event->filtersPass(app()); // Returns true
    
    Carbon::setTestNow('2023-01-17 10:00:00');
    
    $event->isDue(app()) && $event->filtersPass(app()); // Returns false
    

    If you wanted to run this in a unit test you can get the schedules defined in the kernel with resolve(IlluminateConsoleSchedulingSchedule::class)->events(); you can then find the event in question and call the isDue and filtersPass method to see if it would run at the specified time. Laravel also offers some helper methods in the base TestCase class for mocking out the time: https://laravel.com/docs/9.x/mocking#interacting-with-time

    Login or Signup to reply.
  2. From laravel documentation:

    If you would like to view an overview of your scheduled tasks and the
    next time they are scheduled to run, you may use the schedule:list
    Artisan command

    php artisan schedule:list
    

    If I do it with the code you provided this is what I get

    +------------------------------------+------------+-------------+----------------------------+
    | Command                            | Interval   | Description | Next Due                   |
    +------------------------------------+------------+-------------+----------------------------+
    | '/usr/local/bin/php' 'artisan' foo | 0 10 1 * 2 |             | 2023-01-17 10:00:00 +00:00 |
    +------------------------------------+------------+-------------+----------------------------+
    

    And if we take the listed interval and put it into a tool like cronhub’s crontab expression generator, it will tell you it will run At 10:00 AM, on day 1 of the month, and on Tuesday. This means that it will only run every 1st of the month, but only if it falls on Tuesday. Although in Next due column it shows it will run next Tuesday 2023-01-17 10:00:00 +00:00 I think this is an error in Laravel.

    So, what you actually want to do is remove the ->monthly() interval so that the command runs every Tuesday, then a callback function you provided with when() is executed and if it returns true it will proceed, meaning your command will only run every second Tuesday in a month.

    Or, you could acomplish the same thing by using the cron expressions directly, like this:

    $schedule->command('foo')
        ->cron('0 10 * * TUE#2')
        ->withoutOverlapping();
        ->appendOutputTo(storage_path($logPath));
    

    If I run php artisan schedule:list today, it will correctly list the next due date to be February 14th 2023, which is second Tuesday is that month

    +------------------------------------+----------------+-------------+----------------------------+
    | Command                            | Interval       | Description | Next Due                   |
    +------------------------------------+----------------+-------------+----------------------------+
    | '/usr/local/bin/php' 'artisan' foo | 0 10 * * TUE#2 |             | 2023-02-14 10:00:00 +00:00 |
    +------------------------------------+----------------+-------------+----------------------------+
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search