diff --git a/phpstan-baseline-7.4.neon b/phpstan-baseline-7.4.neon index 1b464cbfcd..9b750ce1a2 100644 --- a/phpstan-baseline-7.4.neon +++ b/phpstan-baseline-7.4.neon @@ -216,6 +216,12 @@ parameters: count: 1 path: src/lib/MVC/Symfony/Matcher/ContentBased/UrlAlias.php + - + message: '#^PHPDoc tag @throws with type Ibexa\\Contracts\\Core\\Repository\\Exceptions\\InvalidArgumentException\|Psr\\Cache\\InvalidArgumentException is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 1 + path: src/lib/Persistence/Cache/ContentHandler.php + - message: '#^PHPDoc tag @throws with type Psr\\Cache\\InvalidArgumentException is not subtype of Throwable$#' identifier: throws.notThrowable diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 58cf6c53fd..3930bf00ed 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -60516,12 +60516,6 @@ parameters: count: 1 path: tests/lib/Persistence/Cache/ContentHandlerTest.php - - - message: '#^Method Ibexa\\Tests\\Core\\Persistence\\Cache\\ContentHandlerTest\:\:testDeleteContent\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/lib/Persistence/Cache/ContentHandlerTest.php - - message: '#^Method Ibexa\\Tests\\Core\\Persistence\\Cache\\ContentLanguageHandlerTest\:\:providerForCachedLoadMethodsHit\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/src/lib/Persistence/Cache/ContentHandler.php b/src/lib/Persistence/Cache/ContentHandler.php index 68d8027f02..29764f9a8b 100644 --- a/src/lib/Persistence/Cache/ContentHandler.php +++ b/src/lib/Persistence/Cache/ContentHandler.php @@ -329,18 +329,40 @@ public function updateMetadata($contentId, MetadataUpdateStruct $struct) } /** - * {@inheritdoc} + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Psr\Cache\InvalidArgumentException */ - public function updateContent($contentId, $versionNo, UpdateStruct $struct) + public function updateContent($contentId, $versionNo, UpdateStruct $struct): Content { $this->logger->logCall(__METHOD__, ['content' => $contentId, 'version' => $versionNo, 'struct' => $struct]); $content = $this->persistenceHandler->contentHandler()->updateContent($contentId, $versionNo, $struct); - $this->cache->invalidateTags([ - $this->cacheIdentifierGenerator->generateTag( + $locations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentId); + $locationTags = array_map(function (Content\Location $location): string { + return $this->cacheIdentifierGenerator->generateTag(self::LOCATION_IDENTIFIER, [$location->id]); + }, $locations); + $locationPathTags = array_map(function (Content\Location $location): string { + return $this->cacheIdentifierGenerator->generateTag(self::LOCATION_PATH_IDENTIFIER, [$location->id]); + }, $locations); + + $versionTags = []; + $versionTags[] = $this->cacheIdentifierGenerator->generateTag( + self::CONTENT_VERSION_IDENTIFIER, + [$contentId, $versionNo] + ); + + if ($versionNo > 1) { + $versionTags[] = $this->cacheIdentifierGenerator->generateTag( self::CONTENT_VERSION_IDENTIFIER, - [$contentId, $versionNo] - ), - ]); + [$contentId, $versionNo - 1] + ); + } + + $tags = array_merge( + $locationTags, + $locationPathTags, + $versionTags + ); + $this->cache->invalidateTags($tags); return $content; } @@ -357,6 +379,7 @@ public function deleteContent($contentId) $contentId, APIRelation::FIELD | APIRelation::ASSET ); + $contentLocations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentId); $return = $this->persistenceHandler->contentHandler()->deleteContent($contentId); @@ -372,6 +395,10 @@ function ($relation) { $tags = []; } $tags[] = $this->cacheIdentifierGenerator->generateTag(self::CONTENT_IDENTIFIER, [$contentId]); + foreach ($contentLocations as $location) { + $tags[] = $this->cacheIdentifierGenerator->generateTag(self::LOCATION_IDENTIFIER, [$location->id]); + } + $this->cache->invalidateTags($tags); return $return; diff --git a/src/lib/Persistence/Cache/TrashHandler.php b/src/lib/Persistence/Cache/TrashHandler.php index a8f5fdd39f..23a4c7b144 100644 --- a/src/lib/Persistence/Cache/TrashHandler.php +++ b/src/lib/Persistence/Cache/TrashHandler.php @@ -8,12 +8,15 @@ use Ibexa\Contracts\Core\Persistence\Content\Location\Trash\Handler as TrashHandlerInterface; use Ibexa\Contracts\Core\Persistence\Content\Relation; +use Ibexa\Contracts\Core\Persistence\Content\VersionInfo; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; class TrashHandler extends AbstractHandler implements TrashHandlerInterface { private const EMPTY_TRASH_BULK_SIZE = 100; private const CONTENT_IDENTIFIER = 'content'; + private const CONTENT_VERSION_IDENTIFIER = 'content_version'; + private const LOCATION_IDENTIFIER = 'location'; private const LOCATION_PATH_IDENTIFIER = 'location_path'; /** @@ -34,8 +37,11 @@ public function trashSubtree($locationId) $this->logger->logCall(__METHOD__, ['locationId' => $locationId]); $location = $this->persistenceHandler->locationHandler()->load($locationId); - $reverseRelations = $this->persistenceHandler->contentHandler()->loadRelations($location->contentId); + $contentId = $location->contentId; + $contentHandler = $this->persistenceHandler->contentHandler(); + $reverseRelations = $contentHandler->loadRelations($contentId); + $versions = $contentHandler->listVersions($contentId); $return = $this->persistenceHandler->trashHandler()->trashSubtree($locationId); $relationTags = []; @@ -48,12 +54,21 @@ public function trashSubtree($locationId) }, $reverseRelations); } + $versionTags = array_map(function (VersionInfo $versionInfo) use ($contentId): string { + return $this->cacheIdentifierGenerator->generateTag( + self::CONTENT_VERSION_IDENTIFIER, + [$contentId, $versionInfo->versionNo] + ); + }, $versions); + $tags = array_merge( + $versionTags, + $relationTags, [ - $this->cacheIdentifierGenerator->generateTag(self::CONTENT_IDENTIFIER, [$location->contentId]), + $this->cacheIdentifierGenerator->generateTag(self::CONTENT_IDENTIFIER, [$contentId]), $this->cacheIdentifierGenerator->generateTag(self::LOCATION_PATH_IDENTIFIER, [$locationId]), - ], - $relationTags + $this->cacheIdentifierGenerator->generateTag(self::LOCATION_IDENTIFIER, [$locationId]), + ] ); $this->cache->invalidateTags(array_values(array_unique($tags))); diff --git a/tests/lib/Persistence/Cache/ContentHandlerTest.php b/tests/lib/Persistence/Cache/ContentHandlerTest.php index 7ce7d7bd6a..537115fb8e 100644 --- a/tests/lib/Persistence/Cache/ContentHandlerTest.php +++ b/tests/lib/Persistence/Cache/ContentHandlerTest.php @@ -10,6 +10,7 @@ use Ibexa\Contracts\Core\Persistence\Content\ContentInfo; use Ibexa\Contracts\Core\Persistence\Content\CreateStruct; use Ibexa\Contracts\Core\Persistence\Content\Handler as SPIContentHandler; +use Ibexa\Contracts\Core\Persistence\Content\Location\Handler as PersistenceLocationHandler; use Ibexa\Contracts\Core\Persistence\Content\MetadataUpdateStruct; use Ibexa\Contracts\Core\Persistence\Content\Relation; use Ibexa\Contracts\Core\Persistence\Content\Relation as SPIRelation; @@ -48,7 +49,8 @@ public function providerForUnCachedMethods(): array ['setStatus', [2, 0, 1], [['content_version', [2, 1], false]], null, ['c-2-v-1']], ['setStatus', [2, 1, 1], [['content', [2], false]], null, ['c-2']], ['updateMetadata', [2, new MetadataUpdateStruct()], [['content', [2], false]], null, ['c-2']], - ['updateContent', [2, 1, new UpdateStruct()], [['content_version', [2, 1], false]], null, ['c-2-v-1']], + //updateContent has its own test now due to relations complexity + //['updateContent', [2, 1, new UpdateStruct()], [['content_version', [2, 1], false]], null, ['c-2-v-1']], //['deleteContent', [2]], own tests for relations complexity ['deleteVersion', [2, 1], [['content_version', [2, 1], false]], null, ['c-2-v-1']], ['addRelation', [new RelationCreateStruct(['destinationContentId' => 2, 'sourceContentId' => 4])], [['content', [2], false], ['content', [4], false]], null, ['c-2', 'c-4']], @@ -437,53 +439,135 @@ public function providerForCachedLoadMethodsMiss(): array ]; } - public function testDeleteContent() + public function testUpdateContent(): void { $this->loggerMock->expects($this->once())->method('logCall'); - $innerHandlerMock = $this->createMock(SPIContentHandler::class); - $this->persistenceHandlerMock - ->expects($this->exactly(2)) - ->method('contentHandler') - ->willReturn($innerHandlerMock); + $innerContentHandlerMock = $this->createMock(SPIContentHandler::class); + $innerLocationHandlerMock = $this->createMock(PersistenceLocationHandler::class); - $innerHandlerMock - ->expects($this->once()) - ->method('loadReverseRelations') - ->with(2, APIRelation::FIELD | APIRelation::ASSET) - ->willReturn( + $this->prepareHandlerMocks( + $innerContentHandlerMock, + $innerLocationHandlerMock, + 1, + 1, + 0 + ); + + $innerContentHandlerMock + ->expects(self::once()) + ->method('updateContent') + ->with(2, 1, new UpdateStruct()) + ->willReturn(new Content()); + + $this->cacheIdentifierGeneratorMock + ->expects(self::exactly(5)) + ->method('generateTag') + ->willReturnMap( [ - new SPIRelation(['sourceContentId' => 42]), + ['location', [3], false, 'l-3'], + ['location', [4], false, 'l-4'], + ['location_path', [3], false, 'lp-3'], + ['location_path', [4], false, 'lp-4'], + ['content_version', [2, 1], false, 'c-2-v-1'], ] ); - $innerHandlerMock - ->expects($this->once()) + $this->cacheMock + ->expects(self::once()) + ->method('invalidateTags') + ->with(['l-3', 'l-4', 'lp-3', 'lp-4', 'c-2-v-1']); + + $handler = $this->persistenceCacheHandler->contentHandler(); + $handler->updateContent(2, 1, new UpdateStruct()); + } + + public function testDeleteContent(): void + { + $this->loggerMock->expects(self::once())->method('logCall'); + + $innerContentHandlerMock = $this->createMock(SPIContentHandler::class); + $innerLocationHandlerMock = $this->createMock(PersistenceLocationHandler::class); + + $this->prepareHandlerMocks( + $innerContentHandlerMock, + $innerLocationHandlerMock, + 2 + ); + + $innerContentHandlerMock + ->expects(self::once()) ->method('deleteContent') ->with(2) ->willReturn(true); $this->cacheMock - ->expects($this->never()) + ->expects(self::never()) ->method('deleteItem'); $this->cacheIdentifierGeneratorMock - ->expects($this->exactly(2)) + ->expects(self::exactly(4)) ->method('generateTag') ->withConsecutive( ['content', [42], false], - ['content', [2], false] + ['content', [2], false], + ['location', [3], false], + ['location', [4], false] ) - ->willReturnOnConsecutiveCalls('c-42', 'c-2'); + ->willReturnOnConsecutiveCalls('c-42', 'c-2', 'l-3', 'l-4'); $this->cacheMock - ->expects($this->once()) + ->expects(self::once()) ->method('invalidateTags') - ->with(['c-42', 'c-2']); + ->with(['c-42', 'c-2', 'l-3', 'l-4']); $handler = $this->persistenceCacheHandler->contentHandler(); $handler->deleteContent(2); } + + /** + * @param \Ibexa\Contracts\Core\Persistence\Content\Handler|\PHPUnit\Framework\MockObject\MockObject $innerContentHandlerMock + * @param \Ibexa\Contracts\Core\Persistence\Content\Location\Handler|\PHPUnit\Framework\MockObject\MockObject $innerLocationHandlerMock + */ + private function prepareHandlerMocks( + SPIContentHandler $innerContentHandlerMock, + PersistenceLocationHandler $innerLocationHandlerMock, + int $contentHandlerCount = 1, + int $locationHandlerCount = 1, + int $loadReverseRelationsCount = 1, + int $loadLocationsByContentCount = 1 + ): void { + $this->persistenceHandlerMock + ->expects(self::exactly($contentHandlerCount)) + ->method('contentHandler') + ->willReturn($innerContentHandlerMock); + + $innerContentHandlerMock + ->expects(self::exactly($loadReverseRelationsCount)) + ->method('loadReverseRelations') + ->with(2, APIRelation::FIELD | APIRelation::ASSET) + ->willReturn( + [ + new SPIRelation(['sourceContentId' => 42]), + ] + ); + + $this->persistenceHandlerMock + ->expects(self::exactly($locationHandlerCount)) + ->method('locationHandler') + ->willReturn($innerLocationHandlerMock); + + $innerLocationHandlerMock + ->expects(self::exactly($loadLocationsByContentCount)) + ->method('loadLocationsByContent') + ->with(2) + ->willReturn( + [ + new Content\Location(['id' => 3]), + new Content\Location(['id' => 4]), + ] + ); + } } class_alias(ContentHandlerTest::class, 'eZ\Publish\Core\Persistence\Cache\Tests\ContentHandlerTest'); diff --git a/tests/lib/Persistence/Cache/TrashHandlerTest.php b/tests/lib/Persistence/Cache/TrashHandlerTest.php index dcfc8712bd..39da8c92c2 100644 --- a/tests/lib/Persistence/Cache/TrashHandlerTest.php +++ b/tests/lib/Persistence/Cache/TrashHandlerTest.php @@ -10,6 +10,7 @@ use Ibexa\Contracts\Core\Persistence\Content\Location\Trash\Handler as TrashHandler; use Ibexa\Contracts\Core\Persistence\Content\Location\Trashed; use Ibexa\Contracts\Core\Persistence\Content\Relation; +use Ibexa\Contracts\Core\Persistence\Content\VersionInfo; use Ibexa\Contracts\Core\Repository\Values\Content\Trash\TrashItemDeleteResult; use Ibexa\Core\Persistence\Cache\ContentHandler; use Ibexa\Core\Persistence\Cache\LocationHandler; @@ -118,10 +119,13 @@ public function testTrashSubtree() { $locationId = 6; $contentId = 42; + $versionNo = 1; $tags = [ + 'c-' . $contentId . '-v-' . $versionNo, 'c-' . $contentId, 'lp-' . $locationId, + 'l-' . $locationId, ]; $handlerMethodName = $this->getHandlerMethodName(); @@ -145,27 +149,38 @@ public function testTrashSubtree() ->willReturn($locationHandlerMock); $this->persistenceHandlerMock - ->expects($this->once()) + ->expects(self::once()) ->method($handlerMethodName) ->willReturn($innerHandler); + $contentHandlerMock + ->expects(self::once()) + ->method('listVersions') + ->with($contentId) + ->willReturn( + [ + new VersionInfo(['versionNo' => $versionNo]), + ] + ); $innerHandler - ->expects($this->once()) + ->expects(self::once()) ->method('trashSubtree') ->with($locationId) ->willReturn(null); $this->cacheIdentifierGeneratorMock - ->expects($this->exactly(2)) + ->expects(self::exactly(4)) ->method('generateTag') ->withConsecutive( + ['content_version', [$contentId, $versionNo], false], ['content', [$contentId], false], - ['location_path', [$locationId], false] + ['location_path', [$locationId], false], + ['location', [$locationId], false], ) ->willReturnOnConsecutiveCalls(...$tags); $this->cacheMock - ->expects($this->once()) + ->expects(self::once()) ->method('invalidateTags') ->with($tags);