Skip to content

Commit 3e8f002

Browse files
committed
Merge branch 'release/2.1.3'
2 parents a8485bf + 3ee7237 commit 3e8f002

File tree

6 files changed

+119
-55
lines changed

6 files changed

+119
-55
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [2.1.3] - 2024-10-25
8+
9+
- [#220](https://github.com/os2display/display-api-service/pull/220)
10+
- Fixed issue where saving a screen changed all regions with same ID.
11+
712
## [2.1.2] - 2024-10-24
813

914
- [#213](https://github.com/os2display/display-api-service/pull/213)
@@ -16,7 +21,7 @@ All notable changes to this project will be documented in this file.
1621
- Update psalm baseline
1722
- Add regions/playlists and groups to POST screen test
1823
- `composer update symfony/* --with-dependencies`
19-
-
24+
2025
## [2.1.1] - 2024-10-23
2126

2227
- [#217](https://github.com/os2display/display-api-service/pull/217)

psalm-baseline.xml

+6-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="5.22.2@d768d914152dbbf3486c36398802f74e80cfde48">
2+
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
33
<file src="src/Command/Feed/CreateFeedSourceCommand.php">
44
<RiskyTruthyFalsyComparison>
55
<code><![CDATA[$ulid]]></code>
@@ -525,11 +525,6 @@
525525
<code><![CDATA[$value]]></code>
526526
</MissingParamType>
527527
</file>
528-
<file src="src/Security/AzureOidcAuthenticator.php">
529-
<RiskyTruthyFalsyComparison>
530-
<code><![CDATA[empty($signInName)]]></code>
531-
</RiskyTruthyFalsyComparison>
532-
</file>
533528
<file src="src/Security/Voter/UserVoter.php">
534529
<MissingTemplateParam>
535530
<code><![CDATA[UserVoter]]></code>
@@ -565,7 +560,6 @@
565560
</LessSpecificReturnStatement>
566561
<MissingParamType>
567562
<code><![CDATA[$data]]></code>
568-
<code><![CDATA[$object]]></code>
569563
</MissingParamType>
570564
<MissingTemplateParam>
571565
<code><![CDATA[ProcessorInterface]]></code>
@@ -670,21 +664,12 @@
670664
</PossiblyNullPropertyAssignmentValue>
671665
</file>
672666
<file src="src/State/ScreenProcessor.php">
673-
<NullableReturnStatement>
667+
<InvalidReturnStatement>
674668
<code><![CDATA[$screen]]></code>
675-
</NullableReturnStatement>
676-
<PossiblyNullReference>
677-
<code><![CDATA[setCreatedBy]]></code>
678-
<code><![CDATA[setDescription]]></code>
679-
<code><![CDATA[setEnableColorSchemeChange]]></code>
680-
<code><![CDATA[setLocation]]></code>
681-
<code><![CDATA[setModifiedBy]]></code>
682-
<code><![CDATA[setOrientation]]></code>
683-
<code><![CDATA[setResolution]]></code>
684-
<code><![CDATA[setScreenLayout]]></code>
685-
<code><![CDATA[setSize]]></code>
686-
<code><![CDATA[setTitle]]></code>
687-
</PossiblyNullReference>
669+
</InvalidReturnStatement>
670+
<InvalidReturnType>
671+
<code><![CDATA[Screen]]></code>
672+
</InvalidReturnType>
688673
</file>
689674
<file src="src/State/ScreenProvider.php">
690675
<PossiblyNullPropertyAssignmentValue>

psalm.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
resolveFromConfigFile="true"
55
findUnusedBaselineEntry="true"
66
findUnusedCode="false"
7-
errorBaseline="./psalm-baseline.xml"
7+
errorBaseline="psalm-baseline.xml"
88
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
99
xmlns="https://getpsalm.org/schema/config"
1010
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"

src/State/AbstractProcessor.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ public function toOutput(object $object): object
5353
*
5454
* This is needed to get an object handled by entity manager.
5555
*
56-
* @param $object
56+
* @param \App\Entity\Tenant\Playlist|\App\Entity\Tenant\Screen|\App\Entity\Tenant\ScreenGroup|\App\Entity\Tenant\Slide|\App\Entity\Tenant\Theme $object
5757
*
5858
* @return mixed|object|null
5959
*/
60-
protected function loadPrevious($object, array $context)
60+
protected function loadPrevious(\App\Entity\Tenant\Playlist|\App\Entity\Tenant\ScreenGroup|\App\Entity\Tenant\Screen|\App\Entity\Tenant\Slide|\App\Entity\Tenant\Theme $object, array $context)
6161
{
6262
if ($previous = $context['previous_data'] ?? null) {
6363
$repository = $this->entityManager->getRepository($object::class);

src/State/ScreenProcessor.php

+75-28
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use ApiPlatform\Metadata\Operation;
99
use ApiPlatform\State\ProcessorInterface;
1010
use App\Dto\ScreenInput;
11-
use App\Entity\Tenant\Playlist;
1211
use App\Entity\Tenant\PlaylistScreenRegion;
1312
use App\Entity\Tenant\Screen;
1413
use App\Repository\PlaylistRepository;
@@ -32,7 +31,7 @@ public function __construct(
3231
EntityManagerInterface $entityManager,
3332
ProcessorInterface $persistProcessor,
3433
ProcessorInterface $removeProcessor,
35-
ScreenProvider $provider
34+
ScreenProvider $provider,
3635
) {
3736
parent::__construct($entityManager, $persistProcessor, $removeProcessor, $provider);
3837
}
@@ -42,6 +41,10 @@ protected function fromInput(mixed $object, Operation $operation, array $uriVari
4241
// FIXME Do we really have to do (something like) this to load an existing object into the entity manager?
4342
$screen = $this->loadPrevious(new Screen(), $context);
4443

44+
if (!$screen instanceof Screen) {
45+
throw new InvalidArgumentException('object must be of type Screen');
46+
}
47+
4548
assert($object instanceof ScreenInput);
4649
empty($object->title) ?: $screen->setTitle($object->title);
4750
empty($object->description) ?: $screen->setDescription($object->description);
@@ -56,57 +59,67 @@ protected function fromInput(mixed $object, Operation $operation, array $uriVari
5659
$screen->setEnableColorSchemeChange($object->enableColorSchemeChange);
5760
}
5861

59-
// Adding relations for playlist/screen/region
60-
if (isset($object->regions) && isset($screen)) {
62+
// Adding relations for playlist/screen/region if object has region property.
63+
if (isset($object->regions)) {
64+
// Ensure regions object has valid structure
65+
$this->validateRegionsAndPlaylists($object->regions);
66+
67+
$existingPlaylistScreenRegions = $screen->getPlaylistScreenRegions();
68+
6169
foreach ($object->regions as $regionAndPlaylists) {
62-
// Relevant region
63-
$region = $this->screenLayoutRegionsRepository->findOneBy(['id' => $regionAndPlaylists['regionId']]);
70+
$regionId = $regionAndPlaylists['regionId'];
71+
72+
$region = $this->screenLayoutRegionsRepository->findOneBy(['id' => $regionId]);
6473

6574
if (is_null($region)) {
66-
throw new InvalidArgumentException('Unknown region resource');
75+
throw new InvalidArgumentException(sprintf('Unknown region resource (id: %s)', $regionId));
6776
}
6877

69-
// Collection to be saved.
70-
$playlistScreenRegionCollection = new ArrayCollection();
78+
$existingPlaylistScreenRegionsInRegion = $existingPlaylistScreenRegions->filter(
79+
fn (PlaylistScreenRegion $psr) => $psr->getRegion()?->getId() == $regionId
80+
);
81+
82+
$inputPlaylists = $regionAndPlaylists['playlists'];
83+
$inputPlaylistIds = array_map(fn (array $entry): string => $entry['id'], $inputPlaylists);
84+
85+
// Remove playlist screen regions that should not exist in region.
86+
/** @var PlaylistScreenRegion $existingPSR */
87+
foreach ($existingPlaylistScreenRegionsInRegion as $existingPSR) {
88+
if (!in_array($existingPSR->getPlaylist()?->getId(), $inputPlaylistIds)) {
89+
$screen->removePlaylistScreenRegion($existingPSR);
90+
}
91+
}
7192

72-
// Looping through playlists connected to region
73-
foreach ($regionAndPlaylists['playlists'] as $inputPlaylist) {
74-
// Checking if playlists exists
93+
// Add or update the input playlists.
94+
foreach ($inputPlaylists as $inputPlaylist) {
7595
$playlist = $this->playlistRepository->findOneBy(['id' => $inputPlaylist['id']]);
7696

7797
if (is_null($playlist)) {
78-
throw new InvalidArgumentException('Unknown playlist resource');
98+
throw new InvalidArgumentException(sprintf('Unknown playlist resource (id: %s)', $inputPlaylist['id']));
7999
}
80100

81-
// See if relation already exists
82-
$existingPlaylistScreenRegion = $this->playlistScreenRegionRepository->findOneBy([
83-
'screen' => $screen,
84-
'region' => $region,
101+
$existingPlaylistScreenRegionRelation = $this->playlistScreenRegionRepository->findOneBy([
85102
'playlist' => $playlist,
103+
'region' => $region,
104+
'screen' => $screen,
86105
]);
87106

88-
if (is_null($existingPlaylistScreenRegion)) {
89-
// If relation does not exist, create new PlaylistScreenRegion
107+
if (!is_null($existingPlaylistScreenRegionRelation)) {
108+
$existingPlaylistScreenRegionRelation->setWeight($inputPlaylist['weight'] ?? 0);
109+
} else {
90110
$newPlaylistScreenRegionRelation = new PlaylistScreenRegion();
91111
$newPlaylistScreenRegionRelation->setPlaylist($playlist);
92112
$newPlaylistScreenRegionRelation->setRegion($region);
93113
$newPlaylistScreenRegionRelation->setScreen($screen);
94114
$newPlaylistScreenRegionRelation->setWeight($inputPlaylist['weight'] ?? 0);
95-
/** @psalm-suppress InvalidArgument */
96-
$playlistScreenRegionCollection->add($newPlaylistScreenRegionRelation);
97-
} else {
98-
// Update weight, add existing relation
99-
$existingPlaylistScreenRegion->setWeight($inputPlaylist['weight'] ?? 0);
100-
/** @psalm-suppress InvalidArgument */
101-
$playlistScreenRegionCollection->add($existingPlaylistScreenRegion);
115+
$screen->addPlaylistScreenRegion($newPlaylistScreenRegionRelation);
102116
}
103117
}
104-
$region->setPlaylistScreenRegions($playlistScreenRegionCollection);
105118
}
106119
}
107120

108121
// Maps ids of existing groups
109-
if (isset($object->groups) && isset($screen)) {
122+
if (isset($object->groups)) {
110123
$groupCollection = new ArrayCollection();
111124
foreach ($object->groups as $group) {
112125
$groupToSave = $this->groupRepository->findOneBy(['id' => $group]);
@@ -134,4 +147,38 @@ protected function fromInput(mixed $object, Operation $operation, array $uriVari
134147

135148
return $screen;
136149
}
150+
151+
private function validateRegionsAndPlaylists(array $regions): void
152+
{
153+
foreach ($regions as $region) {
154+
$this->validateRegion($region);
155+
156+
foreach ($region['playlists'] as $playlist) {
157+
$this->validatePlaylist($playlist);
158+
}
159+
}
160+
}
161+
162+
private function validateRegion(array $region): void
163+
{
164+
if (!isset($region['regionId']) || !is_string($region['regionId'])) {
165+
throw new InvalidArgumentException('All regions must specify a valid Ulid');
166+
}
167+
168+
if (!isset($region['playlists']) || !is_array($region['playlists'])) {
169+
throw new InvalidArgumentException('All regions must specify a list of playlists');
170+
}
171+
}
172+
173+
private function validatePlaylist(array $playlist): void
174+
{
175+
if (!isset($playlist['id']) || !is_string($playlist['id'])) {
176+
throw new InvalidArgumentException('All playlists must specify a valid Ulid');
177+
178+
}
179+
180+
if (isset($playlist['weight']) && !is_integer($playlist['weight'])) {
181+
throw new InvalidArgumentException('Playlists weight must be an integer');
182+
}
183+
}
137184
}

tests/Api/ScreensTest.php

+29-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,27 @@
77
use App\Entity\ScreenLayout;
88
use App\Entity\ScreenLayoutRegions;
99
use App\Entity\Tenant\Playlist;
10+
use App\Entity\Tenant\PlaylistScreenRegion;
1011
use App\Entity\Tenant\Screen;
1112
use App\Entity\Tenant\ScreenGroup;
1213
use App\Tests\AbstractBaseApiTestCase;
14+
use Doctrine\ORM\EntityManager;
1315

1416
class ScreensTest extends AbstractBaseApiTestCase
1517
{
18+
private ?EntityManager $entityManager;
19+
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$kernel = self::bootKernel();
25+
26+
$this->entityManager = $kernel->getContainer()
27+
->get('doctrine')
28+
->getManager();
29+
}
30+
1631
public function testGetCollection(): void
1732
{
1833
$response = $this->getAuthenticatedClient('ROLE_ADMIN')->request('GET', '/v2/screens?itemsPerPage=5', ['headers' => ['Content-Type' => 'application/ld+json']]);
@@ -168,12 +183,21 @@ public function testCreateInvalidScreen(): void
168183

169184
public function testUpdateScreen(): void
170185
{
186+
$playlistScreenRegionRepository = $this->entityManager->getRepository(PlaylistScreenRegion::class);
187+
$playlistScreenRegionCountBefore = $playlistScreenRegionRepository->count([]);
188+
189+
$playlistIri = $this->findIriBy(Playlist::class, ['title' => 'playlist_abc_3']);
190+
$playlistUlid = $this->iriHelperUtils->getUlidFromIRI($playlistIri);
191+
$regionIri = $this->findIriBy(ScreenLayoutRegions::class, ['title' => 'full']);
192+
$regionUlid = $this->iriHelperUtils->getUlidFromIRI($regionIri);
193+
171194
$client = $this->getAuthenticatedClient('ROLE_ADMIN');
172-
$iri = $this->findIriBy(Screen::class, ['tenant' => $this->tenant]);
195+
$iri = $this->findIriBy(Screen::class, ['title' => 'screen_abc_1']);
173196

174-
$client->request('PUT', $iri, [
197+
$response = $client->request('PUT', $iri, [
175198
'json' => [
176199
'title' => 'Updated title',
200+
'regions' => [['playlists' => [['id' => $playlistUlid, 'weight' => 0]], 'regionId' => $regionUlid]],
177201
],
178202
'headers' => [
179203
'Content-Type' => 'application/ld+json',
@@ -185,7 +209,10 @@ public function testUpdateScreen(): void
185209
'@type' => 'Screen',
186210
'@id' => $iri,
187211
'title' => 'Updated title',
212+
'regions' => ['/v2/screens/'.$response->toArray()['id'].'/regions/'.$regionUlid.'/playlists'],
188213
]);
214+
$playlistScreenRegionCountAfter = $playlistScreenRegionRepository->count([]);
215+
$this->assertEquals($playlistScreenRegionCountBefore, $playlistScreenRegionCountAfter, 'PlaylistScreenRegion count should not change');
189216
}
190217

191218
public function testDeleteScreen(): void

0 commit comments

Comments
 (0)