PHP
Article
By Reza Lavaryan

Framework-Agnostic PHP Cronjobs Made Easy with Crunz!

By Reza Lavaryan

In this article, we’re going to learn about a relatively new job scheduling library named Crunz. Crunz is a framework-agnostic library inspired by Laravel’s Task Scheduler, but improved in many ways. Full disclosure: I’m the author and am welcoming contributions and comments on how to improve it further!

Clock photo

Before getting started, you should have a firm grasp of cronjobs, so please read our in-depth walkthrough if you’re unfamiliar with how they work.

Installation

To install it, we use Composer as usual:

composer require lavary/crunz

A command-line utility named crunz will be symlinked to vendor/bin of our project. This command-line utility provides a set of useful commands, which we’ll discuss shortly.

How Does it Work?

Instead of installing cron jobs in a crontab file, we define them in one or several PHP files, by using the Crunz interface.

Here’s a basic example:

<?php
// tasks/backupTasks.php

use Crunz\Schedule;

$schedule = new Schedule();
$schedule->run('cp project project-bk')       
         ->daily();

return $schedule;

To run the tasks, we install an ordinary cron job (a crontab entry) which runs every minute, and delegates the responsibility to Crunz’s event runner:

* * * * * /project/vendor/bin/crunz schedule:run

The command schedule:run is responsible for collecting all the PHP task files and runs the tasks which are due.

Task Files

Task files resemble crontab files. Just like crontab files, they can contain one or more tasks.

To create a task file, we need to decide where we want to keep them. The location doesn’t matter as long as we let Crunz know the location. Normally, we create our task files in tasks/ within the project’s root directory. However, we can keep them outside of the project’s directory if there’s a reason for that.

By default, Crunz assumes all the task files reside in the tasks/ directory within the project’s root directory.

There are two ways to specify the source directory: configuration file (more on this below) and as a parameter to the event runner command:

* * * * * /project/vendor/bin/crunz schedule:run /path/to/tasks/directory

Creating a Simple Task

Let’s create our first task.

<?php
// tasks/FirstTasks.php

use Crunz\Schedule;

$schedule = new Schedule();

$schedule->run('cp project project-bk')       
         ->daily()
         ->description('Create a backup of the project directory.');

// ...

// IMPORTANT: You must return the schedule object
return $schedule; 

In the preceding code, first we make an instance of the Schedule class. run() specifies the command to be executed. daily() runs the command on a daily basis, and description() adds a description to the task (descriptions are useful for identifying the tasks in the log files).

There are some conventions for creating a task file. The filename should end with Tasks.php unless we change this via the configuration settings. Additionally, we must return the instance of the Schedule class at the end of each file, otherwise, all the tasks inside the file will be skipped by the event runner.

Since Crunz scans the tasks directory recursively, we can either put all the tasks in one file or across different files (or directories) based on their usage. This behavior helps us have a well organized tasks directory.

The Command

We can run any command or PHP closure by using run(). This method accepts two arguments: the command or closure to be executed, and the command options (as an associative array) if the first argument is a command.

<?php
use Crunz\Schedule;

$schedule = new Schedule();

$schedule->run('/usr/bin/php backup.php', ['--destination' => 'path/to/destination'])       
         ->everyMinute()
         ->description('Copying the project directory');

return $schedule;

In the above example, --destination is an option supported by the backup.php script.

Closures

Other than commands, it is also possible to schedule a PHP closure, by passing it to the run() method.

<?php
use Crunz\Schedule;

$schedule = new Schedule();

$x = 'Some data';
$schedule->run(function() use ($x) { 
   echo 'Doing some cool stuff in here with ' . $x;
})       
->everyMinute()
->description('Cool stuff');

return $schedule;

If there’s a PHP error on any level, it will be caught, logged and reported.

Changing Directories

It is also possible to change the current working directory before running a task – by using the in() method:

<?php

// ...

$schedule->run('./deploy.sh')
         ->in('/home')
         ->daily();
// ...

return $schedule;

Frequency of Execution

There are a variety of ways to specify when and how often a task should run. We can combine these methods to get our desired frequencies.

Units of Time

Frequency methods usually end with ly, as in hourly(), daily(), weekly(), monthly(), quarterly(), and yearly() .

<?php
// ...
$schedule->run('/usr/bin/php backup.php')       
         ->daily();
// ...

The above task will run daily at midnight.

Here’s another one, which runs on the first day of each month.

<?php
// ...
$schedule->run('/usr/bin/php email.php')       
         ->monthly();
// ...

All the events scheduled with this set of methods happen at the beginning of that time unit. For example weekly() will run the event on Sundays, and monthly() will run it on the first day of each month.

Dynamic Methods

Dynamic methods give us a wide variety of frequency options on the fly. We just need to follow this pattern:

every[NumberInCamelCaseWords]Minute|Hour|Day|Months?

The method should start with the word every, followed by a number in camel-case words, ending with one of these units of time: minute, hour, day, and month.

The s at the end is optional and it’s just used for grammar’s sake.

That said, the following methods are valid:

  • everyFiveMinutes()
  • everyMinute()
  • everyTwelveHours()
  • everyMonth()
  • everySixMonths()
  • everyFifteenDays()
  • everyFiveHundredThirtySevenMinutes()
  • everyThreeThousandAndFiveHundredFiftyNineMinutes()

This is how we use it in a task file:

<?php
// ...

$schedule->run('/usr/bin/php email.php')       
         ->everyTenDays();

$schedule->run('/usr/bin/php some_other_stuff.php')       
         ->everyThirteenMinutes();
// ...

return $schedule;

Running Events at a Certain Time

To schedule one-off tasks, we can use the on() method like this:

<?php
// ...
$schedule->run('/usr/bin/php email.php')       
         ->on('13:30 2016-03-01');
// ...

The above the task will run on the first of March 2016 at 01:30 pm.

on() accepts any date format parsed by PHP’s strtotime function.

To specify only the time, we use at():

<?php
// ...
$schedule->run('/usr/bin/php script.php')       
         ->daily()
         ->at('13:30');
// ...

We can use dailyAt() to get the same result:

<?php
// ...
$schedule->run('/usr/bin/php script.php')       
         ->dailyAt('13:30');
// ...

If we only pass time to on(), it has the same effect as using at()

<?php
// ...
$schedule->run('/usr/bin/php email.php')       
         ->mondays()
         ->on('13:30');

// is the sames as
$schedule->run('/usr/bin/php email.php')       
         ->mondays()
         ->at('13:30');
// ...

Weekdays

Crunz also provides a set of methods which specify a certain day in the week. These methods have been designed to be used as a constraint and should not be used alone. The reason is that weekday methods just modify the Day of Week field of the cron job expression.

Consider the following example:

<?php
// Cron equivalent:  * * * * 1
$schedule->run('/usr/bin/php email.php')       
         ->mondays();

At first glance, the task seems to run every Monday, but since it only modifies the “day of week” field of the cron job expression, the task runs every minute on Mondays.

This is the correct way of using weekday methods:

<?php
// ...
$schedule->run('/usr/bin/php email.php')       
         ->everyThreeHours()
         ->mondays();
// ...

In the above task, we use mondays() as a constraint to run the task every three hours on Mondays.

Setting Individual Fields

Crunz’s methods are not limited to the ready-made methods explained. We can also set individual fields to compose our custom frequencies. These methods either accept an array of values, or list arguments separated by commas:

<?php
// ...
$schedule->run('/usr/bin/php email.php')       
         ->minute(['1-30', 45, 55])
         ->hour('1-5', 7, 8)
         ->dayOfMonth(12, 15)
         ->month(1);

Or:

<?php
// ...
$schedule->run('/usr/bin/php email.php')       
         ->minute('30')
         ->hour('13')
         ->month([1,2])
         ->dayofWeek('Mon', 'Fri', 'Sat');

// ...

The Classic Way

We can also do the scheduling the old way, just like we do in a crontab file:

<?php
$schedule->run('/usr/bin/php email.php')
         ->cron('30 12 * 5-6,9 Mon,Fri');     

Task Lifetime

In a crontab entry, we can not easily specify a task’s lifetime (the period during which the task is active). With Crunz, it’s possible:

<?php
//
$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->from('12:30 2016-03-04')
         ->to('04:55 2016-03-10');
 //       

Alternatively, we can use the between() method to get the same result:

<?php
//
$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->between('12:30 2016-03-04', '04:55 2016-03-10');

 //       

If we don’t specify the date portion, the task will be active every day but only within the specified duration:

<?php
//
$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->between('12:30', '04:55');

 //       

The above task runs every five minutes between 12:30 pm and 4:55 pm every day.

Running Conditions

Another thing that we cannot do very easily in a traditional crontab file is make conditions for running the tasks. Enter when() and skip() methods.

Consider the following code:

<?php
//
$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->between('12:30 2016-03-04', '04:55 2016-03-10')
         ->when(function() {
           if ($some_condition_here) { return true; }
         });

 //       

when() accepts a callback which must return TRUE for the task to run. This can be really useful when we need to check our resources before performing a resource-hungry task, for example. We can also skip a task under certain conditions by using the skip() method. If the passed callback returns TRUE, the task will be skipped.

<?php
//
$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->between('12:30 2016-03-04', '04:55 2016-03-10')
         ->skip(function() {
             if ($some_condition_here) { return true; }  
    });

 //       

We can use these methods multiple times for a single task. They are evaluated sequentially.

Configuration

There are a few configuration options provided in YAML format. It is highly recommended to have your own copy of the configuration file, instead modifying the original one.

To create a copy of the configuration file, first we need to publish the configuration file:

crunz publish:config
The configuration file was generated successfully

As a result, a copy of the configuration file will be created within our project’s root directory.

The configuration file looks like this:

# Crunz Configuration Settings

# This option defines where the task files and
# directories reside.
# The path is relative to the project's root directory,
# where the Crunz is installed (Trailing slashes will be ignored).
source: tasks

# The suffix is meant to target the task files inside the ":source" directory.
# Please note if you change this value, you need
# to make sure all the existing tasks files are renamed accordingly.
suffix: Tasks.php

# By default the errors are not logged by Crunz
# You may set the value to true for logging the errors
log_errors: false

# This is the absolute path to the errors' log file
# You need to make sure you have the required permission to write to this file though.
errors_log_file:

# By default the output is not logged as they are redirected to the
# null output.
# Set this to true if you want to keep the outputs
log_output: false

# This is the absolute path to the global output log file
# The events which have dedicated log files (defined with them), won't be
# logged to this file though.
output_log_file:

# This option determines whether the output should be emailed or not.
email_output: false

# This option determines whether the error messages should be emailed or not.
email_errors: false

# Global Swift Mailer settings
#
mailer:
    # Possible values: smtp, mail, and sendmail
    transport: smtp
    recipients:
    sender_name:
    sender_email:


# SMTP settings
#
smtp:
    host:
    port:
    username:
    password:
    encryption:

Each time we run Crunz commands, it will look into the project’s root directory to see if there’s a user-modified configuration file. If the configuration file doesn’t exist, it will use the default one.

Parallelism and the Locking Mechanism

Crunz runs the scheduled events in parallel (in separate processes), so all the events which have the same frequency of execution will run at the same time, asynchronously. To achieve this, Crunz utilizes symfony/Process for running the tasks in sub-processes.

If the execution of a task lasts until the next interval or even beyond that, we say that the same instances of a task are overlapping. In some cases, this is a not a problem, but they are times when these tasks are modifying the database data or files, which might cause unexpected behavior, or even data loss.

To prevent critical tasks from overlapping, Crunz provides a locking mechanism through preventOverlapping() which ensures no task runs if the previous instance is already running. Behind the scenes, Crunz creates a special file for each task, storing the process ID of the last running instance. Every time a task is about to run, it reads the process ID of the respective task, and checks if it’s still running. If it’s not, it’s time to run a new instance.

<?php
//
$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->preventOverlapping();
 //       

Keeping the Output

Cron jobs usually have output, which is normally emailed to the owner of the crontab file, or the user or users set by the MAILTO environment variable in the crontab file.

We can also redirect the standard output to a physical file using the >> operator:

* * * * * /command/to/run >> /var/log/crons/cron.log

This has been automated in Crunz. To automatically send each event’s output to a log file, we can set log_output and output_log_file in the configuration file like this:

# Configuration settings

## ...
log_output:      true
output_log_file: /var/log/crunz.log
## ...

This will send the event’s output (if executed successfully) to /var/log/crunz.log. However, we need to make sure we are permitted to write to the respective directory.

If we need to log the output on a per-event basis, we can use appendOutputTo() or sendOutputTo() methods like this:

<?php
//
$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->appendOutputTo('/var/log/crunz/emails.log');

 //       

Method appendOutputTo() appends the output to the specified file. To override the log file with new data after each run, we use saveOutputTo().

It is also possible to send the output as emails to a group of recipients by setting email_output and mailer settings in the configuration file.

Error Handling

Error handling is quite easy with Crunz. We can configure Crunz to log the error or notify us via email. Additionally, we can define several callbacks to be invoked in case of an error.

To log the possible errors during each run, we can use the log_error and error_log_file settings in the configuration file as below:

# Configuration settings

# ...
log_errors:      true
errors_log_file: /var/log/error.log
# ...

As a result, if the execution of an event is unsuccessful for some reason, the error message is appended to the specified error log file. Each entry provides useful information including the time it happened, the event’s description, the executed command which caused the error, and the error message itself.

Just like the event’s output, it is also possible to send the errors via email to a group of recipients – by setting email_error and mailer settings in the configuration file.

Error Callbacks

We can set as many callbacks as needed to run in case of an error. They will be invoked sequentially:

<?php

use Crunz\Schedule;
$schedule = new Schedule();

$schedule->add('command/to/run')
         ->everyFiveMinutes();

$schedule
->onError(function(){
   // Send mail
})
->onError(function(){
   // Do something else
});

return $schedule;

Pre-Process and Post-Process Hooks

There are times when we want to do some operations before and after an event. This is possible by attaching pre-process and post-process callbacks to the respective event or schedule object by using before() and after() methods, passing a callback function to them.

We can have pre-process and post-process callbacks on both event and schedule levels. The schedule-level pre-process callbacks will be invoked before any events (in the schedule object) have started. The schedule-level post-process callbacks will be invoked after all the events have been completed – whether successfully or with problems.

<?php
// ...

$schedule = new Schedule();

$schedule->run('/usr/bin/php email.php')
         ->everyFiveMinutes()
         ->before(function() { 
             // Do something before the task runs
         })
         ->before(function() { 
                 // Do something else
         })
         ->after(function() {
             // After the task is run
         });

$schedule

->before(function () {
   // Do something before all events
})
->after(function () {
   // Do something after all events are finished
}
->before(function () {
   // Do something before all events
});

//  ...   

We can use these methods as many times as we need by chaining them. Event-based post-execution callbacks are only invoked if the execution of the event was successful.

Other Useful Commands

We’ve already used a few crunz commands like schedule:run and publish:config.

To see all the valid options and arguments of crunz, we can run the following command:

vendor/bin/crunz --help

Listing Tasks

One of these commands is crunz schedule:list, which lists the defined tasks (in collected *.Tasks.php files) in a tabular format.

vendor/bin/crunz schedule:list

+---+---------------+-------------+--------------------+
| # | Task          | Expression  | Command to Run     |
+---+---------------+-------------+--------------------+
| 1 | Sample Task   | * * * * 1 * | command/to/execute |
+---+---------------+-------------+--------------------+

Generating Tasks

There’s also a useful command make:task, which generates a task file skeleton with all the defaults, so one doesn’t have to write them from scratch.

For example, to create a task which runs /var/www/script.php every hour on Mondays, we run the following command:

vendor/bin/crunz make:task exampleOne --run scripts.php --in /var/www --frequency everyHour --constraint mondays
Where do you want to save the file? (Press enter for the current directory)

As a result, the event is defined in exampleOneTasks.php within the specified tasks directory.

To see if the event has been created successfully, we can list the events:

crunz schedule:list

+---+------------------+-------------+----------------+
| # | Task             | Expression  | Command to Run |
+---+------------------+-------------+----------------+
| 1 | Task description | 0 * * * 1 * | scripts.php    |
+---+------------------+-------------+----------------+

To see all the options of make:task with all the defaults, we run it with help:

vendor/bin/crunz make:task --help

Using Crunz with a Web-based Interface

Creating a web-based interface for Crunz is quite easy. We’ll just need to put the events’ properties inside a database table. Then, we can fetch all the records through an API. Finally, we iterate over the fetched records, creating the event objects. Whether an event has been created dynamically or statically, Crunz will run it as long as it is located in a valid task file.

If you need to try something ready-made and expand it based on your requirements, you may use this web interface created with Laravel: lavary/crunz-ui. You can read the installation instructions in the package’s README.md file. In order to let users implement their own authentication mechanism, this web-based interface does not provide any authentication system out of the box.

Conclusion

By having our tasks in the code, we can bring them under version control, so other developers can add or modify items without having to access the server.

That way, we can control the tasks in any way we need, and to stop all of them permanently at once, we simply uninstall the master cron job.

If you have a questions or comments, please post them below and we’ll do our best to reply in a timely manner – and don’t hesitate to throw some suggestions and opinions about the package itself in there!

  • Josh Hartman

    Thanks for posting about this new utility. How does it determine whether a task has completed successfully? Also, does it have any ability to retry tasks dependent on output or error code? I’m looking for a task scheduler for enterprise customers.

    • Reza Lavaryan

      All the tasks are executed as separate processes. If a process exits with a non-zero code, it is considered as an unsuccessful task. As a result, the post-execution callbacks won’t be executed. Additionally, the error output is logged or sent by email (if configured) and the on-error callbacks will be executed.

      If you also need to be notified whether a task has been completed successfully, you can configure Crunz to log or email the output, or even ping a url.

  • Interesting! I stumbled upon the Laravel link when researching for existing libs to manage cron tasks in a programming way.

    Are you / is anyone aware of a GUI to administrate those cron tasks by a user? Use case: list of pending tasks, last execution time, possible logs, trigger the task manually…

    • Reza Lavaryan

      lavary/crunz-ui might be something you can start with. Although it’s currently for creating the tasks and running them with Crunz. It’s under active development now, but it’s gonna take a while to support the things you need.

      • Ok, great! Added this project in my watching list.

  • This looks interesting, I had my ghetto task runner in some projects (symfony2). But running it from cron every minute looks cringy :| . Maybe a deamon would be better.

Recommended
Sponsors
Get the latest in PHP, once a week, for free.