Skip to content

Commit a1aa369

Browse files
authored
Merge pull request #355 from os2display/release/2.6.1
Release 2.6.1
2 parents 1b87c53 + ab7fb30 commit a1aa369

6 files changed

Lines changed: 635 additions & 305 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [2.6.1] - 2026-03-06
8+
9+
- [#347](https://github.com/os2display/display-api-service/pull/347)
10+
- Added onFlush listener to handle ManyToMany collection changes for relations checksum propagation.
11+
- Added command to refresh relation checksums.
12+
713
## [2.6.0] - 2025-12-05
814

915
- [#330](https://github.com/os2display/display-api-service/pull/330)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Command\Checksum;
6+
7+
use App\Entity\Tenant;
8+
use App\Entity\Tenant\Media;
9+
use App\Entity\Tenant\Slide;
10+
use App\Repository\TenantRepository;
11+
use App\Service\RelationsChecksumCalculator;
12+
use Doctrine\ORM\EntityManagerInterface;
13+
use Symfony\Component\Console\Attribute\AsCommand;
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Completion\CompletionInput;
16+
use Symfony\Component\Console\Completion\CompletionSuggestions;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Symfony\Component\Stopwatch\Stopwatch;
22+
23+
#[AsCommand(
24+
name: 'app:checksum:recalculate',
25+
description: 'Recalculate relation checksums for slides and media entities'
26+
)]
27+
class RecalculateChecksumCommand extends Command
28+
{
29+
private const string OPTION_TENANT = 'tenant';
30+
private const string OPTION_MODIFIED_AFTER = 'modified-after';
31+
32+
public function __construct(
33+
private readonly EntityManagerInterface $entityManager,
34+
private readonly TenantRepository $tenantRepository,
35+
private readonly RelationsChecksumCalculator $calculator,
36+
) {
37+
parent::__construct();
38+
}
39+
40+
#[\Override]
41+
protected function configure(): void
42+
{
43+
$this
44+
->addOption(self::OPTION_TENANT, null, InputOption::VALUE_REQUIRED, 'Filter by tenant key')
45+
->addOption(self::OPTION_MODIFIED_AFTER, null, InputOption::VALUE_REQUIRED, 'Filter by modified_at >= date (e.g. "2024-01-01" or "2024-01-01 12:00:00")')
46+
->setHelp(<<<'HELP'
47+
The <info>%command.name%</info> command recalculates relation checksums for slides and media,
48+
then propagates the changes up the entity tree.
49+
50+
<info>php %command.full_name%</info>
51+
52+
You can filter by tenant key and/or modification date:
53+
54+
<info>php %command.full_name% --tenant=ABC</info>
55+
<info>php %command.full_name% --modified-after="2024-01-01"</info>
56+
<info>php %command.full_name% --tenant=ABC --modified-after="2024-01-01 12:00:00"</info>
57+
58+
Without any filters, all slides and media will be recalculated.
59+
HELP)
60+
;
61+
}
62+
63+
#[\Override]
64+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
65+
{
66+
if ($input->mustSuggestOptionValuesFor(self::OPTION_TENANT)) {
67+
$tenants = $this->tenantRepository->findAll();
68+
foreach ($tenants as $tenant) {
69+
$suggestions->suggestValue($tenant->getTenantKey());
70+
}
71+
}
72+
}
73+
74+
#[\Override]
75+
protected function execute(InputInterface $input, OutputInterface $output): int
76+
{
77+
$io = new SymfonyStyle($input, $output);
78+
$stopwatch = new Stopwatch();
79+
$stopwatch->start('checksum-recalculate');
80+
81+
$tenantKey = $input->getOption(self::OPTION_TENANT);
82+
$modifiedAfterStr = $input->getOption(self::OPTION_MODIFIED_AFTER);
83+
84+
// Resolve tenant
85+
$tenant = null;
86+
if (null !== $tenantKey) {
87+
$tenant = $this->tenantRepository->findOneBy(['tenantKey' => $tenantKey]);
88+
if (null === $tenant) {
89+
$io->error(sprintf('Tenant with key "%s" not found.', $tenantKey));
90+
91+
return Command::FAILURE;
92+
}
93+
$io->info(sprintf('Filtering by tenant: %s', $tenantKey));
94+
}
95+
96+
// Parse date
97+
$modifiedAfter = null;
98+
if (null !== $modifiedAfterStr) {
99+
try {
100+
$modifiedAfter = new \DateTimeImmutable($modifiedAfterStr);
101+
} catch (\Exception) {
102+
$io->error(sprintf('Invalid date format: "%s". Use formats like "Y-m-d" or "Y-m-d H:i:s".', $modifiedAfterStr));
103+
104+
return Command::FAILURE;
105+
}
106+
$io->info(sprintf('Filtering by modified after: %s', $modifiedAfter->format('Y-m-d H:i:s')));
107+
}
108+
109+
// Mark matching slides and media as changed using DQL UPDATE
110+
$targetEntities = [
111+
'slide' => Slide::class,
112+
'media' => Media::class,
113+
];
114+
$totalAffected = 0;
115+
116+
foreach ($targetEntities as $label => $entityClass) {
117+
$qb = $this->entityManager->createQueryBuilder()
118+
->update($entityClass, 'e')
119+
->set('e.changed', ':changed')
120+
->setParameter('changed', true);
121+
122+
if (null !== $tenant) {
123+
$qb->andWhere('e.tenant = :tenant')
124+
->setParameter('tenant', $tenant, Tenant::class);
125+
}
126+
127+
if (null !== $modifiedAfter) {
128+
$qb->andWhere('e.modifiedAt >= :modifiedAfter')
129+
->setParameter('modifiedAfter', $modifiedAfter, 'datetime_immutable');
130+
}
131+
132+
$affected = $qb->getQuery()->execute();
133+
$totalAffected += $affected;
134+
$io->info(sprintf('Marked %d rows in "%s" as changed.', $affected, $label));
135+
}
136+
137+
if (0 === $totalAffected) {
138+
$io->warning('No rows matched the given filters. Nothing to recalculate.');
139+
140+
return Command::SUCCESS;
141+
}
142+
143+
// Propagate checksums through entity tree
144+
$io->info('Propagating checksums through entity tree...');
145+
$this->calculator->execute(withWhereClause: true);
146+
147+
$event = $stopwatch->stop('checksum-recalculate');
148+
149+
$io->success(sprintf(
150+
'Checksums recalculated. %d rows marked. Elapsed: %.2f ms, Memory: %.2f MB',
151+
$totalAffected,
152+
$event->getDuration(),
153+
$event->getMemory() / (1024 ** 2)
154+
));
155+
156+
return Command::SUCCESS;
157+
}
158+
}

src/DataFixtures/Loader/DoctrineOrmLoaderDecorator.php

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use App\EventListener\RelationsChecksumListener;
88
use App\EventListener\TimestampableListener;
9+
use App\Service\RelationsChecksumCalculator;
910
use Doctrine\ORM\EntityManagerInterface;
1011
use Hautelook\AliceBundle\Loader\DoctrineOrmLoader;
1112
use Hautelook\AliceBundle\LoaderInterface as AliceBundleLoaderInterface;
@@ -28,6 +29,7 @@ class DoctrineOrmLoaderDecorator implements AliceBundleLoaderInterface, LoggerAw
2829
{
2930
public function __construct(
3031
private readonly DoctrineOrmLoader $decorated,
32+
private readonly RelationsChecksumCalculator $calculator,
3133
) {}
3234

3335
public function load(Application $application, EntityManagerInterface $manager, array $bundles, string $environment, bool $append, bool $purgeWithTruncate, bool $noBundles = false): array
@@ -59,7 +61,7 @@ public function load(Application $application, EntityManagerInterface $manager,
5961
$result = $this->decorated->load($application, $manager, $bundles, $environment, $append, $purgeWithTruncate, $noBundles);
6062

6163
// Apply the SQL statements from the disabled "postFlush" listener
62-
$this->applyRelationsModified($manager);
64+
$this->applyRelationsModified();
6365

6466
// Re-enable listeners
6567
$eventManager->addEventListener('postFlush', $relationsModifiedAtListener);
@@ -77,16 +79,8 @@ public function withLogger(LoggerInterface $logger): static
7779
$this->decorated->withLogger($logger);
7880
}
7981

80-
private function applyRelationsModified(EntityManagerInterface $manager): void
82+
private function applyRelationsModified(): void
8183
{
82-
$connection = $manager->getConnection();
83-
84-
$sqlQueries = RelationsChecksumListener::getUpdateRelationsAtQueries(withWhereClause: false);
85-
86-
$rows = 0;
87-
foreach ($sqlQueries as $sqlQuery) {
88-
$stm = $connection->prepare($sqlQuery);
89-
$rows += $stm->executeStatement();
90-
}
84+
$this->calculator->execute(withWhereClause: false);
9185
}
9286
}

0 commit comments

Comments
 (0)