Kategorie: PHP
Opublikowano 2020-10-28 17:43
Laravel udostępnia szybki i łatwy sposób na tworzenie job schedulera dla zadań CRON, niestety może okazać się on bezużyteczny na niektórych hostingach. Powodem jest wyłączona funkcja PHP o nazwie "proc_open", którą wykorzystuje biblioteka Laravel. W tym tutorialu utworzymy własny scheduler dla zadań utworzonych w Laravel'u.
Wielu dostawców hostingu ze względów bezpieczeństwa wyłącza funkcję PHP o nazwie "proc_open", służącą do wykonania komendy powłoki (tzw. shell). Niestety jest ona wymagana przez domyślny scheduler Laravel (a wlaściwie przez komponenty, których używa), czyli App\Console\Kernel.php. Z tego powodu przy próbie wykonania CRON na serwerze możemy zobaczyć błąd:
"The Process class relies on proc_open, which is not available on your PHP installation. at .../ domains/ example.com/ vendor/ symfony/ process/ Process.php:143".
Na dowód tego załączam fragment klasy Symfony\Component\Process\Process.php:
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.');
}
Jak widać, musimy sobie radzić samemu. Na szczęście ten fakt nie jest tak straszny jak może wyglądać na początku (a tak wyglądał dla mnie, gdy dowiedziałem się, że nie mogę użyć domyślnego narzędzia Laravel).
Utworzony przez nas Scheduler będzie bardzo podobny do domyślnego, a jego użycie będzie niezwykle łatwe. Podobnie jak w oryginalnej wersji, czas wykonania zadań będziemy określać w środku jednej funkcji, dzięki czemu łatwo będzie nam analizować i zmieniać harmonogram. Również samo ustalanie harmonogramu będzie bardzo podobne i łatwe. Oto kilka przykładów:
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');
}
}
Użycie domyślnego schedulera:
Artisan::call('schedule:run');
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)
];
}
Użycie naszego schedulera:
TasksScheduler::run();
Zadania będziesz mógł wykonywać:
Ponadto, wszystkie opcje (poza interwałem) mogą być ze sobą łączone.
Przejdźmy do tworzenia naszego schedulera w 3 prostych krokach.
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;
}
}
}
}
Klasa zastępuje Illuminate\Console\Scheduling\Schedule i zachowuje się do niego bardzo podobnie (jej użycie zostało przedstawione w pierwszej sekcji). Ponadto, dziedziczy ona po klasie Command, a utworzone komendy dziedziczą po niej, dzięki czemu są wciąż kompatybline z domyślnym schedulerem Laravel.
Oto jak wygląda klasa Task:
namespace App\Console;
use App\Helpers\CarbonHelper;
use Illuminate\Console\Command;
class ScheduledTask extends Command{
// Dwa możliwe tryby harmonogramu
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:
Aby nie zaśmiecać kodu i móc wykorzystać jego fragmenty w przyszłości, utworzymy osobną klasę CarbonHelper, bazującą na rozszerzeniu Carbon, które Laravel zawiera w pakiecie. Dzięki niemu w łatwy sposób uzyskamy potrzebne dane dotyczące czasu.
namespace App\Helpers;
use Carbon\Carbon;
class CarbonHelper{
public static function getDayOfWeekAndHourAndMinute(&$day, &$hour, &$minute){
$currentTime = Carbon::now();
// Niedziela jest traktowana przez Carbon jako zerowy dzień tygodnia! Najlepiej, abyś sprawdził jak się to zachowuje w Twoim środowisku
$day = ($currentTime->dayOfWeek === Carbon::SUNDAY ? 7 : $currentTime->dayOfWeek);
$hour = $currentTime->hour;
$minute = $currentTime->minute;
}
public static function getMinutesSinceStartOfDay(){
$zeroTime = Carbon::createFromTimeString('00:00:00');
return Carbon::now()->diffInMinutes( $zeroTime );
}
}
Ostatnią rzeczą, jaką musimy zrobić, jest zmiana rodzica utworzonych już zadań z Illuminate\Console\Command na App\Console\ScheduledTask. Pamiętaj, że zadania są wciąż zgodne z domyślnym schedulerem!
namespace App\Console\Commands;
use App\Console\ScheduledTask;
// use Illuminate\Console\Command;
class DummyTask extends Task /* Command */ {
protected $signature = 'cron:dummy-task';
protected $description = 'Dummy task description.';
public function handle()
{
//
}
}
Nasz scheduler jest gotowy do użycia. Teraz pozostaje tylko ustawić harmonogramy zadań w konstruktorze klasy TasksScheduler za pomocą metod klasy ScheduledTask. Na koniec wystarczy wywołać "TasksScheduler::run()" w dowolnym ze sposobów, które opisałem w tym artykule.
Masz jakieś uwagi? Czy pomógł Ci ten artykuł? Podziel się swoją opinią w komentarzu.
Dołącz do newslettera i sam decyduj jakie treści chcesz otrzymywać.