Select Git revision
CronjobSchedule.class.php
Forked from
Stud.IP / Stud.IP
Source project has a limited visibility.
-
Jan-Hendrik Willms authored
Closes #2624 Merge request studip/studip!1773
Jan-Hendrik Willms authoredCloses #2624 Merge request studip/studip!1773
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CronjobSchedule.class.php 10.77 KiB
<?php
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
// CronjobSchedule.class.php
//
// Copyright (C) 2013 Jan-Hendrik Willms <tleilax+studip@gmail.com>
// +---------------------------------------------------------------------------+
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or any later version.
// +---------------------------------------------------------------------------+
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
// +---------------------------------------------------------------------------+
/**
* CronjobSchedule - Model for the database table "cronjobs_schedules"
*
* @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
* @since 2.4
*
* @property string schedule_id database column
* @property string id alias column for schedule_id
* @property string task_id database column
* @property string active database column
* @property string title database column
* @property string description database column
* @property string parameters database column
* @property string priority database column
* @property string type database column
* @property string minute database column
* @property string hour database column
* @property string day database column
* @property string month database column
* @property string day_of_week database column
* @property string next_execution database column
* @property string last_execution database column
* @property string last_result database column
* @property string execution_count database column
* @property string mkdate database column
* @property string chdate database column
* @property SimpleORMapCollection logs has_many CronjobLog
* @property CronjobTask task belongs_to CronjobTask
*/
class CronjobSchedule extends SimpleORMap
{
const PRIORITY_LOW = 'low';
const PRIORITY_NORMAL = 'normal';
const PRIORITY_HIGH = 'high';
protected static function configure($config = [])
{
$config['db_table'] = 'cronjobs_schedules';
$config['belongs_to']['task'] = [
'class_name' => CronjobTask::class,
'foreign_key' => 'task_id',
];
$config['has_many']['logs'] = [
'class_name' => CronjobLog::class,
'on_delete' => 'delete',
'on_store' => 'store',
];
$config['registered_callbacks']['before_store'][] = 'cbJsonifyParameters';
$config['registered_callbacks']['after_store'][] = 'cbJsonifyParameters';
$config['registered_callbacks']['after_initialize'][] = 'cbJsonifyParameters';
parent::configure($config);
}
/**
* Returns a mapped version of the priorities (key = priority value,
* value = localized priority label).
*
* @return Array The mapped priorities
*/
public static function getPriorities()
{
$mapping = [];
$mapping[self::PRIORITY_LOW] = _('niedrig');
$mapping[self::PRIORITY_NORMAL] = _('normal');
$mapping[self::PRIORITY_HIGH] = _('hoch');
return $mapping;
}
/**
* Maps a priority value to it's localized label.
*
* @param String $priority Priority value
* @return String The localized label
* @throws RuntimeException when an unknown priority value is passed
*/
public static function describePriority($priority)
{
$priority = $priority ?? 'normal';
$mapping = self::getPriorities();
if (!isset($mapping[$priority])) {
throw new RuntimeException('Access to unknown priority "' . $priority . '"');
}
return $mapping[$priority];
}
/**
* replaces title with task name if title is empty.
*
* @return string the title or the task name
*/
public function getTitle()
{
return ($this->content['title'] ?: $this->task->name) ?? '';
}
protected function cbJsonifyParameters($type)
{
if ($type === 'before_store' && !is_string($this->parameters)) {
$this->parameters = json_encode($this->parameters ?: null);
}
if (in_array($type, ['after_initialize', 'after_store']) && is_string($this->parameters)) {
$parameters = json_decode($this->parameters, true) ?: [];
if ($this->task->valid) {
$default_parameters = $this->task->extractDefaultParameters();
foreach ($default_parameters as $key => $value) {
if (!isset($parameters[$key])) {
$parameters[$key] = $value;
}
}
}
$this->parameters = $parameters;
}
}
/**
* Stores the schedule in database. Will bail out with an exception if
* the provided task does not exists. Will also nullify the title if it
* matches the task name (see CronjobSchedule::getTitle()).
*
* @return CronjobSchedule Returns itself to allow chaining
*/
public function store()
{
if ($this->task === null) {
$message = sprintf('A task with the id "%s" does not exist.', $this->task_id);
throw new InvalidArgumentException($message);
}
// Remove title if it is the default (task's name)
if ($this->title === $this->task->name) {
$this->title = null;
}
parent::store();
return $this;
}
/**
* Activates this schedule.
*
* @return CronjobSchedule Returns itself to allow chaining
*/
public function activate()
{
$this->active = 1;
$this->next_execution = $this->calculateNextExecution();
$this->store();
return $this;
}
/**
* Deactivates this schedule.
*
* @return CronjobSchedule Returns itself to allow chaining
*/
public function deactivate()
{
$this->active = 0;
$this->store();
return $this;
}
/**
* Executes this schedule.
*
* @param bool $force Pass true to force execution of the schedule even
* if it's not activated
* @return mixed The result of the execution
* @throws RuntimeException When either the schedule or the according is
* not activated
*/
public function execute($force = false)
{
if (!$force && !$this->active) {
throw new RuntimeException('Execution aborted. Schedule is not active');
}
if (!$this->task->active) {
throw new RuntimeException('Execution aborted. Associated task is not active');
}
$this->last_execution = time();
$this->execution_count += 1;
$this->next_execution = $this->calculateNextExecution();
$this->store();
$this->task->execution_count += 1;
$this->task->store();
$result = $this->task->engage($this->last_result, $this->parameters);
if ($this->type === 'once') {
$this->active = 0;
}
$this->last_result = $result;
$this->store();
return $result;
}
/**
* Determines whether the schedule should execute given the provided
* timestamp.
*
* @param mixed $now Defines the temporal fix point
* @return bool Whether the schedule should execute or not.
*/
public function shouldExecute($now = null)
{
return ($now ?: time()) >= $this->next_execution;
}
/**
* Calculates the next execution for this schedule.
*
* For schedules of type 'once' the check solely tests whether the
* timestamp has already passed and will return false in that case.
* Otherwise the defined timestamp will be returned.
*
* For schedules of type 'periodic' the next execution
* is calculated by increasing the current timestamp and testing
* whether all conditions match. This is not the best method to test
* and should be optimized sooner or later.
*
* @param mixed $now Defines the temporal fix point
* @return int Timestamp of calculated next execution
* @throws RuntimeException When calculation takes too long (you should
* check the conditions for validity in that case)
*/
public function calculateNextExecution($now = null)
{
$now = $now ?: time();
if ($this->type === 'once') {
return $now <= $this->next_execution
? $this->next_execution
: false;
}
$result = $now;
$result -= $result % 60;
$i = 366 * 24 * 60; // Maximum: A year
$offset = 60;
do {
$result += $offset;
// TODO: Performance - Adjust result according to conditions
// See http://coderzone.org/library/PHP-PHP-Cron-Parser-Class_1084.htm
$valid = $this->testTimestamp($result, $this->minute, 'i')
&& $this->testTimestamp($result, $this->hour, 'H')
&& $this->testTimestamp($result, $this->day, 'd')
&& $this->testTimestamp($result, $this->month, 'm')
&& $this->testTimestamp($result, $this->day_of_week, 'N');
} while (!$valid && $i-- > 0);
if ($i <= 0) {
throw new RuntimeException('No result, current: ' . date('d.m.Y H:i', $result));
}
$this->next_execution = $result;
return $result;
}
/**
* Tests a timestamp against the passed condition.
*
* @param int $timestamp The timestamp to test
* @param mixed $condition Can be either null for "don't care", a positive
* number for an exact moment or a negative number
* for a repeating moment
* @param String $format Format for date() to extract a portion of the
* timestamp
*/
protected function testTimestamp($timestamp, $condition, $format)
{
if ($condition === null) {
return true;
}
$probe = (int) date($format, $timestamp);
$condition = (int) $condition;
if ($condition < 0) {
return ($probe % abs($condition)) === 0;
}
return $probe === $condition;
}
}