From 4bd6faaca15aa98923744bd4026cdbfe8f2d47a4 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 13 Apr 2025 01:22:15 +0200 Subject: [PATCH 1/5] enable caching for periods/{periodId}/schedule_entries --- api/config/packages/http_cache.yaml | 2 +- .../dev-data/Version202504121559.php | 8 - .../dev-data/Version202504130046.php | 31 +++ api/migrations/dev-data/data.sql | 7 +- api/src/State/PeriodPersistProcessor.php | 31 +-- e2e/package.json | 2 +- e2e/specs/constants.js | 6 + .../schedule_entries_collection.json | 203 ++++++++++++++++++ e2e/specs/httpCache/scheduleEntries.cy.js | 187 ++++++++++++++++ 9 files changed, 452 insertions(+), 25 deletions(-) create mode 100644 api/migrations/dev-data/Version202504130046.php create mode 100644 e2e/specs/httpCache/responses/schedule_entries_collection.json create mode 100644 e2e/specs/httpCache/scheduleEntries.cy.js diff --git a/api/config/packages/http_cache.yaml b/api/config/packages/http_cache.yaml index 05fcc4ca67..90d7780021 100644 --- a/api/config/packages/http_cache.yaml +++ b/api/config/packages/http_cache.yaml @@ -1,5 +1,5 @@ parameters: - app.httpCache.matchPath: '^/?($|index.jsonhal$|content_types|camps/[0-9a-f]*/categories)' + app.httpCache.matchPath: '^/?($|index.jsonhal$|content_types|camps/[0-9a-f]*/categories|periods/[0-9a-f]*/schedule_entries)' fos_http_cache: debug: diff --git a/api/migrations/dev-data/Version202504121559.php b/api/migrations/dev-data/Version202504121559.php index 76d5f1b268..1925b4af4c 100644 --- a/api/migrations/dev-data/Version202504121559.php +++ b/api/migrations/dev-data/Version202504121559.php @@ -16,14 +16,6 @@ public function getDescription(): string { public function up(Schema $schema): void { // START PHP CODE - $this->addSql(createTruncateDatabaseCommand()); - - $statements = getStatementsForMigrationFile(); - foreach ($statements as $statement) { - if (trim($statement)) { - $this->addSql($statement); - } - } // END PHP CODE } diff --git a/api/migrations/dev-data/Version202504130046.php b/api/migrations/dev-data/Version202504130046.php new file mode 100644 index 0000000000..a3eb6c0c24 --- /dev/null +++ b/api/migrations/dev-data/Version202504130046.php @@ -0,0 +1,31 @@ +addSql(createTruncateDatabaseCommand()); + + $statements = getStatementsForMigrationFile(); + foreach ($statements as $statement) { + if (trim($statement)) { + $this->addSql($statement); + } + } + // END PHP CODE + } + + public function down(Schema $schema): void {} +} diff --git a/api/migrations/dev-data/data.sql b/api/migrations/dev-data/data.sql index f2ee9f894b..33da0e8c73 100644 --- a/api/migrations/dev-data/data.sql +++ b/api/migrations/dev-data/data.sql @@ -1976,7 +1976,8 @@ INSERT INTO public.period (id, description, start, "end", createtime, updatetime ('c085d1d5ddfa', 'Die Jedi-Akademie', '2026-07-14', '2026-07-16', '2023-08-13 06:32:29', '2023-08-13 06:32:29', '0969e3c95dfc'), ('7fa4564a5d5d', 'Main', '2031-01-24', '2031-01-30', '2023-09-29 23:24:38', '2023-09-29 23:24:38', '70ca971c992f'), ('88f1f55a69d7', 'Hauptlager', '2025-07-13', '2025-07-20', '2023-08-08 09:22:58', '2023-08-08 09:48:01', '5d28f99890bc'), - ('05938f2a5372', 'Hauptlager', '2022-01-02', '2022-01-02', '2024-09-28 21:19:13', '2024-09-28 21:22:24', '25a82475e0b7'); + ('05938f2a5372', 'Hauptlager', '2022-01-02', '2022-01-02', '2024-09-28 21:19:13', '2024-09-28 21:22:24', '25a82475e0b7'), + ('c550b8707c26', 'Nachweekend', '2025-08-09', '2025-08-10', '2025-04-12 22:30:55', '2025-04-12 22:30:55', '6973c230d6b1'); @@ -2020,7 +2021,9 @@ INSERT INTO public.day (id, dayoffset, createtime, updatetime, periodid) VALUES ('273358ce9d70', 5, '2023-08-08 09:22:58', '2023-08-08 09:22:58', '88f1f55a69d7'), ('40e3a286dc08', 6, '2023-08-08 09:22:58', '2023-08-08 09:22:58', '88f1f55a69d7'), ('485e190f4852', 7, '2023-08-08 09:22:58', '2023-08-08 09:22:58', '88f1f55a69d7'), - ('fb406d22f8b9', 0, '2024-09-28 21:19:13', '2024-09-28 21:19:13', '05938f2a5372'); + ('fb406d22f8b9', 0, '2024-09-28 21:19:13', '2024-09-28 21:19:13', '05938f2a5372'), + ('be09cc00bfb5', 0, '2025-04-12 22:30:55', '2025-04-12 22:30:55', 'c550b8707c26'), + ('ee1a47697fd0', 1, '2025-04-12 22:30:55', '2025-04-12 22:30:55', 'c550b8707c26'); diff --git a/api/src/State/PeriodPersistProcessor.php b/api/src/State/PeriodPersistProcessor.php index 7bfdd862d4..d3ee6990aa 100644 --- a/api/src/State/PeriodPersistProcessor.php +++ b/api/src/State/PeriodPersistProcessor.php @@ -2,19 +2,21 @@ namespace App\State; -use ApiPlatform\Metadata\Operation; -use ApiPlatform\State\ProcessorInterface; -use App\Entity\Day; -use App\Entity\Period; -use App\State\Util\AbstractPersistProcessor; +use FOS\HttpCacheBundle\CacheManager; use App\Util\DateTimeUtil; +use App\State\Util\AbstractPersistProcessor; +use App\Entity\Period; +use App\Entity\Day; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\Metadata\Operation; /** * @template-extends AbstractPersistProcessor */ class PeriodPersistProcessor extends AbstractPersistProcessor { public function __construct( - ProcessorInterface $decorated + ProcessorInterface $decorated, + private readonly CacheManager $cacheManager ) { parent::__construct($decorated); } @@ -23,26 +25,29 @@ public function __construct( * @param Period $data */ public function onBefore($data, Operation $operation, array $uriVariables = [], array $context = []): Period { - self::moveDaysAndScheduleEntries($data, $context['previous_data'] ?? null); + $this->moveDaysAndScheduleEntries($data, $context['previous_data'] ?? null); self::removeExtraDays($data); self::addMissingDays($data); return $data; } - public static function moveDaysAndScheduleEntries(Period $period, ?Period $originalPeriod = null) { + public function moveDaysAndScheduleEntries(Period $period, ?Period $originalPeriod = null) { if (!$originalPeriod) { return; } - // moveScheduleEntries === true: scheduleEntries move relative to the start date (no change of offset needed -> return) - // moveScheduleEntries === false: scheduleEntries stay absolutely on the scheduled calendar date (change of offset needed) - if ($period->moveScheduleEntries) { + $deltaMinutes = DateTimeUtil::differenceInMinutes($originalPeriod->start, $period->start); + if (0 === $deltaMinutes) { return; } - $deltaMinutes = DateTimeUtil::differenceInMinutes($originalPeriod->start, $period->start); - if (0 === $deltaMinutes) { + // start date shifts --> purge schedule_entries subresource to reflect changes in dates + numbers + $this->cacheManager->invalidateTags(["/api/periods/{$period->getId()}/schedule_entries"]); + + // moveScheduleEntries === true: scheduleEntries move relative to the start date (no change of offset needed -> return) + // moveScheduleEntries === false: scheduleEntries stay absolutely on the scheduled calendar date (change of offset needed) + if ($period->moveScheduleEntries) { return; } diff --git a/e2e/package.json b/e2e/package.json index 9153a8f538..8ab25f15eb 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -29,4 +29,4 @@ "overrides": { "uri-js": "npm:uri-js-replace" } -} +} \ No newline at end of file diff --git a/e2e/specs/constants.js b/e2e/specs/constants.js index e0d2bad527..6d072377f9 100644 --- a/e2e/specs/constants.js +++ b/e2e/specs/constants.js @@ -3,6 +3,12 @@ export const cachedEndpoint = Cypress.env('API_ROOT_URL_CACHED') export const grgrCampId = '3c79b99ab424' export const loremIpsumCampId = '9c2447aefe38' +export const skilagerPeriodId = '7fa4564a5d5d' +export const grgrPeriodId = '76be24bce434' + +export const harryMainPeriodId = 'fe47dfd2b541' +export const harrySecondPeriodId = 'c550b8707c26' + export const bipiUser = 'test@example.com' export const castorUser = 'castor@example.com' export const felicitySmoakUser = 'felicity@smoak.com' diff --git a/e2e/specs/httpCache/responses/schedule_entries_collection.json b/e2e/specs/httpCache/responses/schedule_entries_collection.json new file mode 100644 index 0000000000..f128f12edb --- /dev/null +++ b/e2e/specs/httpCache/responses/schedule_entries_collection.json @@ -0,0 +1,203 @@ +{ + "_links": { + "self": { + "href": "/api/periods/7fa4564a5d5d/schedule_entries.jsonhal" + }, + "items": [ + { + "href": "/api/schedule_entries/e68f4e47517a" + }, + { + "href": "/api/schedule_entries/f0883e931649" + }, + { + "href": "/api/schedule_entries/29c9e9a07d82" + }, + { + "href": "/api/schedule_entries/ee85308a97d1" + }, + { + "href": "/api/schedule_entries/f08d69cae18a" + }, + { + "href": "/api/schedule_entries/7e8086d94633" + }, + { + "href": "/api/schedule_entries/f89a1501dbb6" + } + ] + }, + "totalItems": 7, + "_embedded": { + "items": [ + { + "_links": { + "self": { + "href": "/api/schedule_entries/e68f4e47517a" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/27ad4212466a" + } + }, + "left": 0, + "width": 1, + "id": "e68f4e47517a", + "start": "2031-01-24T08:00:00+00:00", + "end": "2031-01-24T18:00:00+00:00", + "dayNumber": 1, + "scheduleEntryNumber": 1, + "number": "1.1" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/f0883e931649" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/69426b72ce46" + } + }, + "left": 0, + "width": 1, + "id": "f0883e931649", + "start": "2031-01-25T08:00:00+00:00", + "end": "2031-01-25T18:00:00+00:00", + "dayNumber": 2, + "scheduleEntryNumber": 1, + "number": "2.1" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/29c9e9a07d82" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/a13fadc97610" + }, + "day": { + "href": "/api/days/5dc6586312e0" + } + }, + "left": 0, + "width": 1, + "id": "29c9e9a07d82", + "start": "2031-01-26T08:00:00+00:00", + "end": "2031-01-26T19:00:00+00:00", + "dayNumber": 3, + "scheduleEntryNumber": 1, + "number": "3.A" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/ee85308a97d1" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/839d27065403" + } + }, + "left": 0, + "width": 1, + "id": "ee85308a97d1", + "start": "2031-01-27T08:00:00+00:00", + "end": "2031-01-27T18:00:00+00:00", + "dayNumber": 4, + "scheduleEntryNumber": 1, + "number": "4.1" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/f08d69cae18a" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/a13fadc97610" + }, + "day": { + "href": "/api/days/755cbc1d9852" + } + }, + "left": 0, + "width": 1, + "id": "f08d69cae18a", + "start": "2031-01-28T08:00:00+00:00", + "end": "2031-01-28T19:00:00+00:00", + "dayNumber": 5, + "scheduleEntryNumber": 1, + "number": "5.A" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/7e8086d94633" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/a13fadc97610" + }, + "day": { + "href": "/api/days/3a84cfaf795c" + } + }, + "left": 0, + "width": 1, + "id": "7e8086d94633", + "start": "2031-01-29T08:00:00+00:00", + "end": "2031-01-29T19:00:00+00:00", + "dayNumber": 6, + "scheduleEntryNumber": 1, + "number": "6.A" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/f89a1501dbb6" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/5e0c146f4a75" + } + }, + "left": 0, + "width": 1, + "id": "f89a1501dbb6", + "start": "2031-01-30T08:00:00+00:00", + "end": "2031-01-30T18:00:00+00:00", + "dayNumber": 7, + "scheduleEntryNumber": 1, + "number": "7.1" + } + ] + } +} \ No newline at end of file diff --git a/e2e/specs/httpCache/scheduleEntries.cy.js b/e2e/specs/httpCache/scheduleEntries.cy.js new file mode 100644 index 0000000000..73039a828e --- /dev/null +++ b/e2e/specs/httpCache/scheduleEntries.cy.js @@ -0,0 +1,187 @@ +import { + bipiUser, + bruceWayneUser, + cachedEndpoint, + castorUser, + skilagerPeriodId, + grgrPeriodId, + harryMainPeriodId, + harrySecondPeriodId, +} from '../constants' +import collectionResponse from './responses/schedule_entries_collection.json' + +const collectionXKeys = + /* campCollaboration for bipiUser */ + '10d8f02ce5b4 ' + + /* scheduleEntries + links */ + 'e68f4e47517a e68f4e47517a#period e68f4e47517a#activity e68f4e47517a#day ' + + 'f0883e931649 f0883e931649#period f0883e931649#activity f0883e931649#day ' + + '29c9e9a07d82 29c9e9a07d82#period 29c9e9a07d82#activity 29c9e9a07d82#day ' + + 'ee85308a97d1 ee85308a97d1#period ee85308a97d1#activity ee85308a97d1#day ' + + 'f08d69cae18a f08d69cae18a#period f08d69cae18a#activity f08d69cae18a#day ' + + '7e8086d94633 7e8086d94633#period 7e8086d94633#activity 7e8086d94633#day ' + + 'f89a1501dbb6 f89a1501dbb6#period f89a1501dbb6#activity f89a1501dbb6#day ' + + /* collection URI (for detecting addition of new schedule entries) */ + '/api/periods/7fa4564a5d5d/schedule_entries' + +describe('cache test: /periods/{periodId}/scheduleEntries', () => { + it('caches /periods/{periodId}/schedule_entries separately for each login', () => { + const uri = `/api/periods/${skilagerPeriodId}/schedule_entries` + + Cypress.session.clearAllSavedSessions() + cy.login(bipiUser) + + // first request is a cache miss + cy.request(`${cachedEndpoint}${uri}.jsonhal`).then((response) => { + const headers = response.headers + expect(headers.xkey).to.eq(collectionXKeys) + expect(headers['x-cache']).to.eq('MISS') + expect(response.body).to.deep.equal(collectionResponse) + }) + + // second request is a cache hit + cy.expectCacheHit(uri) + + // request with a new user is a cache miss + cy.login(bruceWayneUser) + cy.expectCacheMiss(uri) + }) + + it('invalidates /periods/{periodId}/schedule_entries for all users on scheduleEntry patch', () => { + const uri = `/api/periods/${grgrPeriodId}/schedule_entries` + const scheduleEntryId = '12f34c89ce11' + + // bring data into defined state + Cypress.session.clearAllSavedSessions() + cy.login(bipiUser) + cy.apiPatch(`/api/schedule_entries/${scheduleEntryId}`, { + start: '2025-05-10T16:00:00+00:00', + }) + + // warm up cache + cy.waitForCacheMiss(uri) + cy.expectCacheHit(uri) + + cy.login(castorUser) + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // touch scheduleEntry + cy.apiPatch(`/api/schedule_entries/${scheduleEntryId}`, { + start: '2025-05-10T17:00:00+00:00', + }) + + // ensure cache was invalidated + cy.waitForCacheMiss(uri) + cy.expectCacheHit(uri) + + cy.login(bipiUser) + cy.expectCacheMiss(uri) + }) + + it('invalidates /periods/{periodId}/schedule_entries for new scheduleEntry', () => { + const uri = `/api/periods/${grgrPeriodId}/schedule_entries` + + Cypress.session.clearAllSavedSessions() + cy.login(bipiUser) + + // warm up cache + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // add new scheduleEntry to period + cy.apiPost('/api/schedule_entries', { + start: '2025-05-10T10:00:00+00:00', + end: '2025-05-10T11:00:00+00:00', + period: `/api/periods/${grgrPeriodId}`, + activity: `/api/activities/ffd08c52288c`, + }).then((response) => { + const newScheduleEntryUri = response.body._links.self.href + + // ensure cache was invalidated + cy.waitForCacheMiss(uri) + cy.expectCacheHit(uri) + + // delete newly created scheduleEntry + cy.apiDelete(newScheduleEntryUri) + + // ensure cache was invalidated + cy.waitForCacheMiss(uri) + cy.expectCacheHit(uri) + }) + }) + + it('invalidates /periods/{periodId}/schedule_entries when moving a schedule entry to another period', () => { + const uri1 = `/api/periods/${harryMainPeriodId}/schedule_entries` + const uri2 = `/api/periods/${harrySecondPeriodId}/schedule_entries` + const scheduleEntryId = '9a4173c9bb73' + + Cypress.session.clearAllSavedSessions() + cy.login(bipiUser) + + // warm up cache + cy.expectCacheMiss(uri1) + cy.expectCacheMiss(uri2) + cy.expectCacheHit(uri1) + cy.expectCacheHit(uri2) + + // move scheduleEntry to 2nd period + cy.apiPatch(`/api/schedule_entries/${scheduleEntryId}`, { + start: '2025-08-09T15:00:00+00:00', + end: '2025-08-09T17:00:00+00:00', + period: `/periods/${harrySecondPeriodId}`, + }) + + // ensure cache was invalidated + cy.waitForCacheMiss(uri1) + cy.waitForCacheMiss(uri2) + cy.expectCacheHit(uri1) + cy.expectCacheHit(uri2) + + // move scheduleEntry back + cy.apiPatch(`/api/schedule_entries/${scheduleEntryId}`, { + start: '2025-07-20T15:00:00+00:00', + end: '2025-07-20T17:00:00+00:00', + period: `/periods/${harryMainPeriodId}`, + }) + + // ensure cache was invalidated + cy.waitForCacheMiss(uri1) + cy.waitForCacheMiss(uri2) + cy.expectCacheHit(uri1) + cy.expectCacheHit(uri2) + }) + + it('invalidates /periods/{periodId}/schedule_entries when changing the period dates', () => { + const uri = `/api/periods/${grgrPeriodId}/schedule_entries` + + Cypress.session.clearAllSavedSessions() + cy.login(bipiUser) + + // warm up cache + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // move period start date + cy.apiPatch(`/api/periods/${grgrPeriodId}`, { + start: '2025-05-09', + end: '2025-05-12', + moveScheduleEntries: true, + }) + + // ensure cache was invalidated + cy.waitForCacheMiss(uri) + cy.expectCacheHit(uri) + + // move period start date + cy.apiPatch(`/api/periods/${grgrPeriodId}`, { + start: '2025-05-10', + end: '2025-05-13', + moveScheduleEntries: true, + }) + + // ensure cache was invalidated + cy.waitForCacheMiss(uri) + cy.expectCacheHit(uri) + }) +}) From b4f09a116292619651121cc0d8df2cdfc21a9785 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 13 Apr 2025 01:26:46 +0200 Subject: [PATCH 2/5] run linter --- api/src/State/PeriodPersistProcessor.php | 14 +- e2e/package.json | 2 +- .../schedule_entries_collection.json | 402 +++++++++--------- 3 files changed, 209 insertions(+), 209 deletions(-) diff --git a/api/src/State/PeriodPersistProcessor.php b/api/src/State/PeriodPersistProcessor.php index d3ee6990aa..1a3e0f34aa 100644 --- a/api/src/State/PeriodPersistProcessor.php +++ b/api/src/State/PeriodPersistProcessor.php @@ -2,13 +2,13 @@ namespace App\State; -use FOS\HttpCacheBundle\CacheManager; -use App\Util\DateTimeUtil; -use App\State\Util\AbstractPersistProcessor; -use App\Entity\Period; -use App\Entity\Day; -use ApiPlatform\State\ProcessorInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Entity\Day; +use App\Entity\Period; +use App\State\Util\AbstractPersistProcessor; +use App\Util\DateTimeUtil; +use FOS\HttpCacheBundle\CacheManager; /** * @template-extends AbstractPersistProcessor @@ -32,7 +32,7 @@ public function onBefore($data, Operation $operation, array $uriVariables = [], return $data; } - public function moveDaysAndScheduleEntries(Period $period, ?Period $originalPeriod = null) { + public function moveDaysAndScheduleEntries(Period $period, ?Period $originalPeriod = null) { if (!$originalPeriod) { return; } diff --git a/e2e/package.json b/e2e/package.json index 8ab25f15eb..9153a8f538 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -29,4 +29,4 @@ "overrides": { "uri-js": "npm:uri-js-replace" } -} \ No newline at end of file +} diff --git a/e2e/specs/httpCache/responses/schedule_entries_collection.json b/e2e/specs/httpCache/responses/schedule_entries_collection.json index f128f12edb..c50254406f 100644 --- a/e2e/specs/httpCache/responses/schedule_entries_collection.json +++ b/e2e/specs/httpCache/responses/schedule_entries_collection.json @@ -1,203 +1,203 @@ { - "_links": { - "self": { - "href": "/api/periods/7fa4564a5d5d/schedule_entries.jsonhal" - }, - "items": [ - { - "href": "/api/schedule_entries/e68f4e47517a" - }, - { - "href": "/api/schedule_entries/f0883e931649" - }, - { - "href": "/api/schedule_entries/29c9e9a07d82" - }, - { - "href": "/api/schedule_entries/ee85308a97d1" - }, - { - "href": "/api/schedule_entries/f08d69cae18a" - }, - { - "href": "/api/schedule_entries/7e8086d94633" - }, - { - "href": "/api/schedule_entries/f89a1501dbb6" - } - ] + "_links": { + "self": { + "href": "/api/periods/7fa4564a5d5d/schedule_entries.jsonhal" }, - "totalItems": 7, - "_embedded": { - "items": [ - { - "_links": { - "self": { - "href": "/api/schedule_entries/e68f4e47517a" - }, - "period": { - "href": "/api/periods/7fa4564a5d5d" - }, - "activity": { - "href": "/api/activities/b29d387cc403" - }, - "day": { - "href": "/api/days/27ad4212466a" - } - }, - "left": 0, - "width": 1, - "id": "e68f4e47517a", - "start": "2031-01-24T08:00:00+00:00", - "end": "2031-01-24T18:00:00+00:00", - "dayNumber": 1, - "scheduleEntryNumber": 1, - "number": "1.1" - }, - { - "_links": { - "self": { - "href": "/api/schedule_entries/f0883e931649" - }, - "period": { - "href": "/api/periods/7fa4564a5d5d" - }, - "activity": { - "href": "/api/activities/b29d387cc403" - }, - "day": { - "href": "/api/days/69426b72ce46" - } - }, - "left": 0, - "width": 1, - "id": "f0883e931649", - "start": "2031-01-25T08:00:00+00:00", - "end": "2031-01-25T18:00:00+00:00", - "dayNumber": 2, - "scheduleEntryNumber": 1, - "number": "2.1" - }, - { - "_links": { - "self": { - "href": "/api/schedule_entries/29c9e9a07d82" - }, - "period": { - "href": "/api/periods/7fa4564a5d5d" - }, - "activity": { - "href": "/api/activities/a13fadc97610" - }, - "day": { - "href": "/api/days/5dc6586312e0" - } - }, - "left": 0, - "width": 1, - "id": "29c9e9a07d82", - "start": "2031-01-26T08:00:00+00:00", - "end": "2031-01-26T19:00:00+00:00", - "dayNumber": 3, - "scheduleEntryNumber": 1, - "number": "3.A" - }, - { - "_links": { - "self": { - "href": "/api/schedule_entries/ee85308a97d1" - }, - "period": { - "href": "/api/periods/7fa4564a5d5d" - }, - "activity": { - "href": "/api/activities/b29d387cc403" - }, - "day": { - "href": "/api/days/839d27065403" - } - }, - "left": 0, - "width": 1, - "id": "ee85308a97d1", - "start": "2031-01-27T08:00:00+00:00", - "end": "2031-01-27T18:00:00+00:00", - "dayNumber": 4, - "scheduleEntryNumber": 1, - "number": "4.1" - }, - { - "_links": { - "self": { - "href": "/api/schedule_entries/f08d69cae18a" - }, - "period": { - "href": "/api/periods/7fa4564a5d5d" - }, - "activity": { - "href": "/api/activities/a13fadc97610" - }, - "day": { - "href": "/api/days/755cbc1d9852" - } - }, - "left": 0, - "width": 1, - "id": "f08d69cae18a", - "start": "2031-01-28T08:00:00+00:00", - "end": "2031-01-28T19:00:00+00:00", - "dayNumber": 5, - "scheduleEntryNumber": 1, - "number": "5.A" - }, - { - "_links": { - "self": { - "href": "/api/schedule_entries/7e8086d94633" - }, - "period": { - "href": "/api/periods/7fa4564a5d5d" - }, - "activity": { - "href": "/api/activities/a13fadc97610" - }, - "day": { - "href": "/api/days/3a84cfaf795c" - } - }, - "left": 0, - "width": 1, - "id": "7e8086d94633", - "start": "2031-01-29T08:00:00+00:00", - "end": "2031-01-29T19:00:00+00:00", - "dayNumber": 6, - "scheduleEntryNumber": 1, - "number": "6.A" - }, - { - "_links": { - "self": { - "href": "/api/schedule_entries/f89a1501dbb6" - }, - "period": { - "href": "/api/periods/7fa4564a5d5d" - }, - "activity": { - "href": "/api/activities/b29d387cc403" - }, - "day": { - "href": "/api/days/5e0c146f4a75" - } - }, - "left": 0, - "width": 1, - "id": "f89a1501dbb6", - "start": "2031-01-30T08:00:00+00:00", - "end": "2031-01-30T18:00:00+00:00", - "dayNumber": 7, - "scheduleEntryNumber": 1, - "number": "7.1" - } - ] - } -} \ No newline at end of file + "items": [ + { + "href": "/api/schedule_entries/e68f4e47517a" + }, + { + "href": "/api/schedule_entries/f0883e931649" + }, + { + "href": "/api/schedule_entries/29c9e9a07d82" + }, + { + "href": "/api/schedule_entries/ee85308a97d1" + }, + { + "href": "/api/schedule_entries/f08d69cae18a" + }, + { + "href": "/api/schedule_entries/7e8086d94633" + }, + { + "href": "/api/schedule_entries/f89a1501dbb6" + } + ] + }, + "totalItems": 7, + "_embedded": { + "items": [ + { + "_links": { + "self": { + "href": "/api/schedule_entries/e68f4e47517a" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/27ad4212466a" + } + }, + "left": 0, + "width": 1, + "id": "e68f4e47517a", + "start": "2031-01-24T08:00:00+00:00", + "end": "2031-01-24T18:00:00+00:00", + "dayNumber": 1, + "scheduleEntryNumber": 1, + "number": "1.1" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/f0883e931649" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/69426b72ce46" + } + }, + "left": 0, + "width": 1, + "id": "f0883e931649", + "start": "2031-01-25T08:00:00+00:00", + "end": "2031-01-25T18:00:00+00:00", + "dayNumber": 2, + "scheduleEntryNumber": 1, + "number": "2.1" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/29c9e9a07d82" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/a13fadc97610" + }, + "day": { + "href": "/api/days/5dc6586312e0" + } + }, + "left": 0, + "width": 1, + "id": "29c9e9a07d82", + "start": "2031-01-26T08:00:00+00:00", + "end": "2031-01-26T19:00:00+00:00", + "dayNumber": 3, + "scheduleEntryNumber": 1, + "number": "3.A" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/ee85308a97d1" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/839d27065403" + } + }, + "left": 0, + "width": 1, + "id": "ee85308a97d1", + "start": "2031-01-27T08:00:00+00:00", + "end": "2031-01-27T18:00:00+00:00", + "dayNumber": 4, + "scheduleEntryNumber": 1, + "number": "4.1" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/f08d69cae18a" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/a13fadc97610" + }, + "day": { + "href": "/api/days/755cbc1d9852" + } + }, + "left": 0, + "width": 1, + "id": "f08d69cae18a", + "start": "2031-01-28T08:00:00+00:00", + "end": "2031-01-28T19:00:00+00:00", + "dayNumber": 5, + "scheduleEntryNumber": 1, + "number": "5.A" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/7e8086d94633" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/a13fadc97610" + }, + "day": { + "href": "/api/days/3a84cfaf795c" + } + }, + "left": 0, + "width": 1, + "id": "7e8086d94633", + "start": "2031-01-29T08:00:00+00:00", + "end": "2031-01-29T19:00:00+00:00", + "dayNumber": 6, + "scheduleEntryNumber": 1, + "number": "6.A" + }, + { + "_links": { + "self": { + "href": "/api/schedule_entries/f89a1501dbb6" + }, + "period": { + "href": "/api/periods/7fa4564a5d5d" + }, + "activity": { + "href": "/api/activities/b29d387cc403" + }, + "day": { + "href": "/api/days/5e0c146f4a75" + } + }, + "left": 0, + "width": 1, + "id": "f89a1501dbb6", + "start": "2031-01-30T08:00:00+00:00", + "end": "2031-01-30T18:00:00+00:00", + "dayNumber": 7, + "scheduleEntryNumber": 1, + "number": "7.1" + } + ] + } +} From bef362cd40d9917b0d424080d64995b8c38e8f82 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 13 Apr 2025 01:39:12 +0200 Subject: [PATCH 3/5] fix tests --- api/tests/State/PeriodPersistProcessorTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/tests/State/PeriodPersistProcessorTest.php b/api/tests/State/PeriodPersistProcessorTest.php index 92b6d20d05..a975e679be 100644 --- a/api/tests/State/PeriodPersistProcessorTest.php +++ b/api/tests/State/PeriodPersistProcessorTest.php @@ -11,6 +11,7 @@ use App\Entity\ScheduleEntry; use App\State\PeriodPersistProcessor; use Doctrine\ORM\EntityManagerInterface; +use FOS\HttpCacheBundle\CacheManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -23,8 +24,6 @@ class PeriodPersistProcessorTest extends TestCase { private ScheduleEntry $scheduleEntry; private DayResponsible $dayResponsible; - private EntityManagerInterface|MockObject $em; - private PeriodPersistProcessor $processor; /** @@ -53,11 +52,11 @@ protected function setUp(): void { $day2->addDayResponsible($this->dayResponsible); $decoratedProcessor = $this->createMock(ProcessorInterface::class); - $this->em = $this->createMock(EntityManagerInterface::class); + $cacheManager = $this->createMock(CacheManager::class); $this->processor = new PeriodPersistProcessor( $decoratedProcessor, - $this->em + $cacheManager ); } From 745c981e851b65f6a5b5f92907e29a0ee143b550 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 13 Apr 2025 01:41:18 +0200 Subject: [PATCH 4/5] cs fixer --- api/tests/State/PeriodPersistProcessorTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/tests/State/PeriodPersistProcessorTest.php b/api/tests/State/PeriodPersistProcessorTest.php index a975e679be..f46aaa3193 100644 --- a/api/tests/State/PeriodPersistProcessorTest.php +++ b/api/tests/State/PeriodPersistProcessorTest.php @@ -10,9 +10,7 @@ use App\Entity\Period; use App\Entity\ScheduleEntry; use App\State\PeriodPersistProcessor; -use Doctrine\ORM\EntityManagerInterface; use FOS\HttpCacheBundle\CacheManager; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** From 079e06ee868e272e4d314645c5fc0f6efb974dcd Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 13 Apr 2025 10:44:59 +0200 Subject: [PATCH 5/5] add period id in tag-list, when normalizing a ScheduleEntry --- api/src/HttpCache/TagCollector.php | 7 +++++++ api/src/State/PeriodPersistProcessor.php | 21 +++++++------------ .../State/PeriodPersistProcessorTest.php | 5 +---- e2e/specs/httpCache/scheduleEntries.cy.js | 3 ++- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 490be4437e..bec2fba34f 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -8,6 +8,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Serializer\TagCollectorInterface; use App\Entity\HasId; +use App\Entity\ScheduleEntry; /** * Collects cache tags during normalization. @@ -48,6 +49,12 @@ public function collect(array $context = []): void { } $this->addCacheTagForResource($iri); + + // add resource specific tags + if ($object instanceof ScheduleEntry) { + // various content of ScheduleEntry such as Start, DayNumber, ScheduleNumber is calculated from period properties + $this->addCacheTagForResource($object->getPeriod()->getId()); + } } private function addCacheTagForResource(string $iri): void { diff --git a/api/src/State/PeriodPersistProcessor.php b/api/src/State/PeriodPersistProcessor.php index 1a3e0f34aa..7bfdd862d4 100644 --- a/api/src/State/PeriodPersistProcessor.php +++ b/api/src/State/PeriodPersistProcessor.php @@ -8,15 +8,13 @@ use App\Entity\Period; use App\State\Util\AbstractPersistProcessor; use App\Util\DateTimeUtil; -use FOS\HttpCacheBundle\CacheManager; /** * @template-extends AbstractPersistProcessor */ class PeriodPersistProcessor extends AbstractPersistProcessor { public function __construct( - ProcessorInterface $decorated, - private readonly CacheManager $cacheManager + ProcessorInterface $decorated ) { parent::__construct($decorated); } @@ -25,32 +23,29 @@ public function __construct( * @param Period $data */ public function onBefore($data, Operation $operation, array $uriVariables = [], array $context = []): Period { - $this->moveDaysAndScheduleEntries($data, $context['previous_data'] ?? null); + self::moveDaysAndScheduleEntries($data, $context['previous_data'] ?? null); self::removeExtraDays($data); self::addMissingDays($data); return $data; } - public function moveDaysAndScheduleEntries(Period $period, ?Period $originalPeriod = null) { + public static function moveDaysAndScheduleEntries(Period $period, ?Period $originalPeriod = null) { if (!$originalPeriod) { return; } - $deltaMinutes = DateTimeUtil::differenceInMinutes($originalPeriod->start, $period->start); - if (0 === $deltaMinutes) { - return; - } - - // start date shifts --> purge schedule_entries subresource to reflect changes in dates + numbers - $this->cacheManager->invalidateTags(["/api/periods/{$period->getId()}/schedule_entries"]); - // moveScheduleEntries === true: scheduleEntries move relative to the start date (no change of offset needed -> return) // moveScheduleEntries === false: scheduleEntries stay absolutely on the scheduled calendar date (change of offset needed) if ($period->moveScheduleEntries) { return; } + $deltaMinutes = DateTimeUtil::differenceInMinutes($originalPeriod->start, $period->start); + if (0 === $deltaMinutes) { + return; + } + // Move ScheduleEntries // --> existing scheduleEntries outside the new period boundary are not possible (validation should have already failed) foreach ($period->scheduleEntries as $scheduleEntry) { diff --git a/api/tests/State/PeriodPersistProcessorTest.php b/api/tests/State/PeriodPersistProcessorTest.php index f46aaa3193..d996cf1425 100644 --- a/api/tests/State/PeriodPersistProcessorTest.php +++ b/api/tests/State/PeriodPersistProcessorTest.php @@ -10,7 +10,6 @@ use App\Entity\Period; use App\Entity\ScheduleEntry; use App\State\PeriodPersistProcessor; -use FOS\HttpCacheBundle\CacheManager; use PHPUnit\Framework\TestCase; /** @@ -50,11 +49,9 @@ protected function setUp(): void { $day2->addDayResponsible($this->dayResponsible); $decoratedProcessor = $this->createMock(ProcessorInterface::class); - $cacheManager = $this->createMock(CacheManager::class); $this->processor = new PeriodPersistProcessor( - $decoratedProcessor, - $cacheManager + $decoratedProcessor ); } diff --git a/e2e/specs/httpCache/scheduleEntries.cy.js b/e2e/specs/httpCache/scheduleEntries.cy.js index 73039a828e..027d47bcd0 100644 --- a/e2e/specs/httpCache/scheduleEntries.cy.js +++ b/e2e/specs/httpCache/scheduleEntries.cy.js @@ -14,7 +14,8 @@ const collectionXKeys = /* campCollaboration for bipiUser */ '10d8f02ce5b4 ' + /* scheduleEntries + links */ - 'e68f4e47517a e68f4e47517a#period e68f4e47517a#activity e68f4e47517a#day ' + + /* the first scheduleEntry also includes the period id 7fa4564a5d5d */ + 'e68f4e47517a 7fa4564a5d5d e68f4e47517a#period e68f4e47517a#activity e68f4e47517a#day ' + 'f0883e931649 f0883e931649#period f0883e931649#activity f0883e931649#day ' + '29c9e9a07d82 29c9e9a07d82#period 29c9e9a07d82#activity 29c9e9a07d82#day ' + 'ee85308a97d1 ee85308a97d1#period ee85308a97d1#activity ee85308a97d1#day ' +