Skip to content

Commit

Permalink
IBX-8726: Added IsBookmarked criterion (#417)
Browse files Browse the repository at this point in the history
For more details see https://issues.ibexa.co/browse/IBX-8726 and #417

Key changes:

* Added loadUserIdsByLocation method to bookmark gateway

* Added method loadUserIdsByLocation to bookmark handler

* Added IsBookmarked criterion

* Added IsBookmarked criterion handler for LSE

* [Tests] Added integration tests for search using IsBookmarked criterion

---------

Co-Authored-By: Konrad Oboza <konradoboza@users.noreply.github.com>
  • Loading branch information
ciastektk and konradoboza authored Sep 4, 2024
1 parent f15e57c commit 0ba8cd1
Show file tree
Hide file tree
Showing 17 changed files with 598 additions and 2 deletions.
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|fetchAllAssociative|fetchAssociative|fetchAllKeyValue|fetchFirstColumn)\\(\\) on Doctrine\\\\DBAL\\\\ForwardCompatibility\\\\Result\\|int\\|string\\.$#"
paths:
- src/*
- tests/*
Expand Down
9 changes: 9 additions & 0 deletions src/contracts/Persistence/Bookmark/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace Ibexa\Contracts\Core\Persistence\Bookmark;

use Ibexa\Contracts\Core\Persistence\Content\Location;

interface Handler
{
/**
Expand Down Expand Up @@ -38,6 +40,13 @@ public function delete(int $bookmarkId): void;
*/
public function loadByUserIdAndLocationId(int $userId, array $locationIds): array;

/**
* Get user ids who have bookmarked given location.
*
* @return array<int>
*/
public function loadUserIdsByLocation(Location $location): array;

/**
* Loads bookmarks owned by user.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\Contracts\Core\Repository\Values\Content\Query\Criterion\Location;

use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Location;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Operator;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Operator\Specifications;
use Ibexa\Contracts\Core\Repository\Values\Filter\FilteringCriterion;

/**
* This criterion only works for current user reference.
*/
final class IsBookmarked extends Location implements FilteringCriterion
{
public function __construct(bool $value = true)
{
parent::__construct(
null,
Operator::EQ,
$value,
);
}

public function getSpecifications(): array
{
return [
new Specifications(
Operator::EQ,
Specifications::FORMAT_SINGLE,
Specifications::TYPE_BOOLEAN
),
];
}
}
3 changes: 3 additions & 0 deletions src/contracts/Test/IbexaTestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use FOS\JsRoutingBundle\FOSJsRoutingBundle;
use Ibexa\Bundle\Core\IbexaCoreBundle;
use Ibexa\Bundle\LegacySearchEngine\IbexaLegacySearchEngineBundle;
use Ibexa\Contracts\Core\Persistence\Handler;
use Ibexa\Contracts\Core\Persistence\TransactionHandler;
use Ibexa\Contracts\Core\Repository;
use Ibexa\Contracts\Core\Test\Persistence\Fixture\YamlFixture;
Expand Down Expand Up @@ -90,6 +91,8 @@ class IbexaTestKernel extends Kernel implements IbexaTestKernelInterface
Repository\UserService::class,
Repository\TokenService::class,
Repository\URLAliasService::class,
Repository\BookmarkService::class,
Handler::class,
];

/**
Expand Down
10 changes: 10 additions & 0 deletions src/lib/Persistence/Cache/BookmarkHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Ibexa\Contracts\Core\Persistence\Bookmark\Bookmark;
use Ibexa\Contracts\Core\Persistence\Bookmark\CreateStruct;
use Ibexa\Contracts\Core\Persistence\Bookmark\Handler as BookmarkHandlerInterface;
use Ibexa\Contracts\Core\Persistence\Content\Location;

class BookmarkHandler extends AbstractHandler implements BookmarkHandlerInterface
{
Expand Down Expand Up @@ -120,6 +121,15 @@ public function locationSwapped(int $location1Id, int $location2Id): void
// Cache clearing is already done by locationHandler
$this->persistenceHandler->bookmarkHandler()->locationSwapped($location1Id, $location2Id);
}

public function loadUserIdsByLocation(Location $location): array
{
$this->logger->logCall(__METHOD__, [
'locationId' => $location->id,
]);

return $this->persistenceHandler->bookmarkHandler()->loadUserIdsByLocation($location);
}
}

class_alias(BookmarkHandler::class, 'eZ\Publish\Core\Persistence\Cache\BookmarkHandler');
8 changes: 8 additions & 0 deletions src/lib/Persistence/Legacy/Bookmark/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Ibexa\Core\Persistence\Legacy\Bookmark;

use Ibexa\Contracts\Core\Persistence\Bookmark\Bookmark;
use Ibexa\Contracts\Core\Persistence\Content\Location;

/**
* Base class for bookmark gateways.
Expand Down Expand Up @@ -41,6 +42,13 @@ abstract public function deleteBookmark(int $id): void;
*/
abstract public function loadBookmarkDataByUserIdAndLocationId(int $userId, array $locationIds): array;

/**
* Load user ids by the given $location.
*
* @return array<int>
*/
abstract public function loadUserIdsByLocation(Location $location): array;

/**
* Load data for all bookmarks owned by given $userId.
*
Expand Down
23 changes: 23 additions & 0 deletions src/lib/Persistence/Legacy/Bookmark/Gateway/DoctrineDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
namespace Ibexa\Core\Persistence\Legacy\Bookmark\Gateway;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Ibexa\Contracts\Core\Persistence\Bookmark\Bookmark;
use Ibexa\Contracts\Core\Persistence\Content\Location;
use Ibexa\Core\Persistence\Legacy\Bookmark\Gateway;
use PDO;

Expand Down Expand Up @@ -100,6 +102,27 @@ public function loadBookmarkDataByUserIdAndLocationId(int $userId, array $locati
return $query->execute()->fetchAll(PDO::FETCH_ASSOC);
}

public function loadUserIdsByLocation(Location $location): array
{
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder
->select(self::COLUMN_USER_ID)
->from(self::TABLE_BOOKMARKS)
->andWhere(
$queryBuilder
->expr()
->eq(
self::COLUMN_LOCATION_ID,
$queryBuilder->createNamedParameter(
$location->id,
ParameterType::INTEGER
)
)
);

return $queryBuilder->execute()->fetchFirstColumn();
}

/**
* {@inheritdoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use Doctrine\DBAL\DBALException;
use Ibexa\Contracts\Core\Persistence\Bookmark\Bookmark;
use Ibexa\Contracts\Core\Persistence\Content\Location;
use Ibexa\Core\Base\Exceptions\DatabaseException;
use Ibexa\Core\Persistence\Legacy\Bookmark\Gateway;
use PDOException;
Expand Down Expand Up @@ -82,6 +83,15 @@ public function locationSwapped(int $location1Id, int $location2Id): void
throw DatabaseException::wrap($e);
}
}

public function loadUserIdsByLocation(Location $location): array
{
try {
return $this->innerGateway->loadUserIdsByLocation($location);
} catch (DBALException | PDOException $e) {
throw DatabaseException::wrap($e);
}
}
}

class_alias(ExceptionConversion::class, 'eZ\Publish\Core\Persistence\Legacy\Bookmark\Gateway\ExceptionConversion');
9 changes: 9 additions & 0 deletions src/lib/Persistence/Legacy/Bookmark/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Ibexa\Contracts\Core\Persistence\Bookmark\Bookmark;
use Ibexa\Contracts\Core\Persistence\Bookmark\CreateStruct;
use Ibexa\Contracts\Core\Persistence\Bookmark\Handler as HandlerInterface;
use Ibexa\Contracts\Core\Persistence\Content\Location;

/**
* Storage Engine handler for bookmarks.
Expand Down Expand Up @@ -98,6 +99,14 @@ public function locationSwapped(int $location1Id, int $location2Id): void
{
$this->gateway->locationSwapped($location1Id, $location2Id);
}

public function loadUserIdsByLocation(Location $location): array
{
return array_map(
static fn ($userId): int => (int)$userId,
$this->gateway->loadUserIdsByLocation($location)
);
}
}

class_alias(Handler::class, 'eZ\Publish\Core\Persistence\Legacy\Bookmark\Handler');
2 changes: 1 addition & 1 deletion src/lib/Resources/settings/search_engines/common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ services:

Ibexa\Core\Search\Common\EventSubscriber\:
resource: '../../../Search/Common/EventSubscriber/*'
exclude: '../../../Search/Common/EventSubscriber/{AbstractSearchEventSubscriber.php}'
exclude: '../../../Search/Common/EventSubscriber/{AbstractSearchEventSubscriber}'
autoconfigure: true
autowire: true
public: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,11 @@ services:
class: Ibexa\Core\Search\Legacy\Content\Location\Gateway\CriterionHandler\Visibility
tags:
- {name: ibexa.search.legacy.gateway.criterion_handler.location}

Ibexa\Core\Search\Legacy\Content\Location\Gateway\CriterionHandler\Location\IsBookmarked:
parent: Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler
arguments:
$connection: '@ibexa.persistence.connection'
$permissionResolver: '@Ibexa\Contracts\Core\Repository\PermissionResolver'
tags:
- { name: ibexa.search.legacy.gateway.criterion_handler.location }
55 changes: 55 additions & 0 deletions src/lib/Search/Common/EventSubscriber/BookmarkEventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?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\Core\Search\Common\EventSubscriber;

use Ibexa\Contracts\Core\Repository\Events\Bookmark\CreateBookmarkEvent;
use Ibexa\Contracts\Core\Repository\Events\Bookmark\DeleteBookmarkEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* @internal
*/
final class BookmarkEventSubscriber extends AbstractSearchEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
CreateBookmarkEvent::class => ['onCreateBookmark', -100],
DeleteBookmarkEvent::class => ['onDeleteBookmark', -100],
];
}

public function onCreateBookmark(CreateBookmarkEvent $event): void
{
$location = $event->getLocation();
$this->updateContentIndex($location->getContentId());
$this->updateLocationIndex($location->getId());
}

public function onDeleteBookmark(DeleteBookmarkEvent $event): void
{
$location = $event->getLocation();
$this->updateContentIndex($location->getContentId());
$this->updateLocationIndex($location->getId());
}

private function updateContentIndex(int $contentId): void
{
$persistenceContent = $this->persistenceHandler->contentHandler()->load($contentId);

$this->searchHandler->indexContent($persistenceContent);
}

private function updateLocationIndex(int $locationId): void
{
$persistenceLocation = $this->persistenceHandler->locationHandler()->load($locationId);

$this->searchHandler->indexLocation($persistenceLocation);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?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\Core\Search\Legacy\Content\Location\Gateway\CriterionHandler\Location;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Query\QueryBuilder;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion;
use Ibexa\Core\Persistence\Legacy\Bookmark\Gateway\DoctrineDatabase;
use Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriteriaConverter;
use Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler;
use LogicException;

final class IsBookmarked extends CriterionHandler
{
private PermissionResolver $permissionResolver;

public function __construct(
Connection $connection,
PermissionResolver $permissionResolver
) {
parent::__construct($connection);

$this->permissionResolver = $permissionResolver;
}

public function accept(Criterion $criterion): bool
{
return $criterion instanceof Criterion\Location\IsBookmarked
&& $criterion->operator === Criterion\Operator::EQ;
}

/**
* @param array{languages: string[]} $languageSettings
*/
public function handle(
CriteriaConverter $converter,
QueryBuilder $queryBuilder,
Criterion $criterion,
array $languageSettings
) {
if (!is_array($criterion->value)) {
throw new LogicException(sprintf(
'Expected %s Criterion value to be an array, %s received',
IsBookmarked::class,
get_debug_type($criterion->value),
));
}

$userId = $this->permissionResolver
->getCurrentUserReference()
->getUserId();

$subQueryBuilder = $this->connection->createQueryBuilder();
$subQueryBuilder
->select('1')
->from(DoctrineDatabase::TABLE_BOOKMARKS, 'b')
->andWhere(
$queryBuilder
->expr()
->eq(
'b.' . DoctrineDatabase::COLUMN_USER_ID,
$queryBuilder->createNamedParameter($userId, ParameterType::INTEGER)
),
$queryBuilder
->expr()
->eq('b.node_id', 't.node_id')
);

$query = 'EXISTS (%s)';
if (!$criterion->value[0]) {
$query = 'NOT ' . $query;
}

return sprintf(
$query,
$subQueryBuilder->getSQL()
);
}
}
Loading

0 comments on commit 0ba8cd1

Please sign in to comment.