Skip to content

Commit f0438b1

Browse files
bastien70jmsche
andauthored
Added backup custom periodicity support (#73)
* Added backup custom periodicity support * Updated migration file * Update migrations/Version20220801120509.php Co-authored-by: jmsche <[email protected]> * Updated BackupTask.php asserts * Fixed review * Updated migration * Updated migration Co-authored-by: jmsche <[email protected]>
1 parent 3ffea89 commit f0438b1

File tree

15 files changed

+467
-12
lines changed

15 files changed

+467
-12
lines changed

.gitignore

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@
1515
/phpunit.xml
1616
###< symfony/phpunit-bridge ###
1717

18-
###> phpunit/phpunit ###
19-
/phpunit.xml
20-
.phpunit.result.cache
21-
###< phpunit/phpunit ###
22-
2318
###> friendsofphp/php-cs-fixer ###
2419
/.php-cs-fixer.php
2520
/.php-cs-fixer.cache
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use App\Entity\Enum\BackupTaskPeriodicity;
8+
use Doctrine\DBAL\Schema\Schema;
9+
use Doctrine\Migrations\AbstractMigration;
10+
11+
final class Version20220801120509 extends AbstractMigration
12+
{
13+
public function getDescription(): string
14+
{
15+
return 'Added backup custom periodicity support';
16+
}
17+
18+
public function up(Schema $schema): void
19+
{
20+
$this->addSql('ALTER TABLE `database` ADD backup_task_periodicity VARCHAR(255) NOT NULL, ADD backup_task_periodicity_number INT NOT NULL, ADD backup_task_start_from DATETIME NOT NULL, ADD backup_task_next_iteration DATETIME NOT NULL');
21+
$this->addSql('UPDATE `database` SET backup_task_periodicity = :periodicity, backup_task_periodicity_number = :periodicity_number, backup_task_start_from = :start_from, backup_task_next_iteration = :next_iteration;', [
22+
'periodicity_number' => 1,
23+
'periodicity' => BackupTaskPeriodicity::WEEK->value,
24+
'start_from' => (new \DateTime())->format('Y-m-d'),
25+
'next_iteration' => (new \DateTime('+1 week'))->format('Y-m-d'),
26+
]);
27+
}
28+
29+
public function down(Schema $schema): void
30+
{
31+
$this->addSql('ALTER TABLE `database` DROP backup_task_periodicity, DROP backup_task_periodicity_number, DROP backup_task_start_from, DROP backup_task_next_iteration');
32+
}
33+
34+
public function isTransactional(): bool
35+
{
36+
return false;
37+
}
38+
}

src/Command/BackupCommand.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
3232
{
3333
$io = new SymfonyStyle($input, $output);
3434

35-
$databases = $this->backupService->getDatabases();
35+
$databases = $this->databaseRepository->getDatabasesToBackup();
3636
$databasesCount = \count($databases);
3737

3838
if ($databasesCount > 0) {
@@ -44,6 +44,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
4444
$backupStatus = $this->backupService->backup($database, Backup::CONTEXT_AUTOMATIC);
4545
if (BackupStatus::STATUS_OK === $backupStatus->getStatus()) {
4646
$database->setStatus(Database::STATUS_OK);
47+
$backupTask = $database->getBackupTask();
48+
$backupTask->setNextIteration($backupTask->calculateNextIteration());
4749
} else {
4850
$database->setStatus(Database::STATUS_ERROR);
4951
$errors[] = [

src/Controller/Admin/DatabaseCrudController.php

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
use App\Entity\AdapterConfig;
99
use App\Entity\Backup;
1010
use App\Entity\Database;
11+
use App\Entity\Embed\BackupTask;
12+
use App\Entity\Enum\BackupTaskPeriodicity;
1113
use App\Entity\User;
1214
use App\Helper\DatabaseHelper;
1315
use App\Security\Voter\DatabaseVoter;
1416
use App\Service\BackupService;
1517
use App\Service\BackupStatus;
18+
use DateTime;
1619
use Doctrine\ORM\EntityManagerInterface;
1720
use Doctrine\ORM\QueryBuilder;
1821
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
@@ -27,7 +30,9 @@
2730
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
2831
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
2932
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
30-
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
33+
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
34+
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
35+
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
3136
use EasyCorp\Bundle\EasyAdminBundle\Field\NumberField;
3237
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
3338
use EasyCorp\Bundle\EasyAdminBundle\Filter\ChoiceFilter;
@@ -36,8 +41,10 @@
3641
use EasyCorp\Bundle\EasyAdminBundle\Filter\TextFilter;
3742
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
3843
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
44+
use function sprintf;
3945
use Symfony\Component\HttpFoundation\Response;
4046
use Symfony\Component\Translation\TranslatableMessage;
47+
use Symfony\Contracts\Translation\TranslatorInterface;
4148

4249
/**
4350
* @method User|null getUser()
@@ -49,6 +56,7 @@ public function __construct(
4956
private readonly AdminUrlGenerator $adminUrlGenerator,
5057
private readonly DatabaseHelper $databaseHelper,
5158
private readonly EntityManagerInterface $em,
59+
private readonly TranslatorInterface $translator,
5260
) {
5361
}
5462

@@ -212,6 +220,7 @@ public function configureCrud(Crud $crud): Crud
212220

213221
public function configureFields(string $pageName): iterable
214222
{
223+
yield FormField::addPanel('database.panel.main_info', 'fas fa-info-circle');
215224
yield TextField::new('name', 'database.field.name')
216225
->hideOnIndex()
217226
->setColumns(4);
@@ -244,8 +253,26 @@ public function configureFields(string $pageName): iterable
244253
})
245254
->hideOnForm();
246255

247-
yield DateTimeField::new('createdAt', 'database.field.created_at')
248-
->setFormat('dd-MM-Y HH:mm')
256+
yield BadgeField::new('backupTask', 'database.field.backup_task.periodicity')
257+
->formatValue(function (BackupTask $backupTask) {
258+
$plural = $backupTask->getPeriodicityNumber() > 1;
259+
260+
return sprintf(
261+
'%s %s %s',
262+
$this->translator->trans($backupTask->getDescriptionPrefixTranslation()),
263+
$plural ? $backupTask->getPeriodicityNumber() : null,
264+
$this->translator->trans($backupTask->getDescriptionSuffixTranslation())
265+
);
266+
})
267+
->hideOnForm();
268+
yield BadgeField::new('backupTask.nextIteration', 'database.field.backup_task.next_iteration')
269+
->formatValue(function ($value) {
270+
return $value->format($this->translator->trans('global.date_format'));
271+
})
272+
->hideOnForm();
273+
274+
yield DateField::new('createdAt', 'database.field.created_at')
275+
->setFormat($this->translator->trans('global.easy_admin_date_format'))
249276
->hideOnForm();
250277
yield ChoiceField::new('status', 'database.field.status')
251278
->setChoices(array_combine(
@@ -258,5 +285,24 @@ public function configureFields(string $pageName): iterable
258285
Database::STATUS_UNKNOWN => 'secondary',
259286
])
260287
->hideOnForm();
288+
289+
if (Crud::PAGE_INDEX !== $pageName) {
290+
yield FormField::addPanel('database.panel.task_configuration', 'fa-solid fa-calendar');
291+
yield IntegerField::new('backupTask.periodicityNumber', 'database.field.backup_task.periodicity_number')
292+
->setFormTypeOption('attr', ['min' => 1, 'step' => 1])
293+
->setColumns(4);
294+
yield ChoiceField::new('backupTask.periodicity', 'database.field.backup_task.periodicity')
295+
->setChoices(BackupTaskPeriodicity::cases())
296+
->setFormTypeOption('choice_label', function (?BackupTaskPeriodicity $periodicity) {
297+
return $this->translator->trans($periodicity?->formLabel());
298+
})
299+
->setFormTypeOption('choice_value', function (?BackupTaskPeriodicity $periodicity) {
300+
return $periodicity?->value;
301+
})
302+
->setColumns(4);
303+
yield DateField::new('backupTask.startFrom', 'database.field.backup_task.start_from')
304+
->setFormTypeOption('attr', ['min' => (new DateTime('tomorrow'))->format('Y-m-d')])
305+
->setColumns(4);
306+
}
261307
}
262308
}

src/Entity/Database.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace App\Entity;
66

7+
use App\Entity\Embed\BackupTask;
78
use App\Entity\Traits\PrimaryKeyTrait;
89
use App\Repository\DatabaseRepository;
910
use DateTimeImmutable;
@@ -71,8 +72,12 @@ class Database implements \Stringable
7172
#[Assert\NotBlank]
7273
private ?AdapterConfig $adapter = null;
7374

75+
#[ORM\Embedded(class: BackupTask::class)]
76+
private BackupTask $backupTask;
77+
7478
public function __construct()
7579
{
80+
$this->backupTask = new BackupTask();
7681
$this->backups = new ArrayCollection();
7782
$this->createdAt = new DateTimeImmutable();
7883
}
@@ -288,4 +293,16 @@ public function setAdapter(?AdapterConfig $adapter): self
288293

289294
return $this;
290295
}
296+
297+
public function getBackupTask(): BackupTask
298+
{
299+
return $this->backupTask;
300+
}
301+
302+
public function setBackupTask(BackupTask $backupTask): self
303+
{
304+
$this->backupTask = $backupTask;
305+
306+
return $this;
307+
}
291308
}

src/Entity/Embed/BackupTask.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Entity\Embed;
6+
7+
use App\Entity\Enum\BackupTaskPeriodicity;
8+
use DateTime;
9+
use DateTimeInterface;
10+
use Doctrine\DBAL\Types\Types;
11+
use Doctrine\ORM\Mapping as ORM;
12+
use Doctrine\ORM\Mapping\Embeddable;
13+
use function sprintf;
14+
use Symfony\Component\Validator\Constraints as Assert;
15+
16+
#[Embeddable]
17+
class BackupTask
18+
{
19+
#[ORM\Column(type: Types::STRING, length: 255, enumType: BackupTaskPeriodicity::class)]
20+
#[Assert\NotBlank]
21+
private ?BackupTaskPeriodicity $periodicity = null;
22+
23+
#[ORM\Column(type: Types::INTEGER)]
24+
#[Assert\Type(type: 'integer')]
25+
#[Assert\Range(min: 1)]
26+
#[Assert\NotBlank]
27+
private ?int $periodicityNumber = null;
28+
29+
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
30+
#[Assert\GreaterThanOrEqual(value: 'tomorrow')]
31+
#[Assert\NotBlank]
32+
private ?DateTimeInterface $startFrom = null;
33+
34+
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
35+
private ?DateTimeInterface $nextIteration = null;
36+
37+
public function getPeriodicity(): ?BackupTaskPeriodicity
38+
{
39+
return $this->periodicity;
40+
}
41+
42+
public function setPeriodicity(BackupTaskPeriodicity $periodicity): self
43+
{
44+
$this->periodicity = $periodicity;
45+
46+
return $this;
47+
}
48+
49+
public function getPeriodicityNumber(): ?int
50+
{
51+
return $this->periodicityNumber;
52+
}
53+
54+
public function setPeriodicityNumber(int $periodicityNumber): self
55+
{
56+
$this->periodicityNumber = $periodicityNumber;
57+
58+
return $this;
59+
}
60+
61+
public function getStartFrom(): ?DateTimeInterface
62+
{
63+
return $this->startFrom;
64+
}
65+
66+
public function setStartFrom(DateTimeInterface $startFrom): self
67+
{
68+
$this->startFrom = $startFrom;
69+
70+
return $this;
71+
}
72+
73+
public function getNextIteration(): ?DateTimeInterface
74+
{
75+
return $this->nextIteration;
76+
}
77+
78+
public function setNextIteration(?DateTimeInterface $nextIteration): self
79+
{
80+
$this->nextIteration = $nextIteration;
81+
82+
return $this;
83+
}
84+
85+
public function calculateNextIteration(): DateTime|bool
86+
{
87+
$currentIteration = new DateTime();
88+
$currentIteration->setTime(0, 0);
89+
90+
return $currentIteration->modify(
91+
$this->getNextIterationStringAdd(
92+
$this->getPeriodicityNumber(),
93+
$this->getPeriodicity()->value
94+
)
95+
);
96+
}
97+
98+
public function getDescriptionPrefixTranslation(): string
99+
{
100+
if (1 === $this->periodicityNumber) {
101+
return 'enum.backup_task_periodicity.prefix.singular';
102+
}
103+
104+
return match ($this->periodicity) {
105+
BackupTaskPeriodicity::DAY, BackupTaskPeriodicity::MONTH => 'enum.backup_task_periodicity.prefix.masculine_plural',
106+
BackupTaskPeriodicity::WEEK, BackupTaskPeriodicity::YEAR => 'enum.backup_task_periodicity.prefix.feminine_plural',
107+
null => throw new \LogicException('Periodicity is not set'),
108+
};
109+
}
110+
111+
public function getDescriptionSuffixTranslation(): string
112+
{
113+
if (1 === $this->periodicityNumber) {
114+
return sprintf('enum.backup_task_periodicity.suffix.singular.%s', $this->periodicity->value);
115+
}
116+
117+
return sprintf('enum.backup_task_periodicity.suffix.plural.%s', $this->periodicity->value);
118+
}
119+
120+
private function getNextIterationStringAdd(float $value, string $string): string
121+
{
122+
return sprintf('+ %s %s', $value, $string);
123+
}
124+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Entity\Enum;
6+
7+
enum BackupTaskPeriodicity: string
8+
{
9+
case DAY = 'day';
10+
case WEEK = 'week';
11+
case MONTH = 'month';
12+
case YEAR = 'year';
13+
public function formLabel(): string
14+
{
15+
return match ($this) {
16+
self::DAY => 'enum.backup_task_periodicity.select.day',
17+
self::WEEK => 'enum.backup_task_periodicity.select.week',
18+
self::MONTH => 'enum.backup_task_periodicity.select.month',
19+
self::YEAR => 'enum.backup_task_periodicity.select.year',
20+
};
21+
}
22+
}

src/EventSubscriber/DatabaseSubscriber.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public function beforePersistedEvent(BeforeEntityPersistedEvent $event): void
3838

3939
$this->handleDatabasePasswordChange($entity);
4040

41+
$backupTask = $entity->getBackupTask();
42+
$backupTask->setNextIteration($backupTask->getStartFrom());
43+
4144
$user = $this->security->getUser();
4245
\assert($user instanceof User);
4346
$entity->setOwner($user);

0 commit comments

Comments
 (0)