Skip to content

Commit

Permalink
[feature] add task "extra config"
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond committed Jul 16, 2020
1 parent 05a9c99 commit 88ffc99
Show file tree
Hide file tree
Showing 14 changed files with 322 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Task Scheduling feature](https://laravel.com/docs/master/scheduling).
5. [Prevent Overlap](doc/define-tasks.md#prevent-overlap)
6. [Run on Single Server](doc/define-tasks.md#run-on-single-server)
7. [Between](doc/define-tasks.md#between)
7. [Task Config](doc/define-tasks.md#task-config)
5. [Running the Schedule](doc/run-schedule.md)
1. [Cron Job on Server](doc/run-schedule.md#cron-job-on-server)
2. [Symfony Cloud](doc/run-schedule.md#symfony-cloud)
Expand All @@ -67,6 +68,7 @@ Task Scheduling feature](https://laravel.com/docs/master/scheduling).
1. [Custom Tasks](doc/extending.md#custom-tasks)
2. [Custom Extensions](doc/extending.md#custom-extensions)
3. [Events](doc/extending.md#events)
4. [Using Task Config](doc/extending.md#using-task-config)
8. [Full Configuration Reference](#full-configuration-reference)

## Installation
Expand Down Expand Up @@ -344,4 +346,7 @@ zenstruck_schedule:

# Email subject (leave blank to use extension default)
subject: null

# Additional Configuration/Metadata
config: []
```
27 changes: 27 additions & 0 deletions doc/define-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -809,3 +809,30 @@ zenstruck_schedule:
start: 21:30
end: 6:15
```

## Task Config

You can add parameters or metadata to your task. By itself, these parameters don't do anything (except
show in the output of `schedule:list --detail`), but you can use these parameters in your
[customizations](extending.md#using-task-config).

**Define in [PHP](define-schedule.md#schedulebuilder-service):**

```php
/* @var \Zenstruck\ScheduleBundle\Schedule\Task $task */

$task->config()->set('name', 'value');
```

**Define in [Configuration](define-schedule.md#bundle-configuration):**

```yaml
# config/packages/zenstruck_schedule.yaml

zenstruck_schedule:
tasks:
- task: my:command
frequency: '0 * * * *'
config:
name: value
```
28 changes: 28 additions & 0 deletions doc/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,31 @@ class ScheduleWithoutOverlappingSubscriber implements EventSubscriberInterface

**NOTE:** If *autoconfiguration* is not enabled, add the `kernel.event_subscriber`
tag to the service.

## Using Task Config

You can use [task config](define-tasks.md#task-config) to help with customization. These can be
used in conjunction with events if you deem a [task extension](#custom-extensions) too *heavy*.
Alternatively, you can use this config to aid in building some kind of UI for your tasks.

In the following example, we'll assume your tasks have a *group* config parameter. We will use this
to build an admin dashboard displaying all registered tasks. We want the UI to group each task under
*tabs*:

```php
use Zenstruck\ScheduleBundle\Schedule;

public function groupedTasks(Schedule $schedule): array
{
$tasks = ['Default' => []]; // initial array with default group

foreach ($schedule->all() as $task) {
// Use the "Default" group if config option "group" is not set
$tasks[$task->config()->get('group', 'Default')] = $task;
}

return $tasks;
}
```

The UI for this feature could now group the tasks under *tabs* named by the *Group*.
12 changes: 12 additions & 0 deletions src/Command/ScheduleListCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ private function renderDetail(Schedule $schedule, SymfonyStyle $io): int
$details[] = ['Next Run' => $task->getNextRun()->format('D, M d, Y @ g:i (e O)')];

$this->renderDefinitionList($io, $details);

$config = [];

foreach ($task->config()->humanized() as $key => $value) {
$config[] = [$key => $value];
}

if (\count($config)) {
$io->block('Additional Configuration:');
$this->renderDefinitionList($io, $config);
}

$this->renderExtenstions($io, 'Task', $task->getExtensions());

$issues = \iterator_to_array($this->getTaskIssues($task), false);
Expand Down
4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ private static function taskConfiguration(): ArrayNodeDefinition
->append(self::createPingExtension('ping_on_failure', 'Ping a url if the task failed'))
->append(self::createEmailExtension('email_after', 'Send email after task runs'))
->append(self::createEmailExtension('email_on_failure', 'Send email if task fails'))
->arrayNode('config')
->info('Additional Configuration/Metadata')
->scalarPrototype()->end()
->end()
->end()
->end()
;
Expand Down
4 changes: 4 additions & 0 deletions src/EventListener/TaskConfigurationSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ private function addTask(Schedule $schedule, array $config): void
$task->emailOnFailure($config['email_on_failure']['to'], $config['email_on_failure']['subject']);
}

foreach ($config['config'] as $key => $value) {
$task->config()->set($key, $value);
}

$schedule->add($task);
}

Expand Down
11 changes: 11 additions & 0 deletions src/Schedule/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Zenstruck\ScheduleBundle\Schedule\Extension\PingExtension;
use Zenstruck\ScheduleBundle\Schedule\Extension\SingleServerExtension;
use Zenstruck\ScheduleBundle\Schedule\Extension\WithoutOverlappingExtension;
use Zenstruck\ScheduleBundle\Schedule\Task\Config;
use Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext;

/**
Expand All @@ -30,10 +31,12 @@ abstract class Task
private $description;
private $expression = self::DEFAULT_EXPRESSION;
private $timezone;
private $config;

public function __construct(string $description)
{
$this->description = $description;
$this->config = new Config();
}

final public function __toString(): string
Expand Down Expand Up @@ -91,6 +94,14 @@ final public function description(string $description): self
return $this;
}

/**
* Set extra configuration/metadata for this task.
*/
final public function config(): Config
{
return $this->config;
}

/**
* The timezone this task should run in.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Schedule/Task/CompoundTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public function getIterator(): iterable
$task->addExtension($extension);
}

foreach ($this->config()->all() as $key => $value) {
$task->config()->set($key, $task->config()->get($key, $value));
}

yield $task;
}
}
Expand Down
65 changes: 65 additions & 0 deletions src/Schedule/Task/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Zenstruck\ScheduleBundle\Schedule\Task;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class Config
{
private $data = [];

/**
* @param mixed $value
*/
public function set(string $name, $value): self
{
$this->data[$name] = $value;

return $this;
}

/**
* @param mixed $default
*
* @return mixed
*/
public function get(string $name, $default = null)
{
return $this->data[$name] ?? $default;
}

public function all(): array
{
return $this->data;
}

public function humanized(): array
{
$ret = [];

foreach ($this->data as $key => $value) {
switch (true) {
case \is_bool($value):
$value = $value ? 'yes' : 'no';

break;

case \is_scalar($value):
break;

case \is_object($value):
$value = \sprintf('(%s)', \get_class($value));

break;

default:
$value = \sprintf('(%s)', \gettype($value));
}

$ret[$key] = $value;
}

return $ret;
}
}
37 changes: 37 additions & 0 deletions tests/Command/ScheduleListCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,43 @@ public function shows_schedule_issue_for_duplicate_task_id()
$this->assertStringContainsString('[ERROR] Task "MockTask: task3" (* * * * *) is duplicated 3 times. Make their descriptions unique to fix.', $output);
}

/**
* @test
*/
public function shows_extra_configuration_in_detail_view(): void
{
$task = (new MockTask('my task'))->cron('@daily');
$task->config()->set('config1', 'config1 value');
$task->config()->set('config2', ['an', 'array']);
$task->config()->set('config3', true);
$task->config()->set('config4', false);
$task->config()->set('config5', new Schedule());
$runner = (new MockScheduleBuilder())
->addTask($task)
->getRunner()
;

$command = new ScheduleListCommand($runner, new ExtensionHandlerRegistry([]));
$command->setHelperSet(new HelperSet([new FormatterHelper()]));
$command->setApplication(new Application());
$commandTester = new CommandTester($command);

$commandTester->execute(['--detail' => null]);
$output = $this->normalizeOutput($commandTester);

$this->assertStringContainsString('Additional Configuration', $output);
$this->assertStringContainsString('config1', $output);
$this->assertStringContainsString('config1 value', $output);
$this->assertStringContainsString('config2', $output);
$this->assertStringContainsString('(array)', $output);
$this->assertStringContainsString('config3', $output);
$this->assertStringContainsString('yes', $output);
$this->assertStringContainsString('config4', $output);
$this->assertStringContainsString('no', $output);
$this->assertStringContainsString('config5', $output);
$this->assertStringContainsString('('.Schedule::class.')', $output);
}

private function normalizeOutput(CommandTester $tester): string
{
return \preg_replace('/\s+/', ' ', \str_replace("\n", '', $tester->getDisplay(true)));
Expand Down
19 changes: 19 additions & 0 deletions tests/DependencyInjection/ZenstruckScheduleExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,25 @@ public function between_and_unless_between_config_can_be_shortened()
$this->assertSame('13:15', $config['unless_between']['end']);
}

/**
* @test
*/
public function task_config_must_be_an_array(): void
{
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('Invalid type for path "zenstruck_schedule.tasks.0.config"');

$this->load([
'tasks' => [
[
'task' => 'my:command',
'frequency' => '0 * * * *',
'config' => 'not an array',
],
],
]);
}

protected function getContainerExtensions(): array
{
return [new ZenstruckScheduleExtension()];
Expand Down
25 changes: 25 additions & 0 deletions tests/EventListener/TaskConfigurationSubscriberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,31 @@ public function full_task_configuration()
$this->assertSame('On Task Failure, email output to "sales@example.com"', (string) $extensions[8]);
}

/**
* @test
*/
public function can_add_task_config(): void
{
$schedule = $this->createSchedule([
[
'task' => 'my:command',
'frequency' => '0 * * * *',
'config' => [
'foo' => 'bar',
'bar' => 'foo',
],
],
]);

$this->assertSame(
[
'foo' => 'bar',
'bar' => 'foo',
],
$schedule->all()[0]->config()->all()
);
}

private function createSchedule(array $taskConfig): Schedule
{
$processor = new Processor();
Expand Down
38 changes: 38 additions & 0 deletions tests/Schedule/Task/CompoundTaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPUnit\Framework\TestCase;
use Zenstruck\ScheduleBundle\Schedule\Task\CompoundTask;
use Zenstruck\ScheduleBundle\Tests\Fixture\MockTask;

/**
* @author Kevin Bond <kevinbond@gmail.com>
Expand All @@ -22,4 +23,41 @@ public function cannot_nest_compound_tasks()

$task->add(new CompoundTask());
}

/**
* @test
*/
public function config_is_passed_to_sub_tasks(): void
{
$task = new CompoundTask();
$task->add(new MockTask('subtask1'));
$task->add(new MockTask('subtask2'));
$task->config()->set('foo', 'bar');
$task->config()->set('bar', 'foo');

[$subtask1, $subtask2] = \iterator_to_array($task);

$this->assertSame('bar', $subtask1->config()->get('foo'));
$this->assertSame('foo', $subtask1->config()->get('bar'));
$this->assertSame('bar', $subtask2->config()->get('foo'));
$this->assertSame('foo', $subtask2->config()->get('bar'));
}

/**
* @test
*/
public function config_on_sub_tasks_takes_precedence_over_compound_task(): void
{
$subTask = new MockTask('subtask');
$subTask->config()->set('key2', 'subtask value2');
$task = new CompoundTask();
$task->config()->set('key1', 'compound value1');
$task->config()->set('key2', 'compound value2');
$task->add($subTask);

[$subTask] = \iterator_to_array($task);

$this->assertSame('compound value1', $subTask->config()->get('key1'));
$this->assertSame('subtask value2', $subTask->config()->get('key2'));
}
}
Loading

0 comments on commit 88ffc99

Please sign in to comment.