Laravelのスケジュールタスクは便利ですが、内部の仕組みを理解することで他のフレームワークでも同様の機能を実装できるようになります。
Laravelでは、コンテナという概念を使って多くの機能を管理しています。これによりコードは抽象化され、読み解くのが難しくなりますが、開発効率は向上します。
IDEによるコードトレース機能はPHPStormが最も優れていると感じています。他のIDEは機能が限定的です。
Laravelのスケジュールタスクは主にcronを使用して実装されており、1分ごとにartisanコマンドが呼び出されます。
* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1
このcronジョブが呼び出すschedule:runコマンドによって、設定されたタスクの実行時間が到達しているか判定され、実行されます。
以下は、Laravel内でタスクを実行するための一部のコード例です。
public function executeTask($task, array $params = [])
{
if (class_exists($task)) {
$taskInstance = Container::getInstance()->make($task);
$taskName = $taskInstance->getName();
} else {
$taskName = $task;
}
return $this->execute(
Application::formatCommand($taskName), $params
);
}
CronExpressionクラスは、CRON式の解析と次の実行タイミングの計算を行います。
class CronParser
{
const MINUTE = 0;
const HOUR = 1;
const DAY = 2;
const MONTH = 3;
const WEEKDAY = 4;
const YEAR = 5;
private $cronValues;
private $fieldCreator;
private $maxIterations = 1000;
private static $checkOrder = [self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE];
public static function create($expression, FieldCreator $fieldCreator = null)
{
$shortcuts = [
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@hourly' => '0 * * * *'
];
if (isset($shortcuts[$expression])) {
$expression = $shortcuts[$expression];
}
return new static($expression, $fieldCreator ?: new FieldCreator());
}
}
タスクの次の実行時間を計算するメソッドも紹介します。
protected function calculateNextRun($currentDateTime = null, $skip = 0, $reverse = false, $includeCurrent = false)
{
if ($currentDateTime instanceof DateTime) {
$current = clone $currentDateTime;
} else {
$current = new DateTime($currentDateTime ?: 'now');
}
$current->setTime($current->format('H'), $current->format('i'), 0);
$nextRun = clone $current;
$skip = (int)$skip;
$fieldsToCheck = [];
$fieldObjects = [];
foreach (self::$checkOrder as $index) {
$value = $this->getValue($index);
if (null === $value || '*' === $value) {
continue;
}
$fieldsToCheck[$index] = $value;
$fieldObjects[$index] = $this->fieldCreator->getField($index);
}
for ($iteration = 0; $iteration < $this->maxIterations; $iteration++) {
foreach ($fieldsToCheck as $index => $value) {
$valid = false;
$field = $fieldObjects[$index];
if (strpos($value, ',') === false) {
$valid = $field->isValid($nextRun, $value);
} else {
foreach (array_map('trim', explode(',', $value)) as $part) {
if ($field->isValid($nextRun, $part)) {
$valid = true;
break;
}
}
}
if (!$valid) {
$field->adjust($nextRun, $reverse, $value);
continue 2;
}
}
if ((!$includeCurrent && $nextRun == $current) || --$skip > -1) {
$this->fieldCreator->getField(0)->adjust($nextRun, $reverse, isset($fieldsToCheck[0]) ? $fieldsToCheck[0] : null);
continue;
}
return $nextRun;
}
throw new Exception('無効なCRON式です');
}
タスクの排他制御はCacheMutexクラスで実現されています。
namespace App\Scheduling;
use Illuminate\Contracts\Cache\Repository as CacheStore;
class TaskLock
{
private $cache;
public function __construct(CacheStore $cache)
{
$this->cache = $cache;
}
public function acquireLock(ScheduledTask $task)
{
return $this->cache->add(
$task->lockKey(), true, $task->lockDuration
);
}
public function isLocked(ScheduledTask $task)
{
return $this->cache->has($task->lockKey());
}
public function releaseLock(ScheduledTask $task)
{
$this->cache->forget($task->lockKey());
}
}
タスクの実行タイミングが現在時刻を過ぎているかを確認するメソッドも以下の通りです。
protected function isReadyToRun()
{
$now = Carbon::now();
if ($this->timeZone) {
$now->setTimezone($this->timeZone);
}
return CronParser::create($this->schedule)->isDue($now->toDateTimeString());
}
これらのコード例を参考にし、ThinkPHP5にも同様のスケジュールタスク機能を実装することができます。