Framework-Agnostic PHP Cronjobs Made Easy with Crunz!

Share this article

Framework-Agnostic PHP Cronjobs Made Easy with Crunz!

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!

Frequently Asked Questions (FAQs) about Crunz and PHP Cron Jobs

What is Crunz and why is it important for PHP cron jobs?

Crunz is a framework-agnostic PHP library that simplifies the process of managing and scheduling cron jobs. It’s important because it provides a more intuitive and flexible way to handle cron jobs compared to traditional methods. With Crunz, you can write your tasks in pure PHP code, which makes it easier to maintain and debug. It also supports a wide range of scheduling options, allowing you to run tasks at any time interval you need.

How do I install Crunz?

Crunz can be easily installed using Composer, a tool for dependency management in PHP. You can install it by running the command composer require lavary/crunz. After installation, you can use the Crunz command-line tool to manage your tasks.

How do I create a task with Crunz?

To create a task with Crunz, you need to create a PHP file in the tasks directory. In this file, you define your task using the Crunz API. For example, you can use the run method to specify the command to run, and the everyMinute method to schedule the task to run every minute.

How do I schedule a task to run at specific intervals?

Crunz provides a variety of methods to schedule tasks at specific intervals. For example, you can use the everyFiveMinutes method to run a task every five minutes, or the dailyAt method to run a task at a specific time every day. You can also use the cron method to specify a custom cron expression.

Can I run multiple tasks in a single cron job with Crunz?

Yes, you can run multiple tasks in a single cron job with Crunz. You just need to define each task in a separate PHP file in the tasks directory. Crunz will automatically run all tasks according to their schedules.

How do I handle errors and exceptions in Crunz tasks?

Crunz provides a onError method that you can use to handle errors and exceptions in your tasks. You can use this method to specify a callback function that will be called when an error occurs.

Can I use Crunz with any PHP framework?

Yes, Crunz is framework-agnostic, which means it can be used with any PHP framework. You just need to include the Crunz library in your project using Composer.

How do I test my Crunz tasks?

You can test your Crunz tasks by running them manually using the Crunz command-line tool. You can also write unit tests for your tasks using a PHP testing framework like PHPUnit.

Can I use Crunz to run long-running tasks?

Yes, you can use Crunz to run long-running tasks. However, you should be aware that if a task runs for longer than its schedule interval, it may cause overlapping runs, which could lead to unexpected results.

How do I debug my Crunz tasks?

You can debug your Crunz tasks by running them manually using the Crunz command-line tool and checking the output. You can also use PHP’s error logging functions to log errors and exceptions.

Reza LavarianReza Lavarian
View Author

A web developer with a solid background in front-end and back-end development, which is what he's been doing for over ten years. He follows two major principles in his everyday work: beauty and simplicity. He believes everyone should learn something new every day.

automationBrunoScroncronjobcrunzOOPHPPHP
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week