Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-8418: Remove draft when trashing its parent or ancestor location #398

Open
wants to merge 11 commits into
base: 4.6
Choose a base branch
from
25 changes: 5 additions & 20 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -9326,12 +9326,12 @@ parameters:
path: src/lib/FieldType/Image/ImageStorage.php

-
message: "#^Cannot access offset 0 on array\\{0\\: int\\<1, max\\>, 1\\: int\\<1, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
message: "#^Cannot access offset 0 on array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
count: 1
path: src/lib/FieldType/Image/ImageStorage.php

-
message: "#^Cannot access offset 1 on array\\{0\\: int\\<1, max\\>, 1\\: int\\<1, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
message: "#^Cannot access offset 1 on array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
count: 1
path: src/lib/FieldType/Image/ImageStorage.php

Expand Down Expand Up @@ -9675,11 +9675,6 @@ parameters:
count: 2
path: src/lib/FieldType/Keyword/KeywordStorage/Gateway/DoctrineStorage.php

-
message: "#^Cannot call method fetchFirstColumn\\(\\) on Doctrine\\\\DBAL\\\\ForwardCompatibility\\\\Result\\|int\\|string\\.$#"
count: 1
path: src/lib/FieldType/Keyword/KeywordStorage/Gateway/DoctrineStorage.php

-
message: "#^Method Ibexa\\\\Core\\\\FieldType\\\\Keyword\\\\KeywordStorage\\\\Gateway\\\\DoctrineStorage\\:\\:assignKeywords\\(\\) has parameter \\$keywordMap with no value type specified in iterable type array\\.$#"
count: 1
Expand Down Expand Up @@ -10275,11 +10270,6 @@ parameters:
count: 3
path: src/lib/FieldType/Url/UrlStorage/Gateway/DoctrineStorage.php

-
message: "#^Cannot call method fetchFirstColumn\\(\\) on Doctrine\\\\DBAL\\\\ForwardCompatibility\\\\Result\\|int\\|string\\.$#"
count: 1
path: src/lib/FieldType/Url/UrlStorage/Gateway/DoctrineStorage.php

-
message: "#^Method Ibexa\\\\Core\\\\FieldType\\\\Url\\\\UrlStorage\\\\Gateway\\\\DoctrineStorage\\:\\:getIdUrlMap\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
Expand Down Expand Up @@ -11021,17 +11011,17 @@ parameters:
path: src/lib/IO/MetadataHandler.php

-
message: "#^Cannot access offset 'mime' on array\\{0\\: int\\<1, max\\>, 1\\: int\\<1, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
message: "#^Cannot access offset 'mime' on array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
count: 1
path: src/lib/IO/MetadataHandler/ImageSize.php

-
message: "#^Cannot access offset 0 on array\\{0\\: int\\<1, max\\>, 1\\: int\\<1, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
message: "#^Cannot access offset 0 on array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
count: 1
path: src/lib/IO/MetadataHandler/ImageSize.php

-
message: "#^Cannot access offset 1 on array\\{0\\: int\\<1, max\\>, 1\\: int\\<1, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
message: "#^Cannot access offset 1 on array\\{0\\: int\\<0, max\\>, 1\\: int\\<0, max\\>, 2\\: int, 3\\: string, mime\\: string, channels\\?\\: int, bits\\?\\: int\\}\\|false\\.$#"
count: 1
path: src/lib/IO/MetadataHandler/ImageSize.php

Expand Down Expand Up @@ -16455,11 +16445,6 @@ parameters:
count: 12
path: src/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php

-
message: "#^Cannot call method fetchFirstColumn\\(\\) on Doctrine\\\\DBAL\\\\ForwardCompatibility\\\\Result\\|int\\|string\\.$#"
count: 1
path: src/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php

-
message: "#^Method Ibexa\\\\Core\\\\Persistence\\\\Legacy\\\\Content\\\\Location\\\\Gateway\\\\DoctrineDatabase\\:\\:addSort\\(\\) has parameter \\$languageSettings with no value type specified in iterable type array\\.$#"
count: 1
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ parameters:
treatPhpDocTypesAsCertain: false
ignoreErrors:
-
message: "#^Cannot call method (fetchOne|fetchColumn|fetchAllAssociative|fetchAssociative|fetchAllKeyValue)\\(\\) on Doctrine\\\\DBAL\\\\ForwardCompatibility\\\\Result\\|int\\|string\\.$#"
message: "#^Cannot call method (fetchOne|fetchColumn|fetchFirstColumn|fetchAllAssociative|fetchAssociative|fetchAllKeyValue)\\(\\) on Doctrine\\\\DBAL\\\\ForwardCompatibility\\\\Result\\|int\\|string\\.$#"
paths:
- src/*
- tests/*
Expand Down
5 changes: 5 additions & 0 deletions src/contracts/Persistence/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ public function create(CreateStruct $location);
*/
public function removeSubtree($locationId);

/**
* Removes all draft contents that have no location assigned to them under the given parent location.
*/
public function deleteChildrenDrafts(int $locationId): void;

/**
* Set section on all content objects in the subtree.
* Only main locations will be updated.
Expand Down
6 changes: 6 additions & 0 deletions src/contracts/Test/IbexaKernelTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Ibexa\Contracts\Core\Repository\RoleService;
use Ibexa\Contracts\Core\Repository\SearchService;
use Ibexa\Contracts\Core\Repository\SectionService;
use Ibexa\Contracts\Core\Repository\TrashService;
use Ibexa\Contracts\Core\Repository\URLAliasService;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Core\Test\Persistence\Fixture\FixtureImporter;
Expand Down Expand Up @@ -169,6 +170,11 @@ protected static function getUrlAliasService(): URLAliasService
return self::getServiceByClassName(URLAliasService::class);
}

protected static function getTrashService(): TrashService
{
return self::getServiceByClassName(TrashService::class);
}

protected static function setAnonymousUser(): void
{
$anonymousUserId = 10;
Expand Down
1 change: 1 addition & 0 deletions src/contracts/Test/IbexaTestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class IbexaTestKernel extends Kernel implements IbexaTestKernelInterface
Repository\UserService::class,
Repository\TokenService::class,
Repository\URLAliasService::class,
Repository\TrashService::class,
];

/**
Expand Down
14 changes: 14 additions & 0 deletions src/lib/Persistence/Cache/LocationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,20 @@ public function removeSubtree($locationId)
return $return;
}

public function deleteChildrenDrafts(int $locationId): void
{
$this->logger->logCall(__METHOD__, ['location' => $locationId]);

$this->persistenceHandler->locationHandler()->deleteChildrenDrafts($locationId);

$this->cache->invalidateTags([
$this->cacheIdentifierGenerator->generateTag(
self::LOCATION_PATH_IDENTIFIER,
[$locationId],
),
]);
}

/**
* {@inheritdoc}
*/
Expand Down
7 changes: 7 additions & 0 deletions src/lib/Persistence/Legacy/Content/Location/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ abstract public function loadParentLocationsDataForDraftContent(int $contentId):
*/
abstract public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array;

/**
* Finds draft contents created under the given parent location.
*
* @return array<int>
*/
abstract public function getSubtreeChildrenDraftContentIds(int $sourceId): array;

abstract public function getSubtreeSize(string $path): int;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@ public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array
: $results;
}

/**
* @return array<int>
*
* @throws \Doctrine\DBAL\Exception
* @throws \Doctrine\DBAL\Driver\Exception
*/
public function getSubtreeChildrenDraftContentIds(int $sourceId): array
{
$query = $this->connection->createQueryBuilder();
$query
->select('contentobject_id')
->from('eznode_assignment', 'n')
->innerJoin('n', 'ezcontentobject', 'c', 'n.contentobject_id = c.id')
->andWhere('n.parent_node = :parentNode')
->andWhere('c.status = :status')
->setParameter(':parentNode', $sourceId, ParameterType::INTEGER)
->setParameter(':status', ContentInfo::STATUS_DRAFT, ParameterType::INTEGER);

$statement = $query->execute();

return $statement->fetchFirstColumn();
}

public function getSubtreeSize(string $path): int
{
$query = $this->createNodeQueryBuilder([$this->dbPlatform->getCountExpression('node_id')]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array
}
}

/**
* @return array<int>
*/
public function getSubtreeChildrenDraftContentIds(int $sourceId): array
{
try {
return $this->innerGateway->getSubtreeChildrenDraftContentIds($sourceId);
} catch (PDOException $e) {
throw DatabaseException::wrap($e);
}
}

public function getSubtreeSize(string $path): int
{
try {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/Persistence/Legacy/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,11 @@ public function removeSubtree($locationId)
$this->treeHandler->removeSubtree($locationId);
}

public function deleteChildrenDrafts(int $locationId): void
{
$this->treeHandler->deleteChildrenDrafts($locationId);
}

/**
* Set section on all content objects in the subtree.
*
Expand Down
20 changes: 20 additions & 0 deletions src/lib/Persistence/Legacy/Content/TreeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,26 @@ public function removeSubtree($locationId)
$this->locationGateway->deleteNodeAssignment($contentId);
}

/**
* Removes draft contents assigned to the given parent location and its descendant locations.
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
public function deleteChildrenDrafts(int $locationId): void
{
$subLocations = $this->locationGateway->getChildren($locationId);
foreach ($subLocations as $subLocation) {
$this->deleteChildrenDrafts($subLocation['node_id']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A recursive call? Won't this be a massive performance penalty for deep trees?

Copy link
Contributor Author

@barw4 barw4 Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few lines above we have the same situation and it was there for ages 🤷🏻 : https://github.com/ibexa/core/blob/main/src/lib/Persistence/Legacy/Content/TreeHandler.php#L196

Removing it in a single query may not be easy as this table is tree-like with the only information being of a record's parent.

}

// Fetch child draft content ids
$subtreeChildrenDraftIds = $this->locationGateway->getSubtreeChildrenDraftContentIds($locationId);
alongosz marked this conversation as resolved.
Show resolved Hide resolved

foreach ($subtreeChildrenDraftIds as $contentId) {
$this->removeRawContent($contentId);
}
}

/**
* Set section on all content objects in the subtree.
*
Expand Down
1 change: 1 addition & 0 deletions src/lib/Repository/TrashService.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public function trash(Location $location): ?APITrashItem

$this->repository->beginTransaction();
try {
$this->persistenceHandler->locationHandler()->deleteChildrenDrafts($location->id);
$spiTrashItem = $this->persistenceHandler->trashHandler()->trashSubtree($location->id);
$this->persistenceHandler->urlAliasHandler()->locationDeleted($location->id);
$this->repository->commit();
Expand Down
80 changes: 80 additions & 0 deletions tests/integration/Core/Repository/TrashService/TrashTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Integration\Core\Repository\TrashService;

use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
use Ibexa\Tests\Integration\Core\RepositoryTestCase;
use PHPUnit\Framework\Assert;

/**
* @covers \Ibexa\Contracts\Core\Repository\TrashService
*/
final class TrashTest extends RepositoryTestCase
{
/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception
*/
public function testTrashLocationDeletesChildrenDrafts(): void
{
$trashService = self::getTrashService();
$contentService = self::getContentService();

$folder = $this->createFolder(['eng-GB' => 'Folder'], 2);
$folderMainLocationId = $folder->getVersionInfo()->getContentInfo()->getMainLocationId();
Assert::assertIsNumeric($folderMainLocationId);

$childFolder = $this->createFolder(
['eng-GB' => 'Child folder'],
$folderMainLocationId,
);
$childFolderMainLocationId = $childFolder->getVersionInfo()->getContentInfo()->getMainLocationId();
Assert::assertIsNumeric($childFolderMainLocationId);

$secondDepthChildFolder = $this->createFolder(
['eng-GB' => 'Second depth folder'],
$childFolderMainLocationId,
);
$secondDepthChildFolderLocationId = $secondDepthChildFolder
->getVersionInfo()
->getContentInfo()
->getMainLocationId()
;
Assert::assertIsNumeric($secondDepthChildFolderLocationId);

$draft1 = $this->createFolderDraft(['eng-GB' => 'Folder draft 1'], $folderMainLocationId);
$draft2 = $this->createFolderDraft(['eng-GB' => 'Folder draft 2'], $childFolderMainLocationId);
$draft3 = $this->createFolderDraft(['eng-GB' => 'Folder draft 3'], $childFolderMainLocationId);
$draftSecondDepth = $this->createFolderDraft(
['eng-GB' => 'Folder draft 4'],
$secondDepthChildFolderLocationId,
);

$locationToTrash = $this->getLocationService()->loadLocation($folderMainLocationId);
barw4 marked this conversation as resolved.
Show resolved Hide resolved

$trashService->trash($locationToTrash);

$draftIds = [
$draft1->getId(),
$draft2->getId(),
$draft3->getId(),
$draftSecondDepth->getId(),
];

foreach ($draftIds as $draftId) {
try {
$contentService->loadContentInfo($draftId);
$this->fail(
sprintf('Expected NotFoundException not thrown for draft ID = %d', $draftId),
);
} catch (NotFoundException $e) {
self::assertInstanceOf(NotFoundException::class, $e);
}
}
konradoboza marked this conversation as resolved.
Show resolved Hide resolved
}
}
1 change: 1 addition & 0 deletions tests/lib/Persistence/Cache/LocationHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function providerForUnCachedMethods(): array
['c-4', 'ragl-4'],
],
['removeSubtree', [12], [['location_path', [12], false]], null, ['lp-12']],
['deleteChildrenDrafts', [12], [['location_path', [12], false]], null, ['lp-12']],
['setSectionForSubtree', [12, 2], [['location_path', [12], false]], null, ['lp-12']],
['changeMainLocation', [4, 12], [['content', [4], false]], null, ['c-4']],
['countLocationsByContent', [4]],
Expand Down
12 changes: 12 additions & 0 deletions tests/lib/Persistence/Legacy/Content/LocationHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,18 @@ public function testCountLocationsByContent(): void
$handler->countLocationsByContent($contentId);
}

public function testDeleteChildrenDrafts(): void
{
$handler = $this->getLocationHandler();

$this->treeHandler
->expects(self::once())
->method('deleteChildrenDrafts')
->with(42);

$handler->deleteChildrenDrafts(42);
}

/**
* Returns the handler to test with $methods mocked.
*
Expand Down
Loading
Loading