mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-07 01:54:27 +00:00
560 lines
20 KiB
PHP
560 lines
20 KiB
PHP
<?php
|
|
|
|
namespace Alchemy\Phrasea\Controller\Api\V3;
|
|
|
|
use Alchemy\Phrasea\Application\Helper\DispatcherAware;
|
|
use Alchemy\Phrasea\Application\Helper\JsonBodyAware;
|
|
use Alchemy\Phrasea\Collection\Reference\CollectionReference;
|
|
use Alchemy\Phrasea\Controller\Api\InstanceIdAware;
|
|
use Alchemy\Phrasea\Controller\Api\Result;
|
|
use Alchemy\Phrasea\Controller\Controller;
|
|
use Alchemy\Phrasea\Databox\DataboxGroupable;
|
|
use Alchemy\Phrasea\Databox\Record\LegacyRecordRepository;
|
|
use Alchemy\Phrasea\Databox\Subdef\MediaSubdefService;
|
|
use Alchemy\Phrasea\Fractal\CallbackTransformer;
|
|
use Alchemy\Phrasea\Fractal\IncludeResolver;
|
|
use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver;
|
|
use Alchemy\Phrasea\Fractal\TraceableArraySerializer;
|
|
use Alchemy\Phrasea\Media\MediaSubDefinitionUrlGenerator;
|
|
use Alchemy\Phrasea\Model\Manipulator\UserManipulator;
|
|
use Alchemy\Phrasea\Model\RecordReferenceInterface;
|
|
use Alchemy\Phrasea\Record\RecordCollection;
|
|
use Alchemy\Phrasea\Record\RecordReferenceCollection;
|
|
use Alchemy\Phrasea\Search\CaptionView;
|
|
use Alchemy\Phrasea\Search\PermalinkTransformer;
|
|
use Alchemy\Phrasea\Search\PermalinkView;
|
|
use Alchemy\Phrasea\Search\RecordTransformer;
|
|
use Alchemy\Phrasea\Search\RecordView;
|
|
use Alchemy\Phrasea\Search\SearchResultView;
|
|
use Alchemy\Phrasea\Search\StoryView;
|
|
use Alchemy\Phrasea\Search\SubdefTransformer;
|
|
use Alchemy\Phrasea\Search\SubdefView;
|
|
use Alchemy\Phrasea\Search\TechnicalDataTransformer;
|
|
use Alchemy\Phrasea\Search\TechnicalDataView;
|
|
use Alchemy\Phrasea\Search\V3SearchCompositeResultTransformer;
|
|
use Alchemy\Phrasea\Search\V3SearchResultTransformer;
|
|
use Alchemy\Phrasea\Search\V3StoryTransformer;
|
|
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
|
|
use Alchemy\Phrasea\SearchEngine\SearchEngineLogger;
|
|
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
|
|
use Alchemy\Phrasea\SearchEngine\SearchEngineResult;
|
|
use Alchemy\Phrasea\Utilities\Stopwatch;
|
|
use caption_record;
|
|
use League\Fractal\Manager as FractalManager;
|
|
use League\Fractal\Resource\Item;
|
|
use media_Permalink_Adapter;
|
|
use media_subdef;
|
|
use record_adapter;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class V3SearchController extends Controller
|
|
{
|
|
use JsonBodyAware;
|
|
use DispatcherAware;
|
|
use InstanceIdAware;
|
|
|
|
/**
|
|
* Search for results
|
|
*
|
|
* @param Request $request
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function searchAction(Request $request)
|
|
{
|
|
$stopwatch = new Stopwatch("controller");
|
|
|
|
$subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer());
|
|
$technicalDataTransformer = new TechnicalDataTransformer();
|
|
$recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer, $this->getResourceIdResolver());
|
|
$storyTransformer = new V3StoryTransformer($recordTransformer);
|
|
$recordTransformer->setStoryTransformer($storyTransformer);
|
|
$compositeTransformer = new V3SearchCompositeResultTransformer($recordTransformer, $storyTransformer);
|
|
$searchTransformer = new V3SearchResultTransformer($compositeTransformer);
|
|
|
|
$transformerResolver = new SearchResultTransformerResolver([
|
|
'' => $searchTransformer,
|
|
'results' => $compositeTransformer,
|
|
'facets' => new CallbackTransformer(),
|
|
'suggestions' => new CallbackTransformer(),
|
|
'results.stories' => $storyTransformer,
|
|
'results.stories.children' => $recordTransformer,
|
|
'results.records.stories' => $storyTransformer,
|
|
'results.records.stories.thumbnail' => $subdefTransformer,
|
|
'results.records.stories.metadata' => new CallbackTransformer(),
|
|
'results.stories.children.thumbnail' => $subdefTransformer,
|
|
'results.stories.children.technical_informations' => $technicalDataTransformer,
|
|
'results.stories.children.subdefs' => $subdefTransformer,
|
|
'results.stories.children.metadata' => new CallbackTransformer(),
|
|
'results.stories.children.status' => new CallbackTransformer(),
|
|
'results.stories.children.caption' => new CallbackTransformer(),
|
|
'results.records' => $recordTransformer,
|
|
'results.records.thumbnail' => $subdefTransformer,
|
|
'results.stories.thumbnail' => $subdefTransformer,
|
|
'results.records.technical_informations' => $technicalDataTransformer,
|
|
// 'results.stories.technical_informations' => $technicalDataTransformer, // no ti on story
|
|
'results.records.subdefs' => $subdefTransformer,
|
|
'results.stories.subdefs' => $subdefTransformer,
|
|
'results.records.metadata' => new CallbackTransformer(),
|
|
'results.stories.metadata' => new CallbackTransformer(),
|
|
'results.records.metadatas' => new CallbackTransformer(),
|
|
'results.stories.metadatas' => new CallbackTransformer(),
|
|
'results.records.caption' => new CallbackTransformer(),
|
|
'results.stories.caption' => new CallbackTransformer(),
|
|
'results.records.status' => new CallbackTransformer(),
|
|
'results.stories.status' => new CallbackTransformer(),
|
|
]);
|
|
|
|
$includeResolver = new IncludeResolver($transformerResolver);
|
|
|
|
$fractal = new FractalManager();
|
|
$fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher']));
|
|
|
|
// and push everything back to fractal
|
|
$fractal->parseIncludes($this->getIncludes($request));
|
|
|
|
$stopwatch->lap("boot");
|
|
|
|
$result = $this->doSearch($request);
|
|
|
|
$stopwatch->lap("doSearch");
|
|
|
|
$story_children_limit = null;
|
|
// if searching stories
|
|
if ($request->get('search_type') == 1) {
|
|
$story_children_limit = (int)$request->get('story_children_limit') ?: 10;
|
|
}
|
|
|
|
$searchView = $this->buildSearchView(
|
|
$result,
|
|
$includeResolver->resolve($fractal),
|
|
$this->resolveSubdefUrlTTL($request),
|
|
$story_children_limit
|
|
);
|
|
|
|
$stopwatch->lap("buildSearchView");
|
|
|
|
$ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray();
|
|
|
|
$stopwatch->lap("fractal");
|
|
|
|
return Result::create($request, $ret)->createResponse([$stopwatch]);
|
|
}
|
|
|
|
/**
|
|
* Returns requested includes
|
|
*
|
|
* @param Request $request
|
|
* @return string[]
|
|
*/
|
|
private function getIncludes(Request $request)
|
|
{
|
|
// a local fractal manager will help to smartly parse the request parameters.
|
|
$fractal = new FractalManager();
|
|
|
|
// first, get includes from request
|
|
//
|
|
$fractal->parseIncludes($request->get('include', []));
|
|
$includes = $fractal->getRequestedIncludes();
|
|
|
|
if ($request->attributes->get('_extended', false)) {
|
|
// if ($request->get('search_type') != SearchEngineOptions::RECORD_STORY) {
|
|
if(in_array('results.stories.children', $includes)) {
|
|
$includes = array_merge($includes, [
|
|
'results.stories.caption',
|
|
'results.stories.metadata',
|
|
'results.stories.children.subdefs',
|
|
'results.stories.children.metadata',
|
|
'results.stories.children.caption',
|
|
'results.stories.children.status'
|
|
]);
|
|
}
|
|
else {
|
|
$includes = array_merge($includes, [
|
|
'results.stories.caption'
|
|
]);
|
|
}
|
|
|
|
$includes = array_merge($includes, [
|
|
'results.records.subdefs',
|
|
'results.records.metadata',
|
|
'results.records.caption',
|
|
'results.records.status'
|
|
]);
|
|
}
|
|
|
|
// push back to fractal (it will keep values uniques)
|
|
//
|
|
$fractal->parseIncludes($includes);
|
|
|
|
// finally get the result
|
|
//
|
|
return $fractal->getRequestedIncludes();
|
|
}
|
|
|
|
/**
|
|
* @param SearchEngineResult $result
|
|
* @param string[] $includes
|
|
* @param int $urlTTL
|
|
* @param int|null $story_max_records
|
|
* @return SearchResultView
|
|
*/
|
|
private function buildSearchView(SearchEngineResult $result, array $includes, $urlTTL, $story_max_records = null)
|
|
{
|
|
$references = new RecordReferenceCollection($result->getResults());
|
|
|
|
$records = new RecordCollection();
|
|
$stories = new RecordCollection();
|
|
|
|
foreach ($references->toRecords($this->getApplicationBox()) as $record) {
|
|
if ($record->isStory()) {
|
|
$stories[$record->getId()] = $record;
|
|
} else {
|
|
$records[$record->getId()] = $record;
|
|
}
|
|
}
|
|
|
|
$resultView = new SearchResultView($result);
|
|
|
|
if ($stories->count() > 0) {
|
|
$user = $this->getAuthenticatedUser();
|
|
$children = [];
|
|
$childrenCounts = [];
|
|
|
|
// todo : refacto to remove over-usage of array_map, array_combine, array_flip etc.
|
|
//
|
|
foreach ($stories->getDataboxIds() as $databoxId) {
|
|
$storyIds = $stories->getDataboxRecordIds($databoxId);
|
|
|
|
/** @var LegacyRecordRepository $repo */
|
|
$repo = $this->findDataboxById($databoxId)->getRecordRepository();
|
|
|
|
// 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 */
|
|
$storyViews = [];
|
|
/** @var RecordView[] $childrenViews */
|
|
$childrenViews = [];
|
|
|
|
foreach ($stories as $index => $story) {
|
|
$storyView = new StoryView($story);
|
|
|
|
// populate children only if needed
|
|
//
|
|
if(in_array('results.stories.children', $includes, true)) {
|
|
$selection = $children[$story->getDataboxId()][$story->getRecordId()];
|
|
|
|
$childrenView = $this->buildRecordViews($selection);
|
|
|
|
foreach ($childrenView as $view) {
|
|
$childrenViews[spl_object_hash($view)] = $view;
|
|
}
|
|
|
|
$storyView->setChildren($childrenView);
|
|
}
|
|
|
|
$storyView->setData('childrenOffset', 0);
|
|
$storyView->setData('childrenLimit', $story_max_records);
|
|
$storyView->setData('childrenCount', $childrenCounts[$story->getDataboxId()][$story->getRecordId()]);
|
|
|
|
$storyViews[$index] = $storyView;
|
|
}
|
|
|
|
if (in_array('results.stories.thumbnail', $includes, true)) {
|
|
$subdefViews = $this->buildSubdefsViews($stories, ['thumbnail'], $urlTTL);
|
|
|
|
foreach ($storyViews as $index => $storyView) {
|
|
$storyView->setSubdefs($subdefViews[$index]);
|
|
}
|
|
}
|
|
|
|
if (in_array('results.stories.metadata', $includes, true) ||
|
|
in_array('results.stories.caption', $includes, true)) {
|
|
$captions = $this->app['service.caption']->findByReferenceCollection($stories);
|
|
$canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($stories);
|
|
|
|
$this->buildCaptionViews($storyViews, $captions, $canSeeBusiness);
|
|
}
|
|
|
|
$allChildren = new RecordCollection();
|
|
foreach ($childrenViews as $index => $childrenView) {
|
|
$allChildren[$index] = $childrenView->getRecord();
|
|
}
|
|
|
|
$names = in_array('results.stories.children.subdefs', $includes, true) ? null : ['thumbnail'];
|
|
$subdefViews = $this->buildSubdefsViews($allChildren, $names, $urlTTL);
|
|
$technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($allChildren);
|
|
|
|
foreach ($childrenViews as $index => $recordView) {
|
|
$recordView->setSubdefs($subdefViews[$index]);
|
|
$recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index]));
|
|
}
|
|
|
|
if (array_intersect($includes, ['results.stories.children.metadata', 'results.stories.children.caption'])) {
|
|
$captions = $this->app['service.caption']->findByReferenceCollection($allChildren);
|
|
$canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($allChildren);
|
|
|
|
$this->buildCaptionViews($childrenViews, $captions, $canSeeBusiness);
|
|
}
|
|
|
|
$resultView->setStories($storyViews);
|
|
}
|
|
|
|
if ($records->count() > 0) {
|
|
$names = in_array('results.records.subdefs', $includes, true) ? null : ['thumbnail'];
|
|
$recordViews = $this->buildRecordViews($records);
|
|
$subdefViews = $this->buildSubdefsViews($records, $names, $urlTTL);
|
|
|
|
$technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($records);
|
|
|
|
foreach ($recordViews as $index => $recordView) {
|
|
$recordView->setSubdefs($subdefViews[$index]);
|
|
$recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index]));
|
|
}
|
|
|
|
if (array_intersect($includes, ['results.records.metadata', 'results.records.caption'])) {
|
|
$captions = $this->app['service.caption']->findByReferenceCollection($records);
|
|
$canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($records);
|
|
|
|
$this->buildCaptionViews($recordViews, $captions, $canSeeBusiness);
|
|
}
|
|
|
|
$resultView->setRecords($recordViews);
|
|
}
|
|
|
|
return $resultView;
|
|
}
|
|
|
|
/**
|
|
* @param Request $request
|
|
* @return SearchEngineResult
|
|
*/
|
|
private function doSearch(Request $request)
|
|
{
|
|
list($offset, $limit) = V3ResultHelpers::paginationFromRequest($request);
|
|
|
|
$options = SearchEngineOptions::fromRequest($this->app, $request);
|
|
|
|
$options->setFirstResult($offset);
|
|
$options->setMaxResults($limit);
|
|
|
|
$this->getSearchEngine()->resetCache();
|
|
|
|
$search_result = $this->getSearchEngine()->query((string)$request->get('query'), $options);
|
|
|
|
$this->getUserManipulator()->logQuery($this->getAuthenticatedUser(), $search_result->getQueryText());
|
|
|
|
// log array of collectionIds (from $options) for each databox
|
|
$collectionsReferencesByDatabox = $options->getCollectionsReferencesByDatabox();
|
|
foreach ($collectionsReferencesByDatabox as $sbid => $references) {
|
|
$databox = $this->findDataboxById($sbid);
|
|
$collectionsIds = array_map(function(CollectionReference $ref){return $ref->getCollectionId();}, $references);
|
|
$this->getSearchEngineLogger()->log($databox, $search_result->getQueryText(), $search_result->getTotal(), $collectionsIds);
|
|
}
|
|
|
|
$this->getSearchEngine()->clearCache();
|
|
|
|
return $search_result;
|
|
}
|
|
|
|
/**
|
|
* @return SearchEngineInterface
|
|
*/
|
|
private function getSearchEngine()
|
|
{
|
|
return $this->app['phraseanet.SE'];
|
|
}
|
|
|
|
/**
|
|
* @return UserManipulator
|
|
*/
|
|
private function getUserManipulator()
|
|
{
|
|
return $this->app['manipulator.user'];
|
|
}
|
|
|
|
/**
|
|
* @return SearchEngineLogger
|
|
*/
|
|
private function getSearchEngineLogger()
|
|
{
|
|
return $this->app['phraseanet.SE.logger'];
|
|
}
|
|
|
|
/**
|
|
* @param Request $request
|
|
* @return int
|
|
*/
|
|
private function resolveSubdefUrlTTL(Request $request)
|
|
{
|
|
$urlTTL = $request->query->get('subdef_url_ttl');
|
|
|
|
if (null !== $urlTTL) {
|
|
return (int)$urlTTL;
|
|
}
|
|
|
|
return $this->getConf()->get(['registry', 'general', 'default-subdef-url-ttl']);
|
|
}
|
|
|
|
/**
|
|
* @param RecordCollection|record_adapter[] $references
|
|
* @return RecordView[]
|
|
*/
|
|
private function buildRecordViews($references)
|
|
{
|
|
if (!$references instanceof RecordCollection) {
|
|
$references = new RecordCollection($references);
|
|
}
|
|
|
|
$recordViews = [];
|
|
|
|
foreach ($references as $index => $record) {
|
|
$recordViews[$index] = new RecordView($record);
|
|
}
|
|
|
|
return $recordViews;
|
|
}
|
|
|
|
/**
|
|
* @param RecordReferenceInterface[]|RecordReferenceCollection|DataboxGroupable $references
|
|
* @param array|null $names
|
|
* @param int $urlTTL
|
|
* @return SubdefView[][]
|
|
*/
|
|
private function buildSubdefsViews($references, array $names = null, $urlTTL)
|
|
{
|
|
/** @var MediaSubdefService $MediaSubdefService */
|
|
$MediaSubdefService = $this->app['service.media_subdef'];
|
|
$subdefGroups = $MediaSubdefService
|
|
->findSubdefsByRecordReferenceFromCollection($references, $names);
|
|
|
|
$fakeSubdefs = [];
|
|
|
|
foreach ($subdefGroups as $index => $subdefGroup) {
|
|
if (!isset($subdefGroup['thumbnail'])) {
|
|
$fakeSubdef = new media_subdef($this->app, $references[$index], 'thumbnail', true, []);
|
|
$fakeSubdefs[spl_object_hash($fakeSubdef)] = $fakeSubdef;
|
|
|
|
$subdefGroups[$index]['thumbnail'] = $fakeSubdef;
|
|
}
|
|
}
|
|
|
|
$allSubdefs = $this->mergeGroupsIntoOneList($subdefGroups);
|
|
$allPermalinks = media_Permalink_Adapter::getMany(
|
|
$this->app,
|
|
array_filter($allSubdefs, function (media_subdef $subdef) use ($fakeSubdefs) {
|
|
return !isset($fakeSubdefs[spl_object_hash($subdef)]);
|
|
})
|
|
);
|
|
|
|
/** @var MediaSubDefinitionUrlGenerator $urlGenerator */
|
|
$urlGenerator = $this->app['media_accessor.subdef_url_generator'];
|
|
$urls = $urlGenerator->generateMany($this->getAuthenticatedUser(), $allSubdefs, $urlTTL);
|
|
|
|
$subdefViews = [];
|
|
|
|
/** @var media_subdef $subdef */
|
|
foreach ($allSubdefs as $index => $subdef) {
|
|
$subdefView = new SubdefView($subdef);
|
|
|
|
if (isset($allPermalinks[$index])) {
|
|
$subdefView->setPermalinkView(new PermalinkView($allPermalinks[$index]));
|
|
}
|
|
|
|
$subdefView->setUrl($urls[$index]);
|
|
$subdefView->setUrlTTL($urlTTL);
|
|
|
|
$subdefViews[spl_object_hash($subdef)] = $subdefView;
|
|
}
|
|
|
|
$reorderedGroups = [];
|
|
|
|
/** @var media_subdef[] $subdefGroup */
|
|
foreach ($subdefGroups as $index => $subdefGroup) {
|
|
$reordered = [];
|
|
|
|
foreach ($subdefGroup as $subdef) {
|
|
$reordered[] = $subdefViews[spl_object_hash($subdef)];
|
|
}
|
|
|
|
$reorderedGroups[$index] = $reordered;
|
|
}
|
|
|
|
return $reorderedGroups;
|
|
}
|
|
|
|
/**
|
|
* @param array $groups
|
|
* @return array|mixed
|
|
*/
|
|
private function mergeGroupsIntoOneList(array $groups)
|
|
{
|
|
// Strips keys from the internal array
|
|
array_walk($groups, function (array &$group) {
|
|
$group = array_values($group);
|
|
});
|
|
|
|
if ($groups) {
|
|
return call_user_func_array('array_merge', $groups);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param RecordReferenceInterface[]|DataboxGroupable $references
|
|
* @return array<int, bool>
|
|
*/
|
|
private function retrieveSeeBusinessPerDatabox($references)
|
|
{
|
|
if (!$references instanceof DataboxGroupable) {
|
|
$references = new RecordReferenceCollection($references);
|
|
}
|
|
|
|
$acl = $this->getAclForUser();
|
|
|
|
$canSeeBusiness = [];
|
|
|
|
foreach ($references->getDataboxIds() as $databoxId) {
|
|
$canSeeBusiness[$databoxId] = $acl->can_see_business_fields($this->findDataboxById($databoxId));
|
|
}
|
|
|
|
$rights = [];
|
|
|
|
foreach ($references as $index => $reference) {
|
|
$rights[$index] = $canSeeBusiness[$reference->getDataboxId()];
|
|
}
|
|
|
|
return $rights;
|
|
}
|
|
|
|
/**
|
|
* @param RecordView[] $recordViews
|
|
* @param caption_record[] $captions
|
|
* @param bool[] $canSeeBusiness
|
|
*/
|
|
private function buildCaptionViews($recordViews, $captions, $canSeeBusiness)
|
|
{
|
|
foreach ($recordViews as $index => $recordView) {
|
|
$caption = $captions[$index];
|
|
|
|
$captionView = new CaptionView($caption);
|
|
|
|
$captionView->setFields($caption->get_fields(null, isset($canSeeBusiness[$index]) && (bool)$canSeeBusiness[$index]));
|
|
|
|
$recordView->setCaption($captionView);
|
|
}
|
|
}
|
|
}
|