- multiple pagination methods
- "include" parameter in /search route :
-- facets : facets are no more returned by default
-- results.stories.children : to populate the children of stories
- "story_children_limit" parameter in /search route
  will limit the number of children populated by story
- "count" (for results) and "children_count" (for stories in results) is the number of items in the current page
- "total" (for results) and "children_total" (for stories in results) is the total number of items

todo: swagger doc (but impossible to describe related/excluded parameters (like pagination methods in /search)
This commit is contained in:
jygaulier
2020-09-22 21:30:19 +02:00
parent f9fa3e0802
commit e79f2cb4f2
12 changed files with 538 additions and 70 deletions

View File

@@ -32,6 +32,27 @@ components:
flows: flows:
password: # <-- OAuth flow(authorizationCode, implicit, password or clientCredentials) password: # <-- OAuth flow(authorizationCode, implicit, password or clientCredentials)
tokenUrl: azea tokenUrl: azea
parameters:
offsetParam:
name: offset
in: query
description: offset in records count
required: false
schema:
type: integer
minimum: 0
default: 0
limitParam:
name: limit
in: query
description: number of results
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 10
paths: paths:
'/me': '/me':
get: get:
@@ -44,6 +65,96 @@ paths:
default: default:
description: Any error description: Any error
# ---------------- search ----------------
'/search':
post:
tags:
- search
description: 'Fulltext search for records or stories.
pagination: use (offset/limit) OR (page/per_page)'
parameters:
- name: query
in: query
description: 'The fulltext query (<empty> = search all)'
required: false
schema:
type: string
example: 'dogs OR cats'
default: ''
- name: search_type
in: query
description: 0 to search for records ; 1 to search for stories
required: false
schema:
type: integer
minimum: 0
maximum: 1
default: 0
- name: offset
in: query
description: offset in records count, from 0. Use along with "limit"
required: false
schema:
type: integer
minimum: 0
default: 0
- name: limit
in: query
description: number of results. Use along with "offset"
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 10
- name: page
in: query
description: page number, from 1. Use along with "per_page"
required: false
schema:
type: integer
minimum: 1
default: 1
- name: per_page
in: query
description: number of results per page. Use along with "page"
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 10
- name: story_children_limit
in: query
description: For stories, include N children
required: false
schema:
type: integer
minimum: 0
maximum: 10
default: 0
- name: include
in: query
description: Elements to be included in response
required: false
schema:
anyOf:
- enum:
- facets
- result.stories.children
explode: true
responses:
200:
description: ok
content:
application/json:
schema:
$ref: 'schemas.yaml#/ApiResponse_search'
default:
$ref: 'responses.yaml#/error_response'
# ------------ record -----------
'/records/{base_id}': '/records/{base_id}':
post: post:
tags: tags:

View File

@@ -63,6 +63,43 @@ ApiResponse_record:
$ref: '#/ApiResponse_meta' $ref: '#/ApiResponse_meta'
response: response:
$ref: '#/Record' $ref: '#/Record'
# -------------------- search ---------------
ApiResponse_search:
type: object
properties:
meta:
$ref: '#/ApiResponse_meta'
response:
$ref: '#/ApiResponse_search_response'
ApiResponse_search_response:
type: object
properties:
offset:
type: integer
description: 'The pagination offset as passed in request'
limit:
type: integer
description: 'The pagination limit as passed in request'
page_result_count:
type: integer
description: 'The number of results in this page [0...limit]'
total_result_count:
type: integer
description: 'The total number of results request'
results:
type: object
properties:
stories:
type: array
items:
$ref: '#/Record'
records:
type: array
items:
$ref: '#/Record'
ID: ID:
type: integer type: integer
Permalink: Permalink:

View File

@@ -178,9 +178,9 @@ class V3ResultHelpers
); );
} }
// if($record->isStory()) { if($record->isStory()) {
// $data['children_total'] = $record->getChildrenCount();
// } }
return $data; return $data;
} }
@@ -261,6 +261,55 @@ class V3ResultHelpers
return $ret; return $ret;
} }
/**
* @param Request $request
* @return int[] [offset, limit]
*/
public static function paginationFromRequest(Request $request)
{
// we can deal with "page / per_page" OR "offset / limit" OR "cursor / limit"
//
$method = '';
foreach(['page', 'per_page', 'offset', 'limit', 'cursor'] as $v) {
if($request->get($v) !== null) {
$method .= ($method?'+':'') . $v;
}
}
$offset = 0; // default
$limit = 10; // default
switch($method) {
case '': // no parms -> default
break;
case 'page':
case 'per_page':
case 'page+per_page':
$limit = (int)($request->get('per_page') ?: 10);
$offset = ((int)($request->get('page') ?: 1) - 1) * $limit; // page starts at 1
break;
case 'offset':
case 'limit':
case 'offset+limit':
$offset = (int)($request->get('offset') ?: 0);
$limit = (int)($request->get('limit') ?: 10);
break;
case 'cursor':
case 'cursor+limit':
if( ($cursor = $request->get('cursor')) !== null) {
$offset = (int)(base64_decode($cursor));
}
$limit = (int)($request->get('limit') ?: 10);
break;
default:
// any other combination is invalid
throw new \InvalidArgumentException(sprintf('bad pagination "%s" method', $method));
}
if($offset < 0 || $limit < 1 || $limit > 100) {
throw new \InvalidArgumentException("offset must be > 0 ; limit must be [1...100]");
}
return([$offset, $limit]);
}
//////////////////////// ////////////////////////
private function getAuthenticator() private function getAuthenticator()

View File

@@ -8,6 +8,7 @@ use Alchemy\Phrasea\Collection\Reference\CollectionReference;
use Alchemy\Phrasea\Controller\Api\Result; use Alchemy\Phrasea\Controller\Api\Result;
use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Databox\DataboxGroupable; use Alchemy\Phrasea\Databox\DataboxGroupable;
use Alchemy\Phrasea\Databox\Record\LegacyRecordRepository;
use Alchemy\Phrasea\Fractal\CallbackTransformer; use Alchemy\Phrasea\Fractal\CallbackTransformer;
use Alchemy\Phrasea\Fractal\IncludeResolver; use Alchemy\Phrasea\Fractal\IncludeResolver;
use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver; use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver;
@@ -22,14 +23,14 @@ use Alchemy\Phrasea\Search\PermalinkView;
use Alchemy\Phrasea\Search\RecordTransformer; use Alchemy\Phrasea\Search\RecordTransformer;
use Alchemy\Phrasea\Search\RecordView; use Alchemy\Phrasea\Search\RecordView;
use Alchemy\Phrasea\Search\SearchResultView; use Alchemy\Phrasea\Search\SearchResultView;
use Alchemy\Phrasea\Search\StoryTransformer;
use Alchemy\Phrasea\Search\StoryView; use Alchemy\Phrasea\Search\StoryView;
use Alchemy\Phrasea\Search\SubdefTransformer; use Alchemy\Phrasea\Search\SubdefTransformer;
use Alchemy\Phrasea\Search\SubdefView; use Alchemy\Phrasea\Search\SubdefView;
use Alchemy\Phrasea\Search\TechnicalDataTransformer; use Alchemy\Phrasea\Search\TechnicalDataTransformer;
use Alchemy\Phrasea\Search\TechnicalDataView; use Alchemy\Phrasea\Search\TechnicalDataView;
use Alchemy\Phrasea\Search\V1SearchCompositeResultTransformer; use Alchemy\Phrasea\Search\V1SearchCompositeResultTransformer;
use Alchemy\Phrasea\Search\V1SearchResultTransformer; use Alchemy\Phrasea\Search\V3SearchResultTransformer;
use Alchemy\Phrasea\Search\V3StoryTransformer;
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface; use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
use Alchemy\Phrasea\SearchEngine\SearchEngineLogger; use Alchemy\Phrasea\SearchEngine\SearchEngineLogger;
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
@@ -37,6 +38,7 @@ use Alchemy\Phrasea\SearchEngine\SearchEngineResult;
use caption_record; use caption_record;
use League\Fractal\Manager as FractalManager; use League\Fractal\Manager as FractalManager;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use League\Fractal\Serializer\ArraySerializer;
use media_Permalink_Adapter; use media_Permalink_Adapter;
use media_subdef; use media_subdef;
use record_adapter; use record_adapter;
@@ -60,24 +62,25 @@ class V3SearchController extends Controller
$subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer());
$technicalDataTransformer = new TechnicalDataTransformer(); $technicalDataTransformer = new TechnicalDataTransformer();
$recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer); $recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer);
$storyTransformer = new StoryTransformer($subdefTransformer, $recordTransformer); $storyTransformer = new V3StoryTransformer($subdefTransformer, $recordTransformer);
$compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer); $compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer);
$searchTransformer = new V1SearchResultTransformer($compositeTransformer); $searchTransformer = new V3SearchResultTransformer($compositeTransformer);
$transformerResolver = new SearchResultTransformerResolver([ $transformerResolver = new SearchResultTransformerResolver([
'' => $searchTransformer, '' => $searchTransformer,
'results' => $compositeTransformer, 'results' => $compositeTransformer,
'facets' => new CallbackTransformer(),
'results.stories' => $storyTransformer, 'results.stories' => $storyTransformer,
'results.stories.thumbnail' => $subdefTransformer, 'results.stories.thumbnail' => $subdefTransformer,
'results.stories.metadatas' => new CallbackTransformer(), 'results.stories.metadatas' => new CallbackTransformer(),
'results.stories.caption' => new CallbackTransformer(), 'results.stories.caption' => new CallbackTransformer(),
'results.stories.records' => $recordTransformer, 'results.stories.children' => $recordTransformer,
'results.stories.records.thumbnail' => $subdefTransformer, 'results.stories.children.thumbnail' => $subdefTransformer,
'results.stories.records.technical_informations' => $technicalDataTransformer, 'results.stories.children.technical_informations' => $technicalDataTransformer,
'results.stories.records.subdefs' => $subdefTransformer, 'results.stories.children.subdefs' => $subdefTransformer,
'results.stories.records.metadata' => new CallbackTransformer(), 'results.stories.children.metadata' => new CallbackTransformer(),
'results.stories.records.status' => new CallbackTransformer(), 'results.stories.children.status' => new CallbackTransformer(),
'results.stories.records.caption' => new CallbackTransformer(), 'results.stories.children.caption' => new CallbackTransformer(),
'results.records' => $recordTransformer, 'results.records' => $recordTransformer,
'results.records.thumbnail' => $subdefTransformer, 'results.records.thumbnail' => $subdefTransformer,
'results.records.technical_informations' => $technicalDataTransformer, 'results.records.technical_informations' => $technicalDataTransformer,
@@ -91,21 +94,23 @@ class V3SearchController extends Controller
$fractal = new FractalManager(); $fractal = new FractalManager();
$fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher'])); $fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher']));
$fractal->parseIncludes($this->resolveSearchIncludes($request));
// and push everything back to fractal
$fractal->parseIncludes($this->getIncludes($request));
$result = $this->doSearch($request); $result = $this->doSearch($request);
$story_max_records = null; $story_children_limit = null;
// if search on story // if searching stories
if ($request->get('search_type') == 1) { if ($request->get('search_type') == 1) {
$story_max_records = (int)$request->get('story_max_records') ?: 10; $story_children_limit = (int)$request->get('story_children_limit') ?: 10;
} }
$searchView = $this->buildSearchView( $searchView = $this->buildSearchView(
$result, $result,
$includeResolver->resolve($fractal), $includeResolver->resolve($fractal),
$this->resolveSubdefUrlTTL($request), $this->resolveSubdefUrlTTL($request),
$story_max_records $story_children_limit
); );
$ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray();
@@ -113,25 +118,30 @@ class V3SearchController extends Controller
return Result::create($request, $ret)->createResponse(); return Result::create($request, $ret)->createResponse();
} }
/** /**
* Returns requested includes * Returns requested includes
* *
* @param Request $request * @param Request $request
* @return string[] * @return string[]
*/ */
private function resolveSearchIncludes(Request $request) private function getIncludes(Request $request)
{ {
$includes = [ // a local fractal manager will help to smartly parse the request parameters.
'results.stories.records' $fractal = new FractalManager();
];
// first, get includes from request
//
$fractal->parseIncludes($request->get('include', []));
$includes = $fractal->getRequestedIncludes();
if ($request->attributes->get('_extended', false)) { if ($request->attributes->get('_extended', false)) {
if ($request->get('search_type') != SearchEngineOptions::RECORD_STORY) { // if ($request->get('search_type') != SearchEngineOptions::RECORD_STORY) {
if(in_array('results.stories.children', $includes)) {
$includes = array_merge($includes, [ $includes = array_merge($includes, [
'results.stories.records.subdefs', 'results.stories.children.subdefs',
'results.stories.records.metadata', 'results.stories.children.metadata',
'results.stories.records.caption', 'results.stories.children.caption',
'results.stories.records.status' 'results.stories.children.status'
]); ]);
} }
else { else {
@@ -146,7 +156,13 @@ class V3SearchController extends Controller
]); ]);
} }
return $includes; // push back to fractal (it will keep values uniques)
//
$fractal->parseIncludes($includes);
// finally get the result
//
return $fractal->getRequestedIncludes();
} }
/** /**
@@ -176,14 +192,26 @@ class V3SearchController extends Controller
if ($stories->count() > 0) { if ($stories->count() > 0) {
$user = $this->getAuthenticatedUser(); $user = $this->getAuthenticatedUser();
$children = []; $children = [];
$childrenCounts = [];
// todo : refacto to remove over-usage of array_map, array_combine, array_flip etc.
//
foreach ($stories->getDataboxIds() as $databoxId) { foreach ($stories->getDataboxIds() as $databoxId) {
$storyIds = $stories->getDataboxRecordIds($databoxId); $storyIds = $stories->getDataboxRecordIds($databoxId);
$selections = $this->findDataboxById($databoxId) /** @var LegacyRecordRepository $repo */
->getRecordRepository() $repo = $this->findDataboxById($databoxId)->getRecordRepository();
->findChildren($storyIds, $user,1, $story_max_records);
$children[$databoxId] = array_combine($storyIds, $selections); // nb : findChildren() and getChildrenCounts() acts on MULTIPLE story-ids in single sql
//
$childrenCounts[$databoxId] = $repo->getChildrenCounts($storyIds, $user);
// search children only if needed
//
if(in_array('results.stories.children', $includes, true)) {
$selections = $repo->findChildren($storyIds, $user, 0, $story_max_records);
$children[$databoxId] = array_combine($storyIds, $selections);
}
} }
/** @var StoryView[] $storyViews */ /** @var StoryView[] $storyViews */
@@ -194,15 +222,23 @@ class V3SearchController extends Controller
foreach ($stories as $index => $story) { foreach ($stories as $index => $story) {
$storyView = new StoryView($story); $storyView = new StoryView($story);
$selection = $children[$story->getDataboxId()][$story->getRecordId()]; // populate children only if needed
//
if(in_array('results.stories.children', $includes, true)) {
$selection = $children[$story->getDataboxId()][$story->getRecordId()];
$childrenView = $this->buildRecordViews($selection); $childrenView = $this->buildRecordViews($selection);
foreach ($childrenView as $view) { foreach ($childrenView as $view) {
$childrenViews[spl_object_hash($view)] = $view; $childrenViews[spl_object_hash($view)] = $view;
}
$storyView->setChildren($childrenView);
} }
$storyView->setChildren($childrenView); $storyView->setData('childrenOffset', 0);
$storyView->setData('childrenLimit', $story_max_records);
$storyView->setData('childrenCount', $childrenCounts[$story->getDataboxId()][$story->getRecordId()]);
$storyViews[$index] = $storyView; $storyViews[$index] = $storyView;
} }
@@ -278,9 +314,12 @@ class V3SearchController extends Controller
*/ */
private function doSearch(Request $request) private function doSearch(Request $request)
{ {
list($offset, $limit) = V3ResultHelpers::paginationFromRequest($request);
$options = SearchEngineOptions::fromRequest($this->app, $request); $options = SearchEngineOptions::fromRequest($this->app, $request);
$options->setFirstResult((int)($request->get('offset_start') ?: 0));
$options->setMaxResults((int)$request->get('per_page') ?: 10); $options->setFirstResult($offset);
$options->setMaxResults($limit);
$this->getSearchEngine()->resetCache(); $this->getSearchEngine()->resetCache();

View File

@@ -41,11 +41,10 @@ class V3StoriesController extends V3RecordController
throw new NotFoundHttpException(); throw new NotFoundHttpException();
} }
$per_page = (int)$request->get('per_page')?:10; list($offset, $limit) = V3ResultHelpers::paginationFromRequest($request);
$page = (int)$request->get('page')?:1;
$offset = ($per_page * ($page - 1)) + 1; $ret = $this->listRecords($request, array_values($story->getChildren($offset, $limit)->get_elements()));
$ret = $this->listRecords($request, array_values($story->getChildren($offset, $per_page)->get_elements()));
return Result::create($request, $ret)->createResponse(); return Result::create($request, $ret)->createResponse();
} }
catch (NotFoundHttpException $e) { catch (NotFoundHttpException $e) {
@@ -56,23 +55,6 @@ class V3StoriesController extends V3RecordController
} }
} }
/**
* Retrieve detailed information about one story
*
* @param Request $request
* @param record_adapter $story
* @return array
* @throws Exception
*/
private function listStory(Request $request, record_adapter $story)
{
$ret = $this->getResultHelpers()->listRecord($request, $story, $this->getAclForUser());
$ret['records'] = $this->listRecords($request, array_values($story->getChildren($offset, $per_page)->get_elements()));
return $ret;
}
/** /**
* @param Request $request * @param Request $request

View File

@@ -185,7 +185,70 @@ class LegacyRecordRepository implements RecordRepository
return $this->mapRecordsFromResultSet($result); return $this->mapRecordsFromResultSet($result);
} }
public function findChildren(array $storyIds, $user = null, $offset = 1, $max_items = null) /**
* return the number of VISIBLE children of ONE story, for a specific user
* if user is null -> count all children
*
* @param int $storyId
* @param User|int|null $user // can pass a User, or a user_id
*
* @return int // -1 if story not found
*/
public function getChildrenCount($storyId, $user = null)
{
$r = $this->getChildrenCounts([$storyId], $user);
return $r[$storyId];
}
/**
* return the number of VISIBLE children of MANY stories, for a specific user
* if user is null -> count all children
*
* @param int[] $storyIds
* @param User|int|null $user // can pass a User, or a user_id
*
* @return int[] // story_id => n_children (-1 if story not found)
*/
public function getChildrenCounts(array $storyIds, $user = null)
{
$connection = $this->databox->get_connection();
$parmValues = [
':storyIds' => $storyIds,
];
$parmTypes = [
':storyIds' => Connection::PARAM_INT_ARRAY,
];
// if there is a user, we must join collusr to filter results depending on coll/masks
//
$userFilter = "";
if(!is_null($user)) {
$userFilter = " INNER JOIN collusr c ON c.site = :site AND c.usr_id = :userId AND c.coll_id=r.coll_id AND ((r.status ^ c.mask_xor) & c.mask_and) = 0\n";
$parmValues[':site'] = $this->site;
$parmValues[':userId'] = $user instanceof User ? $user->getId() : (int)$user;
}
$sql = "SELECT g.rid_parent AS story_id, COUNT(*) AS n_children\n"
. " FROM regroup g\n"
. " INNER JOIN record r ON r.record_id=g.rid_child\n"
. $userFilter
. " WHERE g.rid_parent IN( :storyIds )\n"
. " GROUP BY g.rid_parent\n"
;
$r = array_fill_keys($storyIds, -1);
foreach($connection->fetchAll($sql, $parmValues, $parmTypes) as $row) {
$r[$row['story_id']] = (int)$row['n_children'];
}
return $r;
}
public function findChildren(array $storyIds, $user = null, $offset = 0, $max_items = null)
{ {
if (!$storyIds) { if (!$storyIds) {
return []; return [];
@@ -216,7 +279,7 @@ class LegacyRecordRepository implements RecordRepository
$parmValues[':userId'] = $user instanceof User ? $user->getId() : (int)$user; $parmValues[':userId'] = $user instanceof User ? $user->getId() : (int)$user;
} }
if ($max_items) { if ($max_items !== null) {
// //
// we want paginated results AFTER applying all filters, we build a dynamic cptr // we want paginated results AFTER applying all filters, we build a dynamic cptr
// WARNING : due to bugs (?) in mysql optimizer, do NOT try to optimize this sql (e.g. removing a sub-q, or moving cpt to anothe sub-q) // WARNING : due to bugs (?) in mysql optimizer, do NOT try to optimize this sql (e.g. removing a sub-q, or moving cpt to anothe sub-q)
@@ -235,11 +298,11 @@ class LegacyRecordRepository implements RecordRepository
. " ORDER BY g.rid_parent, g.ord ASC\n" . " ORDER BY g.rid_parent, g.ord ASC\n"
. " ) t\n" . " ) t\n"
. ") r\n" . ") r\n"
. "WHERE CPT BETWEEN :offset AND :maxresult" . "WHERE CPT BETWEEN :cptmin AND :cptmax"
; ;
$parmValues[':offset'] = $offset; $parmValues[':cptmin'] = $offset + 1;
$parmValues[':maxresult'] = ($offset + $max_items -1); $parmValues[':cptmax'] = $offset + $max_items;
$connection->executeQuery('SET @cpt = 1'); $connection->executeQuery('SET @cpt = 1');
$connection->executeQuery('SET @old_rid_parent = -1'); $connection->executeQuery('SET @old_rid_parent = -1');
@@ -264,6 +327,8 @@ class LegacyRecordRepository implements RecordRepository
$records = $this->mapRecordsFromResultSet($data); $records = $this->mapRecordsFromResultSet($data);
// todo : refacto to remove over-usage of array_map, array_combine, array_flip etc.
//
$selections = array_map( $selections = array_map(
function () { function () {
return new set_selection($this->app); return new set_selection($this->app);

View File

@@ -61,7 +61,7 @@ interface RecordRepository
* @param null|int $max_items * @param null|int $max_items
* @return \set_selection[] * @return \set_selection[]
*/ */
public function findChildren(array $storyIds, $user = null, $offset = 1, $max_items = null); public function findChildren(array $storyIds, $user = null, $offset = 0, $max_items = null);
/** /**

View File

@@ -12,6 +12,17 @@ namespace Alchemy\Phrasea\Search;
use Assert\Assertion; use Assert\Assertion;
/**
* used as a (temporary / specific) view of a story
* WARNING : the children DO NOT NECESSARILY reflect the whole content of a story
* children may contain only a subset :
* - visible for a specific user
* - paginated
*
* Class StoryView
* @package Alchemy\Phrasea\Search
*/
class StoryView class StoryView
{ {
use SubdefsAware; use SubdefsAware;
@@ -21,11 +32,20 @@ class StoryView
* @var \record_adapter * @var \record_adapter
*/ */
private $story; private $story;
/** /**
* @var RecordView[] * @var RecordView[]
* may be a subset of all children (only visibles for a user and/or paginated)
*/ */
private $children = []; private $children = [];
/**
* @var mixed[]
* "personal use" data to be stored/retreived for a specific usage
* e.g. api V3 will store pagination infos to be rendered on output
*/
private $_data = [];
/** /**
* @param \record_adapter $story * @param \record_adapter $story
*/ */
@@ -44,12 +64,15 @@ class StoryView
/** /**
* @param RecordView[] $children * @param RecordView[] $children
* @return self
*/ */
public function setChildren($children) public function setChildren($children)
{ {
Assertion::allIsInstanceOf($children, RecordView::class); Assertion::allIsInstanceOf($children, RecordView::class);
$this->children = $children instanceof \Traversable ? iterator_to_array($children, false) : array_values($children); $this->children = $children instanceof \Traversable ? iterator_to_array($children, false) : array_values($children);
return $this;
} }
/** /**
@@ -59,4 +82,29 @@ class StoryView
{ {
return $this->children; return $this->children;
} }
/**
* set a "personal usage" data
*
* @param string $k
* @param mixed $v
* @return self
*/
public function setData($k, $v)
{
$this->_data[$k] = $v;
return $this;
}
/**
* get a "personal usage" data (null if not found)
*
* @param string $k
* @return mixed|null
*/
public function getData($k)
{
return isset($this->_data[$k]) ? $this->_data[$k] : null;
}
} }

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use League\Fractal\TransformerAbstract;
class V3SearchResultTransformer extends TransformerAbstract
{
protected $availableIncludes = ['results', 'facets'];
protected $defaultIncludes = ['results'];
/**
* @var TransformerAbstract
*/
private $transformer;
public function __construct(TransformerAbstract $transformer)
{
$this->transformer = $transformer;
}
public function transform(SearchResultView $resultView)
{
$result = $resultView->getResult();
return [
'query' => $result->getQueryText(),
'offset' => $result->getOptions()->getFirstResult(),
'limit' => $result->getOptions()->getMaxResults(),
'count' => $result->getAvailable(),
'total' => $result->getTotal(),
'error' => (string)$result->getError(),
'warning' => (string)$result->getWarning(),
'query_time' => $result->getDuration(),
'search_indexes' => $result->getIndexes(),
// 'facets' => $result->getFacets(),
];
}
public function includeResults(SearchResultView $resultView)
{
return $this->item($resultView, $this->transformer);
}
public function includeFacets(SearchResultView $resultView)
{
return $this->item($resultView->getResult()->getFacets(), function ($x) {
return $x;
});
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\Utilities\NullableDateTime;
class V3StoryTransformer extends StoryTransformer
{
/**
* @var RecordTransformer
*/
private $recordTransformer;
/**
* @var array
*/
protected $availableIncludes = ['thumbnail', 'metadatas', 'children', 'caption'];
/**
* @var array
*/
protected $defaultIncludes = ['thumbnail', 'metadatas'];
/**
* @param SubdefTransformer $subdefTransformer
* @param RecordTransformer $recordTransformer
*/
public function __construct(SubdefTransformer $subdefTransformer, RecordTransformer $recordTransformer)
{
parent::__construct($subdefTransformer, $recordTransformer);
$this->recordTransformer = $recordTransformer;
}
public function transform(StoryView $storyView)
{
$story = $storyView->getStory();
return [
'databox_id' => $story->getDataboxId(),
'story_id' => $story->getRecordId(),
'updated_on' => NullableDateTime::format($story->getUpdated()),
'created_on' => NullableDateTime::format($story->getUpdated()),
'collection_id' => $story->getCollectionId(),
'base_id' => $story->getBaseId(),
'uuid' => $story->getUuid(),
'children_offset' => $storyView->getData('childrenOffset'),
'children_limit' => $storyView->getData('childrenLimit'),
'children_count' => count($storyView->getChildren()),
'children_total' => $storyView->getData('childrenCount') // fix V1 wrong count
];
}
public function includeChildren(StoryView $storyView)
{
return $this->collection($storyView->getChildren(), $this->recordTransformer);
}
}

View File

@@ -320,7 +320,7 @@ class ElasticSearchEngine implements SearchEngineInterface
$queryESLib, $queryESLib,
$res['took'], // duration $res['took'], // duration
$options->getFirstResult(), $options->getFirstResult(),
$res['hits']['total'], // available count($res['hits']['hits']), // available
$res['hits']['total'], // total $res['hits']['total'], // total
null, // error null, // error
null, // warning null, // warning

View File

@@ -1784,7 +1784,7 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
* @throws Exception * @throws Exception
* @throws DBALException * @throws DBALException
*/ */
public function getChildren($offset = 1, $max_items = null) public function getChildren($offset = 0, $max_items = null)
{ {
if (!$this->isStory()) { if (!$this->isStory()) {
throw new Exception('This record is not a grouping'); throw new Exception('This record is not a grouping');
@@ -1797,6 +1797,17 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
return reset($selections); return reset($selections);
} }
public function getChildrenCount()
{
if (!$this->isStory()) {
throw new Exception('This record is not a grouping');
}
$user = $this->getAuthenticatedUser();
return $this->getDatabox()->getRecordRepository()->getChildrenCount($this->getRecordId(), $user);
}
/** /**
* @return set_selection * @return set_selection
*/ */