Categories: PHP
Published 2020-10-28 17:43
Is Laravel scheduler not working on your shared hosting when you're executing CRON? This may be due to the disabled PHP function "proc_open", which is required by Laravel components. In this article I'll show you how to make an alternative and simple scheduler.
In a lot of shared hostings you can find out that PHP "proc_open" function is disabled for security reasons. Unfortunetly, it means that you cannot use the default Laravel scheduler, cause it uses Symfony component called Process, which requires "proc_open" function. Here is the proof:
namespace Symfony\Component\Process;
(...)
public function __construct($command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
{
if (!\function_exists('proc_open')) {
throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
}
( If you're not sure if "proc_open" function is disabled, run phpinfo() and look if it is on the "disable_functions" list. )
Well, it's a pity you can't use the default Laravel tool, but I can cheer you up - making a scheduler isn't as difficult as it seems to be.
Our scheduler will be very similarly in usage compared to the Kernel from Laravel. Tasks and their schedules will be defined inside the one function like in Kernel class. Also setting the schedule will be very easy and similar. Here are a few examples:
Default Kernel:
namespace App\Console;
class Kernel extends ConsoleKernel{
protected function schedule(Schedule $schedule){
$schedule->command('cron:dummy-task')->dailyAt('07:40');
$schedule->command('cron:dummy-task')->weeklyOn(7, '8:00');
$schedule->command('cron:dummy-task')->everyThirtyMinutes();
$schedule->command('cron:dummy-task')->weekdays()->dailyAt('12:20');
}
}
Usage of default scheduler:
Artisan::call('schedule:run');
Our TasksScheduler:
namespace App\Console;
class TasksScheduler{
public function __construct(){
$this->commands = [
ScheduledTask::schedule( DummyTask::class )->dailyAt(7, 40)
ScheduledTask::schedule( DummyTask::class )->staticTime(7, 8, 0)
ScheduledTask::schedule( DummyTask::class )->interval(30)
ScheduledTask::schedule( DummyTask::class )->weekdaysAt(12,20)
];
}
Usage of our scheduler:
TasksScheduler::run();
You'll be able to run a task:
Furthermore, all of these options (except an interval) can be combined together.
Let's start making our scheduler in 3 easy steps.
namespace App\Console;
use App\Console\Commands\WeeklyReportForEmployer;
class TasksScheduler{
public function __construct(){
$this->commands = [
ScheduledTask::schedule( WeeklyReportForEmployer::class )->everyMinute()
];
}
public static function run(){
return ( new self() )->executeTasksForThisTime();
}
public function executeTasksForThisTime(){
$tasksToExecute = [];
foreach( $this->commands as $command ){
if( $command->isTimeToExecute() )
$tasksToExecute[] = $command;
}
foreach( $tasksToExecute as $task ){
try{
$task->handle();
}catch(\Exception $e){
continue;
}
}
}
}
The class is a replacement of Illuminate\Console\Scheduling\Schedule and it behaves very similar to this one (it's usage has been presented in the first section). Moreover, it inherits from Command class and your commands will inherit from it, so they will be still compatible with default Artisan scheduler.
This is how our class looks like:
namespace App\Console;
use App\Helpers\CarbonHelper;
use Illuminate\Console\Command;
class ScheduledTask extends Command{
// Two possible modes of a schedule
private $staticTimes = [];
private $intervalMinutes = null;
public static function schedule( $command ){
return new $command();
}
public function isTimeToExecute(){
if( $this->intervalMinutes ){
$status = ( CarbonHelper::getMinutesSinceStartOfDay() % $this->intervalMinutes == 0 );
}else{
CarbonHelper::getDayOfWeekAndHourAndMinute($day, $hour, $minute);
$status = false;
foreach( $this->staticTimes as $time ){
if( $day == $time['day'] && $hour == $time['hour'] && $minute == $time['minute'] ){
$status = true;
break;
}
}
}
return $status ? true : false;
}
public function every( $minutes ){
$this->setIntervalMinutes($minutes);
return $this;
}
public function everyMinute(){
$this->setIntervalMinutes(1);
return $this;
}
public function everyThirtyMinutes(){
$this->setIntervalMinutes(30);
return $this;
}
public function everyHour(){
$this->setIntervalMinutes(60);
return $this;
}
public function staticTime($day, $hour, $minute ){
$this->validateStaticTime($day, $hour, $minute);
$this->setStaticTime($day, $hour, $minute);
return $this;
}
public function dailyAt( $hour, $minute ){
$this->validateStaticTime(null, $hour, $minute);
for( $day = 1; $day <= 7; $day++ )
$this->setStaticTime($day, $hour, $minute);
return $this;
}
public function weekdaysAt( $hour, $minute ){
$this->validateStaticTime(null, $hour, $minute);
for( $day = 1; $day <= 5; $day++ )
$this->setStaticTime($day, $hour, $minute);
return $this;
}
public function weekendsAt( $hour, $minute ){
$this->validateStaticTime(null, $hour, $minute);
for( $day = 6; $day <= 7; $day++ )
$this->setStaticTime($day, $hour, $minute);
return $this;
}
private function setIntervalMinutes( $minutes ){
$this->intervalMinutes = $minutes;
}
private function setStaticTime( $day, $hour, $minute ){
$this->intervalMinutes = null;
$this->staticTimes[] = compact('day', 'hour', 'minute');
}
private function validateStaticTime( $day = null, $hour, $minute ){
if( $day !== null && ($day < 1 || $day > 7) )
throw new \Exception('Day must be a number between 1 and 7.');
if( $hour < 0 || $hour > 23 )
throw new \Exception('Hour must be a number between 0 and 23.');
if( $minute < 0 || $minute > 59 )
throw new \Exception('Minute must be a number between 0 and 59.');
}
}
CarbonHelper:
To avoid messing up our code and to be able to use its fragments in the future, we'll create a separate class based on Carbon extension, which is included in Laravel by default.
namespace App\Helpers;
use Carbon\Carbon;
class CarbonHelper{
public static function getDayOfWeekAndHourAndMinute(&$day, &$hour, &$minute){
$currentTime = Carbon::now();
// Carbon treats Sunday as the zero day of the week! Check how it behaves in your environment
$day = ($currentTime->dayOfWeek === Carbon::SUNDAY ? 7 : $currentTime->dayOfWeek);
$hour = $currentTime->hour;
$minute = $currentTime->minute;
}
public static function getMinutesSinceStartOfDay(){
$zeroTime = Carbon::createFromTimeString('00:00:00');
$minutes = Carbon::now()->diffInMinutes( $zeroTime );
return $minutes;
}
}
The last thing we need to do is to change the parent of already existing commands from Illuminate\Console\Command to App\Console\ScheduledTask. Note that those commands are still compatible with default Kernel class.
namespace App\Console\Commands;
use App\Console\ScheduledTask;
// use Illuminate\Console\Command;
class DummyTask extends ScheduledTask /* Command */ {
protected $signature = 'cron:dummy-task';
protected $description = 'Dummy task description.';
public function handle()
{
//
}
}
Our scheduler is ready to use. All we need to do now is to set up schedules of commands in TasksScheduler's constructor using methods of ScheduledTask class. At the end just call "TasksScheduler::run()" in any method I had descriped in this article.
Join newsletter and decide what content you want to receive.