Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions example/05-explain-crontabs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
use GT\Cron\CrontabParser;
use GT\Cron\CronExplainer;

chdir(dirname(__DIR__));
require "vendor/autoload.php";

$crontab = <<<'CRON'
# Explain a few different schedule styles.
@DAILY printf 'Daily backup.\n'
*/10s * * * * printf 'Heartbeat tick.\n'
0 9 * * MON,WED,FRI printf 'Team summary.\n'
05 01 * MAY SUN#2 printf 'May maintenance window.\n'
CRON;

$parser = new CrontabParser();
$explainer = new CronExplainer();

echo "Crontab:" . PHP_EOL;
echo $crontab . PHP_EOL . PHP_EOL;
echo "Explanations:" . PHP_EOL;

foreach(explode("\n", $crontab) as $line) {
$line = trim($line);
if($line === "" || $line[0] === "#") {
continue;
}

[$expression, $command] = $parser->parseLine($line);
echo $expression
. " " . $command
. PHP_EOL . " -> "
. $explainer->explain($expression)
. PHP_EOL;
}
1 change: 1 addition & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ php example/01-run-due-jobs.php
php example/02-next-job.php
php example/03-nickname-expressions.php
php example/04-custom-expression-factory.php
php example/05-explain-crontabs.php
```

Each script embeds its own crontab string so you can read the schedule and the code together.
82 changes: 82 additions & 0 deletions src/Cli/LocalTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
namespace GT\Cron\Cli;

use DateTime;
use DateTimeZone;

class LocalTime {
public function applySystemTimezone():void {
if($timezone = $this->detectSystemTimezone()) {
date_default_timezone_set($timezone);
}
}

public function format(DateTime $dateTime):string {
$local = clone $dateTime;
$local->setTimezone(new DateTimeZone(date_default_timezone_get()));
$message = $local->format("H:i:s");

if($local->getOffset() !== 0) {
$utc = clone $local;
$utc->setTimezone(new DateTimeZone("UTC"));
$message .= " (" . $utc->format("H:i:s") . " UTC)";
}

return $message;
}

protected function detectSystemTimezone():?string {
return $this->detectTimezoneFromEnvironment()
?? $this->detectTimezoneFromLocaltime()
?? $this->detectTimezoneFromTimezoneFile();
}

protected function detectTimezoneFromEnvironment():?string {
$environmentTimezone = getenv("TZ");
if($environmentTimezone !== false
&& $this->isValidTimezone($environmentTimezone)) {
return $environmentTimezone;
}

return null;
}

protected function detectTimezoneFromLocaltime():?string {
$localtimePath = "/etc/localtime";
if(is_link($localtimePath)) {
$link = readlink($localtimePath);
if($link !== false
&& preg_match("#/zoneinfo/(.+)$#", $link, $match)
&& $this->isValidTimezone($match[1])) {
return $match[1];
}
}

return null;
}

protected function detectTimezoneFromTimezoneFile():?string {
$timezonePath = "/etc/timezone";
if(is_file($timezonePath)) {
$timezone = file_get_contents($timezonePath);
if($timezone === false) {
return null;
}

$timezone = trim($timezone);
if($this->isValidTimezone($timezone)) {
return $timezone;
}
}

return null;
}

protected function isValidTimezone(string $timezone):bool {
return in_array(
$timezone,
DateTimeZone::listIdentifiers(),
true
);
}
}
104 changes: 37 additions & 67 deletions src/Cli/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@
namespace GT\Cron\Cli;

use DateTime;
use DateTimeZone;
use Gt\Cli\Argument\ArgumentValueList;
use Gt\Cli\Command\Command;
use Gt\Cli\Parameter\NamedParameter;
use Gt\Cli\Parameter\Parameter;
use Gt\Cli\Stream;
use GT\Cron\CronException;
use GT\Cron\CronExplainer;
use GT\Cron\CrontabNotFoundException;
use GT\Cron\CrontabParser;
use GT\Cron\FunctionExecutionException;
use GT\Cron\JobNotFoundException;
use GT\Cron\ParseException;
use GT\Cron\Runner;
use GT\Cron\RunnerFactory;
use GT\Cron\ScriptExecutionException;

class RunCommand extends Command {
private ?LocalTime $localTime = null;

/** @SuppressWarnings(PHPMD.ExitExpression) */
public function run(?ArgumentValueList $arguments = null):int {
$this->applySystemTimezone();
$this->localTime()->applySystemTimezone();

$filename = $arguments->get("file", "crontab");
$filePath = implode(DIRECTORY_SEPARATOR, [
Expand Down Expand Up @@ -50,6 +54,11 @@ public function run(?ArgumentValueList $arguments = null):int {
exit(0);
}

if($arguments->contains("explain")) {
$this->explainCrontab(file_get_contents($filePath));
return 0;
}

$runner->setRunCallback([$this, "cronRunStep"]);

$nowStatusCode = $this->runNowJobs($runner, $arguments);
Expand Down Expand Up @@ -144,7 +153,7 @@ public function cronRunStep(
?string $nextCommand = null
):void {
$now = new DateTime();
$this->stream->writeLine("Current time: " . $this->formatLocalTime($now));
$this->stream->writeLine("Current time: " . $this->localTime()->format($now));

if(is_null($wait)) {
$this->writeLine("No tasks in crontab.");
Expand All @@ -166,7 +175,7 @@ function(string $command):string {

$this->stream->writeLine($message);

$message = "Next job at: " . $this->formatLocalTime($wait);
$message = "Next job at: " . $this->localTime()->format($wait);
if($nextCommand) {
$message .= " [" . $this->displayCommandName($nextCommand) . "]";
}
Expand Down Expand Up @@ -202,79 +211,34 @@ protected function displayCommandName(string $command):string {
return basename(str_replace("\\", "/", $script));
}

protected function applySystemTimezone():void {
if($timezone = $this->detectSystemTimezone()) {
date_default_timezone_set($timezone);
}
}

protected function detectSystemTimezone():?string {
return $this->detectTimezoneFromEnvironment()
?? $this->detectTimezoneFromLocaltime()
?? $this->detectTimezoneFromTimezoneFile();
}

protected function detectTimezoneFromEnvironment():?string {
$environmentTimezone = getenv("TZ");
if($environmentTimezone !== false
&& $this->isValidTimezone($environmentTimezone)) {
return $environmentTimezone;
}
protected function explainCrontab(string $contents):void {
$parser = new CrontabParser();
$explainer = new CronExplainer();

return null;
}

protected function detectTimezoneFromLocaltime():?string {
$localtimePath = "/etc/localtime";
if(is_link($localtimePath)) {
$link = readlink($localtimePath);
if($link !== false
&& preg_match("#/zoneinfo/(.+)$#", $link, $match)
&& $this->isValidTimezone($match[1])) {
return $match[1];
foreach(explode("\n", $contents) as $line) {
$line = trim($line);
if($line === "" || $line[0] === "#") {
continue;
}
}

return null;
}

protected function detectTimezoneFromTimezoneFile():?string {
$timezonePath = "/etc/timezone";
if(is_file($timezonePath)) {
$timezone = file_get_contents($timezonePath);
if($timezone === false) {
return null;
try {
[$crontab, $command] = $parser->parseLine($line);
$explanation = $explainer->explain($crontab);
}

$timezone = trim($timezone);
if($this->isValidTimezone($timezone)) {
return $timezone;
catch(ParseException) {
throw new ParseException("Invalid syntax: $line");
}
}

return null;
}

protected function isValidTimezone(string $timezone):bool {
return in_array(
$timezone,
DateTimeZone::listIdentifiers(),
true
);
$this->writeLine("$crontab $command\t\t$explanation");
}
}

protected function formatLocalTime(DateTime $dateTime):string {
$local = clone $dateTime;
$local->setTimezone(new DateTimeZone(date_default_timezone_get()));
$message = $local->format("H:i:s");

if($local->getOffset() !== 0) {
$utc = clone $local;
$utc->setTimezone(new DateTimeZone("UTC"));
$message .= " (" . $utc->format("H:i:s") . " UTC)";
private function localTime():LocalTime {
if(is_null($this->localTime)) {
$this->localTime = new LocalTime();
}

return $message;
return $this->localTime;
}

public function getName():string {
Expand Down Expand Up @@ -317,6 +281,12 @@ public function getOptionalParameterList():array {
null,
"Check the syntax of the crontab file without running anything."
),
new Parameter(
false,
"explain",
null,
"List the cron jobs and explain when each one will run."
),
new Parameter(
true,
"now",
Expand Down
Loading
Loading