Menu

Alternatywny Scheduler w Laravel - rozwiązanie dla wyłączonego proc_open

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.

Spis treści:

 

Wstęp: Artisan nie działa, musimy stworzyć własny Scheduler

 

Dlaczego Artisan nie działa?

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).

Szybkie porównanie i możliwości

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ć:

  • cyklicznie, np. co godzinę
  • codziennie o określonych porach
  • tylko w dni robocze / weekendy
  • konkretnego dnia o konkretnej porze (lub porach)

Ponadto, wszystkie opcje (poza interwałem) mogą być ze sobą łączone.

Tworzymy scheduler w 3 krokach

Przejdźmy do tworzenia naszego schedulera w 3 prostych krokach.

1. TasksScheduler - plik, którego będziemy używać

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;
            }
        }
    }
}

 

2. ScheduledTask - rdzeń naszego schedulera

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 );
    }
}

 

3. Zmiana rodzica komend

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()
    {
        //
    }
}

 

Gotowe!

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.


Jesteś 450 osobą, która czyta ten wpis
Jak Ci się podoba?
Bardzo pomocne: 1
Polubienia: 0
Wyszukiwarka wiedzy
Obszary tematyczne
Wszystkie kategorie
Zostań na dłużej!

Dołącz do newslettera i sam decyduj jakie treści chcesz otrzymywać.