diff --git a/eZ/Publish/API/Repository/Tests/LocationServiceTest.php b/eZ/Publish/API/Repository/Tests/LocationServiceTest.php index 0f60cbe4240..31838e0d7c9 100644 --- a/eZ/Publish/API/Repository/Tests/LocationServiceTest.php +++ b/eZ/Publish/API/Repository/Tests/LocationServiceTest.php @@ -8,15 +8,17 @@ */ namespace eZ\Publish\API\Repository\Tests; +use Exception; use eZ\Publish\API\Repository\Exceptions\BadStateException; +use eZ\Publish\API\Repository\Exceptions\NotFoundException; use eZ\Publish\API\Repository\Values\Content\Content; +use eZ\Publish\API\Repository\Values\Content\ContentInfo; use eZ\Publish\API\Repository\Values\Content\Location; use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct; use eZ\Publish\API\Repository\Values\Content\LocationList; -use eZ\Publish\API\Repository\Values\Content\ContentInfo; -use eZ\Publish\API\Repository\Exceptions\NotFoundException; -use Exception; use eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct; +use eZ\Publish\API\Repository\Values\Content\Query; +use eZ\Publish\API\Repository\Values\Content\Search\SearchHit; /** * Test case for operations in the LocationService using in memory storage. @@ -1405,6 +1407,218 @@ public function testSwapLocation() ); } + /** + * Test swapping secondary Location with main Location. + * + * @covers \eZ\Publish\API\Repository\LocationService::swapLocation + * + * @see https://jira.ez.no/browse/EZP-28663 + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + * + * @return int[] + */ + public function testSwapLocationForMainAndSecondaryLocation(): array + { + $repository = $this->getRepository(); + $locationService = $repository->getLocationService(); + $contentService = $repository->getContentService(); + + $folder1 = $this->createFolder(['eng-GB' => 'Folder1'], 2); + $folder2 = $this->createFolder(['eng-GB' => 'Folder2'], 2); + $folder3 = $this->createFolder(['eng-GB' => 'Folder3'], 2); + + $primaryLocation = $locationService->loadLocation($folder1->contentInfo->mainLocationId); + $parentLocation = $locationService->loadLocation($folder2->contentInfo->mainLocationId); + $secondaryLocation = $locationService->createLocation( + $folder1->contentInfo, + $locationService->newLocationCreateStruct($parentLocation->id) + ); + + $targetLocation = $locationService->loadLocation($folder3->contentInfo->mainLocationId); + + // perform sanity checks + $this->assertContentHasExpectedLocations([$primaryLocation, $secondaryLocation], $folder1); + + // begin use case + $locationService->swapLocation($secondaryLocation, $targetLocation); + + // test results + $primaryLocation = $locationService->loadLocation($primaryLocation->id); + $secondaryLocation = $locationService->loadLocation($secondaryLocation->id); + $targetLocation = $locationService->loadLocation($targetLocation->id); + + self::assertEquals($folder1->id, $primaryLocation->contentInfo->id); + self::assertEquals($folder1->id, $targetLocation->contentInfo->id); + self::assertEquals($folder3->id, $secondaryLocation->contentInfo->id); + + $this->assertContentHasExpectedLocations([$primaryLocation, $targetLocation], $folder1); + + self::assertEquals( + $folder1, + $contentService->loadContent($folder1->id) + ); + + self::assertEquals( + $folder2, + $contentService->loadContent($folder2->id) + ); + + // only in case of Folder 3, main location id changed due to swap + self::assertEquals( + $secondaryLocation->id, + $contentService->loadContent($folder3->id)->contentInfo->mainLocationId + ); + + return [$folder1, $folder2, $folder3]; + } + + /** + * Compare Ids of expected and loaded Locations for the given Content. + * + * @param \eZ\Publish\API\Repository\Values\Content\Location[] $expectedLocations + * @param \eZ\Publish\API\Repository\Values\Content\Content $content + * + * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException + */ + private function assertContentHasExpectedLocations(array $expectedLocations, Content $content) + { + $repository = $this->getRepository(false); + $locationService = $repository->getLocationService(); + + $expectedLocationIds = array_map( + function (Location $location) { + return (int)$location->id; + }, + $expectedLocations + ); + + $actualLocationsIds = array_map( + function (Location $location) { + return $location->id; + }, + $locationService->loadLocations($content->contentInfo) + ); + self::assertCount(count($expectedLocations), $actualLocationsIds); + + // perform unordered equality assertion + self::assertEquals( + $expectedLocationIds, + $actualLocationsIds, + sprintf( + 'Content %d contains Locations %s, but expected: %s', + $content->id, + implode(', ', $actualLocationsIds), + implode(', ', $expectedLocationIds) + ), + 0.0, + 10, + true + ); + } + + /** + * @depends testSwapLocationForMainAndSecondaryLocation + * + * @param \eZ\Publish\API\Repository\Values\Content\Content[] $contentItems Content items created by testSwapLocationForSecondaryLocation + * + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + */ + public function testSwapLocationDoesNotCorruptSearchResults(array $contentItems) + { + $repository = $this->getRepository(false); + $searchService = $repository->getSearchService(); + + $this->refreshSearch($repository); + + $contentIds = array_map( + function (Content $content) { + return $content->id; + }, + $contentItems + ); + + $query = new Query(); + $query->filter = new Query\Criterion\ContentId($contentIds); + + $searchResult = $searchService->findContent($query); + + self::assertEquals(count($contentItems), $searchResult->totalCount); + self::assertEquals( + $searchResult->totalCount, + count($searchResult->searchHits), + 'Total count of search result hits does not match the actual number of found results' + ); + $foundContentIds = array_map( + function (SearchHit $searchHit) { + return $searchHit->valueObject->id; + }, + $searchResult->searchHits + ); + sort($contentIds); + sort($foundContentIds); + self::assertSame( + $contentIds, + $foundContentIds, + 'Got different than expected Content item Ids' + ); + } + + /** + * Test swapping two secondary (non-main) Locations. + * + * @covers \eZ\Publish\API\Repository\LocationService::swapLocation + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + public function testSwapLocationForSecondaryLocations() + { + $repository = $this->getRepository(); + $locationService = $repository->getLocationService(); + $contentService = $repository->getContentService(); + + $folder1 = $this->createFolder(['eng-GB' => 'Folder1'], 2); + $folder2 = $this->createFolder(['eng-GB' => 'Folder2'], 2); + $parentFolder1 = $this->createFolder(['eng-GB' => 'Parent1'], 2); + $parentFolder2 = $this->createFolder(['eng-GB' => 'Parent2'], 2); + + $parentLocation1 = $locationService->loadLocation($parentFolder1->contentInfo->mainLocationId); + $parentLocation2 = $locationService->loadLocation($parentFolder2->contentInfo->mainLocationId); + $secondaryLocation1 = $locationService->createLocation( + $folder1->contentInfo, + $locationService->newLocationCreateStruct($parentLocation1->id) + ); + $secondaryLocation2 = $locationService->createLocation( + $folder2->contentInfo, + $locationService->newLocationCreateStruct($parentLocation2->id) + ); + + // begin use case + $locationService->swapLocation($secondaryLocation1, $secondaryLocation2); + + // test results + $secondaryLocation1 = $locationService->loadLocation($secondaryLocation1->id); + $secondaryLocation2 = $locationService->loadLocation($secondaryLocation2->id); + + self::assertEquals($folder2->id, $secondaryLocation1->contentInfo->id); + self::assertEquals($folder1->id, $secondaryLocation2->contentInfo->id); + + self::assertEquals( + $folder1, + $contentService->loadContent($folder1->id) + ); + + self::assertEquals( + $folder2, + $contentService->loadContent($folder2->id) + ); + } + /** * Test swapping Main Location of a Content with another one updates Content item Main Location. * diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/TrashHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/TrashHandlerTest.php index 561b58982af..36e2e9442ea 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/TrashHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/TrashHandlerTest.php @@ -12,6 +12,8 @@ use eZ\Publish\Core\Persistence\Cache\LocationHandler; use eZ\Publish\SPI\Persistence\Content\Location; use eZ\Publish\SPI\Persistence\Content\Location\Trash\Handler as TrashHandler; +use eZ\Publish\SPI\Persistence\Content\Location\Trashed; +use eZ\Publish\SPI\Persistence\Content\Relation; /** * Test case for Persistence\Cache\SectionHandler. @@ -33,8 +35,6 @@ public function providerForUnCachedMethods(): array // string $method, array $arguments, array? $tags, string? $key return [ ['loadTrashItem', [6]], - ['emptyTrash', []], - ['deleteTrashItem', [6]], ]; } @@ -149,4 +149,95 @@ public function testTrashSubtree() $handler = $this->persistenceCacheHandler->$handlerMethodName(); $handler->trashSubtree($locationId); } + + public function testDeleteTrashItem() + { + $trashedId = 6; + $contentId = 42; + $relationSourceContentId = 42; + + $handlerMethodName = $this->getHandlerMethodName(); + + $innerHandler = $this->createMock($this->getHandlerClassName()); + + $innerHandler + ->expects($this->once()) + ->method('loadTrashItem') + ->with($trashedId) + ->will($this->returnValue(new Trashed(['id' => $trashedId, 'contentId' => $contentId]))); + + $this->persistenceHandlerMock + ->method($handlerMethodName) + ->will($this->returnValue($innerHandler)); + + $contentHandlerMock = $this->createMock(ContentHandler::class); + + $contentHandlerMock + ->expects($this->once()) + ->method('loadReverseRelations') + ->with($contentId) + ->will($this->returnValue([new Relation(['sourceContentId' => $relationSourceContentId])])); + + $this->persistenceHandlerMock + ->method('contentHandler') + ->will($this->returnValue($contentHandlerMock)); + + $tags = [ + 'content-fields-' . $relationSourceContentId, + ]; + + $this->cacheMock + ->expects($this->once()) + ->method('invalidateTags') + ->with($tags); + + /** @var \eZ\Publish\SPI\Persistence\Content\Location\Trash\Handler $handler */ + $handler = $this->persistenceCacheHandler->$handlerMethodName(); + $handler->deleteTrashItem($trashedId); + } + + public function testEmptyTrash() + { + $trashedId = 6; + $contentId = 42; + $relationSourceContentId = 42; + + $handlerMethodName = $this->getHandlerMethodName(); + + $innerHandler = $this->createMock($this->getHandlerClassName()); + + $innerHandler + ->expects($this->once()) + ->method('findTrashItems') + ->will($this->returnValue([new Trashed(['id' => $trashedId, 'contentId' => $contentId])])); + + $this->persistenceHandlerMock + ->method($handlerMethodName) + ->will($this->returnValue($innerHandler)); + + $contentHandlerMock = $this->createMock(ContentHandler::class); + + $contentHandlerMock + ->expects($this->once()) + ->method('loadReverseRelations') + ->with($contentId) + ->will($this->returnValue([new Relation(['sourceContentId' => $relationSourceContentId])])); + + $this->persistenceHandlerMock + ->method('contentHandler') + ->will($this->returnValue($contentHandlerMock)); + + $tags = [ + 'content-fields-' . $relationSourceContentId, + ]; + + $this->cacheMock + ->expects($this->once()) + ->method('invalidateTags') + ->with($tags); + + /** @var \eZ\Publish\SPI\Persistence\Content\Location\Trash\Handler $handler */ + $handler = $this->persistenceCacheHandler->$handlerMethodName(); + $handler->emptyTrash(); + } } diff --git a/eZ/Publish/Core/Persistence/Cache/TrashHandler.php b/eZ/Publish/Core/Persistence/Cache/TrashHandler.php index fce471d4080..f7262b74622 100644 --- a/eZ/Publish/Core/Persistence/Cache/TrashHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/TrashHandler.php @@ -100,7 +100,26 @@ public function emptyTrash() { $this->logger->logCall(__METHOD__, array()); - return $this->persistenceHandler->trashHandler()->emptyTrash(); + // We can not use the return value of emptyTrash method because, in the next step, we are not able + // to fetch the reverse relations of deleted content. + $trashedItems = $this->persistenceHandler->trashHandler()->findTrashItems(); + + $tags = []; + foreach ($trashedItems as $trashedItem) { + $reverseRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations($trashedItem->contentId); + + foreach ($reverseRelations as $relation) { + $tags[] = 'content-fields-' . $relation->sourceContentId; + } + } + + $return = $this->persistenceHandler->trashHandler()->emptyTrash(); + + if (!empty($tags)) { + $this->cache->invalidateTags(array_unique($tags)); + } + + return $return; } /** @@ -110,6 +129,21 @@ public function deleteTrashItem($trashedId) { $this->logger->logCall(__METHOD__, array('id' => $trashedId)); - return $this->persistenceHandler->trashHandler()->deleteTrashItem($trashedId); + // We can not use the return value of deleteTrashItem method because, in the next step, we are not able + // to fetch the reverse relations of deleted content. + $trashed = $this->persistenceHandler->trashHandler()->loadTrashItem($trashedId); + + $reverseRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations($trashed->contentId); + $tags = array_map(function (Relation $relation) { + return 'content-fields-' . $relation->sourceContentId; + }, $reverseRelations); + + $return = $this->persistenceHandler->trashHandler()->deleteTrashItem($trashedId); + + if (!empty($tags)) { + $this->cache->invalidateTags($tags); + } + + return $return; } } diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php index 3f7e36403c6..640a711f62f 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php @@ -939,12 +939,24 @@ private function internalLoadContent(array $contentIds, int $version = null, arr * * @see loadContentInfo(), loadContentInfoByRemoteId(), loadContentInfoList(), loadContentInfoByLocationId() * + * @param bool $joinMainLocation + * * @return \Doctrine\DBAL\Query\QueryBuilder */ - private function createLoadContentInfoQueryBuilder(): DoctrineQueryBuilder + private function createLoadContentInfoQueryBuilder(bool $joinMainLocation = true): DoctrineQueryBuilder { $queryBuilder = $this->connection->createQueryBuilder(); $expr = $queryBuilder->expr(); + + $joinCondition = $expr->eq('c.id', 't.contentobject_id'); + if ($joinMainLocation) { + // wrap join condition with AND operator and join by a Main Location + $joinCondition = $expr->andX( + $joinCondition, + $expr->eq('t.node_id', 't.main_node_id') + ); + } + $queryBuilder ->select('c.*', 't.main_node_id AS ezcontentobject_tree_main_node_id') ->from('ezcontentobject', 'c') @@ -952,10 +964,7 @@ private function createLoadContentInfoQueryBuilder(): DoctrineQueryBuilder 'c', 'ezcontentobject_tree', 't', - $expr->andX( - $expr->eq('c.id', 't.contentobject_id'), - $expr->eq('t.node_id', 't.main_node_id') - ) + $joinCondition ); return $queryBuilder; @@ -1037,14 +1046,14 @@ public function loadContentInfoByRemoteId($remoteId) */ public function loadContentInfoByLocationId($locationId) { - $queryBuilder = $this->createLoadContentInfoQueryBuilder(); + $queryBuilder = $this->createLoadContentInfoQueryBuilder(false); $queryBuilder - ->where('t.main_node_id = :id') + ->where('t.node_id = :id') ->setParameter('id', $locationId, ParameterType::INTEGER); $results = $queryBuilder->execute()->fetchAll(FetchMode::ASSOCIATIVE); if (empty($results)) { - throw new NotFound('content', "main_node_id: $locationId"); + throw new NotFound('content', "node_id: $locationId"); } return $results[0]; diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php index bac3cfdc2e0..7ef1078f74b 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php @@ -176,12 +176,12 @@ abstract public function unHideSubtree($pathString); * Make the location identified by $locationId1 refer to the Content * referred to by $locationId2 and vice versa. * - * @param mixed $locationId1 - * @param mixed $locationId2 + * @param int $locationId1 + * @param int $locationId2 * * @return bool */ - abstract public function swap($locationId1, $locationId2); + abstract public function swap(int $locationId1, int $locationId2): bool; /** * Creates a new location in given $parentNode. diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php index cf99600eadf..cdfe2861297 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php @@ -574,70 +574,87 @@ function ($pathString) use ($query, $handler) { * Make the location identified by $locationId1 refer to the Content * referred to by $locationId2 and vice versa. * - * @param mixed $locationId1 - * @param mixed $locationId2 + * @param int $locationId1 + * @param int $locationId2 * * @return bool */ - public function swap($locationId1, $locationId2) + public function swap(int $locationId1, int $locationId2): bool { - $query = $this->handler->createSelectQuery(); - $query - ->select( - $this->handler->quoteColumn('node_id'), - $this->handler->quoteColumn('contentobject_id'), - $this->handler->quoteColumn('contentobject_version') - ) - ->from($this->handler->quoteTable('ezcontentobject_tree')) + $queryBuilder = $this->connection->createQueryBuilder(); + $expr = $queryBuilder->expr(); + $queryBuilder + ->select('node_id', 'main_node_id', 'contentobject_id', 'contentobject_version') + ->from('ezcontentobject_tree') ->where( - $query->expr->in( - $this->handler->quoteColumn('node_id'), - array($locationId1, $locationId2) + $expr->in( + 'node_id', + ':locationIds' ) - ); - $statement = $query->prepare(); - $statement->execute(); - foreach ($statement->fetchAll() as $row) { + ) + ->setParameter('locationIds', [$locationId1, $locationId2], Connection::PARAM_INT_ARRAY) + ; + $statement = $queryBuilder->execute(); + $contentObjects = []; + foreach ($statement->fetchAll(FetchMode::ASSOCIATIVE) as $row) { + $row['is_main_node'] = (int)$row['main_node_id'] === (int)$row['node_id']; $contentObjects[$row['node_id']] = $row; } - $query = $this->handler->createUpdateQuery(); - $query - ->update($this->handler->quoteTable('ezcontentobject_tree')) - ->set( - $this->handler->quoteColumn('contentobject_id'), - $query->bindValue($contentObjects[$locationId2]['contentobject_id']) - ) - ->set( - $this->handler->quoteColumn('contentobject_version'), - $query->bindValue($contentObjects[$locationId2]['contentobject_version']) - ) - ->where( - $query->expr->eq( - $this->handler->quoteColumn('node_id'), - $query->bindValue($locationId1) + if (!isset($contentObjects[$locationId1], $contentObjects[$locationId2])) { + throw new RuntimeException( + sprintf( + '%s: failed to fetch either Location %d or Location %d', + __METHOD__, + $locationId1, + $locationId2 ) ); - $query->prepare()->execute(); + } + $content1data = $contentObjects[$locationId1]; + $content2data = $contentObjects[$locationId2]; - $query = $this->handler->createUpdateQuery(); - $query - ->update($this->handler->quoteTable('ezcontentobject_tree')) - ->set( - $this->handler->quoteColumn('contentobject_id'), - $query->bindValue($contentObjects[$locationId1]['contentobject_id']) - ) - ->set( - $this->handler->quoteColumn('contentobject_version'), - $query->bindValue($contentObjects[$locationId1]['contentobject_version']) - ) + $queryBuilder = $this->connection->createQueryBuilder(); + $queryBuilder + ->update('ezcontentobject_tree') + ->set('contentobject_id', ':contentId') + ->set('contentobject_version', ':versionNo') + ->set('main_node_id', ':mainNodeId') ->where( - $query->expr->eq( - $this->handler->quoteColumn('node_id'), - $query->bindValue($locationId2) - ) + $expr->eq('node_id', ':locationId') ); - $query->prepare()->execute(); + + $queryBuilder + ->setParameter(':contentId', $content2data['contentobject_id']) + ->setParameter(':versionNo', $content2data['contentobject_version']) + ->setParameter( + ':mainNodeId', + // make main Location main again, preserve main Location id of non-main one + $content2data['is_main_node'] + ? $content1data['node_id'] + : $content2data['main_node_id'] + ) + ->setParameter('locationId', $locationId1); + + // update Location 1 entry + $queryBuilder->execute(); + + $queryBuilder + ->setParameter(':contentId', $content1data['contentobject_id']) + ->setParameter(':versionNo', $content1data['contentobject_version']) + ->setParameter( + ':mainNodeId', + $content1data['is_main_node'] + // make main Location main again, preserve main Location id of non-main one + ? $content2data['node_id'] + : $content1data['main_node_id'] + ) + ->setParameter('locationId', $locationId2); + + // update Location 2 entry + $queryBuilder->execute(); + + return true; } /** diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php index 3a2d7acf48b..fe7fbf1b5e8 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php @@ -304,12 +304,12 @@ public function unHideSubtree($pathString) * Make the location identified by $locationId1 refer to the Content * referred to by $locationId2 and vice versa. * - * @param mixed $locationId1 - * @param mixed $locationId2 + * @param int $locationId1 + * @param int $locationId2 * * @return bool */ - public function swap($locationId1, $locationId2) + public function swap(int $locationId1, int $locationId2): bool { try { return $this->innerGateway->swap($locationId1, $locationId2); diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/UrlAlias/Gateway/DoctrineDatabase.php b/eZ/Publish/Core/Persistence/Legacy/Content/UrlAlias/Gateway/DoctrineDatabase.php index b67c58b6b30..ce45a0ea225 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/UrlAlias/Gateway/DoctrineDatabase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/UrlAlias/Gateway/DoctrineDatabase.php @@ -1133,31 +1133,19 @@ public function loadAutogeneratedEntries($parentId, $includeHistory = false) public function getLocationContentMainLanguageId($locationId) { - $dbHandler = $this->dbHandler; - $query = $dbHandler->createSelectQuery(); - $query - ->select($dbHandler->quoteColumn('initial_language_id', 'ezcontentobject')) - ->from($dbHandler->quoteTable('ezcontentobject')) - ->innerJoin( - $dbHandler->quoteTable('ezcontentobject_tree'), - $query->expr->lAnd( - $query->expr->eq( - $dbHandler->quoteColumn('contentobject_id', 'ezcontentobject_tree'), - $dbHandler->quoteColumn('id', 'ezcontentobject') - ), - $query->expr->eq( - $dbHandler->quoteColumn('node_id', 'ezcontentobject_tree'), - $dbHandler->quoteColumn('main_node_id', 'ezcontentobject_tree') - ), - $query->expr->eq( - $dbHandler->quoteColumn('node_id', 'ezcontentobject_tree'), - $query->bindValue($locationId, null, \PDO::PARAM_INT) - ) - ) - ); + $queryBuilder = $this->connection->createQueryBuilder(); + $expr = $queryBuilder->expr(); + $queryBuilder + ->select('c.initial_language_id') + ->from('ezcontentobject', 'c') + ->join('c', 'ezcontentobject_tree', 't', $expr->eq('t.contentobject_id', 'c.id')) + ->where( + $expr->eq('t.node_id', ':locationId') + ) + ->setParameter('locationId', $locationId, ParameterType::INTEGER) + ; - $statement = $query->prepare(); - $statement->execute(); + $statement = $queryBuilder->execute(); $languageId = $statement->fetchColumn(); if ($languageId === false) {