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
12 changes: 0 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -32430,12 +32430,6 @@ parameters:
count: 1
path: tests/integration/Core/Repository/BaseURLServiceTest.php

-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\Values\\\\Bookmark\\\\BookmarkList'' and Ibexa\\Contracts\\Core\\Repository\\Values\\Bookmark\\BookmarkList will always evaluate to true\.$#'
identifier: method.alreadyNarrowedType
count: 1
path: tests/integration/Core/Repository/BookmarkServiceTest.php

-
message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\BookmarkServiceTest\:\:testCreateBookmark\(\) has no return type specified\.$#'
identifier: missingType.return
Expand Down Expand Up @@ -69774,12 +69768,6 @@ parameters:
count: 1
path: tests/lib/Repository/Service/Mock/BookmarkTest.php

-
message: '#^Method Ibexa\\Tests\\Core\\Repository\\Service\\Mock\\BookmarkTest\:\:testLoadBookmarksEmptyList\(\) has no return type specified\.$#'
identifier: missingType.return
count: 1
path: tests/lib/Repository/Service/Mock/BookmarkTest.php

-
message: '#^Method Ibexa\\Tests\\Core\\Repository\\Service\\Mock\\BookmarkTest\:\:testLocationShouldBeBookmarked\(\) has no return type specified\.$#'
identifier: missingType.return
Expand Down
4 changes: 4 additions & 0 deletions src/contracts/Persistence/Bookmark/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public function loadUserIdsByLocation(Location $location): array;
/**
* Loads bookmarks owned by user.
*
* @deprecated The "Handler::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
Copy link
Member

Choose a reason for hiding this comment

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

Deprecating it in 6.0 is fine for now, but the annotation format is not correct and most likely it's gonna affect or even crash phpDocumentor ;-)
Please see PHP Conventions: Deprecation standards.

*
* @param int $userId
* @param int $offset the start offset for paging
* @param int $limit the number of bookmarked locations returned
Expand All @@ -61,6 +63,8 @@ public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1)
/**
* Count bookmarks owned by user.
*
* @deprecated The "Handler::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
*
* @param int $userId
*
* @return int
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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;

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

/**
* A criterion that matches locations of bookmarks for a given user id.
*
* Supported operators:
* - EQ: matches against a unique user id
*/
final class IsBookmarked extends Criterion implements FilteringCriterion
Copy link
Member

Choose a reason for hiding this comment

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

Extending criterion means that this would be available not just for filtering but for search too. Hence it would require implementing handlers for all supported search engines.

Filtering was supposed initially to mirror search service, so there are no examples of Filtering Criteria that are not Search Criteria at the same time, but the intention of Filtering was that it can exist as a separate concepts for data which are not supposed to be a part of Search Index but a database.

ATM it's possible to use it with search and get the following fatal error at runtime:

Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException : Intentionally not implemented: No visitor available for: Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\IsBookmarked with operator =

If it doesn't rely on Criterion, the error should appear on at least static analysis level, which provides better DX.

{
public function __construct(int $value)
Copy link
Member

Choose a reason for hiding this comment

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

Important

API design remark

Typically you'd want here bool $isBookmarked = true. Semantically IsBookmarked(14) doesn't read well and is rather not expected with the verb Is.

User ID value should be part of a handler (query builder), not a criterion. That being said, I see you provided a case for IsBookmark of user X or user Z. It's a bit out of scope of the usage of this criterion, but if you indeed want such flexibility, user ID should be an optional 2nd argument (by default null, taken from current user in query builder).

{
parent::__construct(null, null, $value);
}

public function getSpecifications(): array
{
return [
new Specifications(Operator::EQ, Specifications::FORMAT_SINGLE, Specifications::TYPE_INTEGER),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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\SortClause;

use Ibexa\Contracts\Core\Repository\Values\Content\Query;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause;
use Ibexa\Contracts\Core\Repository\Values\Filter\FilteringSortClause;

/**
* Sets sort direction on the bookmark id for a location query containing IsBookmarked criterion.
*/
final class BookmarkId extends SortClause implements FilteringSortClause
{
public function __construct(string $sortDirection = Query::SORT_ASC)
{
parent::__construct('id', $sortDirection);
}
}
4 changes: 4 additions & 0 deletions src/lib/Persistence/Cache/BookmarkHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ function (Bookmark $bookmark) {
}

/**
* @deprecated The "BookmarkHandler::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
*
* {@inheritdoc}
Comment on lines +86 to 88
Copy link
Member

Choose a reason for hiding this comment

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

Please don't repeat the same message here. It should be present on the interface only.
In fact, you can drop the entire block because doc inheritance is done by default (it's legacy annotation here).

*/
public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1): array
Expand All @@ -97,6 +99,8 @@ public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1)
}

/**
* @deprecated The "BookmarkHandler::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
*
* {@inheritdoc}
*/
public function countUserBookmarks(int $userId): int
Expand Down
4 changes: 4 additions & 0 deletions src/lib/Persistence/Legacy/Bookmark/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ abstract public function loadUserIdsByLocation(Location $location): array;
/**
* Load data for all bookmarks owned by given $userId.
*
* @deprecated Gateway::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
*
* @param int $userId ID of user
* @param int $offset Offset to start listing from, 0 by default
* @param int $limit Limit for the listing. -1 by default (no limit)
Expand All @@ -63,6 +65,8 @@ abstract public function loadUserBookmarks(int $userId, int $offset = 0, int $li
/**
* Count bookmarks owned by given $userId.
*
* @deprecated The "Gateway::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
*
* @param int $userId ID of user
*
* @return int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ public function loadUserIdsByLocation(Location $location): array
}

/**
* @deprecated The "DoctrineDatabase::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
*
* {@inheritdoc}
*/
public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1): array
Expand All @@ -146,6 +148,8 @@ public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1)
}

/**
* @deprecated The "DoctrineDatabase::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
*
Comment on lines 127 to +152
Copy link
Member

Choose a reason for hiding this comment

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

Same remark here

* {@inheritdoc}
*/
public function countUserBookmarks(int $userId): int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public function loadBookmarkDataByUserIdAndLocationId(int $userId, array $locati
}
}

/**
* @deprecated The "ExceptionConversion::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
*
* @return array
Copy link
Member

Choose a reason for hiding this comment

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

This PHPDoc return doesn't provide any extra value. It should be documented as at least array<string, mixed> but on the interface (or abstract) level.

*/
public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1): array
{
try {
Expand All @@ -66,6 +71,9 @@ public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1)
}
}

/**
* @deprecated The "ExceptionConversion::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
Comment on lines 61 to +75
Copy link
Member

Choose a reason for hiding this comment

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

Here too

*/
public function countUserBookmarks(int $userId): int
{
try {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/Persistence/Legacy/Bookmark/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public function loadByUserIdAndLocationId(int $userId, array $locationIds): arra
}

/**
* @deprecated The "Handler::loadUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::find()" and "Criterion\IsBookmarked" instead.
*
* {@inheritdoc}
*/
public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1): array
Expand All @@ -85,6 +87,8 @@ public function loadUserBookmarks(int $userId, int $offset = 0, int $limit = -1)
}

/**
* @deprecated The "Handler::countUserBookmarks()" method is deprecated, will be removed in 6.0.0. Use "LocationService::count()" and "Criterion\IsBookmarked" instead.
*
* {@inheritdoc}
*/
public function countUserBookmarks(int $userId): int
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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\Persistence\Legacy\Filter\CriterionQueryBuilder\Location;

use Doctrine\DBAL\ParameterType;
use Ibexa\Contracts\Core\Persistence\Filter\Doctrine\FilteringQueryBuilder;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\IsBookmarked;
use Ibexa\Contracts\Core\Repository\Values\Filter\FilteringCriterion;
use Ibexa\Core\Persistence\Legacy\Bookmark\Gateway\DoctrineDatabase;

/**
* @internal for internal use by Repository Filtering
*/
final class BookmarkQueryBuilder extends BaseLocationCriterionQueryBuilder
{
public function accepts(FilteringCriterion $criterion): bool
{
return $criterion instanceof IsBookmarked;
}

public function buildQueryConstraint(
FilteringQueryBuilder $queryBuilder,
FilteringCriterion $criterion
): string {
$queryBuilder
->joinOnce(
'location',
DoctrineDatabase::TABLE_BOOKMARKS,
'bookmark',
'location.node_id = bookmark.node_id'
);

/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\IsBookmarked $criterion */
$value = $criterion->value;
Copy link
Member

Choose a reason for hiding this comment

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

As described earlier, this should be part of the logic here, not user input by default. Inject PermissionResolver and get current user ID


if (\is_array($value)) {
if (!isset($value[0])) {
throw new \InvalidArgumentException('IsBookmarked criterion value must contain userId at index 0.');
}
$value = $value[0];
}

return $queryBuilder->expr()->eq(
'bookmark.user_id',
$queryBuilder->createNamedParameter(
(int)$value,
ParameterType::INTEGER
)
);
Comment on lines +39 to +55
Copy link
Member

Choose a reason for hiding this comment

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

Following the API design remark, This could be either:

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

or simply

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

PermissionResolver should be injected via constructor.

As for boolean value of is or is not bookmarked, I'd probably use ternary expression based on $criterion->isBookmarked() getter and call neq or eq depending on the value.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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\Persistence\Legacy\Filter\SortClauseQueryBuilder\Bookmark;

use Ibexa\Contracts\Core\Persistence\Filter\Doctrine\FilteringQueryBuilder;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause\BookmarkId;
use Ibexa\Contracts\Core\Repository\Values\Filter\FilteringSortClause;
use Ibexa\Contracts\Core\Repository\Values\Filter\SortClauseQueryBuilder;

final class IdSortClauseQueryBuilder implements SortClauseQueryBuilder
{
public function accepts(FilteringSortClause $sortClause): bool
{
return $sortClause instanceof BookmarkId;
}

public function buildQuery(
FilteringQueryBuilder $queryBuilder,
FilteringSortClause $sortClause
): void {
if (!$sortClause instanceof BookmarkId) {
throw new \InvalidArgumentException(sprintf(
'Expected %s, got %s',
BookmarkId::class,
get_class($sortClause),
));
}
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause\BookmarkId $sortClause */
$queryBuilder->addSelect('bookmark.id');
$queryBuilder->addOrderBy('bookmark.id', $sortClause->direction);
}
}
45 changes: 31 additions & 14 deletions src/lib/Repository/BookmarkService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,40 @@
namespace Ibexa\Core\Repository;

use Exception;
use Ibexa\Contracts\Core\Persistence\Bookmark\Bookmark;
use Ibexa\Contracts\Core\Persistence\Bookmark\CreateStruct;
use Ibexa\Contracts\Core\Persistence\Bookmark\Handler as BookmarkHandler;
use Ibexa\Contracts\Core\Repository\BookmarkService as BookmarkServiceInterface;
use Ibexa\Contracts\Core\Repository\Exceptions\BadStateException;
use Ibexa\Contracts\Core\Repository\Repository as RepositoryInterface;
use Ibexa\Contracts\Core\Repository\Values\Bookmark\BookmarkList;
use Ibexa\Contracts\Core\Repository\Values\Content\Location;
use Ibexa\Contracts\Core\Repository\Values\Content\Query;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause;
use Ibexa\Contracts\Core\Repository\Values\Filter\Filter;
use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class BookmarkService implements BookmarkServiceInterface
{
/** @var \Ibexa\Contracts\Core\Repository\Repository */
protected $repository;
protected RepositoryInterface $repository;

/** @var \Ibexa\Contracts\Core\Persistence\Bookmark\Handler */
protected $bookmarkHandler;
protected BookmarkHandler $bookmarkHandler;

private LoggerInterface $logger;

/**
* BookmarkService constructor.
*
* @param \Ibexa\Contracts\Core\Repository\Repository $repository
* @param \Ibexa\Contracts\Core\Persistence\Bookmark\Handler $bookmarkHandler
*/
public function __construct(RepositoryInterface $repository, BookmarkHandler $bookmarkHandler)
public function __construct(RepositoryInterface $repository, BookmarkHandler $bookmarkHandler, LoggerInterface $logger = null)
{
$this->repository = $repository;
$this->bookmarkHandler = $bookmarkHandler;
$this->logger = $logger ?? new NullLogger();
}

/**
Expand Down Expand Up @@ -97,16 +104,26 @@ public function loadBookmarks(int $offset = 0, int $limit = 25): BookmarkList
{
$currentUserId = $this->getCurrentUserId();

$list = new BookmarkList();
$list->totalCount = $this->bookmarkHandler->countUserBookmarks($currentUserId);
if ($list->totalCount > 0) {
$bookmarks = $this->bookmarkHandler->loadUserBookmarks($currentUserId, $offset, $limit);

$list->items = array_map(function (Bookmark $bookmark) {
return $this->repository->getLocationService()->loadLocation($bookmark->locationId);
}, $bookmarks);
$filter = new Filter();
try {
$filter
->withCriterion(new Criterion\IsBookmarked($currentUserId))
->withSortClause(new SortClause\BookmarkId(Query::SORT_DESC))
->sliceBy($limit, $offset);

$result = $this->repository->getLocationService()->find($filter, []);
} catch (BadStateException $e) {
$this->logger->debug($e->getMessage(), [
'exception' => $e,
]);

return new BookmarkList();
}

$list = new BookmarkList();
$list->totalCount = $result->totalCount;
$list->items = $result->locations;
Copy link
Member

Choose a reason for hiding this comment

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

locations is a magic property, which using of should be avoided due to performance. You should rather do iterator_to_array here.


return $list;
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/Repository/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,8 @@ public function getBookmarkService(): BookmarkServiceInterface
if ($this->bookmarkService === null) {
$this->bookmarkService = new BookmarkService(
$this,
$this->persistenceHandler->bookmarkHandler()
$this->persistenceHandler->bookmarkHandler(),
$this->logger
);
}

Expand Down
Loading
Loading