Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions api/src/HttpCache/TagCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Serializer\TagCollectorInterface;
use App\Entity\HasId;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;

/**
* Collects cache tags during normalization.
Expand All @@ -17,7 +20,10 @@
class TagCollector implements TagCollectorInterface {
public const IRI_RELATION_DELIMITER = '#';

public function __construct(private ResponseTagger $responseTagger) {}
public function __construct(
private ResponseTagger $responseTagger,
private EntityManagerInterface $em
) {}

/**
* Collect cache tags for cache invalidation.
Expand All @@ -37,7 +43,7 @@ public function collect(array $context = []): void {
}

if (isset($context['property_metadata'])) {
$this->addCacheTagsForRelation($context, $iri, $context['property_metadata']);
$this->addCacheTagsForRelation($object, $context, $iri, $context['property_metadata']);

return;
}
Expand All @@ -59,7 +65,7 @@ private function addCacheTagForResource(string $iri): void {
$this->responseTagger->addTags([$iri]);
}

private function addCacheTagsForRelation(array $context, string $iri, ApiProperty $propertyMetadata): void {
private function addCacheTagsForRelation(mixed $object, array $context, string $iri, ApiProperty $propertyMetadata): void {
if (isset($propertyMetadata->getExtraProperties()['cacheDependencies'])) {
foreach ($propertyMetadata->getExtraProperties()['cacheDependencies'] as $dependency) {
$cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$dependency;
Expand All @@ -69,6 +75,13 @@ private function addCacheTagsForRelation(array $context, string $iri, ApiPropert
return;
}

$associationMappings = $this->em->getClassMetadata(ClassUtils::getClass($object))->getAssociationMappings();
$associationMapping = $associationMappings[$context['api_attribute']] ?? null;
// we have currently no use-case where we rely on a ManyToOne for tag invalidation
if ($associationMapping instanceof AssociationMapping && $associationMapping->isManyToOne()) {
return;
}

$cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$context['api_attribute'];
$this->responseTagger->addTags([$cacheTag]);
}
Expand Down
10 changes: 9 additions & 1 deletion api/tests/HttpCache/TagCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use App\HttpCache\ResponseTagger;
use App\HttpCache\TagCollector;
use App\Tests\HttpCache\Entity\Dummy;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
Expand All @@ -20,11 +22,17 @@ class TagCollectorTest extends TestCase {

private TagCollectorInterface $tagCollector;
private ObjectProphecy $responseTaggerProphecy;
private ObjectProphecy $em;
private ObjectProphecy $classMetadata;

protected function setUp(): void {
// given
$this->responseTaggerProphecy = $this->prophesize(ResponseTagger::class);
$this->tagCollector = new TagCollector($this->responseTaggerProphecy->reveal());
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->classMetadata = $this->prophesize(ClassMetadata::class);
$this->em->getClassMetadata(Argument::any())->willReturn($this->classMetadata);
$this->classMetadata->getAssociationMappings(Argument::any())->willReturn([]);
$this->tagCollector = new TagCollector($this->responseTaggerProphecy->reveal(), $this->em->reveal());
}

public function testNoTagForEmptyContext() {
Expand Down
45 changes: 22 additions & 23 deletions e2e/specs/httpCache/activities.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,48 @@ const collectionXKeys =
/**
* activitiy "Snowboardfahren"
*/
'a13fadc97610 a13fadc97610#scheduleEntries a13fadc97610#camp a13fadc97610#category a13fadc97610#progressLabel a13fadc97610#activityResponsibles a13fadc97610#rootContentNode ' +
'a13fadc97610 a13fadc97610#scheduleEntries a13fadc97610#activityResponsibles a13fadc97610#rootContentNode ' +
/* embedded progress labels: */
'af92782262d7 af92782262d7#camp a13fadc97610#embeddedProgressLabel ' +
'af92782262d7 ' +
'a13fadc97610#embeddedProgressLabel ' +
/* embedded schedule entries: */
/* (the first schedule entry also includes the period id) */
'29c9e9a07d82 7fa4564a5d5d 29c9e9a07d82#period 29c9e9a07d82#activity 29c9e9a07d82#day ' +
'f08d69cae18a f08d69cae18a#period f08d69cae18a#activity f08d69cae18a#day ' +
'7e8086d94633 7e8086d94633#period 7e8086d94633#activity 7e8086d94633#day ' +
'29c9e9a07d82 7fa4564a5d5d 29c9e9a07d82#day ' +
'f08d69cae18a f08d69cae18a#day ' +
'7e8086d94633 7e8086d94633#day ' +
'a13fadc97610#embeddedScheduleEntries ' +
/* embedded activitiy responsibles: */
'06743ccfeedd 06743ccfeedd#activity 06743ccfeedd#campCollaboration ' +
'21bc6661c569 21bc6661c569#activity 21bc6661c569#campCollaboration ' +
'a13fadc97610#embeddedActivityResponsibles a13fadc97610#contentNodes ' +
'06743ccfeedd ' +
'21bc6661c569 ' +
'a13fadc97610#embeddedActivityResponsibles ' +
'a13fadc97610#contentNodes ' +
/**
* activity "Skifahren"
*/
'b29d387cc403 b29d387cc403#scheduleEntries b29d387cc403#camp b29d387cc403#category b29d387cc403#progressLabel b29d387cc403#activityResponsibles ' +
'b29d387cc403 b29d387cc403#scheduleEntries b29d387cc403#activityResponsibles b29d387cc403#rootContentNode ' +
/* embedded progress labels: */
'b29d387cc403#rootContentNode b29d387cc403#embeddedProgressLabel ' +
'b29d387cc403#embeddedProgressLabel ' +
/* embedded schedule entries: */
'e68f4e47517a e68f4e47517a#period e68f4e47517a#activity e68f4e47517a#day ' +
'f0883e931649 f0883e931649#period f0883e931649#activity f0883e931649#day ' +
'ee85308a97d1 ee85308a97d1#period ee85308a97d1#activity ee85308a97d1#day ' +
'f89a1501dbb6 f89a1501dbb6#period f89a1501dbb6#activity f89a1501dbb6#day ' +
'e68f4e47517a e68f4e47517a#day ' +
'f0883e931649 f0883e931649#day ' +
'ee85308a97d1 ee85308a97d1#day ' +
'f89a1501dbb6 f89a1501dbb6#day ' +
/* embedded activitiy responsibles: */
'b29d387cc403#embeddedScheduleEntries ' +
'a9a760e36fd8 a9a760e36fd8#activity a9a760e36fd8#campCollaboration b29d387cc403#embeddedActivityResponsibles ' +
'a9a760e36fd8 ' +
'b29d387cc403#embeddedActivityResponsibles ' +
'b29d387cc403#contentNodes ' +
/* collection URI (for detecting addition of new activities) */
'/api/camps/70ca971c992f/activities'

describe('cache test: /camps/{campId}/activities', () => {
beforeEach(() => {
cy.wrap(Cypress.session.clearAllSavedSessions())
})

it('caches /camps/{campId}/activities separately for each login', () => {
const uri = `/api/camps/${skilagerCampId}/activities`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// first request is a cache miss
Expand All @@ -75,7 +81,6 @@ describe('cache test: /camps/{campId}/activities', () => {
const activityId = '3d1e5c91ceb2'

// bring data into defined state
Cypress.session.clearAllSavedSessions()
cy.login(bruceWayneUser)
cy.apiPatch(`/api/activities/${activityId}`, {
title: 'Breakfast',
Expand Down Expand Up @@ -105,7 +110,6 @@ describe('cache test: /camps/{campId}/activities', () => {
it('invalidates /camps/{campId}/activities for new activity', () => {
const uri = `/api/camps/${grgrCampId}/activities`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down Expand Up @@ -142,7 +146,6 @@ describe('cache test: /camps/{campId}/activities', () => {
it('invalidates /camps/{campId}/activities when adding a scheduleEntry', () => {
const uri = `/api/camps/${grgrCampId}/activities`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down Expand Up @@ -176,7 +179,6 @@ describe('cache test: /camps/{campId}/activities', () => {
const progressLabelId = '82547049ea38'

// bring data into defined state
Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)
cy.apiPatch(`/api/activity_progress_labels/${progressLabelId}`, {
title: 'Planned',
Expand All @@ -199,7 +201,6 @@ describe('cache test: /camps/{campId}/activities', () => {
it('invalidates /camps/{campId}/activities when adding an activity responsible', () => {
const uri = `/api/camps/${grgrCampId}/activities`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down Expand Up @@ -229,7 +230,6 @@ describe('cache test: /camps/{campId}/activities', () => {
it('invalidates /camps/{campId}/activities when changing the period dates (moveScheduleEntries: true)', () => {
const uri = `/api/camps/${grgrCampId}/activities`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down Expand Up @@ -262,7 +262,6 @@ describe('cache test: /camps/{campId}/activities', () => {
it('invalidates /camps/{campId}/activities when changing the period dates (moveScheduleEntries: false)', () => {
const uri = `/api/camps/${grgrCampId}/activities`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down
16 changes: 8 additions & 8 deletions e2e/specs/httpCache/categories.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,24 @@ const collectionXKeys =
/* campCollaboration for bipiUser */
'b0bdb7202a9d ' +
/* Category ES */
'ebfd46a1c181 ebfd46a1c181#camp ebfd46a1c181#preferredContentTypes ebfd46a1c181#rootContentNode ebfd46a1c181#contentNodes ' +
'ebfd46a1c181 ebfd46a1c181#preferredContentTypes ebfd46a1c181#rootContentNode ebfd46a1c181#contentNodes ' +
/* Category LA */
'1a869b162875 1a869b162875#camp 1a869b162875#preferredContentTypes 1a869b162875#rootContentNode 1a869b162875#contentNodes ' +
'1a869b162875 1a869b162875#preferredContentTypes 1a869b162875#rootContentNode 1a869b162875#contentNodes ' +
/* Category LP */
'dfa531302823 dfa531302823#camp dfa531302823#preferredContentTypes dfa531302823#rootContentNode dfa531302823#contentNodes ' +
'dfa531302823 dfa531302823#preferredContentTypes dfa531302823#rootContentNode dfa531302823#contentNodes ' +
/* Category LS */
'a023e85227ac a023e85227ac#camp a023e85227ac#preferredContentTypes a023e85227ac#rootContentNode a023e85227ac#contentNodes ' +
'a023e85227ac a023e85227ac#preferredContentTypes a023e85227ac#rootContentNode a023e85227ac#contentNodes ' +
/* collection URI (for detecting addition of new categories) */
'/api/camps/3c79b99ab424/categories'

describe('cache test: /camps/{campId}/categories', () => {
beforeEach(() => {
cy.wrap(Cypress.session.clearAllSavedSessions())
})

it('caches /camps/{campId}/categories separately for each login', () => {
const uri = `/api/camps/${grgrCampId}/categories`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// first request is a cache miss
Expand All @@ -50,7 +53,6 @@ describe('cache test: /camps/{campId}/categories', () => {
const uri = `/api/camps/${loremIpsumCampId}/categories`

// bring data into defined state
Cypress.session.clearAllSavedSessions()
cy.login(bruceWayneUser)
cy.apiPatch('/api/categories/c5e1bc565094', {
name: 'old_name',
Expand Down Expand Up @@ -80,7 +82,6 @@ describe('cache test: /camps/{campId}/categories', () => {
it('invalidates /camps/{campId}/categories for new category', () => {
const uri = `/api/camps/${grgrCampId}/categories`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down Expand Up @@ -111,7 +112,6 @@ describe('cache test: /camps/{campId}/categories', () => {
})

it('invalidates cached data when user leaves a camp', () => {
Cypress.session.clearAllSavedSessions()
const uri = `/api/camps/${grgrCampId}/categories`

cy.intercept('PATCH', '/api/camp_collaborations/**').as('camp_collaboration')
Expand Down
9 changes: 5 additions & 4 deletions e2e/specs/httpCache/checklists.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ const collectionXKeys =
/* campCollaboration for bipiUser */
'146c0608237f ' +
/* checklist entry */
'ebbd0c61eb85 ebbd0c61eb85#camp ' +
'ebbd0c61eb85 ' +
/* collection URI (for detecting addition of new checklists) */
'/api/camps/5d28f99890bc/checklists'

describe('cache test: /camps/checklists', () => {
beforeEach(() => {
cy.wrap(Cypress.session.clearAllSavedSessions())
})

it('caches /camp/{campId}/checklists separately for each login', () => {
const uri = `/api/camps/${basiskursCampId}/checklists`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// first request is a cache miss
Expand All @@ -36,7 +39,6 @@ describe('cache test: /camps/checklists', () => {
const uri = `/api/camps/${basiskursCampId}/checklists`

// bring data into defined state
Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)
cy.apiPatch('/api/checklists/ebbd0c61eb85', {
name: 'Training targets',
Expand All @@ -62,7 +64,6 @@ describe('cache test: /camps/checklists', () => {
() => {
const uri = `/api/camps/${basiskursCampId}/checklists`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down
6 changes: 4 additions & 2 deletions e2e/specs/httpCache/content-types.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ const collectionXKeys =
'a4211c11211c c462edd869f3 5e2028c55ee4 a4211c112939 f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types'

describe('cache test: /content-types', () => {
beforeEach(() => {
cy.wrap(Cypress.session.clearAllSavedSessions())
})

it('caches collection separately for each login', () => {
const uri = '/api/content_types'

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// first request is a cache miss
Expand All @@ -32,7 +35,6 @@ describe('cache test: /content-types', () => {
const contentTypeId = '318e064ea0c9'
const uri = `/api/content_types/${contentTypeId}`

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// first request is a cache miss
Expand Down
23 changes: 11 additions & 12 deletions e2e/specs/httpCache/scheduleEntries.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,24 @@ const collectionXKeys =
'10d8f02ce5b4 ' +
/* scheduleEntries + links */
/* 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 ' +
'f08d69cae18a f08d69cae18a#period f08d69cae18a#activity f08d69cae18a#day ' +
'7e8086d94633 7e8086d94633#period 7e8086d94633#activity 7e8086d94633#day ' +
'f89a1501dbb6 f89a1501dbb6#period f89a1501dbb6#activity f89a1501dbb6#day ' +
'e68f4e47517a 7fa4564a5d5d e68f4e47517a#day ' +
'f0883e931649 f0883e931649#day ' +
'29c9e9a07d82 29c9e9a07d82#day ' +
'ee85308a97d1 ee85308a97d1#day ' +
'f08d69cae18a f08d69cae18a#day ' +
'7e8086d94633 7e8086d94633#day ' +
'f89a1501dbb6 f89a1501dbb6#day ' +
/* collection URI (for detecting addition of new schedule entries) */
'/api/periods/7fa4564a5d5d/schedule_entries'

describe('cache test: /periods/{periodId}/scheduleEntries', () => {
beforeEach(() => {
cy.wrap(Cypress.session.clearAllSavedSessions())
})

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
Expand All @@ -53,7 +56,6 @@ describe('cache test: /periods/{periodId}/scheduleEntries', () => {
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',
Expand Down Expand Up @@ -83,7 +85,6 @@ describe('cache test: /periods/{periodId}/scheduleEntries', () => {
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
Expand Down Expand Up @@ -117,7 +118,6 @@ describe('cache test: /periods/{periodId}/scheduleEntries', () => {
const uri2 = `/api/periods/${harrySecondPeriodId}/schedule_entries`
const scheduleEntryId = '9a4173c9bb73'

Cypress.session.clearAllSavedSessions()
cy.login(bipiUser)

// warm up cache
Expand Down Expand Up @@ -156,7 +156,6 @@ describe('cache test: /periods/{periodId}/scheduleEntries', () => {
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
Expand Down
4 changes: 3 additions & 1 deletion e2e/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import 'cypress-wait-until'

Cypress.Commands.add('login', (identifier) => {
cy.session(identifier, () => {
const randomSessionId =
Date.now().toString(36) + Math.random().toString(36).substring(2)
cy.session(randomSessionId, () => {
cy.request({
method: 'POST',
url: Cypress.env('API_ROOT_URL') + '/authentication_token',
Expand Down
Loading