Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[make:schedule] a new command for creating recurring Symfony Schedules #1487

Merged
merged 5 commits into from Mar 27, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
140 changes: 140 additions & 0 deletions src/Maker/MakeSchedule.php
@@ -0,0 +1,140 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Maker;

use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
use Symfony\Contracts\Cache\CacheInterface;

/**
* @author Jesse Rushlow <jr@rushlow.dev>
*
* @internal
*/
final class MakeSchedule extends AbstractMaker
{
private string $scheduleName;
private ?string $message = null;

public function __construct(
private FileManager $fileManager,
private Finder $finder = new Finder(),
) {
}

public static function getCommandName(): string
{
return 'make:schedule';
}

public static function getCommandDescription(): string
{
return 'Create a scheduler component';
}

public function configureCommand(Command $command, InputConfiguration $inputConfig): void
{
$command
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeScheduler.txt'))
;
}

public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
if (!class_exists(AsSchedule::class)) {
$io->writeln('Running composer require symfony/scheduler');
$process = Process::fromShellCommandline('composer require symfony/scheduler');
$process->run();
$io->writeln('Scheduler successfully installed!');
}

// Loop over existing src/Message/* and ask which message the user would like to schedule
$availableMessages = ['Empty Schedule'];
$messageDir = $this->fileManager->getRootDirectory().'/src/Message';

if ($this->fileManager->fileExists($messageDir)) {
$finder = $this->finder->in($this->fileManager->getRootDirectory().'/src/Message');

foreach ($finder->files() as $file) {
$availableMessages[] = $file->getFilenameWithoutExtension();
}
}

$scheduleNameHint = 'MainSchedule';

// If the count is 1, no other messages were found - don't ask to create a message
if (1 !== \count($availableMessages)) {
$selectedMessage = $io->choice('Select which message', $availableMessages);

if ('Empty Schedule' !== $selectedMessage) {
$this->message = $selectedMessage;

// We don't want SomeMessageSchedule, so remove the "Message" suffix to give us SomeSchedule
$scheduleNameHint = sprintf('%sSchedule', Str::removeSuffix($selectedMessage, 'Message'));
}
}

// Ask the name of the new schedule
$this->scheduleName = $io->ask(question: 'What should we call the new schedule?', default: $scheduleNameHint);
}

public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
$scheduleClassDetails = $generator->createClassNameDetails(
$this->scheduleName,
'Scheduler\\',
);

$useStatements = new UseStatementGenerator([
AsSchedule::class,
RecurringMessage::class,
Schedule::class,
ScheduleProviderInterface::class,
CacheInterface::class,
]);

if (null !== $this->message) {
$useStatements->addUseStatement('App\\Message\\'.$this->message);
}

$generator->generateClass(
$scheduleClassDetails->getFullName(),
'scheduler/Schedule.tpl.php',
[
'use_statements' => $useStatements,
'has_custom_message' => null !== $this->message,
'message_class_name' => $this->message,
],
);

$generator->writeChanges();

$this->writeSuccessMessage($io);
}

public function configureDependencies(DependencyBuilder $dependencies): void
{
}
}
5 changes: 5 additions & 0 deletions src/Resources/config/makers.xml
Expand Up @@ -92,6 +92,11 @@
<tag name="maker.command" />
</service>

<service id="maker.maker.make_schedule" class="Symfony\Bundle\MakerBundle\Maker\MakeSchedule">
<argument type="service" id="maker.file_manager" />
<tag name="maker.command" />
</service>

<service id="maker.maker.make_serializer_encoder" class="Symfony\Bundle\MakerBundle\Maker\MakeSerializerEncoder">
<tag name="maker.command" />
</service>
Expand Down
8 changes: 8 additions & 0 deletions src/Resources/help/MakeScheduler.txt
@@ -0,0 +1,8 @@
The <info>%command.name%</info> command generates a schedule to automate repeated
tasks using Symfony's Scheduler Component.

If the Scheduler Component is not installed, <info>%command.name%</info> will
install it automatically using composer. You can of course do this manually by
running <info>composer require symfony/scheduler</info>.

<info>php %command.full_name%</info>
30 changes: 30 additions & 0 deletions src/Resources/skeleton/scheduler/Schedule.tpl.php
@@ -0,0 +1,30 @@
<?= "<?php\n" ?>

namespace <?= $namespace; ?>;

<?= $use_statements; ?>

#[AsSchedule]
final class <?= $class_name; ?> implements ScheduleProviderInterface
{
public function __construct(
private CacheInterface $cache,
) {
}

public function getSchedule(): Schedule
{
return (new Schedule())
->add(
<?php if ($has_custom_message): ?>
// @TODO - Modify the frequency to suite your needs
RecurringMessage::every('1 hour', new <?= $message_class_name; ?>()),
<?php else: ?>
// @TODO - Create a Message to schedule
// RecurringMessage::every('1 hour', new App\Message\Message()),
<?php endif ?>
)
->stateful($this->cache)
;
}
}
86 changes: 86 additions & 0 deletions tests/Maker/MakeScheduleTest.php
@@ -0,0 +1,86 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Tests\Maker;

use Symfony\Bundle\MakerBundle\Maker\MakeSchedule;
use Symfony\Bundle\MakerBundle\Test\MakerTestCase;
use Symfony\Bundle\MakerBundle\Test\MakerTestRunner;

class MakeScheduleTest extends MakerTestCase
{
protected function getMakerClass(): string
{
return MakeSchedule::class;
}

public function getTestDetails(): \Generator
{
yield 'it_generates_a_schedule' => [$this->createMakerTest()
->run(function (MakerTestRunner $runner) {
$output = $runner->runMaker([
'', // use default schedule name "MainSchedule"
]);

$this->assertStringContainsString('Success', $output);

self::assertFileEquals(
\dirname(__DIR__).'/fixtures/make-schedule/expected/DefaultScheduleEmpty.php',
$runner->getPath('src/Scheduler/MainSchedule.php')
);
}),
];

yield 'it_generates_a_schedule_select_empty' => [$this->createMakerTest()
->preRun(function (MakerTestRunner $runner) {
$runner->copy(
'make-schedule/standard_setup',
''
);
})
->run(function (MakerTestRunner $runner) {
$output = $runner->runMaker([
0, // Select "Empty Schedule"
'MySchedule', // Go with the default name "MainSchedule"
]);

$this->assertStringContainsString('Success', $output);

self::assertFileEquals(
\dirname(__DIR__).'/fixtures/make-schedule/expected/MyScheduleEmpty.php',
$runner->getPath('src/Scheduler/MySchedule.php')
);
}),
];

yield 'it_generates_a_schedule_select_existing_message' => [$this->createMakerTest()
->preRun(function (MakerTestRunner $runner) {
$runner->copy(
'make-schedule/standard_setup',
''
);
})
->run(function (MakerTestRunner $runner) {
$output = $runner->runMaker([
1, // Select "MyMessage" from choice
'', // Go with the default name "MessageFixtureSchedule"
]);

$this->assertStringContainsString('Success', $output);

self::assertFileEquals(
\dirname(__DIR__).'/fixtures/make-schedule/expected/MyScheduleWithMessage.php',
$runner->getPath('src/Scheduler/MessageFixtureSchedule.php')
);
}),
];
}
}
29 changes: 29 additions & 0 deletions tests/fixtures/make-schedule/expected/DefaultScheduleEmpty.php
@@ -0,0 +1,29 @@
<?php

namespace App\Scheduler;

use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
use Symfony\Contracts\Cache\CacheInterface;

#[AsSchedule]
final class MainSchedule implements ScheduleProviderInterface
{
public function __construct(
private CacheInterface $cache,
) {
}

public function getSchedule(): Schedule
{
return (new Schedule())
->add(
// @TODO - Create a Message to schedule
// RecurringMessage::every('1 hour', new App\Message\Message()),
)
->stateful($this->cache)
;
}
}
29 changes: 29 additions & 0 deletions tests/fixtures/make-schedule/expected/MyScheduleEmpty.php
@@ -0,0 +1,29 @@
<?php

namespace App\Scheduler;

use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
use Symfony\Contracts\Cache\CacheInterface;

#[AsSchedule]
final class MySchedule implements ScheduleProviderInterface
{
public function __construct(
private CacheInterface $cache,
) {
}

public function getSchedule(): Schedule
{
return (new Schedule())
->add(
// @TODO - Create a Message to schedule
// RecurringMessage::every('1 hour', new App\Message\Message()),
)
->stateful($this->cache)
;
}
}
30 changes: 30 additions & 0 deletions tests/fixtures/make-schedule/expected/MyScheduleWithMessage.php
@@ -0,0 +1,30 @@
<?php

namespace App\Scheduler;

use App\Message\MessageFixture;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
use Symfony\Contracts\Cache\CacheInterface;

#[AsSchedule]
final class MessageFixtureSchedule implements ScheduleProviderInterface
{
public function __construct(
private CacheInterface $cache,
) {
}

public function getSchedule(): Schedule
{
return (new Schedule())
->add(
// @TODO - Modify the frequency to suite your needs
RecurringMessage::every('1 hour', new MessageFixture()),
)
->stateful($this->cache)
;
}
}
@@ -0,0 +1,11 @@
<?php

namespace App\Message;

final class MessageFixture
{
public function __construct(
public string $message = 'Howdy!',
) {
}
}