8000 [Scheduler] add "hashed" cron expression support · symfony/symfony@76316a7 · GitHub
[go: up one dir, main page]

Skip to content
Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 76316a7

Browse files
committed
[Scheduler] add "hashed" cron expression support
1 parent aadd302 commit 76316a7

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

src/Symfony/Component/Scheduler/RecurringMessage.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Scheduler\Exception\InvalidArgumentException;
1515
use Symfony\Component\Scheduler\Trigger\CronExpressionTrigger;
1616
use Symfony\Component\Scheduler\Trigger\DateIntervalTrigger;
17+
use Symfony\Component\Scheduler\Trigger\HashedCronExpressionTrigger;
1718
use Symfony\Component\Scheduler\Trigger\TriggerInterface;
1819

1920
/**
@@ -46,6 +47,11 @@ public static function cron(string $expression, object $message): self
4647
return new self(CronExpressionTrigger::fromSpec($expression), $message);
4748
}
4849

50+
public static function hashedCron(string $expression, string $context, object $message): self
51+
{
52+
return new self(new HashedCronExpressionTrigger($expression, $context), $message);
53+
}
54+
4955
public static function trigger(TriggerInterface $trigger, object $message): self
5056
{
5157
return new self($trigger, $message);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Scheduler\Tests\Trigger;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Scheduler\Trigger\HashedCronExpressionTrigger;
16+
17+
final class HashedCronExpressionTriggerTest extends TestCase
18+
{
19+
/**
20+
* @dataProvider hashedExpressionProvider
21+
*/
22+
public function testExpressionParsing(string $input, string $expected)
23+
{
24+
$expressionA = new HashedCronExpressionTrigger($input, 'my task');
25+
$expressionB = new HashedCronExpressionTrigger($input, 'my task');
26+
$expressionC = new HashedCronExpressionTrigger($input, 'another task');
27+
28+
$this->assertSame($expected, $expressionA->getExpression());
29+
$this->assertSame($expressionB->getExpression(), $expressionA->getExpression());
30+
$this->assertNotSame($expressionC->getExpression(), $expressionA->getExpression());
31+
}
32+
33+
public static function hashedExpressionProvider(): array
34+
{
35+
return [
36+
['# * * * *', '56 * * * *'],
37+
['# # * * *', '56 20 * * *'],
38+
['# # # * *', '56 20 1 * *'],
39+
['# # # # *', '56 20 1 9 *'],
40+
['# # # # #', '56 20 1 9 0'],
41+
['# # 1,15 1-11 *', '56 20 1,15 1-11 *'],
42+
['# # 1,15 * *', '56 20 1,15 * *'],
43+
['#hourly', '56 * * * *'],
44+
['#daily', '56 20 * * *'],
45+
['#weekly', '56 20 * * 0'],
46+
['#weekly@midnight', '56 2 * * 0'],
47+
['#monthly', '56 20 1 * *'],
48+
['#monthly@midnight', '56 2 1 * *'],
49+
['#yearly', '56 20 1 9 *'],
50+
['#yearly@midnight', '56 2 1 9 *'],
51+
['#annually', '56 20 1 9 *'],
52+
['#annually@midnight', '56 2 1 9 *'],
53+
['#midnight', '56 2 * * *'],
54+
['#(1-15) * * * *', '12 * * * *'],
55+
['#(1-15) * * * #(3-5)', '12 * * * 5'],
56+
['#(1-15) * # * #(3-5)', '12 * 1 * 5'],
57+
];
58+
}
59+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Scheduler\Trigger;
13+
14+
use Cron\CronExpression;
15+
use Symfony\Component\Scheduler\Exception\InvalidArgumentException;
16+
17+
/**
18+
* Use "hashed" cron expressions to describe a periodical trigger.
19+
*
20+
* @author Kevin Bond <kevinbond@gmail.com>
21+
*
22+
* @experimental
23+
*/
24+
final class HashedCronExpressionTrigger implements TriggerInterface
25+
{
26+
private const ALIAS_MAP = [
27+
'#hourly' => '# * * * *',
28+
'#daily' => '# # * * *',
29+
'#weekly' => '# # * * #',
30+
'#weekly@midnight' => '# #(0-2) * * #',
31+
'#monthly' => '# # # * *',
32+
'#monthly@midnight' => '# #(0-2) # * *',
33+
'#annually' => '# # # # *',
34+
'#annually@midnight' => '# #(0-2) # # *',
35+
'#yearly' => '# # # # *',
36+
'#yearly@midnight' => '# #(0-2) # # *',
37+
'#midnight' => '# #(0-2) * * *',
38+
];
39+
private const RANGES = [
40+
[0, 59],
41+
[0, 23],
42+
[1, 28],
43+
[1, 12],
44+
[0, 6],
45+
];
46+
47+
private CronExpressionTrigger $trigger;
48+
private CronExpression $expression;
49+
50+
public function __construct(string $expression, string $context)
51+
{
52+
$this->trigger = new CronExpressionTrigger(
53+
$this->expression = new CronExpression(self::parse($expression, $context))
54+
);
55+
}
56+
57+
public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable
58+
{
59+
return $this->trigger->getNextRunDate($run);
60+
}
61+
62+
public function getExpression(): string
63+
{
64+
return $this->expression;
65+
}
66+
67+
private static function parse(string $expression, string $context): string
68+
{
69+
$expression = self::ALIAS_MAP[$expression] ?? $expression;
70+
$parts = explode(' ', $expression);
71+
72+
if (count($parts) !== 5) {
73+
throw new InvalidArgumentException(sprintf('"%s" is an invalid cron expression.', $expression));
74+
}
75+
76+
foreach ($parts as $position => $part) {
77+
if (preg_match('#^\#(\((\d+)-(\d+)\))?$#', $part, $matches)) {
78+
$parts[$position] = self::hashField(
79+
(int) ($matches[2] ?? self::RANGES[$position][0]),
80+
(int) ($matches[3] ?? self::RANGES[$position][1]),
81+
$context
82+
);
83+
}
84+
}
85+
86+
return implode(' ', $parts);
87+
}
88+
89+
private static function hashField(int $start, int $end, string $context): string
90+
{
91+
$possibleValues = range($start, $end);
92+
93+
return $possibleValues[(int) fmod(hexdec(substr(md5($context), 0, 10)), count($possibleValues))];
94+
}
95+
}

0 commit comments

Comments
 (0)
0