Handle pagination through BookmarkService

Handle all search results through SearchResult object.
This is a required step toward implementing a BookmarkService based on SQL database.

Related to #953
This commit is contained in:
ArthurHoaro 2021-01-20 14:45:59 +01:00
parent 055d97f9a9
commit 9b8c0a4560
20 changed files with 420 additions and 149 deletions

View file

@ -36,13 +36,6 @@ class Links extends ApiController
public function getLinks($request, $response) public function getLinks($request, $response)
{ {
$private = $request->getParam('visibility'); $private = $request->getParam('visibility');
$bookmarks = $this->bookmarkService->search(
[
'searchtags' => $request->getParam('searchtags', ''),
'searchterm' => $request->getParam('searchterm', ''),
],
$private
);
// Return bookmarks from the {offset}th link, starting from 0. // Return bookmarks from the {offset}th link, starting from 0.
$offset = $request->getParam('offset'); $offset = $request->getParam('offset');
@ -50,9 +43,6 @@ public function getLinks($request, $response)
throw new ApiBadParametersException('Invalid offset'); throw new ApiBadParametersException('Invalid offset');
} }
$offset = ! empty($offset) ? intval($offset) : 0; $offset = ! empty($offset) ? intval($offset) : 0;
if ($offset > count($bookmarks)) {
return $response->withJson([], 200, $this->jsonStyle);
}
// limit parameter is either a number of bookmarks or 'all' for everything. // limit parameter is either a number of bookmarks or 'all' for everything.
$limit = $request->getParam('limit'); $limit = $request->getParam('limit');
@ -61,24 +51,34 @@ public function getLinks($request, $response)
} elseif (ctype_digit($limit)) { } elseif (ctype_digit($limit)) {
$limit = intval($limit); $limit = intval($limit);
} elseif ($limit === 'all') { } elseif ($limit === 'all') {
$limit = count($bookmarks); $limit = null;
} else { } else {
throw new ApiBadParametersException('Invalid limit'); throw new ApiBadParametersException('Invalid limit');
} }
$searchResult = $this->bookmarkService->search(
[
'searchtags' => $request->getParam('searchtags', ''),
'searchterm' => $request->getParam('searchterm', ''),
],
$private,
false,
false,
false,
[
'limit' => $limit,
'offset' => $offset,
'allowOutOfBounds' => true,
]
);
// 'environment' is set by Slim and encapsulate $_SERVER. // 'environment' is set by Slim and encapsulate $_SERVER.
$indexUrl = index_url($this->ci['environment']); $indexUrl = index_url($this->ci['environment']);
$out = []; $out = [];
$index = 0; foreach ($searchResult->getBookmarks() as $bookmark) {
foreach ($bookmarks as $bookmark) {
if (count($out) >= $limit) {
break;
}
if ($index++ >= $offset) {
$out[] = ApiUtils::formatLink($bookmark, $indexUrl); $out[] = ApiUtils::formatLink($bookmark, $indexUrl);
} }
}
return $response->withJson($out, 200, $this->jsonStyle); return $response->withJson($out, 200, $this->jsonStyle);
} }

View file

@ -122,12 +122,12 @@ public function putTag($request, $response, $args)
throw new ApiBadParametersException('New tag name is required in the request body'); throw new ApiBadParametersException('New tag name is required in the request body');
} }
$bookmarks = $this->bookmarkService->search( $searchResult = $this->bookmarkService->search(
['searchtags' => $args['tagName']], ['searchtags' => $args['tagName']],
BookmarkFilter::$ALL, BookmarkFilter::$ALL,
true true
); );
foreach ($bookmarks as $bookmark) { foreach ($searchResult->getBookmarks() as $bookmark) {
$bookmark->renameTag($args['tagName'], $data['name']); $bookmark->renameTag($args['tagName'], $data['name']);
$this->bookmarkService->set($bookmark, false); $this->bookmarkService->set($bookmark, false);
$this->history->updateLink($bookmark); $this->history->updateLink($bookmark);
@ -157,12 +157,12 @@ public function deleteTag($request, $response, $args)
throw new ApiTagNotFoundException(); throw new ApiTagNotFoundException();
} }
$bookmarks = $this->bookmarkService->search( $searchResult = $this->bookmarkService->search(
['searchtags' => $args['tagName']], ['searchtags' => $args['tagName']],
BookmarkFilter::$ALL, BookmarkFilter::$ALL,
true true
); );
foreach ($bookmarks as $bookmark) { foreach ($searchResult->getBookmarks() as $bookmark) {
$bookmark->deleteTag($args['tagName']); $bookmark->deleteTag($args['tagName']);
$this->bookmarkService->set($bookmark, false); $this->bookmarkService->set($bookmark, false);
$this->history->updateLink($bookmark); $this->history->updateLink($bookmark);

View file

@ -55,8 +55,12 @@ class BookmarkFileService implements BookmarkServiceInterface
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn) public function __construct(
{ ConfigManager $conf,
History $history,
Mutex $mutex,
bool $isLoggedIn
) {
$this->conf = $conf; $this->conf = $conf;
$this->history = $history; $this->history = $history;
$this->mutex = $mutex; $this->mutex = $mutex;
@ -129,8 +133,9 @@ public function search(
string $visibility = null, string $visibility = null,
bool $caseSensitive = false, bool $caseSensitive = false,
bool $untaggedOnly = false, bool $untaggedOnly = false,
bool $ignoreSticky = false bool $ignoreSticky = false,
) { array $pagination = []
): SearchResult {
if ($visibility === null) { if ($visibility === null) {
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
} }
@ -143,13 +148,20 @@ public function search(
$this->bookmarks->reorder('DESC', true); $this->bookmarks->reorder('DESC', true);
} }
return $this->bookmarkFilter->filter( $bookmarks = $this->bookmarkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
[$searchTags, $searchTerm], [$searchTags, $searchTerm],
$caseSensitive, $caseSensitive,
$visibility, $visibility,
$untaggedOnly $untaggedOnly
); );
return SearchResult::getSearchResult(
$bookmarks,
$pagination['offset'] ?? 0,
$pagination['limit'] ?? null,
$pagination['allowOutOfBounds'] ?? false
);
} }
/** /**
@ -282,7 +294,7 @@ public function exists(int $id, string $visibility = null): bool
*/ */
public function count(string $visibility = null): int public function count(string $visibility = null): int
{ {
return count($this->search([], $visibility)); return $this->search([], $visibility)->getResultCount();
} }
/** /**
@ -305,10 +317,10 @@ public function save(): void
*/ */
public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
{ {
$bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); $searchResult = $this->search(['searchtags' => $filteringTags], $visibility);
$tags = []; $tags = [];
$caseMapping = []; $caseMapping = [];
foreach ($bookmarks as $bookmark) { foreach ($searchResult->getBookmarks() as $bookmark) {
foreach ($bookmark->getTags() as $tag) { foreach ($bookmark->getTags() as $tag) {
if ( if (
empty($tag) empty($tag)
@ -357,7 +369,7 @@ public function findByDate(
$previous = null; $previous = null;
$next = null; $next = null;
foreach ($this->search([], null, false, false, true) as $bookmark) { foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
if ($to < $bookmark->getCreated()) { if ($to < $bookmark->getCreated()) {
$next = $bookmark->getCreated(); $next = $bookmark->getCreated();
} elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
@ -378,7 +390,7 @@ public function findByDate(
*/ */
public function getLatest(): ?Bookmark public function getLatest(): ?Bookmark
{ {
foreach ($this->search([], null, false, false, true) as $bookmark) { foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
return $bookmark; return $bookmark;
} }

View file

@ -44,16 +44,18 @@ public function findByUrl(string $url): ?Bookmark;
* @param bool $caseSensitive * @param bool $caseSensitive
* @param bool $untaggedOnly * @param bool $untaggedOnly
* @param bool $ignoreSticky * @param bool $ignoreSticky
* @param array $pagination This array can contain the following keys for pagination: limit, offset.
* *
* @return Bookmark[] * @return SearchResult
*/ */
public function search( public function search(
array $request = [], array $request = [],
string $visibility = null, string $visibility = null,
bool $caseSensitive = false, bool $caseSensitive = false,
bool $untaggedOnly = false, bool $untaggedOnly = false,
bool $ignoreSticky = false bool $ignoreSticky = false,
); array $pagination = []
): SearchResult;
/** /**
* Get a single bookmark by its ID. * Get a single bookmark by its ID.

View file

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Shaarli\Bookmark;
/**
* Read-only class used to represent search result, including pagination.
*/
class SearchResult
{
/** @var Bookmark[] List of result bookmarks with pagination applied */
protected $bookmarks;
/** @var int number of Bookmarks found, with pagination applied */
protected $resultCount;
/** @var int total number of result found */
protected $totalCount;
/** @var int pagination: limit number of result bookmarks */
protected $limit;
/** @var int pagination: offset to apply to complete result list */
protected $offset;
public function __construct(array $bookmarks, int $totalCount, int $offset, ?int $limit)
{
$this->bookmarks = $bookmarks;
$this->resultCount = count($bookmarks);
$this->totalCount = $totalCount;
$this->limit = $limit;
$this->offset = $offset;
}
/**
* Build a SearchResult from provided full result set and pagination settings.
*
* @param Bookmark[] $bookmarks Full set of result which will be filtered
* @param int $offset Start recording results from $offset
* @param int|null $limit End recording results after $limit bookmarks is reached
* @param bool $allowOutOfBounds Set to false to display the last page if the offset is out of bound,
* return empty result set otherwise (default: false)
*
* @return SearchResult
*/
public static function getSearchResult(
$bookmarks,
int $offset = 0,
?int $limit = null,
bool $allowOutOfBounds = false
): self {
$totalCount = count($bookmarks);
if (!$allowOutOfBounds && $offset > $totalCount) {
$offset = $limit === null ? 0 : $limit * -1;
}
if ($bookmarks instanceof BookmarkArray) {
$buffer = [];
foreach ($bookmarks as $key => $value) {
$buffer[$key] = $value;
}
$bookmarks = $buffer;
}
return new static(
array_slice($bookmarks, $offset, $limit, true),
$totalCount,
$offset,
$limit
);
}
/** @return Bookmark[] List of result bookmarks with pagination applied */
public function getBookmarks(): array
{
return $this->bookmarks;
}
/** @return int number of Bookmarks found, with pagination applied */
public function getResultCount(): int
{
return $this->resultCount;
}
/** @return int total number of result found */
public function getTotalCount(): int
{
return $this->totalCount;
}
/** @return int pagination: limit number of result bookmarks */
public function getLimit(): ?int
{
return $this->limit;
}
/** @return int pagination: offset to apply to complete result list */
public function getOffset(): int
{
return $this->offset;
}
/** @return int Current page of result set in complete results */
public function getPage(): int
{
if (empty($this->limit)) {
return $this->offset === 0 ? 1 : 2;
}
$base = $this->offset >= 0 ? $this->offset : $this->totalCount + $this->offset;
return (int) ceil($base / $this->limit) + 1;
}
/** @return int Get the # of the last page */
public function getLastPage(): int
{
if (empty($this->limit)) {
return $this->offset === 0 ? 1 : 2;
}
return (int) ceil($this->totalCount / $this->limit);
}
/** @return bool Either the current page is the last one or not */
public function isLastPage(): bool
{
return $this->getPage() === $this->getLastPage();
}
/** @return bool Either the current page is the first one or not */
public function isFirstPage(): bool
{
return $this->offset === 0;
}
}

View file

@ -102,22 +102,16 @@ public function buildData(string $feedType, ?array $userInput)
$userInput['searchtags'] = false; $userInput['searchtags'] = false;
} }
$limit = $this->getLimit($userInput);
// Optionally filter the results: // Optionally filter the results:
$linksToDisplay = $this->linkDB->search($userInput ?? [], null, false, false, true); $searchResult = $this->linkDB->search($userInput ?? [], null, false, false, true, ['limit' => $limit]);
$nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
// Can't use array_keys() because $link is a LinkDB instance and not a real array.
$keys = [];
foreach ($linksToDisplay as $key => $value) {
$keys[] = $key;
}
$pageaddr = escape(index_url($this->serverInfo)); $pageaddr = escape(index_url($this->serverInfo));
$this->formatter->addContextData('index_url', $pageaddr); $this->formatter->addContextData('index_url', $pageaddr);
$linkDisplayed = []; $links = [];
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { foreach ($searchResult->getBookmarks() as $key => $bookmark) {
$linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); $links[$key] = $this->buildItem($feedType, $bookmark, $pageaddr);
} }
$data['language'] = $this->getTypeLanguage($feedType); $data['language'] = $this->getTypeLanguage($feedType);
@ -128,7 +122,7 @@ public function buildData(string $feedType, ?array $userInput)
$data['self_link'] = $pageaddr . $requestUri; $data['self_link'] = $pageaddr . $requestUri;
$data['index_url'] = $pageaddr; $data['index_url'] = $pageaddr;
$data['usepermalinks'] = $this->usePermalinks === true; $data['usepermalinks'] = $this->usePermalinks === true;
$data['links'] = $linkDisplayed; $data['links'] = $links;
return $data; return $data;
} }
@ -268,19 +262,18 @@ protected function getIsoDate(string $feedType, DateTime $date, $format = false)
* If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
* If 'nb' is set to 'all', display all filtered bookmarks (max parameter). * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
* *
* @param int $max maximum number of bookmarks to display.
* @param array $userInput $_GET. * @param array $userInput $_GET.
* *
* @return int number of bookmarks to display. * @return int number of bookmarks to display.
*/ */
protected function getNbLinks($max, ?array $userInput) protected function getLimit(?array $userInput)
{ {
if (empty($userInput['nb'])) { if (empty($userInput['nb'])) {
return self::$DEFAULT_NB_LINKS; return self::$DEFAULT_NB_LINKS;
} }
if ($userInput['nb'] == 'all') { if ($userInput['nb'] == 'all') {
return $max; return null;
} }
$intNb = intval($userInput['nb']); $intNb = intval($userInput['nb']);

View file

@ -57,9 +57,12 @@ public function save(Request $request, Response $response): Response
} }
// TODO: move this to bookmark service // TODO: move this to bookmark service
$count = 0; $searchResult = $this->container->bookmarkService->search(
$bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true); ['searchtags' => $fromTag],
foreach ($bookmarks as $bookmark) { BookmarkFilter::$ALL,
true
);
foreach ($searchResult->getBookmarks() as $bookmark) {
if (false === $isDelete) { if (false === $isDelete) {
$bookmark->renameTag($fromTag, $toTag); $bookmark->renameTag($fromTag, $toTag);
} else { } else {
@ -68,11 +71,11 @@ public function save(Request $request, Response $response): Response
$this->container->bookmarkService->set($bookmark, false); $this->container->bookmarkService->set($bookmark, false);
$this->container->history->updateLink($bookmark); $this->container->history->updateLink($bookmark);
$count++;
} }
$this->container->bookmarkService->save(); $this->container->bookmarkService->save();
$count = $searchResult->getResultCount();
if (true === $isDelete) { if (true === $isDelete) {
$alert = sprintf( $alert = sprintf(
t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count), t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count),

View file

@ -22,7 +22,7 @@ class ThumbnailsController extends ShaarliAdminController
public function index(Request $request, Response $response): Response public function index(Request $request, Response $response): Response
{ {
$ids = []; $ids = [];
foreach ($this->container->bookmarkService->search() as $bookmark) { foreach ($this->container->bookmarkService->search()->getBookmarks() as $bookmark) {
// A note or not HTTP(S) // A note or not HTTP(S)
if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) { if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) {
continue; continue;

View file

@ -36,7 +36,6 @@ public function index(Request $request, Response $response): Response
$searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); $searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
$searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? '')); $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));
;
// Filter bookmarks according search parameters. // Filter bookmarks according search parameters.
$visibility = $this->container->sessionManager->getSessionParameter('visibility'); $visibility = $this->container->sessionManager->getSessionParameter('visibility');
@ -44,39 +43,26 @@ public function index(Request $request, Response $response): Response
'searchtags' => $searchTags, 'searchtags' => $searchTags,
'searchterm' => $searchTerm, 'searchterm' => $searchTerm,
]; ];
$linksToDisplay = $this->container->bookmarkService->search(
// Select articles according to paging.
$page = (int) ($request->getParam('page') ?? 1);
$page = $page < 1 ? 1 : $page;
$linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
$searchResult = $this->container->bookmarkService->search(
$search, $search,
$visibility, $visibility,
false, false,
!!$this->container->sessionManager->getSessionParameter('untaggedonly') !!$this->container->sessionManager->getSessionParameter('untaggedonly'),
false,
['offset' => $linksPerPage * ($page - 1), 'limit' => $linksPerPage]
) ?? []; ) ?? [];
// ---- Handle paging.
$keys = [];
foreach ($linksToDisplay as $key => $value) {
$keys[] = $key;
}
$linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
// Select articles according to paging.
$pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1;
$page = (int) $request->getParam('page') ?? 1;
$page = $page < 1 ? 1 : $page;
$page = $page > $pageCount ? $pageCount : $page;
// Start index.
$i = ($page - 1) * $linksPerPage;
$end = $i + $linksPerPage;
$linkDisp = [];
$save = false; $save = false;
while ($i < $end && $i < count($keys)) { $links = [];
$save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save; foreach ($searchResult->getBookmarks() as $key => $bookmark) {
$link = $formatter->format($linksToDisplay[$keys[$i]]); $save = $this->updateThumbnail($bookmark, false) || $save;
$links[$key] = $formatter->format($bookmark);
$linkDisp[$keys[$i]] = $link;
$i++;
} }
if ($save) { if ($save) {
@ -86,15 +72,10 @@ public function index(Request $request, Response $response): Response
// Compute paging navigation // Compute paging navigation
$searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags); $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
$searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm); $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
$page = $searchResult->getPage();
$previous_page_url = ''; $previousPageUrl = !$searchResult->isLastPage() ? '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl : '';
if ($i !== count($keys)) { $nextPageUrl = !$searchResult->isFirstPage() ? '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl : '';
$previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl;
}
$next_page_url = '';
if ($page > 1) {
$next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
}
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
$searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator)); $searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator));
@ -104,16 +85,16 @@ public function index(Request $request, Response $response): Response
$data = array_merge( $data = array_merge(
$this->initializeTemplateVars(), $this->initializeTemplateVars(),
[ [
'previous_page_url' => $previous_page_url, 'previous_page_url' => $previousPageUrl,
'next_page_url' => $next_page_url, 'next_page_url' => $nextPageUrl,
'page_current' => $page, 'page_current' => $page,
'page_max' => $pageCount, 'page_max' => $searchResult->getLastPage(),
'result_count' => count($linksToDisplay), 'result_count' => $searchResult->getTotalCount(),
'search_term' => escape($searchTerm), 'search_term' => escape($searchTerm),
'search_tags' => escape($searchTags), 'search_tags' => escape($searchTags),
'search_tags_url' => $searchTagsUrlEncoded, 'search_tags_url' => $searchTagsUrlEncoded,
'visibility' => $visibility, 'visibility' => $visibility,
'links' => $linkDisp, 'links' => $links,
] ]
); );

View file

@ -100,7 +100,7 @@ public function rss(Request $request, Response $response): Response
$days = []; $days = [];
$format = DailyPageHelper::getFormatByType($type); $format = DailyPageHelper::getFormatByType($type);
$length = DailyPageHelper::getRssLengthByType($type); $length = DailyPageHelper::getRssLengthByType($type);
foreach ($this->container->bookmarkService->search() as $bookmark) { foreach ($this->container->bookmarkService->search()->getBookmarks() as $bookmark) {
$day = $bookmark->getCreated()->format($format); $day = $bookmark->getCreated()->format($format);
// Stop iterating after DAILY_RSS_NB_DAYS entries // Stop iterating after DAILY_RSS_NB_DAYS entries

View file

@ -30,19 +30,19 @@ public function index(Request $request, Response $response): Response
); );
// Optionally filter the results: // Optionally filter the results:
$links = $this->container->bookmarkService->search($request->getQueryParams()); $bookmarks = $this->container->bookmarkService->search($request->getQueryParams())->getBookmarks();
$linksToDisplay = []; $links = [];
// Get only bookmarks which have a thumbnail. // Get only bookmarks which have a thumbnail.
// Note: we do not retrieve thumbnails here, the request is too heavy. // Note: we do not retrieve thumbnails here, the request is too heavy.
$formatter = $this->container->formatterFactory->getFormatter('raw'); $formatter = $this->container->formatterFactory->getFormatter('raw');
foreach ($links as $key => $link) { foreach ($bookmarks as $key => $bookmark) {
if (!empty($link->getThumbnail())) { if (!empty($bookmark->getThumbnail())) {
$linksToDisplay[] = $formatter->format($link); $links[] = $formatter->format($bookmark);
} }
} }
$data = ['linksToDisplay' => $linksToDisplay]; $data = ['linksToDisplay' => $links];
$this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL); $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {

View file

@ -64,7 +64,7 @@ public function filterAndFormat(
} }
$bookmarkLinks = []; $bookmarkLinks = [];
foreach ($this->bookmarkService->search([], $selection) as $bookmark) { foreach ($this->bookmarkService->search([], $selection)->getBookmarks() as $bookmark) {
$link = $formatter->format($bookmark); $link = $formatter->format($bookmark);
$link['taglist'] = implode(',', $bookmark->getTags()); $link['taglist'] = implode(',', $bookmark->getTags());
if ($bookmark->isNote() && $prependNoteUrl) { if ($bookmark->isNote() && $prependNoteUrl) {

View file

@ -152,7 +152,7 @@ public function updateMethodMigrateExistingNotesUrl(): bool
{ {
$updated = false; $updated = false;
foreach ($this->bookmarkService->search() as $bookmark) { foreach ($this->bookmarkService->search()->getBookmarks() as $bookmark) {
if ( if (
$bookmark->isNote() $bookmark->isNote()
&& startsWith($bookmark->getUrl(), '?') && startsWith($bookmark->getUrl(), '?')

View file

@ -807,7 +807,7 @@ public function testFilterString()
$request = ['searchtags' => $tags]; $request = ['searchtags' => $tags];
$this->assertEquals( $this->assertEquals(
2, 2,
count($this->privateLinkDB->search($request, null, true)) count($this->privateLinkDB->search($request, null, true)->getBookmarks())
); );
} }
@ -820,7 +820,7 @@ public function testFilterArray()
$request = ['searchtags' => $tags]; $request = ['searchtags' => $tags];
$this->assertEquals( $this->assertEquals(
2, 2,
count($this->privateLinkDB->search($request, null, true)) count($this->privateLinkDB->search($request, null, true)->getBookmarks())
); );
} }
@ -834,12 +834,12 @@ public function testHiddenTags()
$request = ['searchtags' => $tags]; $request = ['searchtags' => $tags];
$this->assertEquals( $this->assertEquals(
1, 1,
count($this->privateLinkDB->search($request, 'all', true)) count($this->privateLinkDB->search($request, 'all', true)->getBookmarks())
); );
$this->assertEquals( $this->assertEquals(
0, 0,
count($this->publicLinkDB->search($request, 'public', true)) count($this->publicLinkDB->search($request, 'public', true)->getBookmarks())
); );
} }

View file

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Shaarli\Bookmark;
use Shaarli\TestCase;
/**
* Test SearchResult class.
*/
class SearchResultTest extends TestCase
{
/** Create a SearchResult without any pagination parameter. */
public function testResultNoParameters(): void
{
$searchResult = SearchResult::getSearchResult($data = ['a', 'b', 'c', 'd', 'e', 'f']);
static::assertSame($data, $searchResult->getBookmarks());
static::assertSame(6, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(null, $searchResult->getLimit());
static::assertSame(0, $searchResult->getOffset());
static::assertSame(1, $searchResult->getPage());
static::assertSame(1, $searchResult->getLastPage());
static::assertTrue($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with only an offset parameter */
public function testResultWithOffset(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 2);
static::assertSame([2 => 'c', 3 => 'd', 4 => 'e', 5 => 'f'], $searchResult->getBookmarks());
static::assertSame(4, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(null, $searchResult->getLimit());
static::assertSame(2, $searchResult->getOffset());
static::assertSame(2, $searchResult->getPage());
static::assertSame(2, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with only a limit parameter */
public function testResultWithLimit(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 0, 2);
static::assertSame([0 => 'a', 1 => 'b'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(0, $searchResult->getOffset());
static::assertSame(1, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertTrue($searchResult->isFirstPage());
static::assertFalse($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters */
public function testResultWithLimitAndOffset(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 2, 2);
static::assertSame([2 => 'c', 3 => 'd'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(2, $searchResult->getOffset());
static::assertSame(2, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertFalse($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters displaying the last page */
public function testResultWithLimitAndOffsetLastPage(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 4, 2);
static::assertSame([4 => 'e', 5 => 'f'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(4, $searchResult->getOffset());
static::assertSame(3, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters out of bound (display the last page) */
public function testResultWithLimitAndOffsetOutOfBounds(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 12, 2);
static::assertSame([4 => 'e', 5 => 'f'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(-2, $searchResult->getOffset());
static::assertSame(3, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters out of bound (no result) */
public function testResultWithLimitAndOffsetOutOfBoundsNoResult(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 12, 2, true);
static::assertSame([], $searchResult->getBookmarks());
static::assertSame(0, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(12, $searchResult->getOffset());
static::assertSame(7, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertFalse($searchResult->isLastPage());
}
}

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\WrongTokenException; use Shaarli\Front\Exception\WrongTokenException;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
@ -100,11 +101,11 @@ public function testSaveRenameTagValid(): void
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true) ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
->willReturnCallback(function () use ($bookmark1, $bookmark2): array { ->willReturnCallback(function () use ($bookmark1, $bookmark2): SearchResult {
$bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag'); $bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
$bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag'); $bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
return [$bookmark1, $bookmark2]; return SearchResult::getSearchResult([$bookmark1, $bookmark2]);
}) })
; ;
$this->container->bookmarkService $this->container->bookmarkService
@ -153,11 +154,11 @@ public function testSaveDeleteTagValid(): void
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true) ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
->willReturnCallback(function () use ($bookmark1, $bookmark2): array { ->willReturnCallback(function () use ($bookmark1, $bookmark2): SearchResult {
$bookmark1->expects(static::once())->method('deleteTag')->with('old-tag'); $bookmark1->expects(static::once())->method('deleteTag')->with('old-tag');
$bookmark2->expects(static::once())->method('deleteTag')->with('old-tag'); $bookmark2->expects(static::once())->method('deleteTag')->with('old-tag');
return [$bookmark1, $bookmark2]; return SearchResult::getSearchResult([$bookmark1, $bookmark2]);
}) })
; ;
$this->container->bookmarkService $this->container->bookmarkService

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\SearchResult;
use Shaarli\TestCase; use Shaarli\TestCase;
use Shaarli\Thumbnailer; use Shaarli\Thumbnailer;
use Slim\Http\Request; use Slim\Http\Request;
@ -40,12 +41,12 @@ public function testIndex(): void
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('?abcdef')->setTitle('Note 1'), (new Bookmark())->setId(2)->setUrl('?abcdef')->setTitle('Note 1'),
(new Bookmark())->setId(3)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(3)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(4)->setUrl('ftp://domain.tld', ['ftp'])->setTitle('FTP'), (new Bookmark())->setId(4)->setUrl('ftp://domain.tld', ['ftp'])->setTitle('FTP'),
]) ]))
; ;
$result = $this->controller->index($request, $response); $result = $this->controller->index($request, $response);

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
@ -45,13 +46,15 @@ public function testIndexDefaultFirstPage(): void
['searchtags' => '', 'searchterm' => ''], ['searchtags' => '', 'searchterm' => ''],
null, null,
false, false,
false false,
false,
['offset' => 0, 'limit' => 2]
) )
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
] ], 0, 2)
); );
$this->container->sessionManager $this->container->sessionManager
@ -119,13 +122,15 @@ public function testIndexDefaultSecondPage(): void
['searchtags' => '', 'searchterm' => ''], ['searchtags' => '', 'searchterm' => ''],
null, null,
false, false,
false false,
false,
['offset' => 2, 'limit' => 2]
) )
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
]) ], 2, 2))
; ;
$this->container->sessionManager $this->container->sessionManager
@ -207,13 +212,15 @@ public function testIndexDefaultWithFilters(): void
['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'], ['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'],
'private', 'private',
false, false,
true true,
false,
['offset' => 0, 'limit' => 2]
) )
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
]) ], 0, 2))
; ;
$result = $this->controller->index($request, $response); $result = $this->controller->index($request, $response);
@ -358,13 +365,13 @@ public function testThumbnailUpdateFromLinkList(): void
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false), (new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false),
$b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'), $b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false), (new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false),
$b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'), $b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'),
(new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'), (new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'),
]) ]))
; ;
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::exactly(2)) ->expects(static::exactly(2))

View file

@ -5,6 +5,7 @@
namespace Shaarli\Front\Controller\Visitor; namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Feed\CachedPage; use Shaarli\Feed\CachedPage;
use Shaarli\TestCase; use Shaarli\TestCase;
use Slim\Http\Request; use Slim\Http\Request;
@ -347,13 +348,15 @@ public function testValidRssControllerInvokeDefault(): void
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn(
SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
(new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
(new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'), (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'),
]); ])
);
$this->container->pageCacheManager $this->container->pageCacheManager
->expects(static::once()) ->expects(static::once())
@ -454,7 +457,9 @@ public function testValidRssControllerInvokeNoBookmark(): void
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]); $this->container->bookmarkService
->expects(static::once())->method('search')
->willReturn(SearchResult::getSearchResult([]));
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];
@ -613,11 +618,13 @@ public function testSimpleRssWeekly(): void
}); });
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn(
SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
]); ])
);
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];
@ -674,11 +681,13 @@ public function testSimpleRssMonthly(): void
}); });
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn(
SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
]); ])
);
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];

View file

@ -5,6 +5,7 @@
namespace Shaarli\Front\Controller\Visitor; namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\ThumbnailsDisabledException; use Shaarli\Front\Exception\ThumbnailsDisabledException;
use Shaarli\TestCase; use Shaarli\TestCase;
@ -50,17 +51,17 @@ public function testValidControllerInvokeDefault(): void
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->willReturnCallback(function (array $parameters, ?string $visibility): array { ->willReturnCallback(function (array $parameters, ?string $visibility): SearchResult {
// Visibility is set through the container, not the call // Visibility is set through the container, not the call
static::assertNull($visibility); static::assertNull($visibility);
// No query parameters // No query parameters
if (count($parameters) === 0) { if (count($parameters) === 0) {
return [ return SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'), (new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld'), (new Bookmark())->setId(2)->setUrl('http://url2.tld'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'),
]; ]);
} }
}) })
; ;