From 0474703d622bfc92cd3c17be7e0de68f70ceed63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 18 Apr 2016 11:36:03 +0200 Subject: [PATCH 01/22] Change Api SearchController to use Fractal --- .../Controller/Api/SearchController.php | 50 +++++++---------- .../Phrasea/Search/SearchResultView.php | 34 ++++++++++++ .../Phrasea/Search/V2SearchTransformer.php | 54 +++++++++++++++++++ 3 files changed, 107 insertions(+), 31 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Search/SearchResultView.php create mode 100644 lib/Alchemy/Phrasea/Search/V2SearchTransformer.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/SearchController.php b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php index e1113b3919..e9a654cf9e 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/SearchController.php +++ b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php @@ -12,10 +12,15 @@ namespace Alchemy\Phrasea\Controller\Api; use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\Model\Manipulator\UserManipulator; +use Alchemy\Phrasea\Search\SearchResultView; +use Alchemy\Phrasea\Search\V2SearchTransformer; use Alchemy\Phrasea\SearchEngine\SearchEngineInterface; use Alchemy\Phrasea\SearchEngine\SearchEngineLogger; use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; use Alchemy\Phrasea\SearchEngine\SearchEngineResult; +use League\Fractal\Manager; +use League\Fractal\Resource\Item; +use League\Fractal\Serializer\ArraySerializer; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -28,25 +33,21 @@ class SearchController extends Controller */ public function searchAction(Request $request) { - list($ret, $search_result) = $this->searchAndFormatEngineResult($request); + $fractal = new Manager(); + $fractal->setSerializer(new ArraySerializer()); + $fractal->parseIncludes([]); - /** @var SearchEngineResult $search_result */ - $ret['search_type'] = $search_result->getOptions()->getSearchType(); - $ret['results'] = []; - - foreach ($search_result->getResults() as $record) { - $ret['results'][] = [ - 'databox_id' => $record->getDataboxId(), - 'record_id' => $record->getRecordId(), - 'collection_id' => $record->getCollectionId(), - 'version' => $record->getUpdated()->getTimestamp(), - ]; - } + $searchView = new SearchResultView($this->doSearch($request)); + $ret = $fractal->createData(new Item($searchView, new V2SearchTransformer()))->toArray(); return Result::create($request, $ret)->createResponse(); } - private function searchAndFormatEngineResult(Request $request) + /** + * @param Request $request + * @return SearchEngineResult + */ + private function doSearch(Request $request) { $options = SearchEngineOptions::fromRequest($this->app, $request); $options->setFirstResult($request->get('offset_start') ?: 0); @@ -55,9 +56,9 @@ class SearchController extends Controller $query = (string) $request->get('query'); $this->getSearchEngine()->resetCache(); - $search_result = $this->getSearchEngine()->query($query, $options); + $result = $this->getSearchEngine()->query($query, $options); - $this->getUserManipulator()->logQuery($this->getAuthenticatedUser(), $search_result->getQuery()); + $this->getUserManipulator()->logQuery($this->getAuthenticatedUser(), $result->getQuery()); foreach ($options->getDataboxes() as $databox) { $colls = array_map(function (\collection $collection) { @@ -67,25 +68,12 @@ class SearchController extends Controller })); $this->getSearchEngineLogger() - ->log($databox, $search_result->getQuery(), $search_result->getTotal(), $colls); + ->log($databox, $result->getQuery(), $result->getTotal(), $colls); } $this->getSearchEngine()->clearCache(); - $ret = [ - 'offset_start' => $options->getFirstResult(), - 'per_page' => $options->getMaxResults(), - 'available_results' => $search_result->getAvailable(), - 'total_results' => $search_result->getTotal(), - 'error' => (string)$search_result->getError(), - 'warning' => (string)$search_result->getWarning(), - 'query_time' => $search_result->getDuration(), - 'search_indexes' => $search_result->getIndexes(), - 'facets' => $search_result->getFacets(), - 'results' => [], - ]; - - return [$ret, $search_result]; + return $result; } /** diff --git a/lib/Alchemy/Phrasea/Search/SearchResultView.php b/lib/Alchemy/Phrasea/Search/SearchResultView.php new file mode 100644 index 0000000000..47c35e0ab3 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/SearchResultView.php @@ -0,0 +1,34 @@ +result = $result; + } + + /** + * @return SearchEngineResult + */ + public function getResult() + { + return $this->result; + } +} diff --git a/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php b/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php new file mode 100644 index 0000000000..1600302536 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php @@ -0,0 +1,54 @@ + $searchView->getResult()->getOptions()->getFirstResult(), + 'per_page' => $searchView->getResult()->getOptions()->getMaxResults(), + 'available_results' => $searchView->getResult()->getAvailable(), + 'total_results' => $searchView->getResult()->getTotal(), + 'error' => (string)$searchView->getResult()->getError(), + 'warning' => (string)$searchView->getResult()->getWarning(), + 'query_time' => $searchView->getResult()->getDuration(), + 'search_indexes' => $searchView->getResult()->getIndexes(), + 'facets' => $searchView->getResult()->getFacets(), + 'search_type' => $searchView->getResult()->getOptions()->getSearchType(), + 'results' => $this->listResults($searchView->getResult()->getResults()), + ]; + } + + /** + * @param RecordInterface[] $results + * @return array + */ + public function listResults($results) + { + $data = []; + + foreach ($results as $record) { + $data[] = [ + 'databox_id' => $record->getDataboxId(), + 'record_id' => $record->getRecordId(), + 'collection_id' => $record->getCollectionId(), + 'version' => $record->getUpdated()->getTimestamp(), + ]; + } + + return $data; + } +} From 0832343c668455b9eb1754983e0cf7856b29de27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 18 Apr 2016 19:25:59 +0200 Subject: [PATCH 02/22] WIP --- .../Controller/Api/SearchController.php | 2 +- .../Phrasea/Controller/Api/V1Controller.php | 196 +++++++++++++----- .../Phrasea/Fractal/ArraySerializer.php | 49 +++++ lib/Alchemy/Phrasea/Search/CaptionAware.php | 35 ++++ lib/Alchemy/Phrasea/Search/CaptionView.php | 32 +++ .../Phrasea/Search/RecordTransformer.php | 109 ++++++++++ lib/Alchemy/Phrasea/Search/RecordView.php | 53 +++++ .../Phrasea/Search/SearchResultView.php | 49 +++++ .../Phrasea/Search/StoryTransformer.php | 109 ++++++++++ lib/Alchemy/Phrasea/Search/StoryView.php | 63 ++++++ .../Phrasea/Search/SubdefTransformer.php | 21 ++ lib/Alchemy/Phrasea/Search/SubdefView.php | 32 +++ lib/Alchemy/Phrasea/Search/SubdefsAware.php | 52 +++++ .../Search/TechnicalDataTransformer.php | 25 +++ .../Phrasea/Search/TechnicalDataView.php | 34 +++ .../V1SearchCompositeResultTransformer.php | 49 +++++ .../V1SearchRecordsResultTransformer.php | 29 +++ .../Search/V1SearchResultTransformer.php | 31 +++ .../Phrasea/Search/V1SearchTransformer.php | 44 ++++ .../Phrasea/Search/V2SearchTransformer.php | 20 +- 20 files changed, 969 insertions(+), 65 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Fractal/ArraySerializer.php create mode 100644 lib/Alchemy/Phrasea/Search/CaptionAware.php create mode 100644 lib/Alchemy/Phrasea/Search/CaptionView.php create mode 100644 lib/Alchemy/Phrasea/Search/RecordTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/RecordView.php create mode 100644 lib/Alchemy/Phrasea/Search/StoryTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/StoryView.php create mode 100644 lib/Alchemy/Phrasea/Search/SubdefTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/SubdefView.php create mode 100644 lib/Alchemy/Phrasea/Search/SubdefsAware.php create mode 100644 lib/Alchemy/Phrasea/Search/TechnicalDataTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/TechnicalDataView.php create mode 100644 lib/Alchemy/Phrasea/Search/V1SearchCompositeResultTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/V1SearchRecordsResultTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/V1SearchResultTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/V1SearchTransformer.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/SearchController.php b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php index e9a654cf9e..04a4f4e742 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/SearchController.php +++ b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php @@ -11,6 +11,7 @@ namespace Alchemy\Phrasea\Controller\Api; use Alchemy\Phrasea\Controller\Controller; +use Alchemy\Phrasea\Fractal\ArraySerializer; use Alchemy\Phrasea\Model\Manipulator\UserManipulator; use Alchemy\Phrasea\Search\SearchResultView; use Alchemy\Phrasea\Search\V2SearchTransformer; @@ -20,7 +21,6 @@ use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; use Alchemy\Phrasea\SearchEngine\SearchEngineResult; use League\Fractal\Manager; use League\Fractal\Resource\Item; -use League\Fractal\Serializer\ArraySerializer; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index c6d3a068a2..7ea64a89cc 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -32,6 +32,7 @@ use Alchemy\Phrasea\Core\Version; use Alchemy\Phrasea\Feed\Aggregate; use Alchemy\Phrasea\Feed\FeedInterface; use Alchemy\Phrasea\Form\Login\PhraseaRenewPasswordForm; +use Alchemy\Phrasea\Fractal\ArraySerializer; use Alchemy\Phrasea\Model\Entities\ApiOauthToken; use Alchemy\Phrasea\Model\Entities\Basket; use Alchemy\Phrasea\Model\Entities\BasketElement; @@ -54,15 +55,24 @@ use Alchemy\Phrasea\Model\Repositories\FeedRepository; use Alchemy\Phrasea\Model\Repositories\LazaretFileRepository; use Alchemy\Phrasea\Model\Repositories\TaskRepository; use Alchemy\Phrasea\Record\RecordReferenceCollection; +use Alchemy\Phrasea\Search\RecordTransformer; +use Alchemy\Phrasea\Search\RecordView; +use Alchemy\Phrasea\Search\SearchResultView; +use Alchemy\Phrasea\Search\StoryTransformer; +use Alchemy\Phrasea\Search\StoryView; +use Alchemy\Phrasea\Search\SubdefTransformer; +use Alchemy\Phrasea\Search\V1SearchCompositeResultTransformer; +use Alchemy\Phrasea\Search\V1SearchRecordsResultTransformer; +use Alchemy\Phrasea\Search\V1SearchResultTransformer; use Alchemy\Phrasea\SearchEngine\SearchEngineInterface; use Alchemy\Phrasea\SearchEngine\SearchEngineLogger; use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; use Alchemy\Phrasea\SearchEngine\SearchEngineResult; -use Alchemy\Phrasea\SearchEngine\SearchEngineSuggestion; use Alchemy\Phrasea\Status\StatusStructure; use Alchemy\Phrasea\TaskManager\LiveInformation; use Alchemy\Phrasea\Utilities\NullableDateTime; use Doctrine\ORM\EntityManager; +use League\Fractal\Resource\Item; use Symfony\Component\Form\Form; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; @@ -1035,24 +1045,18 @@ class V1Controller extends Controller */ public function searchAction(Request $request) { - list($ret, $search_result) = $this->prepareSearchRequest($request); + $fractal = new \League\Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $fractal->parseIncludes($this->resolveSearchIncludes($request)); - $records = []; - $stories = []; + $searchView = $this->buildSearchView($this->doSearch($request)); - /** @var SearchEngineResult $search_result */ - foreach ($search_result->getResults() as $record) { - if ($record->isStory()) { - $stories[] = $record; - } else { - $records[] = $record; - } - } + $recordTransformer = new RecordTransformer(); + $storyTransformer = new StoryTransformer(new SubdefTransformer(), $recordTransformer); + $compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer); + $searchTransformer = new V1SearchResultTransformer($compositeTransformer); - $ret['results'] = [ - 'records' => $this->listRecords($request, $records), - 'stories' => $this->listStories($request, $stories), - ]; + $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); return Result::create($request, $ret)->createResponse(); } @@ -1068,33 +1072,141 @@ class V1Controller extends Controller */ public function searchRecordsAction(Request $request) { - list($ret, $search_result) = $this->prepareSearchRequest($request); + $fractal = new \League\Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $fractal->parseIncludes($this->resolveSearchRecordsIncludes($request)); - /** @var SearchEngineResult $search_result */ - foreach ($search_result->getResults() as $es_record) { - try { - $record = new \record_adapter($this->app, $es_record->getDataboxId(), $es_record->getRecordId()); - } catch (\Exception $e) { - continue; - } + $searchView = $this->buildSearchRecordsView($this->doSearch($request)); - $ret['results'][] = $this->listRecord($request, $record); - } + $searchTransformer = new V1SearchRecordsResultTransformer(new RecordTransformer()); + + $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); return Result::create($request, $ret)->createResponse(); } - private function prepareSearchRequest(Request $request) + /** + * @param SearchEngineResult $result + * @return SearchResultView + */ + private function buildSearchView(SearchEngineResult $result) + { + + $references = new RecordReferenceCollection($result->getResults()); + + $records = []; + $stories = []; + + foreach ($references->toRecords($this->getApplicationBox()) as $record) { + if ($record->isStory()) { + $stories[] = $record; + } else { + $records[] = $record; + } + } + + $resultView = new SearchResultView($result); + + if ($stories) { + $storyViews = []; + + foreach ($stories as $story) { + $storyViews[] = new StoryView($story); + } + + $resultView->setStories($storyViews); + } + + if ($records) { + $recordViews = []; + + foreach ($records as $record) { + $recordViews[] = new RecordView($record); + } + + $resultView->setRecords($recordViews); + } + + return $resultView; + } + + /** + * @param SearchEngineResult $result + * @return SearchResultView + */ + private function buildSearchRecordsView(SearchEngineResult $result) + { + $references = new RecordReferenceCollection($result->getResults()); + + $recordViews = []; + + foreach ($references->toRecords($this->getApplicationBox()) as $record) { + $recordViews[] = new RecordView($record); + } + + $resultView = new SearchResultView($result); + $resultView->setRecords($recordViews); + + return $resultView; + } + + /** + * Returns requested includes + * + * @param Request $request + * @return string[] + */ + private function resolveSearchIncludes(Request $request) + { + if (!$request->attributes->get('_extended', false)) { + return []; + } + + return [ + 'results.stories.records.subdefs', + 'results.stories.records.metadata', + 'results.stories.records.caption', + 'results.stories.records.status', + 'results.records.subdefs', + 'results.records.metadata', + 'results.records.caption', + 'results.records.status', + ]; + } + + /** + * Returns requested includes + * + * @param Request $request + * @return string[] + */ + private function resolveSearchRecordsIncludes(Request $request) + { + if (!$request->attributes->get('_extended', false)) { + return []; + } + + return [ + 'results.subdefs', + 'results.metadata', + 'results.caption', + 'results.status', + ]; + } + + /** + * @param Request $request + * @return SearchEngineResult + */ + private function doSearch(Request $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((int) ($request->get('offset_start') ?: 0)); - $options->setMaxResults((int) $request->get('per_page') ?: 10); - - $query = (string) $request->get('query'); $this->getSearchEngine()->resetCache(); - $search_result = $this->getSearchEngine()->query($query, $options); + $search_result = $this->getSearchEngine()->query((string)$request->get('query'), $options); $this->getUserManipulator()->logQuery($this->getAuthenticatedUser(), $search_result->getQuery()); @@ -1111,25 +1223,7 @@ class V1Controller extends Controller $this->getSearchEngine()->clearCache(); - $ret = [ - 'offset_start' => $options->getFirstResult(), - 'per_page' => $options->getMaxResults(), - 'available_results' => $search_result->getAvailable(), - 'total_results' => $search_result->getTotal(), - 'error' => (string)$search_result->getError(), - 'warning' => (string)$search_result->getWarning(), - 'query_time' => $search_result->getDuration(), - 'search_indexes' => $search_result->getIndexes(), - 'suggestions' => array_map( - function (SearchEngineSuggestion $suggestion) { - return $suggestion->toArray(); - }, $search_result->getSuggestions()->toArray()), - 'facets' => $search_result->getFacets(), - 'results' => [], - 'query' => $search_result->getQuery(), - ]; - - return [$ret, $search_result]; + return $search_result; } /** diff --git a/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php new file mode 100644 index 0000000000..5ea77600f2 --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php @@ -0,0 +1,49 @@ +caption = $caption; + } + + /** + * @return CaptionView + */ + public function getCaption() + { + return $this->caption; + } +} diff --git a/lib/Alchemy/Phrasea/Search/CaptionView.php b/lib/Alchemy/Phrasea/Search/CaptionView.php new file mode 100644 index 0000000000..a31c584613 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/CaptionView.php @@ -0,0 +1,32 @@ +caption = $caption; + } + + /** + * @return \caption_record + */ + public function getCaption() + { + return $this->caption; + } +} diff --git a/lib/Alchemy/Phrasea/Search/RecordTransformer.php b/lib/Alchemy/Phrasea/Search/RecordTransformer.php new file mode 100644 index 0000000000..e1948dac14 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/RecordTransformer.php @@ -0,0 +1,109 @@ +subdefTransformer = $subdefTransformer; + $this->technicalDataTransformer = $technicalDataTransformer; + } + + public function transform(RecordView $recordView) + { + $record = $recordView->getRecord(); + + return [ + 'databox_id' => $record->getDataboxId(), + 'record_id' => $record->getRecordId(), + 'mime_type' => $record->getMimeType(), + 'title' => $record->get_title(), + 'original_name' => $record->get_original_name(), + 'updated_on' => $record->getUpdated()->format(DATE_ATOM), + 'created_on' => $record->getCreated()->format(DATE_ATOM), + 'collection_id' => $record->getCollectionId(), + 'base_id' => $record->getBaseId(), + 'sha256' => $record->getSha256(), + 'phrasea_type' => $record->getType(), + 'uuid' => $record->getUuid(), + ]; + } + + public function includeThumbnail(RecordView $recordView) + { + return $this->item($recordView->getSubdef('thumbnail'), $this->subdefTransformer); + } + + public function includeTechnicalInformations(RecordView $recordView) + { + return $this->collection($recordView->getTechnicalDataView()->getDataSet(), $this->technicalDataTransformer); + } + + public function includeSubdefs(RecordView $recordView) + { + return $this->collection($recordView->getSubdefs(), $this->subdefTransformer); + } + + public function includeMetadata(RecordView $recordView) + { + $ret = []; + + foreach ($recordView->getCaption()->getCaption()->get_fields() as $field) { + $databox_field = $field->get_databox_field(); + + $fieldData = [ + 'meta_structure_id' => $field->get_meta_struct_id(), + 'name' => $field->get_name(), + 'labels' => [ + 'fr' => $databox_field->get_label('fr'), + 'en' => $databox_field->get_label('en'), + 'de' => $databox_field->get_label('de'), + 'nl' => $databox_field->get_label('nl'), + ], + ]; + + foreach ($field->get_values() as $value) { + $data = [ + 'meta_id' => $value->getId(), + 'value' => $value->getValue(), + ]; + + $ret[] = array_replace($fieldData, $data); + } + } + + return $this->collection($recordView->getCaption(), ) + } + + public function includeStatus(RecordView $recordView) + { + } + + public function includeCaption(RecordView $recordView) + { + } +} diff --git a/lib/Alchemy/Phrasea/Search/RecordView.php b/lib/Alchemy/Phrasea/Search/RecordView.php new file mode 100644 index 0000000000..aa97a210bd --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/RecordView.php @@ -0,0 +1,53 @@ +record = $record; + } + + /** + * @return \record_adapter + */ + public function getRecord() + { + return $this->record; + } + + public function setTechnicalDataView(TechnicalDataView $technicalDataView) + { + $this->technicalDataView = $technicalDataView; + } + + /** + * @return TechnicalDataView + */ + public function getTechnicalDataView() + { + return $this->technicalDataView; + } +} diff --git a/lib/Alchemy/Phrasea/Search/SearchResultView.php b/lib/Alchemy/Phrasea/Search/SearchResultView.php index 47c35e0ab3..e4a80edc24 100644 --- a/lib/Alchemy/Phrasea/Search/SearchResultView.php +++ b/lib/Alchemy/Phrasea/Search/SearchResultView.php @@ -11,6 +11,7 @@ namespace Alchemy\Phrasea\Search; use Alchemy\Phrasea\SearchEngine\SearchEngineResult; +use Assert\Assertion; class SearchResultView { @@ -19,6 +20,16 @@ class SearchResultView */ private $result; + /** + * @var StoryView[] + */ + private $stories = []; + + /** + * @var RecordView[] + */ + private $records = []; + public function __construct(SearchEngineResult $result) { $this->result = $result; @@ -31,4 +42,42 @@ class SearchResultView { return $this->result; } + + /** + * @param StoryView[] $stories + * @return void + */ + public function setStories($stories) + { + Assertion::allIsInstanceOf($this->stories, StoryView::class); + + $this->stories = $stories instanceof \Traversable ? iterator_to_array($stories, false) : array_values($stories); + } + + /** + * @return StoryView[] + */ + public function getStories() + { + return $this->stories; + } + + /** + * @param RecordView[] $records + * @return void + */ + public function setRecords($records) + { + Assertion::allIsInstanceOf($records, RecordView::class); + + $this->records = $records instanceof \Traversable ? iterator_to_array($records, false) : array_values($records); + } + + /** + * @return RecordView[] + */ + public function getRecords() + { + return $this->records; + } } diff --git a/lib/Alchemy/Phrasea/Search/StoryTransformer.php b/lib/Alchemy/Phrasea/Search/StoryTransformer.php new file mode 100644 index 0000000000..df73f5580d --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/StoryTransformer.php @@ -0,0 +1,109 @@ +subdefTransformer = $subdefTransformer; + $this->recordTransformer = $recordTransformer; + } + + public function transform(StoryView $storyView) + { + $story = $storyView->getStory(); + + return [ + '@entity@' => 'http://api.phraseanet.com/api/objects/story', + '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(), + ]; + } + + public function includeThumbnail(StoryView $storyView) + { + return $this->item($storyView->getSubdef('thumbnail'), $this->subdefTransformer); + } + + public function includeMetadatas(StoryView $storyView) + { + return $this->item($storyView->getCaption(), $this->getCaptionTransformer()); + } + + public function includeRecords(StoryView $storyView) + { + return $this->collection($storyView->getChildren(), $this->recordTransformer); + } + + /** + * @return \Closure + */ + private function getCaptionTransformer() + { + /** + * @param \caption_field[] $fields + * @param string $dcField + * @return string|null + */ + $format = function ($fields, $dcField) { + return isset($fields[$dcField]) ? $fields[$dcField]->get_serialized_values() : null; + }; + + return function (CaptionView $captionView) use ($format) { + $caption = $captionView->getCaption()->getDCFields(); + + return [ + '@entity@' => 'http://api.phraseanet.com/api/objects/story-metadata-bag', + 'dc:contributor' => $format($caption, \databox_Field_DCESAbstract::Contributor), + 'dc:coverage' => $format($caption, \databox_Field_DCESAbstract::Coverage), + 'dc:creator' => $format($caption, \databox_Field_DCESAbstract::Creator), + 'dc:date' => $format($caption, \databox_Field_DCESAbstract::Date), + 'dc:description' => $format($caption, \databox_Field_DCESAbstract::Description), + 'dc:format' => $format($caption, \databox_Field_DCESAbstract::Format), + 'dc:identifier' => $format($caption, \databox_Field_DCESAbstract::Identifier), + 'dc:language' => $format($caption, \databox_Field_DCESAbstract::Language), + 'dc:publisher' => $format($caption, \databox_Field_DCESAbstract::Publisher), + 'dc:relation' => $format($caption, \databox_Field_DCESAbstract::Relation), + 'dc:rights' => $format($caption, \databox_Field_DCESAbstract::Rights), + 'dc:source' => $format($caption, \databox_Field_DCESAbstract::Source), + 'dc:subject' => $format($caption, \databox_Field_DCESAbstract::Subject), + 'dc:title' => $format($caption, \databox_Field_DCESAbstract::Title), + 'dc:type' => $format($caption, \databox_Field_DCESAbstract::Type), + ]; + }; + } +} diff --git a/lib/Alchemy/Phrasea/Search/StoryView.php b/lib/Alchemy/Phrasea/Search/StoryView.php new file mode 100644 index 0000000000..30aebafce3 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/StoryView.php @@ -0,0 +1,63 @@ +story = $story; + } + + /** + * @return \record_adapter + */ + public function getStory() + { + return $this->story; + } + + /** + * @param RecordView[] $children + */ + public function setChildren($children) + { + Assertion::allIsInstanceOf($children, RecordView::class); + + $this->children = $children instanceof \Traversable ? iterator_to_array($children, false) : array_values($children); + } + + /** + * @return RecordView[] + */ + public function getChildren() + { + return $this->children; + } +} diff --git a/lib/Alchemy/Phrasea/Search/SubdefTransformer.php b/lib/Alchemy/Phrasea/Search/SubdefTransformer.php new file mode 100644 index 0000000000..24cdee94d9 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/SubdefTransformer.php @@ -0,0 +1,21 @@ +subdef = $subdef; + } + + /** + * @return \media_subdef + */ + public function getSubdef() + { + return $this->subdef; + } +} diff --git a/lib/Alchemy/Phrasea/Search/SubdefsAware.php b/lib/Alchemy/Phrasea/Search/SubdefsAware.php new file mode 100644 index 0000000000..9edf617552 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/SubdefsAware.php @@ -0,0 +1,52 @@ +subdefs = []; + + foreach ($subdefs as $subdef) { + $this->subdefs[$subdef->getSubdef()->get_name()] = $subdef; + } + } + + /** + * @param string $name + * @return SubdefView + */ + public function getSubdef($name) + { + return $this->subdefs[$name]; + } + + /** + * @return SubdefView + */ + public function getSubdefs() + { + return $this->subdefs; + } +} diff --git a/lib/Alchemy/Phrasea/Search/TechnicalDataTransformer.php b/lib/Alchemy/Phrasea/Search/TechnicalDataTransformer.php new file mode 100644 index 0000000000..6b67aba3b5 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/TechnicalDataTransformer.php @@ -0,0 +1,25 @@ + $technicalData->getName(), + 'value' => $technicalData->getValue(), + ]; + } +} diff --git a/lib/Alchemy/Phrasea/Search/TechnicalDataView.php b/lib/Alchemy/Phrasea/Search/TechnicalDataView.php new file mode 100644 index 0000000000..16b00cf259 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/TechnicalDataView.php @@ -0,0 +1,34 @@ +dataSet = $dataSet; + } + + /** + * @return TechnicalDataSet + */ + public function getDataSet() + { + return $this->dataSet; + } +} diff --git a/lib/Alchemy/Phrasea/Search/V1SearchCompositeResultTransformer.php b/lib/Alchemy/Phrasea/Search/V1SearchCompositeResultTransformer.php new file mode 100644 index 0000000000..f8b46894df --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/V1SearchCompositeResultTransformer.php @@ -0,0 +1,49 @@ +recordTransformer = $recordTransformer; + $this->storyTransformer = $storyTransformer; + } + + public function transform() + { + return []; + } + + public function includeRecords(SearchResultView $resultView) + { + return $this->collection($resultView->getRecords(), $this->recordTransformer); + } + + public function includeStories(SearchResultView $resultView) + { + return $this->collection($resultView->getStories(), $this->storyTransformer); + } +} diff --git a/lib/Alchemy/Phrasea/Search/V1SearchRecordsResultTransformer.php b/lib/Alchemy/Phrasea/Search/V1SearchRecordsResultTransformer.php new file mode 100644 index 0000000000..b731ac2495 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/V1SearchRecordsResultTransformer.php @@ -0,0 +1,29 @@ +recordTransformer = $recordTransformer; + } + + public function includeResults(SearchResultView $resultView) + { + return $this->collection($resultView->getRecords(), $this->recordTransformer); + } +} diff --git a/lib/Alchemy/Phrasea/Search/V1SearchResultTransformer.php b/lib/Alchemy/Phrasea/Search/V1SearchResultTransformer.php new file mode 100644 index 0000000000..1e391eb288 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/V1SearchResultTransformer.php @@ -0,0 +1,31 @@ +transformer = $transformer; + } + + public function includeResults(SearchResultView $resultView) + { + return $this->item($resultView, $this->transformer); + } +} diff --git a/lib/Alchemy/Phrasea/Search/V1SearchTransformer.php b/lib/Alchemy/Phrasea/Search/V1SearchTransformer.php new file mode 100644 index 0000000000..af60018284 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/V1SearchTransformer.php @@ -0,0 +1,44 @@ +getResult(); + + return [ + 'offset_start' => $result->getOptions()->getFirstResult(), + 'per_page' => $result->getOptions()->getMaxResults(), + 'available_results' => $result->getAvailable(), + 'total_results' => $result->getTotal(), + 'error' => (string)$result->getError(), + 'warning' => (string)$result->getWarning(), + 'query_time' => $result->getDuration(), + 'search_indexes' => $result->getIndexes(), + 'suggestions' => array_map( + function (SearchEngineSuggestion $suggestion) { + return $suggestion->toArray(); + }, $result->getSuggestions()->toArray()), + 'facets' => $result->getFacets(), + 'query' => $result->getQuery(), + ]; + } + + abstract public function includeResults(SearchResultView $resultView); +} diff --git a/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php b/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php index 1600302536..26c518f2ea 100644 --- a/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php +++ b/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php @@ -15,6 +15,9 @@ use League\Fractal\TransformerAbstract; class V2SearchTransformer extends TransformerAbstract { + protected $availableIncludes = ['results']; + protected $defaultIncludes = ['results']; + public function transform(SearchResultView $searchView) { return [ @@ -28,27 +31,18 @@ class V2SearchTransformer extends TransformerAbstract 'search_indexes' => $searchView->getResult()->getIndexes(), 'facets' => $searchView->getResult()->getFacets(), 'search_type' => $searchView->getResult()->getOptions()->getSearchType(), - 'results' => $this->listResults($searchView->getResult()->getResults()), ]; } - /** - * @param RecordInterface[] $results - * @return array - */ - public function listResults($results) + public function includeResults(SearchResultView $searchView) { - $data = []; - - foreach ($results as $record) { - $data[] = [ + return $this->collection($searchView->getResult()->getResults(), function (RecordInterface $record) { + return [ 'databox_id' => $record->getDataboxId(), 'record_id' => $record->getRecordId(), 'collection_id' => $record->getCollectionId(), 'version' => $record->getUpdated()->getTimestamp(), ]; - } - - return $data; + }); } } From 0030e51561ba8fbfcedca7ee4675be0bc36adecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Tue, 19 Apr 2016 11:04:55 +0200 Subject: [PATCH 03/22] WIP RecordTransformer --- .../Phrasea/Controller/Api/V1Controller.php | 9 ++- lib/Alchemy/Phrasea/Search/CaptionView.php | 29 ++++++++++ .../Phrasea/Search/RecordTransformer.php | 57 ++++++++++++++----- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 7ea64a89cc..391ba31bf4 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -61,6 +61,7 @@ use Alchemy\Phrasea\Search\SearchResultView; use Alchemy\Phrasea\Search\StoryTransformer; use Alchemy\Phrasea\Search\StoryView; use Alchemy\Phrasea\Search\SubdefTransformer; +use Alchemy\Phrasea\Search\TechnicalDataTransformer; use Alchemy\Phrasea\Search\V1SearchCompositeResultTransformer; use Alchemy\Phrasea\Search\V1SearchRecordsResultTransformer; use Alchemy\Phrasea\Search\V1SearchResultTransformer; @@ -1051,8 +1052,9 @@ class V1Controller extends Controller $searchView = $this->buildSearchView($this->doSearch($request)); - $recordTransformer = new RecordTransformer(); - $storyTransformer = new StoryTransformer(new SubdefTransformer(), $recordTransformer); + $subdefTransformer = new SubdefTransformer(); + $recordTransformer = new RecordTransformer($subdefTransformer, new TechnicalDataTransformer()); + $storyTransformer = new StoryTransformer($subdefTransformer, $recordTransformer); $compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer); $searchTransformer = new V1SearchResultTransformer($compositeTransformer); @@ -1078,7 +1080,8 @@ class V1Controller extends Controller $searchView = $this->buildSearchRecordsView($this->doSearch($request)); - $searchTransformer = new V1SearchRecordsResultTransformer(new RecordTransformer()); + $recordTransformer = new RecordTransformer(new SubdefTransformer(), new TechnicalDataTransformer()); + $searchTransformer = new V1SearchRecordsResultTransformer($recordTransformer); $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); diff --git a/lib/Alchemy/Phrasea/Search/CaptionView.php b/lib/Alchemy/Phrasea/Search/CaptionView.php index a31c584613..3a51af0d26 100644 --- a/lib/Alchemy/Phrasea/Search/CaptionView.php +++ b/lib/Alchemy/Phrasea/Search/CaptionView.php @@ -10,6 +10,8 @@ namespace Alchemy\Phrasea\Search; +use Assert\Assertion; + class CaptionView { /** @@ -17,6 +19,11 @@ class CaptionView */ private $caption; + /** + * @var \caption_field[] + */ + private $fields = []; + public function __construct(\caption_record $caption) { $this->caption = $caption; @@ -29,4 +36,26 @@ class CaptionView { return $this->caption; } + + /** + * @param \caption_field[] $fields + */ + public function setFields($fields) + { + Assertion::allIsInstanceOf($fields, \caption_field::class); + + $this->fields = []; + + foreach ($fields as $field) { + $this->fields[$field->get_name()] = $field; + } + } + + /** + * @return \caption_field[] + */ + public function getFields() + { + return $this->fields; + } } diff --git a/lib/Alchemy/Phrasea/Search/RecordTransformer.php b/lib/Alchemy/Phrasea/Search/RecordTransformer.php index e1948dac14..adfebd9147 100644 --- a/lib/Alchemy/Phrasea/Search/RecordTransformer.php +++ b/lib/Alchemy/Phrasea/Search/RecordTransformer.php @@ -55,7 +55,9 @@ class RecordTransformer extends TransformerAbstract public function includeThumbnail(RecordView $recordView) { - return $this->item($recordView->getSubdef('thumbnail'), $this->subdefTransformer); + $thumbnailView = $recordView->getSubdef('thumbnail'); + + return $thumbnailView ? $this->item($thumbnailView, $this->subdefTransformer) : null; } public function includeTechnicalInformations(RecordView $recordView) @@ -70,12 +72,13 @@ class RecordTransformer extends TransformerAbstract public function includeMetadata(RecordView $recordView) { - $ret = []; + $fieldData = []; + $values = []; - foreach ($recordView->getCaption()->getCaption()->get_fields() as $field) { + foreach ($recordView->getCaption()->getFields() as $field) { $databox_field = $field->get_databox_field(); - $fieldData = [ + $fieldData[$field->get_meta_struct_id()] = [ 'meta_structure_id' => $field->get_meta_struct_id(), 'name' => $field->get_name(), 'labels' => [ @@ -86,24 +89,52 @@ class RecordTransformer extends TransformerAbstract ], ]; - foreach ($field->get_values() as $value) { - $data = [ - 'meta_id' => $value->getId(), - 'value' => $value->getValue(), - ]; - - $ret[] = array_replace($fieldData, $data); - } + $values[] = $field->get_values(); } - return $this->collection($recordView->getCaption(), ) + if ($values) { + $values = call_user_func_array('array_merge', $values); + } + + return $this->collection($values, function (\caption_Field_Value $value) use ($fieldData) { + $data = $fieldData[$value->getDatabox_field()->get_id()]; + + $data['meta_id'] = $value->getId(); + $data['value'] = $value->getValue(); + + return $data; + }); } public function includeStatus(RecordView $recordView) { + $data = []; + + $bitMask = $recordView->getRecord()->getStatusBitField(); + + foreach ($recordView->getRecord()->getDatabox()->getStatusStructure() as $bit => $status) { + $data[] = [ + 'bit' => $bit, + 'mask' => $bitMask, + ]; + } + + return $this->collection($data, function (array $bitData) { + return [ + 'bit' => $bitData['bit'], + 'state' => \databox_status::bitIsSet($bitData['mask'], $bitData['bit']), + ]; + }); } public function includeCaption(RecordView $recordView) { + return $this->collection($recordView->getCaption()->getFields(), function (\caption_field $field) { + return [ + 'meta_structure_id' => $field->get_meta_struct_id(), + 'name' => $field->get_name(), + 'value' => $field->get_serialized_values(';'), + ]; + }); } } From 23eff5fe28a80487b6153c45ce20d9f3fcb56849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Thu, 21 Apr 2016 20:52:37 +0200 Subject: [PATCH 04/22] WIP progress on List Records --- .../Phrasea/Controller/Api/V1Controller.php | 168 +++++++++++++++--- .../Databox/Subdef/MediaSubdefService.php | 28 ++- .../Phrasea/Fractal/ArraySerializer.php | 4 +- .../Phrasea/Media/TechnicalDataService.php | 8 +- .../Phrasea/Search/PermalinkTransformer.php | 36 ++++ lib/Alchemy/Phrasea/Search/PermalinkView.php | 32 ++++ .../Phrasea/Search/RecordTransformer.php | 4 +- .../Phrasea/Search/SubdefTransformer.php | 63 ++++++- lib/Alchemy/Phrasea/Search/SubdefView.php | 67 +++++++ lib/Alchemy/Phrasea/Search/SubdefsAware.php | 10 +- 10 files changed, 378 insertions(+), 42 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Search/PermalinkTransformer.php create mode 100644 lib/Alchemy/Phrasea/Search/PermalinkView.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 391ba31bf4..fd703b6ed0 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -55,13 +55,17 @@ use Alchemy\Phrasea\Model\Repositories\FeedRepository; use Alchemy\Phrasea\Model\Repositories\LazaretFileRepository; use Alchemy\Phrasea\Model\Repositories\TaskRepository; use Alchemy\Phrasea\Record\RecordReferenceCollection; +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\StoryTransformer; 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\V1SearchCompositeResultTransformer; use Alchemy\Phrasea\Search\V1SearchRecordsResultTransformer; use Alchemy\Phrasea\Search\V1SearchResultTransformer; @@ -306,7 +310,7 @@ class V1Controller extends Controller 'active' => $conf->get(['main', 'bridge', 'dailymotion', 'enabled']), 'clientId' => $conf->get(['main', 'bridge', 'dailymotion', 'client_id']), 'clientSecret' => $conf->get(['main', 'bridge', 'dailymotion', 'client_secret']), - ] + ], ], 'navigator' => ['active' => $conf->get(['registry', 'api-clients', 'navigator-enabled']),], 'office-plugin' => ['active' => $conf->get(['registry', 'api-clients', 'office-enabled']),], @@ -388,7 +392,7 @@ class V1Controller extends Controller 'validationReminder' => $conf->get(['registry', 'actions', 'validation-reminder-days']), 'expirationValue' => $conf->get(['registry', 'actions', 'validation-expiration-days']), ], - ] + ], ]; } @@ -455,7 +459,7 @@ class V1Controller extends Controller public function getDataboxCollectionsAction(Request $request, $databox_id) { $ret = [ - "collections" => $this->listDataboxCollections($this->findDataboxById($databox_id)) + "collections" => $this->listDataboxCollections($this->findDataboxById($databox_id)), ]; return Result::create($request, $ret)->createResponse(); @@ -534,7 +538,7 @@ class V1Controller extends Controller $ret = [ "document_metadatas" => $this->listDataboxMetadataFields( $this->findDataboxById($databox_id)->get_meta_structure() - ) + ), ]; return Result::create($request, $ret)->createResponse(); @@ -751,7 +755,7 @@ class V1Controller extends Controller 'databox_id' => $base->get_sbas_id(), 'base_id' => $base->get_base_id(), 'collection_id' => $base->get_coll_id(), - 'rights' => $baseGrants + 'rights' => $baseGrants, ]; } @@ -799,7 +803,7 @@ class V1Controller extends Controller $service = $this->getAccountService(); $command = new UpdatePasswordCommand(); $form = $this->app->form(new PhraseaRenewPasswordForm(), $command, [ - 'csrf_protection' => false + 'csrf_protection' => false, ]); $form->handleRequest($request); @@ -1078,9 +1082,14 @@ class V1Controller extends Controller $fractal->setSerializer(new ArraySerializer()); $fractal->parseIncludes($this->resolveSearchRecordsIncludes($request)); - $searchView = $this->buildSearchRecordsView($this->doSearch($request)); + $searchView = $this->buildSearchRecordsView( + $this->doSearch($request), + $fractal->getRequestedIncludes(), + $this->resolveSubdefUrlTTL($request) + ); - $recordTransformer = new RecordTransformer(new SubdefTransformer(), new TechnicalDataTransformer()); + $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); + $recordTransformer = new RecordTransformer($subdefTransformer, new TechnicalDataTransformer()); $searchTransformer = new V1SearchRecordsResultTransformer($recordTransformer); $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); @@ -1135,16 +1144,32 @@ class V1Controller extends Controller /** * @param SearchEngineResult $result + * @param string[] $includes + * @param int $urlTTL * @return SearchResultView */ - private function buildSearchRecordsView(SearchEngineResult $result) + private function buildSearchRecordsView(SearchEngineResult $result, array $includes, $urlTTL) { $references = new RecordReferenceCollection($result->getResults()); + $subdefViews = $this->buildSubdefsViews( + $references, + in_array('results.subdefs', $includes, true) ? null : ['thumbnail'], + $urlTTL + ); + + $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); + if (array_intersect($includes, ['results.metadata', 'results.caption'])) { + } + $recordViews = []; - foreach ($references->toRecords($this->getApplicationBox()) as $record) { - $recordViews[] = new RecordView($record); + foreach ($references->toRecords($this->getApplicationBox()) as $index => $record) { + $recordView = new RecordView($record); + $recordView->setSubdefs($subdefViews[$index]); + $recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index])); + + $recordViews[] = $recordView; } $resultView = new SearchResultView($result); @@ -1153,6 +1178,70 @@ class V1Controller extends Controller return $resultView; } + /** + * @param RecordReferenceCollection $references + * @param array|null $names + * @param int $urlTTL + * @return SubdefView[][] + */ + private function buildSubdefsViews(RecordReferenceCollection $references, array $names = null, $urlTTL) + { + $subdefGroups = $this->app['service.media_subdef'] + ->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)]); + }) + ); + $urls = $this->app['media_accessor.subdef_url_generator'] + ->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; + } + /** * Returns requested includes * @@ -1171,9 +1260,9 @@ class V1Controller extends Controller 'results.stories.records.caption', 'results.stories.records.status', 'results.records.subdefs', - 'results.records.metadata', - 'results.records.caption', - 'results.records.status', + //'results.records.metadata', + //'results.records.caption', + //'results.records.status', ]; } @@ -1191,12 +1280,27 @@ class V1Controller extends Controller return [ 'results.subdefs', - 'results.metadata', - 'results.caption', + //'results.metadata', + //'results.caption', 'results.status', ]; } + /** + * @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 Request $request * @return SearchEngineResult @@ -1495,7 +1599,7 @@ class V1Controller extends Controller foreach ($record->getStatusStructure() as $bit => $status) { $ret[] = [ 'bit' => $bit, - 'state' => \databox_status::bitIsSet($record->getStatusBitField(), $bit) + 'state' => \databox_status::bitIsSet($record->getStatusBitField(), $bit), ]; } @@ -1783,7 +1887,7 @@ class V1Controller extends Controller { $ret = [ "basket" => $this->listBasket($basket), - "basket_elements" => $this->listBasketContent($request, $basket) + "basket_elements" => $this->listBasketContent($request, $basket), ]; return Result::create($request, $ret)->createResponse(); @@ -2186,7 +2290,7 @@ class V1Controller extends Controller $metadatas[] = array( 'meta_struct_id' => $field->get_id(), 'meta_id' => null, - 'value' => $data->{'title'} + 'value' => $data->{'title'}, ); $thumbtitle_set = true; } @@ -2207,7 +2311,7 @@ class V1Controller extends Controller $metadatas[] = array( 'meta_struct_id' => $field->get_id(), 'meta_id' => null, - 'value' => $value + 'value' => $value, ); } } @@ -2351,7 +2455,7 @@ class V1Controller extends Controller { $ret = [ "user" => $this->listUser($this->getAuthenticatedUser()), - "collections" => $this->listUserCollections($this->getAuthenticatedUser()) + "collections" => $this->listUserCollections($this->getAuthenticatedUser()), ]; if (defined('API_SKIP_USER_REGISTRATIONS') && ! constant('API_SKIP_USER_REGISTRATIONS')) { @@ -2414,7 +2518,7 @@ class V1Controller extends Controller $command = new UpdatePasswordCommand(); /** @var Form $form */ $form = $this->app->form(new PhraseaRenewPasswordForm(), $command, [ - 'csrf_protection' => false + 'csrf_protection' => false, ]); $form->submit($data); @@ -2453,7 +2557,7 @@ class V1Controller extends Controller return Result::create($request, [ 'user' => $user, - 'token' => $token + 'token' => $token, ])->createResponse(); } @@ -2711,4 +2815,22 @@ class V1Controller extends Controller return $caption; } + + /** + * @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 []; + } } diff --git a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php index 60a3c9d9d8..af26adff99 100644 --- a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php +++ b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php @@ -30,9 +30,10 @@ class MediaSubdefService * Returns all available subdefs grouped by each record reference and by its name * * @param RecordReferenceInterface[]|RecordReferenceCollection $records + * @param null|array $names * @return \media_subdef[][] */ - public function findSubdefsByRecordReferenceFromCollection($records) + public function findSubdefsByRecordReferenceFromCollection($records, array $names = null) { $subdefs = $this->reduceRecordReferenceCollection( $records, @@ -43,20 +44,28 @@ class MediaSubdefService $carry[$index][$subdef->get_name()] = $subdef; } + + return $carry; }, - array_fill_keys(array_keys(iterator_to_array($records)), []) + array_fill_keys(array_keys($records instanceof \Traversable ? iterator_to_array($records) : $records), []), + $names ); - ksort($subdefs); + $reordered = []; - return $subdefs; + foreach ($records as $index => $record) { + $reordered[$index] = $subdefs[$index]; + } + + return $reordered; } /** * @param RecordReferenceInterface[]|RecordReferenceCollection $records + * @param null|string[] $names * @return \media_subdef[] */ - public function findSubdefsFromRecordReferenceCollection($records) + public function findSubdefsFromRecordReferenceCollection($records, array $names = null) { $groups = $this->reduceRecordReferenceCollection( $records, @@ -65,7 +74,8 @@ class MediaSubdefService return $carry; }, - [] + [], + $names ); if ($groups) { @@ -79,16 +89,18 @@ class MediaSubdefService * @param RecordReferenceInterface[]|RecordReferenceCollection $records * @param callable $process * @param mixed $initialValue + * @param null|string[] $names * @return mixed */ - private function reduceRecordReferenceCollection($records, callable $process, $initialValue) + private function reduceRecordReferenceCollection($records, callable $process, $initialValue, array $names = null) { $records = $this->normalizeRecordCollection($records); $carry = $initialValue; foreach ($records->groupPerDataboxId() as $databoxId => $indexes) { - $subdefs = $this->getRepositoryForDatabox($databoxId)->findByRecordIdsAndNames(array_keys($indexes)); + $subdefs = $this->getRepositoryForDatabox($databoxId) + ->findByRecordIdsAndNames(array_keys($indexes), $names); $carry = $process($carry, $subdefs, $indexes, $databoxId); } diff --git a/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php index 5ea77600f2..a1ca0df69c 100644 --- a/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php +++ b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php @@ -19,12 +19,12 @@ class ArraySerializer extends SerializerAbstract { public function collection($resourceKey, array $data) { - return $data; + return array_values(array_filter($data)); } public function item($resourceKey, array $data) { - return $data; + return $data ?: null; } public function includedData(ResourceInterface $resource, array $data) diff --git a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php index da1449cfc2..984928167d 100644 --- a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php +++ b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php @@ -45,8 +45,12 @@ class TechnicalDataService } } - ksort($sets); + $reorder = []; - return $sets; + foreach ($references as $index => $reference) { + $reorder[$index] = isset($sets[$index]) ? $sets[$index] : new RecordTechnicalDataSet($reference->getRecordId()); + } + + return $reorder; } } diff --git a/lib/Alchemy/Phrasea/Search/PermalinkTransformer.php b/lib/Alchemy/Phrasea/Search/PermalinkTransformer.php new file mode 100644 index 0000000000..6bb22be37d --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/PermalinkTransformer.php @@ -0,0 +1,36 @@ +getPermalink(); + + $downloadUrl = $permalink->get_url(); + $downloadUrl->getQuery()->set('download', '1'); + + return [ + 'created_on' => $permalink->get_created_on()->format(DATE_ATOM), + 'id' => $permalink->get_id(), + 'is_activated' => $permalink->get_is_activated(), + /** @Ignore */ + 'label' => $permalink->get_label(), + 'updated_on' => $permalink->get_last_modified()->format(DATE_ATOM), + 'page_url' => $permalink->get_page(), + 'download_url' => (string)$downloadUrl, + 'url' => (string)$permalink->get_url(), + ]; + } +} diff --git a/lib/Alchemy/Phrasea/Search/PermalinkView.php b/lib/Alchemy/Phrasea/Search/PermalinkView.php new file mode 100644 index 0000000000..72c38b9a78 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/PermalinkView.php @@ -0,0 +1,32 @@ +permalink = $permalink; + } + + /** + * @return \media_Permalink_Adapter + */ + public function getPermalink() + { + return $this->permalink; + } +} diff --git a/lib/Alchemy/Phrasea/Search/RecordTransformer.php b/lib/Alchemy/Phrasea/Search/RecordTransformer.php index adfebd9147..9fdb3a9847 100644 --- a/lib/Alchemy/Phrasea/Search/RecordTransformer.php +++ b/lib/Alchemy/Phrasea/Search/RecordTransformer.php @@ -55,9 +55,7 @@ class RecordTransformer extends TransformerAbstract public function includeThumbnail(RecordView $recordView) { - $thumbnailView = $recordView->getSubdef('thumbnail'); - - return $thumbnailView ? $this->item($thumbnailView, $this->subdefTransformer) : null; + return $this->item($recordView->getSubdef('thumbnail'), $this->subdefTransformer); } public function includeTechnicalInformations(RecordView $recordView) diff --git a/lib/Alchemy/Phrasea/Search/SubdefTransformer.php b/lib/Alchemy/Phrasea/Search/SubdefTransformer.php index 24cdee94d9..c8c63f0bc9 100644 --- a/lib/Alchemy/Phrasea/Search/SubdefTransformer.php +++ b/lib/Alchemy/Phrasea/Search/SubdefTransformer.php @@ -10,12 +10,73 @@ namespace Alchemy\Phrasea\Search; +use Alchemy\Phrasea\Authentication\ACLProvider; +use Alchemy\Phrasea\Model\Entities\User; use League\Fractal\TransformerAbstract; class SubdefTransformer extends TransformerAbstract { + /** + * @var ACLProvider + */ + private $aclProvider; + + /** + * @var User + */ + private $user; + + /** + * @var PermalinkTransformer + */ + private $permalinkTransformer; + + public function __construct(ACLProvider $aclProvider, User $user, PermalinkTransformer $permalinkTransformer) + { + $this->aclProvider = $aclProvider; + $this->user = $user; + $this->permalinkTransformer = $permalinkTransformer; + } + public function transform(SubdefView $subdefView) { - return []; + $media = $subdefView->getSubdef(); + + if (!$media->is_physically_present()) { + return []; + } + + $acl = $this->aclProvider->get($this->user); + $record = $media->get_record(); + + if ($media->get_name() !== 'document' && false === $acl->has_access_to_subdef($record, $media->get_name())) { + return []; + } + if ($media->get_name() === 'document' + && !$acl->has_right_on_base($record->getBaseId(), 'candwnldhd') + && !$acl->has_hd_grant($record) + ) { + return []; + } + + $permalink = $subdefView->getPermalinkView() + ? $this->permalinkTransformer->transform($subdefView->getPermalinkView()) + : null; + + return [ + 'name' => $media->get_name(), + 'permalink' => $permalink, + 'height' => $media->get_height(), + 'width' => $media->get_width(), + 'filesize' => $media->get_size(), + 'devices' => $media->getDevices(), + 'player_type' => $media->get_type(), + 'mime_type' => $media->get_mime(), + 'substituted' => $media->is_substituted(), + 'created_on' => $media->get_creation_date()->format(DATE_ATOM), + 'updated_on' => $media->get_modification_date()->format(DATE_ATOM), + 'url' => $subdefView->getUrl(), + 'url_ttl' => $subdefView->getUrlTTL(), + ]; } } diff --git a/lib/Alchemy/Phrasea/Search/SubdefView.php b/lib/Alchemy/Phrasea/Search/SubdefView.php index c7e7613e32..01064aec89 100644 --- a/lib/Alchemy/Phrasea/Search/SubdefView.php +++ b/lib/Alchemy/Phrasea/Search/SubdefView.php @@ -10,6 +10,8 @@ namespace Alchemy\Phrasea\Search; +use Assert\Assertion; + class SubdefView { /** @@ -17,6 +19,21 @@ class SubdefView */ private $subdef; + /** + * @var PermalinkView + */ + private $permalinkView; + + /** + * @var string + */ + private $url; + + /** + * @var int + */ + private $urlTTL; + public function __construct(\media_subdef $subdef) { $this->subdef = $subdef; @@ -29,4 +46,54 @@ class SubdefView { return $this->subdef; } + + /** + * @param PermalinkView $permalinkView + */ + public function setPermalinkView($permalinkView) + { + $this->permalinkView = $permalinkView; + } + + /** + * @return PermalinkView + */ + public function getPermalinkView() + { + return $this->permalinkView; + } + + /** + * @param string $url + */ + public function setUrl($url) + { + $this->url = (string)$url; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @param null|int $urlTTL + */ + public function setUrlTTL($urlTTL) + { + Assertion::nullOrIntegerish($urlTTL); + + $this->urlTTL = null === $urlTTL ? null : (int)$urlTTL; + } + + /** + * @return null|int + */ + public function getUrlTTL() + { + return $this->urlTTL; + } } diff --git a/lib/Alchemy/Phrasea/Search/SubdefsAware.php b/lib/Alchemy/Phrasea/Search/SubdefsAware.php index 9edf617552..6bf89e055f 100644 --- a/lib/Alchemy/Phrasea/Search/SubdefsAware.php +++ b/lib/Alchemy/Phrasea/Search/SubdefsAware.php @@ -1,5 +1,5 @@ subdefs[$name]; + if (isset($this->subdefs[$name])) { + return $this->subdefs[$name]; + } + + throw new \OutOfBoundsException(sprintf('There are no subdef named "%s"', $name)); } /** From f6c3672eeb33798ff31a1f697ca0c9aece87a977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Fri, 22 Apr 2016 12:22:27 +0200 Subject: [PATCH 05/22] Add RecordCollection class to avoid multiple record_adapter fetching --- .../Phrasea/Databox/DataboxGroupable.php | 41 ++++ .../Phrasea/Record/PerDataboxRecordId.php | 20 ++ .../Phrasea/Record/RecordCollection.php | 218 ++++++++++++++++++ .../Record/RecordReferenceCollection.php | 73 +++++- 4 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Databox/DataboxGroupable.php create mode 100644 lib/Alchemy/Phrasea/Record/PerDataboxRecordId.php create mode 100644 lib/Alchemy/Phrasea/Record/RecordCollection.php diff --git a/lib/Alchemy/Phrasea/Databox/DataboxGroupable.php b/lib/Alchemy/Phrasea/Databox/DataboxGroupable.php new file mode 100644 index 0000000000..75bd392a68 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/DataboxGroupable.php @@ -0,0 +1,41 @@ + + */ + public function groupByDatabox(); + + /** + * Returns databoxes ids + * + * @return int[] + */ + public function getDataboxIds(); + + /** + * @param int $databoxId + * @return array + */ + public function getDataboxGroup($databoxId); + + /** + * Reorder groups if needed + * + * @return void + */ + public function reorderGroups(); +} diff --git a/lib/Alchemy/Phrasea/Record/PerDataboxRecordId.php b/lib/Alchemy/Phrasea/Record/PerDataboxRecordId.php new file mode 100644 index 0000000000..8699990e55 --- /dev/null +++ b/lib/Alchemy/Phrasea/Record/PerDataboxRecordId.php @@ -0,0 +1,20 @@ + + */ + private $groups; + + /** + * @var bool + */ + private $reorderNeeded = false; + + public function __construct($records = []) + { + Assertion::allIsInstanceOf($records, \record_adapter::class); + + foreach ($records as $index => $record) { + $this->add($record, $index); + } + } + + /** + * @param \record_adapter $record + * @param null|int|string $index + * @return void + */ + public function add(\record_adapter $record, $index = null) + { + if (null === $index) { + $this->addWithUnknownIndex($record); + + return; + } + + if (isset($this->records[$index])){ + unset($this->groups[$this->records[$index]->getDataboxId()][$index]); + $this->reorderNeeded = true; + } + + $this->records[$index] = $record; + + $this->addIndexToGroups($record, $index); + } + + /** + * @return \ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator($this->records); + } + + /** + * @param int|string $offset + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->records[$offset]); + } + + /** + * @param int|string $offset + * @return \record_adapter + */ + public function offsetGet($offset) + { + return $this->records[$offset]; + } + + /** + * @param int|string $offset + * @param \record_adapter $value + */ + public function offsetSet($offset, $value) + { + Assertion::isInstanceOf($value, \record_adapter::class); + + $this->add($value, $offset); + } + + /** + * @param int|string $offset + * @return void + */ + public function offsetUnset($offset) + { + if (isset($this->records[$offset])) { + unset($this->groups[$this->records[$offset]->getDataboxId()][$offset]); + } + + unset($this->records[$offset]); + } + + /** + * @return int + */ + public function count() + { + return count($this->records); + } + + /** + * Returns records groups by databoxId (possibly not in order) + * + * @return \record_adapter[][] + */ + public function groupByDatabox() + { + return $this->groups; + } + + /** + * @return int[] + */ + public function getDataboxIds() + { + return array_keys($this->groups); + } + + /** + * @param int $databoxId + * @return \record_adapter[] + */ + public function getDataboxGroup($databoxId) + { + return isset($this->groups[$databoxId]) ? $this->groups[$databoxId] : []; + } + + /** + * @return void + */ + public function reorderGroups() + { + if (!$this->reorderNeeded) { + return; + } + + $groups = []; + + foreach ($this->records as $index => $record) { + $databoxId = $record->getDataboxId(); + + if (!isset($groups[$databoxId])) { + $groups[$databoxId] = []; + } + + $groups[$databoxId][$index] = $record; + } + + $this->groups = $groups; + $this->reorderNeeded = false; + } + + /** + * @param int $databoxId + * @return int[] + */ + public function getDataboxRecordIds($databoxId) + { + $recordIds = []; + + foreach ($this->getDataboxGroup($databoxId) as $record) { + $recordIds[$record->getRecordId()] = true; + } + + return array_keys($recordIds); + } + + /** + * @param \record_adapter $record + * @return void + */ + private function addWithUnknownIndex(\record_adapter $record) + { + $this->records[] = $record; + + end($this->records); + + $this->addIndexToGroups($record, key($this->records)); + } + + /** + * @param \record_adapter $record + * @param int|string $index + * @return void + */ + private function addIndexToGroups(\record_adapter $record, $index) + { + $databoxId = $record->getDataboxId(); + + if (!isset($this->groups[$databoxId])) { + $this->groups[$databoxId] = []; + } + + $this->groups[$databoxId][$index] = $record; + } +} diff --git a/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php b/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php index 97bb80ceef..787bbe5f93 100644 --- a/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php +++ b/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php @@ -10,10 +10,11 @@ namespace Alchemy\Phrasea\Record; +use Alchemy\Phrasea\Databox\DataboxGroupable; use Alchemy\Phrasea\Model\RecordReferenceInterface; use Assert\Assertion; -class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess +class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess, \Countable, DataboxGroupable, PerDataboxRecordId { /** * @param array $records @@ -192,13 +193,17 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess return $records; } + /** + * @param int|string $offset + * @return bool + */ public function offsetExists($offset) { return isset($this->references[$offset]); } /** - * @param mixed $offset + * @param int|string $offset * @return RecordReferenceInterface */ public function offsetGet($offset) @@ -206,6 +211,10 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess return $this->references[$offset]; } + /** + * @param int|string $offset + * @param RecordReferenceInterface $value + */ public function offsetSet($offset, $value) { Assertion::isInstanceOf($value, RecordReferenceInterface::class); @@ -213,9 +222,69 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess $this->add($value, $offset); } + /** + * @param int|string $offset + * @return void + */ public function offsetUnset($offset) { unset($this->references[$offset]); $this->groups = null; } + + /** + * @return RecordReferenceInterface[][] + */ + public function groupByDatabox() + { + $this->reorderGroups(); + + return $this->groups; + } + + public function reorderGroups() + { + if ($this->groups) { + return; + } + + $groups = []; + + foreach ($this->references as $index => $reference) { + if (!isset($groups[$reference->getDataboxId()])) { + $groups[$reference->getDataboxId()] = []; + } + + $groups[$reference->getDataboxId()][$index] = $reference; + } + + $this->groups = $groups; + } + + /** + * @param int $databoxId + * @return RecordReferenceInterface[] + */ + public function getDataboxGroup($databoxId) + { + $this->reorderGroups(); + + return isset($this->groups[$databoxId]) ? $this->groups[$databoxId] : []; + } + + public function getDataboxRecordIds($databoxId) + { + $recordsIds = []; + + foreach ($this->getDataboxGroup($databoxId) as $references) { + $recordsIds[$references->getRecordId()] = true; + } + + return array_keys($recordsIds); + } + + public function count() + { + return count($this->references); + } } From ae4d46216ff94a4a5011ffeef9a5d76d2bda5e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 9 May 2016 18:52:08 +0200 Subject: [PATCH 06/22] Add Caption Service and Repositories --- lib/Alchemy/Phrasea/Application.php | 2 + .../Phrasea/Controller/Api/V1Controller.php | 5 +- .../Caption/CachedCaptionDataRepository.php | 100 +++++++++++++ .../Databox/Caption/CaptionDataRepository.php | 20 +++ .../Databox/Caption/CaptionRepository.php | 74 ++++++++++ .../Caption/CaptionRepositoryFactory.php | 60 ++++++++ .../Databox/Caption/CaptionService.php | 67 +++++++++ .../Caption/CaptionServiceProvider.php | 49 ++++++ .../Caption/DbalCaptionDataRepository.php | 64 ++++++++ .../Phrasea/Media/TechnicalDataService.php | 2 +- lib/classes/caption/record.php | 139 ++++++++++-------- 11 files changed, 518 insertions(+), 64 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepository.php create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/DbalCaptionDataRepository.php diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 2214926e63..cef66745ac 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -73,6 +73,7 @@ use Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider; use Alchemy\Phrasea\Core\Provider\WebhookServiceProvider; use Alchemy\Phrasea\Core\Provider\ZippyServiceProvider; use Alchemy\Phrasea\Core\Provider\WebProfilerServiceProvider as PhraseaWebProfilerServiceProvider; +use Alchemy\Phrasea\Databox\Caption\CaptionServiceProvider; use Alchemy\Phrasea\Databox\Subdef\MediaSubdefServiceProvider; use Alchemy\Phrasea\Exception\InvalidArgumentException; use Alchemy\Phrasea\Filesystem\FilesystemServiceProvider; @@ -194,6 +195,7 @@ class Application extends SilexApplication $this->register(new ManipulatorServiceProvider()); $this->register(new TechnicalDataServiceProvider()); $this->register(new MediaSubdefServiceProvider()); + $this->register(new CaptionServiceProvider()); $this->register(new InstallerServiceProvider()); $this->register(new PhraseaVersionServiceProvider()); diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index fd703b6ed0..1eb0333724 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1159,6 +1159,7 @@ class V1Controller extends Controller ); $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); + if (array_intersect($includes, ['results.metadata', 'results.caption'])) { } @@ -1280,8 +1281,8 @@ class V1Controller extends Controller return [ 'results.subdefs', - //'results.metadata', - //'results.caption', + 'results.metadata', + 'results.caption', 'results.status', ]; } diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php new file mode 100644 index 0000000000..44d214a575 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php @@ -0,0 +1,100 @@ +decorated = $decorated; + $this->cache = $cache instanceof MultiGetCache && $cache instanceof MultiPutCache + ? $cache + : new MultiAdapter($cache); + $this->baseKey = $baseKey; + } + + /** + * @return int + */ + public function getLifeTime() + { + return $this->lifeTime; + } + + /** + * @param int $lifeTime + */ + public function setLifeTime($lifeTime) + { + $this->lifeTime = (int)$lifeTime; + } + + /** + * @param array $recordIds + * @return \array[] + */ + public function findByRecordIds(array $recordIds) + { + $keys = $this->computeKeys($recordIds); + + $data = $this->cache->fetchMultiple($keys); + + if (count($data) === count($keys)) { + return $data; + } + + $data = $this->decorated->findByRecordIds($recordIds); + + $this->cache->saveMultiple(array_combine(array_values($keys), array_values($data))); + + return $data; + } + + /** + * @param int[] $recordIds + * @return string[] + */ + private function computeKeys(array $recordIds) + { + return array_map(function ($recordId) { + return $this->baseKey . 'caption' . json_encode([(int)$recordId]); + }, $recordIds); + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepository.php new file mode 100644 index 0000000000..826df7b0b2 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepository.php @@ -0,0 +1,20 @@ +dataRepository = $dataRepository; + $this->captionFactory = $captionFactory; + } + + public function findByRecordIds(array $recordIds) + { + $this->fetchMissing($recordIds); + + $instances = []; + + foreach ($recordIds as $index => $recordId) { + $instances[$index] = $this->idMap[$recordId]; + } + + return $instances; + } + + public function clear() + { + $this->idMap = []; + } + + private function fetchMissing(array $recordIds) + { + $missing = array_diff($recordIds, array_keys($this->idMap)); + + if (!$missing) { + return; + } + + $data = $this->dataRepository->findByRecordIds($missing); + + $factory = $this->captionFactory; + + foreach ($data as $item) { + Assertion::keyIsset($item, 'record_id'); + + $this->idMap[(int)$item['record_id']] = $factory($item); + } + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php new file mode 100644 index 0000000000..d796224bca --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php @@ -0,0 +1,60 @@ +connectionProvider = $connectionProvider; + $this->cache = $cache; + $this->captionFactoryProvider = $captionFactoryProvider; + } + + public function createRepositoryFor($databoxId) + { + $connection = $this->connectionProvider->getConnection($databoxId); + + $dbalRepository = new DbalCaptionDataRepository($connection); + $dataRepository = new CachedCaptionDataRepository($dbalRepository, $this->cache, sprintf('databox%d:', $databoxId)); + + $provider = $this->captionFactoryProvider; + $factory = $provider($databoxId); + + if (!is_callable($factory)) { + throw new \UnexpectedValueException(sprintf( + 'Caption factory is expected to be callable, got %s', + is_object($factory) ? get_class($factory) : gettype($factory) + )); + } + + return new CaptionRepository($dataRepository, $factory); + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php new file mode 100644 index 0000000000..e27cad063d --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php @@ -0,0 +1,67 @@ +repositoryProvider = $repositoryProvider; + } + + public function findByReferenceCollection($references) + { + $references = $this->normalizeReferenceCollection($references); + + $groups = []; + + foreach ($references->groupPerDataboxId() as $databoxId => $indexes) { + $this->getRepositoryForDatabox($databoxId)->findByRecordIds(array_keys($indexes)); + } + + if ($groups) { + return call_user_func_array('array_merge', $groups); + } + + return []; + } + + /** + * @param RecordReferenceInterface[]|RecordReferenceCollection $references + * @return RecordReferenceCollection + */ + public function normalizeReferenceCollection($references) + { + if ($references instanceof RecordReferenceCollection) { + return $references; + } + + return new RecordReferenceCollection($references); + } + + /** + * @param int $databoxId + * @return CaptionRepository + */ + private function getRepositoryForDatabox($databoxId) + { + return $this->repositoryProvider->getRepositoryForDatabox($databoxId); + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php new file mode 100644 index 0000000000..1163317680 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php @@ -0,0 +1,49 @@ +protect(function ($databoxId) use ($app) { + return function (array $data) use ($app, $databoxId) { + $recordReference = RecordReference::createFromDataboxIdAndRecordId($databoxId, $data['record_id']); + + return new \caption_record($app, $recordReference, $data); + }; + }); + + $app['provider.repo.caption'] = $app->share(function (Application $app) { + $connectionProvider = new DataboxConnectionProvider($app['phraseanet.appbox']); + $factoryProvider = $app['provider.factory.caption']; + + $repositoryFactory = new CaptionRepositoryFactory($connectionProvider, $app['cache'], $factoryProvider); + + return new DataboxBoundRepositoryProvider($repositoryFactory); + }); + + $app['service.caption'] = $app->share(function (Application $app) { + return new CaptionService($app['provider.repo.caption']); + }); + } + + public function boot(Application $app) + { + // no-op + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/DbalCaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/DbalCaptionDataRepository.php new file mode 100644 index 0000000000..a8ab78bc93 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/DbalCaptionDataRepository.php @@ -0,0 +1,64 @@ +connection = $connection; + } + + public function findByRecordIds(array $recordIds) + { + if (!$recordIds) { + return []; + } + + $sql = <<<'SQL' +SELECT m.record_id, m.id AS meta_id, s.id AS structure_id, value, VocabularyType, VocabularyId +FROM metadatas m INNER JOIN metadatas_structure s ON s.id = m.meta_struct_id +WHERE m.record_id IN (:recordIds) +ORDER BY m.record_id ASC, s.sorter ASC +SQL; + + $data = $this->connection->fetchAll( + $sql, ['recordIds' => $recordIds], ['recordIds' => Connection::PARAM_INT_ARRAY] + ); + + return $this->mapByRecordId($data, $recordIds); + } + + /** + * @param array $data + * @param int[] $recordIds + * @return array[] + */ + private function mapByRecordId(array $data, array $recordIds) + { + $groups = array_fill_keys($recordIds, []); + + foreach ($data as $item) { + $recordId = $item['record_id']; + + $groups[$recordId][] = $item; + } + + return $groups; + } +} diff --git a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php index 984928167d..0d443d0228 100644 --- a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php +++ b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php @@ -31,7 +31,7 @@ class TechnicalDataService */ public function fetchRecordsTechnicalData($references) { - if (!$references instanceof RecordReferenceCollection) { + if (!$references instanceof RecordReferenceCollection) { $references = new RecordReferenceCollection($references); } diff --git a/lib/classes/caption/record.php b/lib/classes/caption/record.php index c3199f625a..2b90d0a3d3 100644 --- a/lib/classes/caption/record.php +++ b/lib/classes/caption/record.php @@ -29,10 +29,16 @@ class caption_record implements cache_cacheableInterface */ protected $app; - public function __construct(Application $app, RecordReferenceInterface $record) + /** + * @param Application $app + * @param RecordReferenceInterface $record + * @param array[]|null $fieldsData + */ + public function __construct(Application $app, RecordReferenceInterface $record, array $fieldsData = null) { $this->app = $app; $this->record = $record; + $this->fields = null === $fieldsData ? null : $this->mapFieldsFromData($fieldsData); } public function toArray($includeBusinessFields) @@ -61,8 +67,6 @@ class caption_record implements cache_cacheableInterface return $this->fields; } - $databox = $this->getDatabox(); - try { $fields = $this->get_data_from_cache(); } catch (\Exception $e) { @@ -72,70 +76,14 @@ FROM metadatas m INNER JOIN metadatas_structure s ON s.id = m.meta_struct_id WHERE m.record_id = :record_id ORDER BY s.sorter ASC SQL; - $fields = $databox->get_connection() + $fields = $this->getDatabox()->get_connection() ->executeQuery($sql, [':record_id' => $this->record->getRecordId()]) ->fetchAll(PDO::FETCH_ASSOC); $this->set_data_to_cache($fields); } - $rec_fields = array(); - - if ($fields) { - $databox_descriptionStructure = $databox->get_meta_structure(); - $record = $databox->get_record($this->record->getRecordId()); - - // first group values by field - $caption_fields = []; - foreach ($fields as $row) { - $structure_id = $row['structure_id']; - if(!array_key_exists($structure_id, $caption_fields)) { - $caption_fields[$structure_id] = [ - 'db_field' => $databox_descriptionStructure->get_element($structure_id), - 'values' => [] - ]; - } - - if (count($caption_fields[$structure_id]['values']) > 0 && !$caption_fields[$structure_id]['db_field']->is_multi()) { - // Inconsistent, should not happen - continue; - } - - // build an EMPTY caption_Field_Value - $cfv = new caption_Field_Value( - $this->app, - $caption_fields[$structure_id]['db_field'], - $record, - $row['meta_id'], - caption_Field_Value::DONT_RETRIEVE_VALUES // ask caption_Field_Value "no n+1 sql" - ); - - // inject the value we already know - $cfv->injectValues($row['value'], $row['VocabularyType'], $row['VocabularyId']); - - // add the value to the field - $caption_fields[$structure_id]['values'][] = $cfv; - } - - // now build a "caption_field" with already known "caption_Field_Value"s - foreach($caption_fields as $structure_id => $caption_field) { - - // build an EMPTY caption_field - $cf = new caption_field( - $this->app, - $caption_field['db_field'], - $record, - caption_field::DONT_RETRIEVE_VALUES // ask caption_field "no n+1 sql" - ); - - // inject the value we already know - $cf->injectValues($caption_field['values']); - - // add the field to the fields - $rec_fields[$structure_id] = $cf; - } - } - $this->fields = $rec_fields; + $this->fields = $this->mapFieldsFromData($fields); return $this->fields; } @@ -315,4 +263,73 @@ SQL; { return $this->app->findDataboxById($this->record->getDataboxId()); } + + /** + * @param array $data + * @return caption_field[] + */ + protected function mapFieldsFromData($data) + { + if (!$data) { + return []; + } + + $rec_fields = array(); + + $databox = $this->getDatabox(); + $databox_descriptionStructure = $databox->get_meta_structure(); + $record = $databox->get_record($this->record->getRecordId()); + + // first group values by field + $caption_fields = []; + foreach ($data as $row) { + $structure_id = $row['structure_id']; + if (!array_key_exists($structure_id, $caption_fields)) { + $caption_fields[$structure_id] = [ + 'db_field' => $databox_descriptionStructure->get_element($structure_id), + 'values' => [] + ]; + } + + if (count($caption_fields[$structure_id]['values']) > 0 && !$caption_fields[$structure_id]['db_field']->is_multi()) { + // Inconsistent, should not happen + continue; + } + + // build an EMPTY caption_Field_Value + $cfv = new caption_Field_Value( + $this->app, + $caption_fields[$structure_id]['db_field'], + $record, + $row['meta_id'], + caption_Field_Value::DONT_RETRIEVE_VALUES // ask caption_Field_Value "no n+1 sql" + ); + + // inject the value we already know + $cfv->injectValues($row['value'], $row['VocabularyType'], $row['VocabularyId']); + + // add the value to the field + $caption_fields[$structure_id]['values'][] = $cfv; + } + + // now build a "caption_field" with already known "caption_Field_Value"s + foreach ($caption_fields as $structure_id => $caption_field) { + + // build an EMPTY caption_field + $cf = new caption_field( + $this->app, + $caption_field['db_field'], + $record, + caption_field::DONT_RETRIEVE_VALUES // ask caption_field "no n+1 sql" + ); + + // inject the value we already know + $cf->injectValues($caption_field['values']); + + // add the field to the fields + $rec_fields[$structure_id] = $cf; + } + + return $rec_fields; + } } From 2341641c9e6d83ea1259ea27ba1921a8ad7ffa9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Tue, 10 May 2016 14:24:57 +0200 Subject: [PATCH 07/22] Caption Service tweak --- .../Phrasea/Controller/Api/V1Controller.php | 53 ++++++++++++--- .../Caption/CachedCaptionDataRepository.php | 26 +++++-- .../Databox/Caption/CaptionCacheInvalider.php | 68 +++++++++++++++++++ .../Databox/Caption/CaptionRepository.php | 6 +- .../Caption/CaptionRepositoryFactory.php | 2 +- .../Databox/Caption/CaptionService.php | 28 ++++++-- .../Caption/CaptionServiceProvider.php | 7 +- tests/classes/caption/recordTest.php | 2 +- 8 files changed, 161 insertions(+), 31 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionCacheInvalider.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 1eb0333724..cd6a391596 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -55,6 +55,7 @@ use Alchemy\Phrasea\Model\Repositories\FeedRepository; use Alchemy\Phrasea\Model\Repositories\LazaretFileRepository; use Alchemy\Phrasea\Model\Repositories\TaskRepository; 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; @@ -1160,17 +1161,34 @@ class V1Controller extends Controller $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); - if (array_intersect($includes, ['results.metadata', 'results.caption'])) { - } + $recordViews = $this->buildRecordViews($references); - $recordViews = []; - - foreach ($references->toRecords($this->getApplicationBox()) as $index => $record) { - $recordView = new RecordView($record); + foreach ($recordViews as $index => $recordView) { $recordView->setSubdefs($subdefViews[$index]); $recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index])); + } - $recordViews[] = $recordView; + if (array_intersect($includes, ['results.metadata', 'results.caption'])) { + $acl = $this->getAclForUser(); + + $canSeeBusiness = []; + + foreach ($references->getDataboxIds() as $databoxId) { + $canSeeBusiness[$databoxId] = $acl->can_see_business_fields($this->findDataboxById($databoxId)); + } + + $captions = $this->app['service.caption']->findByReferenceCollection($references); + + foreach ($recordViews as $index => $recordView) { + $caption = $captions[$index]; + + $captionView = new CaptionView($caption); + + $databoxId = $recordView->getRecord()->getDataboxId(); + $captionView->setFields($caption->get_fields(null, $canSeeBusiness[$databoxId])); + + $recordView->setCaption($captionView); + } } $resultView = new SearchResultView($result); @@ -1261,9 +1279,9 @@ class V1Controller extends Controller 'results.stories.records.caption', 'results.stories.records.status', 'results.records.subdefs', - //'results.records.metadata', - //'results.records.caption', - //'results.records.status', + 'results.records.metadata', + 'results.records.caption', + 'results.records.status', ]; } @@ -2834,4 +2852,19 @@ class V1Controller extends Controller return []; } + + /** + * @param RecordReferenceCollection $references + * @return RecordView[] + */ + private function buildRecordViews(RecordReferenceCollection $references) + { + $recordViews = []; + + foreach ($references->toRecords($this->getApplicationBox()) as $index => $record) { + $recordViews[$index] = new RecordView($record); + } + + return $recordViews; + } } diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php index 44d214a575..f10c3e4f00 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php @@ -77,24 +77,40 @@ class CachedCaptionDataRepository implements CaptionDataRepository $data = $this->cache->fetchMultiple($keys); if (count($data) === count($keys)) { - return $data; + return array_combine($recordIds, $data); } $data = $this->decorated->findByRecordIds($recordIds); - $this->cache->saveMultiple(array_combine(array_values($keys), array_values($data))); + $this->cache->saveMultiple(array_combine($keys, $data)); return $data; } + /** + * @param int $recordId + * @return void + */ + public function invalidate($recordId) + { + $this->cache->delete($this->computeKey($recordId)); + } + /** * @param int[] $recordIds * @return string[] */ private function computeKeys(array $recordIds) { - return array_map(function ($recordId) { - return $this->baseKey . 'caption' . json_encode([(int)$recordId]); - }, $recordIds); + return array_map([$this, 'computeKey'], $recordIds); + } + + /** + * @param int $recordId + * @return string + */ + private function computeKey($recordId) + { + return sprintf('%scaption[%d]', $this->baseKey, $recordId); } } diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionCacheInvalider.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionCacheInvalider.php new file mode 100644 index 0000000000..b4efd6d5a3 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionCacheInvalider.php @@ -0,0 +1,68 @@ + 'onMetadataChange', + ]; + } + + /** + * @var callable + */ + private $locator; + + /** + * @param callable $locator CachedCaptionDataRepository provider + */ + public function __construct(callable $locator) + { + $this->locator = $locator; + } + + public function onMetadataChange(MetadataChangedEvent $event) + { + $record = $event->getRecord(); + + $repository = $this->getCaptionRepository($record->getDataboxId()); + $repository->invalidate($record->getRecordId()); + } + + /** + * @param int $databoxId + * @return CachedCaptionDataRepository + */ + private function getCaptionRepository($databoxId) + { + $locator = $this->locator; + + /** @var DataboxBoundRepositoryProvider $repositoryProvider */ + $repositoryProvider = $locator(); + + Assertion::isInstanceOf($repositoryProvider, DataboxBoundRepositoryProvider::class); + + $repository = $repositoryProvider->getRepositoryForDatabox($databoxId); + + Assertion::isInstanceOf($repository, CachedCaptionDataRepository::class); + + return $repository; + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php index ca19468a32..eb9cee0a77 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php @@ -65,10 +65,8 @@ class CaptionRepository $factory = $this->captionFactory; - foreach ($data as $item) { - Assertion::keyIsset($item, 'record_id'); - - $this->idMap[(int)$item['record_id']] = $factory($item); + foreach ($data as $recordId => $item) { + $this->idMap[(int)$recordId] = $factory($recordId, $item); } } } diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php index d796224bca..d48ab0e395 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php @@ -43,7 +43,7 @@ class CaptionRepositoryFactory implements DataboxBoundRepositoryFactory $connection = $this->connectionProvider->getConnection($databoxId); $dbalRepository = new DbalCaptionDataRepository($connection); - $dataRepository = new CachedCaptionDataRepository($dbalRepository, $this->cache, sprintf('databox%d:', $databoxId)); + $dataRepository = new CachedCaptionDataRepository($dbalRepository, $this->cache, sprintf('databox[%d]:', $databoxId)); $provider = $this->captionFactoryProvider; $factory = $provider($databoxId); diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php index e27cad063d..b7c3db9b33 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php @@ -33,21 +33,19 @@ class CaptionService $groups = []; foreach ($references->groupPerDataboxId() as $databoxId => $indexes) { - $this->getRepositoryForDatabox($databoxId)->findByRecordIds(array_keys($indexes)); + $captions = $this->getRepositoryForDatabox($databoxId)->findByRecordIds(array_keys($indexes)); + + $groups[$databoxId] = array_combine($indexes, $captions); } - if ($groups) { - return call_user_func_array('array_merge', $groups); - } - - return []; + return $this->reorderInstances($references, $groups); } /** * @param RecordReferenceInterface[]|RecordReferenceCollection $references * @return RecordReferenceCollection */ - public function normalizeReferenceCollection($references) + private function normalizeReferenceCollection($references) { if ($references instanceof RecordReferenceCollection) { return $references; @@ -64,4 +62,20 @@ class CaptionService { return $this->repositoryProvider->getRepositoryForDatabox($databoxId); } + + /** + * @param RecordReferenceCollection $references + * @param \caption_record[][] $groups + * @return \caption_record[] + */ + private function reorderInstances(RecordReferenceCollection $references, array $groups) + { + $captions = []; + + foreach ($references as $index => $reference) { + $captions[$index] = $groups[$reference->getDataboxId()][$index]; + } + + return $captions; + } } diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php index 1163317680..bc5cbee3bf 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php @@ -10,6 +10,7 @@ namespace Alchemy\Phrasea\Databox\Caption; +use Alchemy\Phrasea\Controller\LazyLocator; use Alchemy\Phrasea\Databox\DataboxBoundRepositoryProvider; use Alchemy\Phrasea\Databox\DataboxConnectionProvider; use Alchemy\Phrasea\Record\RecordReference; @@ -21,8 +22,8 @@ class CaptionServiceProvider implements ServiceProviderInterface public function register(Application $app) { $app['provider.factory.caption'] = $app->protect(function ($databoxId) use ($app) { - return function (array $data) use ($app, $databoxId) { - $recordReference = RecordReference::createFromDataboxIdAndRecordId($databoxId, $data['record_id']); + return function ($recordId, array $data) use ($app, $databoxId) { + $recordReference = RecordReference::createFromDataboxIdAndRecordId($databoxId, $recordId); return new \caption_record($app, $recordReference, $data); }; @@ -44,6 +45,6 @@ class CaptionServiceProvider implements ServiceProviderInterface public function boot(Application $app) { - // no-op + $app['dispatcher']->addSubscriber(new CaptionCacheInvalider(new LazyLocator($app, 'provider.repo.caption'))); } } diff --git a/tests/classes/caption/recordTest.php b/tests/classes/caption/recordTest.php index 02c8eb86a8..b853949550 100644 --- a/tests/classes/caption/recordTest.php +++ b/tests/classes/caption/recordTest.php @@ -17,7 +17,7 @@ class caption_recordTest extends \PhraseanetTestCase public function setUp() { parent::setUp(); - $this->object = new caption_record(self::$DI['app'], self::$DI['record_1'], self::$DI['record_1']->get_databox()); + $this->object = new caption_record(self::$DI['app'], self::$DI['record_1']); } /** From 9f615dfb5dc2d945a0616e70993b09c81ce15127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Tue, 10 May 2016 16:28:38 +0200 Subject: [PATCH 08/22] Subscriber is on DataRepository not actual repository --- .../Phrasea/Controller/Api/V1Controller.php | 42 ++++++------- .../Caption/CaptionDataRepositoryFactory.php | 43 +++++++++++++ .../Databox/Caption/CaptionRepository.php | 2 - .../Caption/CaptionRepositoryFactory.php | 60 ------------------- .../Caption/CaptionServiceProvider.php | 26 ++++++-- .../ClosureDataboxBoundRepositoryFactory.php | 31 ++++++++++ 6 files changed, 115 insertions(+), 89 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepositoryFactory.php delete mode 100644 lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php create mode 100644 lib/Alchemy/Phrasea/Databox/ClosureDataboxBoundRepositoryFactory.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index cd6a391596..d142e5b27a 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1057,7 +1057,7 @@ class V1Controller extends Controller $searchView = $this->buildSearchView($this->doSearch($request)); - $subdefTransformer = new SubdefTransformer(); + $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); $recordTransformer = new RecordTransformer($subdefTransformer, new TechnicalDataTransformer()); $storyTransformer = new StoryTransformer($subdefTransformer, $recordTransformer); $compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer); @@ -1269,20 +1269,20 @@ class V1Controller extends Controller */ private function resolveSearchIncludes(Request $request) { - if (!$request->attributes->get('_extended', false)) { - return []; + if ($request->attributes->get('_extended', false)) { + return [ + 'results.stories.records.subdefs', + 'results.stories.records.metadata', + 'results.stories.records.caption', + 'results.stories.records.status', + 'results.records.subdefs', + 'results.records.metadata', + 'results.records.caption', + 'results.records.status', + ]; } - return [ - 'results.stories.records.subdefs', - 'results.stories.records.metadata', - 'results.stories.records.caption', - 'results.stories.records.status', - 'results.records.subdefs', - 'results.records.metadata', - 'results.records.caption', - 'results.records.status', - ]; + return []; } /** @@ -1293,16 +1293,16 @@ class V1Controller extends Controller */ private function resolveSearchRecordsIncludes(Request $request) { - if (!$request->attributes->get('_extended', false)) { - return []; + if ($request->attributes->get('_extended', false)) { + return [ + 'results.subdefs', + 'results.metadata', + 'results.caption', + 'results.status', + ]; } - return [ - 'results.subdefs', - 'results.metadata', - 'results.caption', - 'results.status', - ]; + return []; } /** diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepositoryFactory.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepositoryFactory.php new file mode 100644 index 0000000000..12faab9deb --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepositoryFactory.php @@ -0,0 +1,43 @@ +connectionProvider = $connectionProvider; + $this->cache = $cache; + } + + public function createRepositoryFor($databoxId) + { + return new CachedCaptionDataRepository( + new DbalCaptionDataRepository($this->connectionProvider->getConnection($databoxId)), + $this->cache, + sprintf('databox[%d]:', $databoxId) + ); + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php index eb9cee0a77..e66de9b226 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php @@ -10,8 +10,6 @@ namespace Alchemy\Phrasea\Databox\Caption; -use Assert\Assertion; - class CaptionRepository { /** diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php deleted file mode 100644 index d48ab0e395..0000000000 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php +++ /dev/null @@ -1,60 +0,0 @@ -connectionProvider = $connectionProvider; - $this->cache = $cache; - $this->captionFactoryProvider = $captionFactoryProvider; - } - - public function createRepositoryFor($databoxId) - { - $connection = $this->connectionProvider->getConnection($databoxId); - - $dbalRepository = new DbalCaptionDataRepository($connection); - $dataRepository = new CachedCaptionDataRepository($dbalRepository, $this->cache, sprintf('databox[%d]:', $databoxId)); - - $provider = $this->captionFactoryProvider; - $factory = $provider($databoxId); - - if (!is_callable($factory)) { - throw new \UnexpectedValueException(sprintf( - 'Caption factory is expected to be callable, got %s', - is_object($factory) ? get_class($factory) : gettype($factory) - )); - } - - return new CaptionRepository($dataRepository, $factory); - } -} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php index bc5cbee3bf..3f63153e33 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php @@ -11,6 +11,7 @@ namespace Alchemy\Phrasea\Databox\Caption; use Alchemy\Phrasea\Controller\LazyLocator; +use Alchemy\Phrasea\Databox\ClosureDataboxBoundRepositoryFactory; use Alchemy\Phrasea\Databox\DataboxBoundRepositoryProvider; use Alchemy\Phrasea\Databox\DataboxConnectionProvider; use Alchemy\Phrasea\Record\RecordReference; @@ -29,13 +30,26 @@ class CaptionServiceProvider implements ServiceProviderInterface }; }); + $app['provider.data_repo.caption'] = $app->share(function (Application $app) { + return new DataboxBoundRepositoryProvider(new CaptionDataRepositoryFactory( + new DataboxConnectionProvider($app['phraseanet.appbox']), + $app['cache'] + )); + }); + $app['provider.repo.caption'] = $app->share(function (Application $app) { - $connectionProvider = new DataboxConnectionProvider($app['phraseanet.appbox']); - $factoryProvider = $app['provider.factory.caption']; + return new DataboxBoundRepositoryProvider( + new ClosureDataboxBoundRepositoryFactory(function ($databoxId) use ($app) { + /** @var CaptionDataRepository $dataRepository */ + $dataRepository = $app['provider.data_repo.caption']->getRepositoryForDatabox($databoxId); + $captionFactoryProvider = $app['provider.factory.caption']; - $repositoryFactory = new CaptionRepositoryFactory($connectionProvider, $app['cache'], $factoryProvider); - - return new DataboxBoundRepositoryProvider($repositoryFactory); + return new CaptionRepository( + $dataRepository, + $captionFactoryProvider($databoxId) + ); + }) + ); }); $app['service.caption'] = $app->share(function (Application $app) { @@ -45,6 +59,6 @@ class CaptionServiceProvider implements ServiceProviderInterface public function boot(Application $app) { - $app['dispatcher']->addSubscriber(new CaptionCacheInvalider(new LazyLocator($app, 'provider.repo.caption'))); + $app['dispatcher']->addSubscriber(new CaptionCacheInvalider(new LazyLocator($app, 'provider.data_repo.caption'))); } } diff --git a/lib/Alchemy/Phrasea/Databox/ClosureDataboxBoundRepositoryFactory.php b/lib/Alchemy/Phrasea/Databox/ClosureDataboxBoundRepositoryFactory.php new file mode 100644 index 0000000000..88776f4bb5 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/ClosureDataboxBoundRepositoryFactory.php @@ -0,0 +1,31 @@ +factory = $factory; + } + + public function createRepositoryFor($databoxId) + { + $factory = $this->factory; + + return $factory($databoxId); + } +} From b61a8a74181ea96562287e550c678c6376b7b550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Tue, 10 May 2016 18:10:08 +0200 Subject: [PATCH 09/22] Fixup calls order to avoid N+1 --- .../Phrasea/Controller/Api/V1Controller.php | 4 +-- lib/classes/caption/record.php | 30 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index d142e5b27a..10109e1743 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1153,6 +1153,8 @@ class V1Controller extends Controller { $references = new RecordReferenceCollection($result->getResults()); + $recordViews = $this->buildRecordViews($references); + $subdefViews = $this->buildSubdefsViews( $references, in_array('results.subdefs', $includes, true) ? null : ['thumbnail'], @@ -1161,8 +1163,6 @@ class V1Controller extends Controller $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); - $recordViews = $this->buildRecordViews($references); - foreach ($recordViews as $index => $recordView) { $recordView->setSubdefs($subdefViews[$index]); $recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index])); diff --git a/lib/classes/caption/record.php b/lib/classes/caption/record.php index 2b90d0a3d3..478cf909a7 100644 --- a/lib/classes/caption/record.php +++ b/lib/classes/caption/record.php @@ -9,6 +9,7 @@ */ use Alchemy\Phrasea\Application; +use Alchemy\Phrasea\Databox\Caption\CachedCaptionDataRepository; use Alchemy\Phrasea\Model\RecordReferenceInterface; use Alchemy\Phrasea\Model\Serializer\CaptionSerializer; @@ -67,23 +68,9 @@ class caption_record implements cache_cacheableInterface return $this->fields; } - try { - $fields = $this->get_data_from_cache(); - } catch (\Exception $e) { - $sql = <<<'SQL' -SELECT m.id AS meta_id, s.id AS structure_id, value, VocabularyType, VocabularyId -FROM metadatas m INNER JOIN metadatas_structure s ON s.id = m.meta_struct_id -WHERE m.record_id = :record_id -ORDER BY s.sorter ASC -SQL; - $fields = $this->getDatabox()->get_connection() - ->executeQuery($sql, [':record_id' => $this->record->getRecordId()]) - ->fetchAll(PDO::FETCH_ASSOC); + $data = $this->getDataRepository()->findByRecordIds([$this->getRecordReference()->getRecordId()]); - $this->set_data_to_cache($fields); - } - - $this->fields = $this->mapFieldsFromData($fields); + $this->fields = $this->mapFieldsFromData(array_shift($data)); return $this->fields; } @@ -253,6 +240,8 @@ SQL; { $this->fields = null; + $this->getDataRepository()->invalidate($this->getRecordReference()->getRecordId()); + return $this->getDatabox()->delete_data_from_cache($this->get_cache_key($option)); } @@ -332,4 +321,13 @@ SQL; return $rec_fields; } + + /** + * @return CachedCaptionDataRepository + */ + private function getDataRepository() + { + return $this->app['provider.data_repo.caption'] + ->getRepositoryForDatabox($this->getRecordReference()->getDataboxId()); + } } From 7da4a65a930ac6c7580818a35eca5575434d9042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Thu, 12 May 2016 14:41:40 +0200 Subject: [PATCH 10/22] Handle non unique recordIds in Repositories --- .../Phrasea/Databox/Caption/CachedCaptionDataRepository.php | 2 +- .../Phrasea/Databox/Subdef/CachedMediaSubdefDataRepository.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php index f10c3e4f00..cdb31aa701 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php @@ -102,7 +102,7 @@ class CachedCaptionDataRepository implements CaptionDataRepository */ private function computeKeys(array $recordIds) { - return array_map([$this, 'computeKey'], $recordIds); + return array_map([$this, 'computeKey'], array_unique($recordIds)); } /** diff --git a/lib/Alchemy/Phrasea/Databox/Subdef/CachedMediaSubdefDataRepository.php b/lib/Alchemy/Phrasea/Databox/Subdef/CachedMediaSubdefDataRepository.php index 712575c628..f2f147bc2b 100644 --- a/lib/Alchemy/Phrasea/Databox/Subdef/CachedMediaSubdefDataRepository.php +++ b/lib/Alchemy/Phrasea/Databox/Subdef/CachedMediaSubdefDataRepository.php @@ -151,11 +151,12 @@ class CachedMediaSubdefDataRepository implements MediaSubdefDataRepository */ private function generateCacheKeys(array $recordIds, array $names) { + $names = array_unique($names); $namesCount = count($names); $keys = array_map(function ($recordId) use ($namesCount, $names) { return array_map([$this, 'getCacheKey'], array_fill(0, $namesCount, $recordId), $names); - }, $recordIds); + }, array_unique($recordIds)); return $keys ? call_user_func_array('array_merge', $keys) : []; } From 9842af9b1aab83cbc22cc1855408618b23e0a09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Thu, 12 May 2016 14:43:50 +0200 Subject: [PATCH 11/22] Error in Assertion in OrderElement add --- .../Order/Controller/BaseOrderController.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/Alchemy/Phrasea/Order/Controller/BaseOrderController.php b/lib/Alchemy/Phrasea/Order/Controller/BaseOrderController.php index 14f10cb5da..dfb3ecc3c4 100644 --- a/lib/Alchemy/Phrasea/Order/Controller/BaseOrderController.php +++ b/lib/Alchemy/Phrasea/Order/Controller/BaseOrderController.php @@ -187,20 +187,23 @@ class BaseOrderController extends Controller return; } - $references = new RecordReferenceCollection(); + $basketReferences = new RecordReferenceCollection(); - $basket->getElements()->forAll(function (BasketElement $element) use ($references) { - $references->addRecordReference($element->getSbasId(), $element->getRecordId()); + $basket->getElements()->forAll(function (BasketElement $element) use ($basketReferences) { + $basketReferences->addRecordReference($element->getSbasId(), $element->getRecordId()); }); + $toAddReferences = new RecordReferenceCollection(); + foreach ($elements as $element) { - $references->addRecordReference($element->getSbasId(), $element->getRecordId()); + $toAddReferences->addRecordReference($element->getSbasId(), $element->getRecordId()); } - $groups = $references->groupPerDataboxId(); + foreach ($toAddReferences->getDataboxIds() as $databoxId) { + $toAddRecordIds = $toAddReferences->getDataboxRecordIds($databoxId); + $basketRecordIds = $basketReferences->getDataboxRecordIds($databoxId); - foreach ($basket->getElements() as $element) { - if (isset($groups[$element->getSbasId()][$element->getRecordId()])) { + if (array_intersect($toAddRecordIds, $basketRecordIds)) { throw new ConflictHttpException('Some records have already been handled'); } } From b80a2767fb544db04de92ef58fda6b723b003538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Thu, 12 May 2016 14:43:13 +0200 Subject: [PATCH 12/22] Remove groupByDataboxIds method as not safe --- .../Databox/Caption/CaptionService.php | 27 +++++++--- .../Databox/Subdef/MediaSubdefService.php | 27 +++++++--- .../Phrasea/Media/TechnicalDataService.php | 19 +++++-- .../Record/RecordReferenceCollection.php | 52 +++++++------------ 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php index b7c3db9b33..42fa2f4a3e 100644 --- a/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php @@ -32,10 +32,10 @@ class CaptionService $groups = []; - foreach ($references->groupPerDataboxId() as $databoxId => $indexes) { - $captions = $this->getRepositoryForDatabox($databoxId)->findByRecordIds(array_keys($indexes)); + foreach ($references->getDataboxIds() as $databoxId) { + $recordIds = $references->getDataboxRecordIds($databoxId); - $groups[$databoxId] = array_combine($indexes, $captions); + $groups[$databoxId] = $this->getRepositoryForDatabox($databoxId)->findByRecordIds($recordIds); } return $this->reorderInstances($references, $groups); @@ -72,10 +72,25 @@ class CaptionService { $captions = []; - foreach ($references as $index => $reference) { - $captions[$index] = $groups[$reference->getDataboxId()][$index]; + foreach ($groups as $databoxId => $group) { + $captions[$databoxId] = array_reduce($group, function (array &$carry, \caption_record $caption) { + $carry[$caption->getRecordReference()->getRecordId()] = $caption; + + return $carry; + }, []); } - return $captions; + $instances = []; + + foreach ($references as $index => $reference) { + $databoxId = $reference->getDataboxId(); + $recordId = $reference->getRecordId(); + + if (isset($captions[$databoxId][$recordId])) { + $instances[$index] = $captions[$databoxId][$recordId]; + } + } + + return $instances; } } diff --git a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php index af26adff99..23802b9424 100644 --- a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php +++ b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php @@ -37,12 +37,23 @@ class MediaSubdefService { $subdefs = $this->reduceRecordReferenceCollection( $records, - function (array &$carry, array $subdefs, array $indexes) { + function (array &$carry, array $subdefs, array $references) { + $subdefsByRecordId = []; + /** @var \media_subdef $subdef */ foreach ($subdefs as $subdef) { - $index = $indexes[$subdef->get_record_id()]; + $recordId = $subdef->get_record_id(); - $carry[$index][$subdef->get_name()] = $subdef; + if (!isset($subdefsByRecordId[$recordId])) { + $subdefsByRecordId[$recordId] = []; + } + + $subdefsByRecordId[$recordId][$subdef->get_name()] = $subdef; + } + + /** @var RecordReferenceInterface $reference */ + foreach ($references as $index => $reference) { + $carry[$index] = $subdefsByRecordId[$reference->getRecordId()]; } return $carry; @@ -98,11 +109,13 @@ class MediaSubdefService $carry = $initialValue; - foreach ($records->groupPerDataboxId() as $databoxId => $indexes) { - $subdefs = $this->getRepositoryForDatabox($databoxId) - ->findByRecordIdsAndNames(array_keys($indexes), $names); + foreach ($records->getDataboxIds() as $databoxId) { + $recordIds = $records->getDataboxRecordIds($databoxId); - $carry = $process($carry, $subdefs, $indexes, $databoxId); + $subdefs = $this->getRepositoryForDatabox($databoxId) + ->findByRecordIdsAndNames($recordIds, $names); + + $carry = $process($carry, $subdefs, $records->getDataboxGroup($databoxId), $databoxId); } return $carry; diff --git a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php index 0d443d0228..977c9b0d1f 100644 --- a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php +++ b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php @@ -37,18 +37,27 @@ class TechnicalDataService $sets = []; - foreach ($references->groupPerDataboxId() as $databoxId => $indexes) { - foreach ($this->provider->getRepositoryFor($databoxId)->findByRecordIds(array_keys($indexes)) as $set) { - $index = $indexes[$set->getRecordId()]; + foreach ($references->getDataboxIds() as $databoxId) { + $recordIds = $references->getDataboxRecordIds($databoxId); - $sets[$index] = $set; + $setPerRecordId = []; + + foreach ($this->provider->getRepositoryFor($databoxId)->findByRecordIds($recordIds) as $set) { + $setPerRecordId[$set->getRecordId()] = $set; } + + $sets[$databoxId] = $setPerRecordId; } $reorder = []; foreach ($references as $index => $reference) { - $reorder[$index] = isset($sets[$index]) ? $sets[$index] : new RecordTechnicalDataSet($reference->getRecordId()); + $databoxId = $reference->getDataboxId(); + $recordId = $reference->getRecordId(); + + $reorder[$index] = isset($sets[$databoxId][$recordId]) + ? $sets[$databoxId][$recordId] + : new RecordTechnicalDataSet($recordId); } return $reorder; diff --git a/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php b/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php index 787bbe5f93..0fb616fc25 100644 --- a/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php +++ b/lib/Alchemy/Phrasea/Record/RecordReferenceCollection.php @@ -127,34 +127,16 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess, \Co return new \ArrayIterator($this->references); } - /** - * @return array> - */ - public function groupPerDataboxId() - { - if (null === $this->groups) { - $this->groups = []; - - foreach ($this->references as $index => $reference) { - $databoxId = $reference->getDataboxId(); - - if (!isset($this->groups[$databoxId])) { - $this->groups[$databoxId] = []; - } - - $this->groups[$databoxId][$reference->getRecordId()] = $index; - } - } - - return $this->groups; - } - /** * @return array */ public function getDataboxIds() { - return array_keys($this->groupPerDataboxId()); + if (null === $this->groups) { + $this->reorderGroups(); + } + + return array_keys($this->groups); } /** @@ -163,12 +145,11 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess, \Co */ public function toRecords(\appbox $appbox) { - $groups = $this->groupPerDataboxId(); - $records = []; - foreach ($groups as $databoxId => $recordIds) { + foreach ($this->getDataboxIds() as $databoxId) { $databox = $appbox->get_databox($databoxId); + $recordIds = $this->getDataboxRecordIds($databoxId); foreach ($databox->getRecordRepository()->findByRecordIds(array_keys($recordIds)) as $record) { $records[$recordIds[$record->getRecordId()]] = $record; @@ -237,14 +218,16 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess, \Co */ public function groupByDatabox() { - $this->reorderGroups(); + if (null === $this->groups) { + $this->reorderGroups(); + } return $this->groups; } public function reorderGroups() { - if ($this->groups) { + if (null !== $this->groups) { return; } @@ -267,20 +250,23 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess, \Co */ public function getDataboxGroup($databoxId) { - $this->reorderGroups(); + // avoid call to reorderGroups when not needed + if (null === $this->groups) { + $this->reorderGroups(); + } return isset($this->groups[$databoxId]) ? $this->groups[$databoxId] : []; } public function getDataboxRecordIds($databoxId) { - $recordsIds = []; + $indexes = []; - foreach ($this->getDataboxGroup($databoxId) as $references) { - $recordsIds[$references->getRecordId()] = true; + foreach ($this->getDataboxGroup($databoxId) as $index => $references) { + $indexes[$references->getRecordId()] = $index; } - return array_keys($recordsIds); + return array_flip($indexes); } public function count() From 3139fccecc9dae57c6ddb1710f40873264fdfb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Fri, 13 May 2016 15:50:06 +0200 Subject: [PATCH 13/22] Add some fractal utility classes --- .../Phrasea/Controller/Api/V1Controller.php | 244 ++++++++++++++---- .../Phrasea/Fractal/CallbackTransformer.php | 37 +++ .../Phrasea/Fractal/IncludeResolver.php | 77 ++++++ lib/Alchemy/Phrasea/Fractal/NullResource.php | 26 ++ .../ResourceTransformerAccessibleScope.php | 49 ++++ .../SearchResultTransformerResolver.php | 38 +++ .../Phrasea/Fractal/TransformerResolver.php | 22 ++ .../Phrasea/Media/TechnicalDataService.php | 6 +- .../Record/RecordReferenceCollection.php | 29 +-- 9 files changed, 459 insertions(+), 69 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Fractal/CallbackTransformer.php create mode 100644 lib/Alchemy/Phrasea/Fractal/IncludeResolver.php create mode 100644 lib/Alchemy/Phrasea/Fractal/NullResource.php create mode 100644 lib/Alchemy/Phrasea/Fractal/ResourceTransformerAccessibleScope.php create mode 100644 lib/Alchemy/Phrasea/Fractal/SearchResultTransformerResolver.php create mode 100644 lib/Alchemy/Phrasea/Fractal/TransformerResolver.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 10109e1743..b9855c56db 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -29,10 +29,14 @@ use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\Core\Event\RecordEdit; use Alchemy\Phrasea\Core\PhraseaEvents; use Alchemy\Phrasea\Core\Version; +use Alchemy\Phrasea\Databox\DataboxGroupable; use Alchemy\Phrasea\Feed\Aggregate; use Alchemy\Phrasea\Feed\FeedInterface; use Alchemy\Phrasea\Form\Login\PhraseaRenewPasswordForm; use Alchemy\Phrasea\Fractal\ArraySerializer; +use Alchemy\Phrasea\Fractal\CallbackTransformer; +use Alchemy\Phrasea\Fractal\IncludeResolver; +use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver; use Alchemy\Phrasea\Model\Entities\ApiOauthToken; use Alchemy\Phrasea\Model\Entities\Basket; use Alchemy\Phrasea\Model\Entities\BasketElement; @@ -54,6 +58,7 @@ use Alchemy\Phrasea\Model\Repositories\FeedEntryRepository; use Alchemy\Phrasea\Model\Repositories\FeedRepository; use Alchemy\Phrasea\Model\Repositories\LazaretFileRepository; use Alchemy\Phrasea\Model\Repositories\TaskRepository; +use Alchemy\Phrasea\Record\RecordCollection; use Alchemy\Phrasea\Record\RecordReferenceCollection; use Alchemy\Phrasea\Search\CaptionView; use Alchemy\Phrasea\Search\PermalinkTransformer; @@ -1051,17 +1056,45 @@ class V1Controller extends Controller */ public function searchAction(Request $request) { + $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); + $technicalDataTransformer = new TechnicalDataTransformer(); + $recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer); + $storyTransformer = new StoryTransformer($subdefTransformer, $recordTransformer); + $compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer); + $searchTransformer = new V1SearchResultTransformer($compositeTransformer); + + $transformerResolver = new SearchResultTransformerResolver([ + '' => $searchTransformer, + 'results' => $compositeTransformer, + 'results.stories' => $storyTransformer, + 'results.stories.thumbnail' => $subdefTransformer, + 'results.stories.metadatas' => new CallbackTransformer(), + 'results.stories.records' => $recordTransformer, + 'results.stories.records.thumbnail' => $subdefTransformer, + 'results.stories.records.technical_informations' => $technicalDataTransformer, + 'results.stories.records.subdefs' => $subdefTransformer, + 'results.stories.records.metadata' => new CallbackTransformer(), + 'results.stories.records.status' => new CallbackTransformer(), + 'results.stories.records.caption' => new CallbackTransformer(), + 'results.records' => $recordTransformer, + 'results.records.thumbnail' => $subdefTransformer, + 'results.records.technical_informations' => $technicalDataTransformer, + 'results.records.subdefs' => $subdefTransformer, + 'results.records.metadata' => new CallbackTransformer(), + 'results.records.status' => new CallbackTransformer(), + 'results.records.caption' => new CallbackTransformer(), + ]); + $includeResolver = new IncludeResolver($transformerResolver); + $fractal = new \League\Fractal\Manager(); $fractal->setSerializer(new ArraySerializer()); $fractal->parseIncludes($this->resolveSearchIncludes($request)); - $searchView = $this->buildSearchView($this->doSearch($request)); - - $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); - $recordTransformer = new RecordTransformer($subdefTransformer, new TechnicalDataTransformer()); - $storyTransformer = new StoryTransformer($subdefTransformer, $recordTransformer); - $compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer); - $searchTransformer = new V1SearchResultTransformer($compositeTransformer); + $searchView = $this->buildSearchView( + $this->doSearch($request), + $includeResolver->resolve($fractal), + $this->resolveSubdefUrlTTL($request) + ); $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); @@ -1100,41 +1133,119 @@ class V1Controller extends Controller /** * @param SearchEngineResult $result + * @param string[] $includes + * @param int $urlTTL * @return SearchResultView */ - private function buildSearchView(SearchEngineResult $result) + private function buildSearchView(SearchEngineResult $result, array $includes, $urlTTL) { - $references = new RecordReferenceCollection($result->getResults()); - $records = []; - $stories = []; + $records = new RecordCollection(); + $stories = new RecordCollection(); foreach ($references->toRecords($this->getApplicationBox()) as $record) { if ($record->isStory()) { - $stories[] = $record; + $stories[$record->getId()] = $record; } else { - $records[] = $record; + $records[$record->getId()] = $record; } } $resultView = new SearchResultView($result); - if ($stories) { - $storyViews = []; + if ($stories->count() > 0) { + $user = $this->getAuthenticatedUser(); + $children = []; - foreach ($stories as $story) { - $storyViews[] = new StoryView($story); + foreach ($stories->getDataboxIds() as $databoxId) { + $storyIds = $stories->getDataboxRecordIds($databoxId); + + $selections = $this->findDataboxById($databoxId) + ->getRecordRepository() + ->findChildren($storyIds, $user); + $children[$databoxId] = array_combine($storyIds, $selections); + } + + /** @var StoryView[] $storyViews */ + $storyViews = []; + /** @var RecordView[] $childrenViews */ + $childrenViews = []; + + foreach ($stories as $index => $story) { + $storyView = new StoryView($story); + + $selection = $children[$story->getDataboxId()][$story->getRecordId()]; + + $childrenView = $this->buildRecordViews($selection); + + foreach ($childrenView as $view) { + $childrenViews[spl_object_hash($view)] = $view; + } + + $storyView->setChildren($childrenView); + + $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.metadatas', $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.records.subdefs', $includes, true) ? null : ['thumbnail']; + $subdefViews = $this->buildSubdefsViews($allChildren, $names, $urlTTL); + $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($allChildren); + + $newHashes = []; + foreach ($childrenViews as $index => $recordView) { + $newHashes[spl_object_hash($recordView)] = $recordView; + $recordView->setSubdefs($subdefViews[$index]); + $recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index])); + } + + if (array_intersect($includes, ['results.stories.records.metadata', 'results.stories.records.caption'])) { + $captions = $this->app['service.caption']->findByReferenceCollection($allChildren); + $canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($allChildren); + + $this->buildCaptionViews($childrenViews, $captions, $canSeeBusiness); } $resultView->setStories($storyViews); } - if ($records) { - $recordViews = []; + if ($records->count() > 0) { + $names = in_array('results.records.subdefs', $includes, true) ? null : ['thumbnail']; + $recordViews = $this->buildRecordViews($records); + $subdefViews = $this->buildSubdefsViews($records, $names, $urlTTL); - foreach ($records as $record) { - $recordViews[] = new RecordView($record); + $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); @@ -1153,14 +1264,10 @@ class V1Controller extends Controller { $references = new RecordReferenceCollection($result->getResults()); + $names = in_array('results.subdefs', $includes, true) ? null : ['thumbnail']; + $recordViews = $this->buildRecordViews($references); - - $subdefViews = $this->buildSubdefsViews( - $references, - in_array('results.subdefs', $includes, true) ? null : ['thumbnail'], - $urlTTL - ); - + $subdefViews = $this->buildSubdefsViews($references, $names, $urlTTL); $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); foreach ($recordViews as $index => $recordView) { @@ -1169,26 +1276,10 @@ class V1Controller extends Controller } if (array_intersect($includes, ['results.metadata', 'results.caption'])) { - $acl = $this->getAclForUser(); - - $canSeeBusiness = []; - - foreach ($references->getDataboxIds() as $databoxId) { - $canSeeBusiness[$databoxId] = $acl->can_see_business_fields($this->findDataboxById($databoxId)); - } - $captions = $this->app['service.caption']->findByReferenceCollection($references); + $canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($references); - foreach ($recordViews as $index => $recordView) { - $caption = $captions[$index]; - - $captionView = new CaptionView($caption); - - $databoxId = $recordView->getRecord()->getDataboxId(); - $captionView->setFields($caption->get_fields(null, $canSeeBusiness[$databoxId])); - - $recordView->setCaption($captionView); - } + $this->buildCaptionViews($recordViews, $captions, $canSeeBusiness); } $resultView = new SearchResultView($result); @@ -1198,12 +1289,12 @@ class V1Controller extends Controller } /** - * @param RecordReferenceCollection $references + * @param RecordReferenceInterface[]|RecordReferenceCollection|DataboxGroupable $references * @param array|null $names * @param int $urlTTL * @return SubdefView[][] */ - private function buildSubdefsViews(RecordReferenceCollection $references, array $names = null, $urlTTL) + private function buildSubdefsViews($references, array $names = null, $urlTTL) { $subdefGroups = $this->app['service.media_subdef'] ->findSubdefsByRecordReferenceFromCollection($references, $names); @@ -2854,17 +2945,68 @@ class V1Controller extends Controller } /** - * @param RecordReferenceCollection $references + * @param RecordReferenceCollection|RecordCollection|\record_adapter[] $references * @return RecordView[] */ - private function buildRecordViews(RecordReferenceCollection $references) + private function buildRecordViews($references) { + if ($references instanceof RecordReferenceCollection) { + $references = new RecordCollection($references->toRecords($this->getApplicationBox())); + } elseif (!$references instanceof RecordCollection) { + $references = new RecordCollection($references); + } + $recordViews = []; - foreach ($references->toRecords($this->getApplicationBox()) as $index => $record) { + foreach ($references as $index => $record) { $recordViews[$index] = new RecordView($record); } return $recordViews; } + + /** + * @param RecordReferenceInterface[]|DataboxGroupable $references + * @return array + */ + 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); + } + } } diff --git a/lib/Alchemy/Phrasea/Fractal/CallbackTransformer.php b/lib/Alchemy/Phrasea/Fractal/CallbackTransformer.php new file mode 100644 index 0000000000..41fb5df8ac --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/CallbackTransformer.php @@ -0,0 +1,37 @@ +callback = $callback; + } + + public function transform() + { + return call_user_func_array($this->callback, func_get_args()); + } +} diff --git a/lib/Alchemy/Phrasea/Fractal/IncludeResolver.php b/lib/Alchemy/Phrasea/Fractal/IncludeResolver.php new file mode 100644 index 0000000000..df76ac97a3 --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/IncludeResolver.php @@ -0,0 +1,77 @@ +transformerResolver = $transformerResolver; + } + + /** + * @param Manager $manager + * @return array + */ + public function resolve(Manager $manager) + { + $scope = new ResourceTransformerAccessibleScope($manager, $this->createNullResource()); + $scopes = []; + + $this->appendScopeIdentifiers($scopes, $scope); + + return array_values(array_filter($scopes)); + } + + private function appendScopeIdentifiers(array &$scopes, ResourceTransformerAccessibleScope $scope) + { + foreach ($this->figureOutWhichIncludes($scope) as $include) { + $scopeIdentifier = $scope->getIdentifier($include); + + $scopes[] = $scopeIdentifier; + + $childScope = $scope->createChildScope($include, $this->createNullResource($scopeIdentifier)); + + $this->appendScopeIdentifiers($scopes, $childScope); + } + } + + private function figureOutWhichIncludes(ResourceTransformerAccessibleScope $scope) + { + $transformer = $scope->getResourceTransformer(); + + $includes = $transformer->getDefaultIncludes(); + + foreach ($transformer->getAvailableIncludes() as $include) { + if ($scope->isRequested($include)) { + $includes[] = $include; + } + } + + return array_values(array_filter(array_unique($includes))); + } + + /** + * @param string $scopeIdentifier + * @return NullResource + */ + private function createNullResource($scopeIdentifier = '') + { + return new NullResource($this->transformerResolver->resolve($scopeIdentifier)); + } +} diff --git a/lib/Alchemy/Phrasea/Fractal/NullResource.php b/lib/Alchemy/Phrasea/Fractal/NullResource.php new file mode 100644 index 0000000000..edc3aa61b8 --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/NullResource.php @@ -0,0 +1,26 @@ +resource->getTransformer(); + + if ($transformer instanceof TransformerAbstract) { + return $transformer; + } + + return new CallbackTransformer($transformer); + } + + /** + * @param string $scopeIdentifier + * @param ResourceInterface $resource + * @return ResourceTransformerAccessibleScope + */ + public function createChildScope($scopeIdentifier, ResourceInterface $resource) + { + $child = new self($this->manager, $resource, $scopeIdentifier); + + $scopeArray = $this->getParentScopes(); + $scopeArray[] = $this->getScopeIdentifier(); + + $child->setParentScopes($scopeArray); + + return $child; + } +} diff --git a/lib/Alchemy/Phrasea/Fractal/SearchResultTransformerResolver.php b/lib/Alchemy/Phrasea/Fractal/SearchResultTransformerResolver.php new file mode 100644 index 0000000000..b935a041e6 --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/SearchResultTransformerResolver.php @@ -0,0 +1,38 @@ +transformers = $transformers; + } + + public function resolve($scopeIdentifier) + { + if (!isset($this->transformers[$scopeIdentifier])) { + throw new \RuntimeException(sprintf('Unknown scope identifier: %s', $scopeIdentifier)); + } + + return $this->transformers[$scopeIdentifier]; + } +} diff --git a/lib/Alchemy/Phrasea/Fractal/TransformerResolver.php b/lib/Alchemy/Phrasea/Fractal/TransformerResolver.php new file mode 100644 index 0000000000..1780e6ce70 --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/TransformerResolver.php @@ -0,0 +1,22 @@ +getDataboxIds(); + $records = array_fill_keys($databoxIds, []); - foreach ($this->getDataboxIds() as $databoxId) { + foreach ($databoxIds as $databoxId) { $databox = $appbox->get_databox($databoxId); $recordIds = $this->getDataboxRecordIds($databoxId); - foreach ($databox->getRecordRepository()->findByRecordIds(array_keys($recordIds)) as $record) { - $records[$recordIds[$record->getRecordId()]] = $record; + foreach ($databox->getRecordRepository()->findByRecordIds($recordIds) as $record) { + $records[$record->getDataboxId()][$record->getRecordId()] = $record; } } - $indexes = array_flip(array_keys($this->references)); + $sorted = []; - uksort($records, function ($keyA, $keyB) use ($indexes) { - $indexA = $indexes[$keyA]; - $indexB = $indexes[$keyB]; + foreach ($this->references as $index => $reference) { + $databoxId = $reference->getDataboxId(); + $recordId = $reference->getRecordId(); - if ($indexA < $indexB) { - return -1; - } elseif ($indexA > $indexB) { - return 1; + if (isset($records[$databoxId][$recordId])) { + $sorted[$index] = $records[$databoxId][$recordId]; } + } - return 0; - }); - - return $records; + return $sorted; } /** From df0aee31a068a9284fe7b007d12297236dc26762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 16 May 2016 18:19:14 +0200 Subject: [PATCH 14/22] Bump fractal version to development one. --- composer.json | 7 +- composer.lock | 99 +++++++++---------- .../Phrasea/Controller/Api/V1Controller.php | 2 - .../Databox/Subdef/MediaSubdefService.php | 4 +- .../Phrasea/Fractal/ArraySerializer.php | 7 +- .../Phrasea/Search/SubdefTransformer.php | 6 +- .../Phrasea/Controller/Api/ApiJsonTest.php | 26 ++--- 7 files changed, 80 insertions(+), 71 deletions(-) diff --git a/composer.json b/composer.json index 9aee8b75f4..d225ce42fd 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,10 @@ { "type": "vcs", "url": "https://github.com/alchemy-fr/embed-bundle.git" + }, + { + "type": "git", + "url": "https://github.com/bburnichon/fractal.git" } ], "require": { @@ -38,7 +42,7 @@ "alchemy/oauth2php": "1.0.0", "alchemy/phlickr": "0.2.9", "alchemy/phpexiftool": "^0.5.0", - "alchemy/rest-bundle": "^0.0.4", + "alchemy/rest-bundle": "^0.0.5", "alchemy/symfony-cors": "^0.1.0", "alchemy/task-manager": "2.0.x-dev@dev", "alchemy/zippy": "^0.3.0", @@ -69,6 +73,7 @@ "justinrainbow/json-schema": "~1.3", "league/flysystem": "^1.0", "league/flysystem-aws-s3-v2": "^1.0", + "league/fractal": "dev-bug/null-resource-serialization#d84aa1e as 0.13.0", "media-alchemyst/media-alchemyst": "^0.5", "monolog/monolog": "~1.3", "mrclay/minify": "~2.1.6", diff --git a/composer.lock b/composer.lock index e52e7ca25b..586c07aff9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "2f0d3c97831221555b3c1d1a725f897e", - "content-hash": "898155bdbf7333a2634c4b04053469ea", + "hash": "364406a454e9033151c8a2075ecb240f", + "content-hash": "e2785509e5ac412cd184fcb0c3f1bd5f", "packages": [ { "name": "alchemy-fr/tcpdf-clone", @@ -15,12 +15,6 @@ "url": "https://github.com/alchemy-fr/tcpdf-clone.git", "reference": "2ba0248a7187f1626df6c128750650416267f0e7" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/alchemy-fr/tcpdf-clone/zipball/2ba0248a7187f1626df6c128750650416267f0e7", - "reference": "2ba0248a7187f1626df6c128750650416267f0e7", - "shasum": "" - }, "require": { "php": ">=5.3.0" }, @@ -67,10 +61,6 @@ "qrcode", "tcpdf" ], - "support": { - "source": "https://github.com/alchemy-fr/tcpdf-clone/tree/6.0.039", - "issues": "https://github.com/alchemy-fr/tcpdf-clone/issues" - }, "time": "2013-10-13 16:11:17" }, { @@ -489,20 +479,21 @@ }, { "name": "alchemy/rest-bundle", - "version": "0.0.4", + "version": "0.0.5", "source": { "type": "git", "url": "https://github.com/alchemy-fr/rest-bundle.git", - "reference": "9048a99dd328cd2d01efaad16e6af648d11ad2b4" + "reference": "e795b3cd565086d575ee919d1b23279656c982ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/alchemy-fr/rest-bundle/zipball/9048a99dd328cd2d01efaad16e6af648d11ad2b4", - "reference": "9048a99dd328cd2d01efaad16e6af648d11ad2b4", + "url": "https://api.github.com/repos/alchemy-fr/rest-bundle/zipball/e795b3cd565086d575ee919d1b23279656c982ad", + "reference": "e795b3cd565086d575ee919d1b23279656c982ad", "shasum": "" }, "require": { - "league/fractal": "^0.12.0", + "league/fractal": "^0.12.0|^0.13.0", + "php": ">=5.4", "willdurand/negotiation": "~2.0@dev" }, "require-dev": { @@ -536,7 +527,7 @@ } ], "description": "Simple REST utility bundle", - "time": "2016-02-20 22:35:16" + "time": "2016-05-16 09:37:34" }, { "name": "alchemy/symfony-cors", @@ -546,12 +537,6 @@ "url": "https://github.com/alchemy-fr/symfony-cors.git", "reference": "dbf7fcff1ce9fc1265db12955476ff169eab7375" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/alchemy-fr/symfony-cors/zipball/dbf7fcff1ce9fc1265db12955476ff169eab7375", - "reference": "dbf7fcff1ce9fc1265db12955476ff169eab7375", - "shasum": "" - }, "require": { "symfony/http-kernel": "^2.3.0|^3.0.0" }, @@ -572,7 +557,11 @@ "Alchemy\\CorsBundle\\": "src/Bundle/" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Alchemy\\Cors\\Tests\\": "tests/unit/Component/" + } + }, "license": [ "MIT" ], @@ -1801,12 +1790,12 @@ "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d" + "reference": "v1.0.0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/fa966683e7df3e5dd5929d984a44abfbd6bafe8d", - "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d", + "url": "https://api.github.com/repos/igorw/evenement/zipball/v1.0.0", + "reference": "v1.0.0", "shasum": "" }, "require": { @@ -1833,7 +1822,7 @@ "keywords": [ "event-dispatcher" ], - "time": "2012-05-30 15:01:08" + "time": "2012-05-30 08:01:08" }, { "name": "facebook/php-sdk", @@ -2822,12 +2811,12 @@ "source": { "type": "git", "url": "https://github.com/hoaproject/Stream.git", - "reference": "3bc446bc00849bf51166adc415d77aa375d48d8c" + "reference": "011ab91d942f1d7096deade4c8a10fe57d51c5b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3bc446bc00849bf51166adc415d77aa375d48d8c", - "reference": "3bc446bc00849bf51166adc415d77aa375d48d8c", + "url": "https://api.github.com/repos/hoaproject/Stream/zipball/011ab91d942f1d7096deade4c8a10fe57d51c5b3", + "reference": "011ab91d942f1d7096deade4c8a10fe57d51c5b3", "shasum": "" }, "require": { @@ -2872,7 +2861,7 @@ "stream", "wrapper" ], - "time": "2015-10-26 12:21:43" + "time": "2015-10-22 06:30:43" }, { "name": "hoa/ustring", @@ -3660,17 +3649,11 @@ }, { "name": "league/fractal", - "version": "0.12.0", + "version": "dev-bug/null-resource-serialization", "source": { "type": "git", - "url": "https://github.com/thephpleague/fractal.git", - "reference": "0a51dcb6398dc2377d086210d0624996a1df8322" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/fractal/zipball/0a51dcb6398dc2377d086210d0624996a1df8322", - "reference": "0a51dcb6398dc2377d086210d0624996a1df8322", - "shasum": "" + "url": "https://github.com/bburnichon/fractal.git", + "reference": "d84aa1e" }, "require": { "php": ">=5.4" @@ -3699,7 +3682,11 @@ "League\\Fractal\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "League\\Fractal\\Test\\": "test" + } + }, "license": [ "MIT" ], @@ -3719,7 +3706,7 @@ "league", "rest" ], - "time": "2015-03-19 15:16:43" + "time": "2016-05-16 14:18:29" }, { "name": "media-alchemyst/media-alchemyst", @@ -3903,7 +3890,7 @@ ], "authors": [ { - "name": "Steve Clay", + "name": "Stephen Clay", "email": "steve@mrclay.org", "homepage": "http://www.mrclay.org/", "role": "Developer" @@ -4089,21 +4076,21 @@ "source": { "type": "git", "url": "https://github.com/romainneutron/Imagine-Silex-Service-Provider.git", - "reference": "a8a7862ae90419f2b23746cd8436c2310e4eb084" + "reference": "0.1.2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/romainneutron/Imagine-Silex-Service-Provider/zipball/a8a7862ae90419f2b23746cd8436c2310e4eb084", - "reference": "a8a7862ae90419f2b23746cd8436c2310e4eb084", + "url": "https://api.github.com/repos/romainneutron/Imagine-Silex-Service-Provider/zipball/0.1.2", + "reference": "0.1.2", "shasum": "" }, "require": { "imagine/imagine": "*", "php": ">=5.3.3", - "silex/silex": "~1.0" + "silex/silex": ">=1.0,<2.0" }, "require-dev": { - "symfony/browser-kit": "~2.0" + "symfony/browser-kit": ">=2.0,<3.0" }, "type": "library", "autoload": { @@ -5576,7 +5563,7 @@ }, { "name": "Phraseanet Team", - "email": "info@alchemy.fr", + "email": "support@alchemy.fr", "homepage": "http://www.phraseanet.com/" } ], @@ -7728,12 +7715,20 @@ "time": "2015-06-21 13:59:46" } ], - "aliases": [], + "aliases": [ + { + "alias": "0.13.0", + "alias_normalized": "0.13.0.0", + "version": "dev-bug/null-resource-serialization", + "package": "league/fractal" + } + ], "minimum-stability": "stable", "stability-flags": { "alchemy/task-manager": 20, "imagine/imagine": 20, "jms/translation-bundle": 20, + "league/fractal": 20, "neutron/process-manager": 20, "roave/security-advisories": 20, "willdurand/negotiation": 15 diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index b9855c56db..83740c3e56 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1212,9 +1212,7 @@ class V1Controller extends Controller $subdefViews = $this->buildSubdefsViews($allChildren, $names, $urlTTL); $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($allChildren); - $newHashes = []; foreach ($childrenViews as $index => $recordView) { - $newHashes[spl_object_hash($recordView)] = $recordView; $recordView->setSubdefs($subdefViews[$index]); $recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index])); } diff --git a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php index 23802b9424..ae7c75d80b 100644 --- a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php +++ b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php @@ -53,7 +53,9 @@ class MediaSubdefService /** @var RecordReferenceInterface $reference */ foreach ($references as $index => $reference) { - $carry[$index] = $subdefsByRecordId[$reference->getRecordId()]; + if (isset($subdefsByRecordId[$reference->getRecordId()])) { + $carry[$index] = $subdefsByRecordId[$reference->getRecordId()]; + }; } return $carry; diff --git a/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php index a1ca0df69c..264b42803b 100644 --- a/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php +++ b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php @@ -24,7 +24,12 @@ class ArraySerializer extends SerializerAbstract public function item($resourceKey, array $data) { - return $data ?: null; + return $data; + } + + public function null($resourceKey) + { + return null; } public function includedData(ResourceInterface $resource, array $data) diff --git a/lib/Alchemy/Phrasea/Search/SubdefTransformer.php b/lib/Alchemy/Phrasea/Search/SubdefTransformer.php index c8c63f0bc9..ecd43ff032 100644 --- a/lib/Alchemy/Phrasea/Search/SubdefTransformer.php +++ b/lib/Alchemy/Phrasea/Search/SubdefTransformer.php @@ -43,20 +43,20 @@ class SubdefTransformer extends TransformerAbstract $media = $subdefView->getSubdef(); if (!$media->is_physically_present()) { - return []; + return null; } $acl = $this->aclProvider->get($this->user); $record = $media->get_record(); if ($media->get_name() !== 'document' && false === $acl->has_access_to_subdef($record, $media->get_name())) { - return []; + return null; } if ($media->get_name() === 'document' && !$acl->has_right_on_base($record->getBaseId(), 'candwnldhd') && !$acl->has_hd_grant($record) ) { - return []; + return null; } $permalink = $subdefView->getPermalinkView() diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php index 6945e4c151..eed46a878d 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php @@ -800,25 +800,29 @@ class ApiJsonTest extends ApiTestCase public function testSearchRoute() { - self::$DI['app']['manipulator.user'] = $this->getMockBuilder('Alchemy\Phrasea\Model\Manipulator\UserManipulator') + $app = $this->getApplication(); + + $app['manipulator.user'] = $this->getMockBuilder('Alchemy\Phrasea\Model\Manipulator\UserManipulator') ->setConstructorArgs([ - self::$DI['app']['model.user-manager'], - self::$DI['app']['auth.password-encoder'], - self::$DI['app']['geonames.connector'], - self::$DI['app']['repo.users'], - self::$DI['app']['random.low'], - self::$DI['app']['dispatcher'], + $app['model.user-manager'], + $app['auth.password-encoder'], + $app['geonames.connector'], + $app['repo.users'], + $app['random.low'], + $app['dispatcher'], ]) ->setMethods(['logQuery']) ->getMock(); - self::$DI['app']['manipulator.user']->expects($this->once())->method('logQuery'); + $app['manipulator.user']->expects($this->once())->method('logQuery'); $this->setToken($this->userAccessToken); - self::$DI['client']->request('POST', '/api/v1/search/', $this->getParameters(), [], ['HTTP_Accept' => $this->getAcceptMimeType()]); - $content = $this->unserialize(self::$DI['client']->getResponse()->getContent()); + $response = $this->request('POST', '/api/v1/search/', $this->getParameters(), [ + 'HTTP_Accept' => $this->getAcceptMimeType(), + ]); + $content = $this->unserialize($response->getContent()); - $this->evaluateResponse200(self::$DI['client']->getResponse()); + $this->evaluateResponse200($response); $this->evaluateMeta200($content); $response = $content['response']; From 6b27cde40f1e03408715822e63500d0ec51b42c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 16 May 2016 19:33:29 +0200 Subject: [PATCH 15/22] Properly initialize RecordCollection --- lib/Alchemy/Phrasea/Record/RecordCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Alchemy/Phrasea/Record/RecordCollection.php b/lib/Alchemy/Phrasea/Record/RecordCollection.php index 8f9eff2fd9..9351cbb71b 100644 --- a/lib/Alchemy/Phrasea/Record/RecordCollection.php +++ b/lib/Alchemy/Phrasea/Record/RecordCollection.php @@ -23,7 +23,7 @@ class RecordCollection implements \IteratorAggregate, \ArrayAccess, \Countable, /** * @var array */ - private $groups; + private $groups = []; /** * @var bool From bf63a9649757943fd602e2c6c533c8f21805179b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 16 May 2016 19:33:54 +0200 Subject: [PATCH 16/22] Remove client dependency in test, use $this->request instead --- .../Tests/Phrasea/Controller/Api/ApiJsonTest.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php index eed46a878d..fd03f65094 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php @@ -841,17 +841,15 @@ class ApiJsonTest extends ApiTestCase self::$DI['record_story_1']; - $client = $this->getClient(); - $client->request( + $response = $this->request( 'POST', '/api/v1/search/', $this->getParameters(['search_type' => SearchEngineOptions::RECORD_GROUPING]), - [], ['HTTP_Accept' => $this->getAcceptMimeType()] ); - $content = $this->unserialize($client->getResponse()->getContent()); + $content = $this->unserialize($response->getContent()); - $this->evaluateResponse200($client->getResponse()); + $this->evaluateResponse200($response); $this->evaluateMeta200($content); $response = $content['response']; @@ -877,10 +875,10 @@ class ApiJsonTest extends ApiTestCase public function testRecordsSearchRoute() { $this->setToken($this->userAccessToken); - self::$DI['client']->request('POST', '/api/v1/records/search/', $this->getParameters(), [], ['HTTP_Accept' => $this->getAcceptMimeType()]); - $content = $this->unserialize(self::$DI['client']->getResponse()->getContent()); + $response = $this->request('POST', '/api/v1/records/search/', $this->getParameters(), ['HTTP_Accept' => $this->getAcceptMimeType()]); + $content = $this->unserialize($response->getContent()); - $this->evaluateResponse200(self::$DI['client']->getResponse()); + $this->evaluateResponse200($response); $this->evaluateMeta200($content); $response = $content['response']; From 99a23a0753109937cbb40be7c6527a3ee213cb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 16 May 2016 19:34:45 +0200 Subject: [PATCH 17/22] Use same init process in searchRecordsAction and searchAction --- .../Phrasea/Controller/Api/V1Controller.php | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 83740c3e56..8fb90c9fd9 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1112,20 +1112,33 @@ class V1Controller extends Controller */ public function searchRecordsAction(Request $request) { + $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); + $technicalDataTransformer = new TechnicalDataTransformer(); + $recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer); + $searchTransformer = new V1SearchRecordsResultTransformer($recordTransformer); + + $transformerResolver = new SearchResultTransformerResolver([ + '' => $searchTransformer, + 'results' => $recordTransformer, + 'results.thumbnail' => $subdefTransformer, + 'results.technical_informations' => $technicalDataTransformer, + 'results.subdefs' => $subdefTransformer, + 'results.metadata' => new CallbackTransformer(), + 'results.status' => new CallbackTransformer(), + 'results.caption' => new CallbackTransformer(), + ]); + $includeResolver = new IncludeResolver($transformerResolver); + $fractal = new \League\Fractal\Manager(); $fractal->setSerializer(new ArraySerializer()); $fractal->parseIncludes($this->resolveSearchRecordsIncludes($request)); $searchView = $this->buildSearchRecordsView( $this->doSearch($request), - $fractal->getRequestedIncludes(), + $includeResolver->resolve($fractal), $this->resolveSubdefUrlTTL($request) ); - $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); - $recordTransformer = new RecordTransformer($subdefTransformer, new TechnicalDataTransformer()); - $searchTransformer = new V1SearchRecordsResultTransformer($recordTransformer); - $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); return Result::create($request, $ret)->createResponse(); From 33ec01a2c83323df993d6cbecc5e14b6de9bf79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 16 May 2016 19:35:26 +0200 Subject: [PATCH 18/22] Use RecordCollection as it filters out missing records --- lib/Alchemy/Phrasea/Controller/Api/V1Controller.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 8fb90c9fd9..8d98033337 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1274,6 +1274,7 @@ class V1Controller extends Controller private function buildSearchRecordsView(SearchEngineResult $result, array $includes, $urlTTL) { $references = new RecordReferenceCollection($result->getResults()); + $references = new RecordCollection($references->toRecords($this->getApplicationBox())); $names = in_array('results.subdefs', $includes, true) ? null : ['thumbnail']; @@ -2956,14 +2957,12 @@ class V1Controller extends Controller } /** - * @param RecordReferenceCollection|RecordCollection|\record_adapter[] $references + * @param RecordCollection|\record_adapter[] $references * @return RecordView[] */ private function buildRecordViews($references) { - if ($references instanceof RecordReferenceCollection) { - $references = new RecordCollection($references->toRecords($this->getApplicationBox())); - } elseif (!$references instanceof RecordCollection) { + if (!$references instanceof RecordCollection) { $references = new RecordCollection($references); } From 1682535dfb5823eafcdc76804cd49c246176b975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 16 May 2016 19:36:04 +0200 Subject: [PATCH 19/22] Bump fractal with fix on null resource --- composer.json | 2 +- composer.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index d225ce42fd..f30a098848 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,7 @@ "justinrainbow/json-schema": "~1.3", "league/flysystem": "^1.0", "league/flysystem-aws-s3-v2": "^1.0", - "league/fractal": "dev-bug/null-resource-serialization#d84aa1e as 0.13.0", + "league/fractal": "dev-bug/null-resource-serialization#891856f as 0.13.0", "media-alchemyst/media-alchemyst": "^0.5", "monolog/monolog": "~1.3", "mrclay/minify": "~2.1.6", diff --git a/composer.lock b/composer.lock index 586c07aff9..9f1115632a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "364406a454e9033151c8a2075ecb240f", - "content-hash": "e2785509e5ac412cd184fcb0c3f1bd5f", + "hash": "685ed8f8578c211ee14ebc00703ab281", + "content-hash": "acd719252dc8e17e752f2e87f571a7b5", "packages": [ { "name": "alchemy-fr/tcpdf-clone", @@ -3653,7 +3653,7 @@ "source": { "type": "git", "url": "https://github.com/bburnichon/fractal.git", - "reference": "d84aa1e" + "reference": "891856f" }, "require": { "php": ">=5.4" @@ -3706,7 +3706,7 @@ "league", "rest" ], - "time": "2016-05-16 14:18:29" + "time": "2016-05-16 16:41:03" }, { "name": "media-alchemyst/media-alchemyst", From ee9c766fa11a55701359dfa6d93c8ede8602d9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Wed, 18 May 2016 11:41:46 +0200 Subject: [PATCH 20/22] Add TraceableArraySerializer to be able to debug calls --- .../Phrasea/Controller/Api/V1Controller.php | 6 +- .../Phrasea/Fractal/GetSerializationEvent.php | 53 ++++++++++++++++ .../Fractal/TraceableArraySerializer.php | 63 +++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Fractal/GetSerializationEvent.php create mode 100644 lib/Alchemy/Phrasea/Fractal/TraceableArraySerializer.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 8d98033337..c80a3e563c 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -37,6 +37,7 @@ use Alchemy\Phrasea\Fractal\ArraySerializer; use Alchemy\Phrasea\Fractal\CallbackTransformer; use Alchemy\Phrasea\Fractal\IncludeResolver; use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver; +use Alchemy\Phrasea\Fractal\TraceableArraySerializer; use Alchemy\Phrasea\Model\Entities\ApiOauthToken; use Alchemy\Phrasea\Model\Entities\Basket; use Alchemy\Phrasea\Model\Entities\BasketElement; @@ -1087,11 +1088,12 @@ class V1Controller extends Controller $includeResolver = new IncludeResolver($transformerResolver); $fractal = new \League\Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); + $fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher'])); $fractal->parseIncludes($this->resolveSearchIncludes($request)); + $result = $this->doSearch($request); $searchView = $this->buildSearchView( - $this->doSearch($request), + $result, $includeResolver->resolve($fractal), $this->resolveSubdefUrlTTL($request) ); diff --git a/lib/Alchemy/Phrasea/Fractal/GetSerializationEvent.php b/lib/Alchemy/Phrasea/Fractal/GetSerializationEvent.php new file mode 100644 index 0000000000..d1f7c1194f --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/GetSerializationEvent.php @@ -0,0 +1,53 @@ +resourceKey = $resourceKey; + $this->data = $data; + } + + /** + * @return mixed + */ + public function getResourceKey() + { + return $this->resourceKey; + } + + /** + * @return mixed + */ + public function getData() + { + return $this->data; + } + + public function setSerialization($serialization) + { + $this->serialization = $serialization; + $this->stopPropagation(); + } + + public function getSerialization() + { + return $this->serialization; + } +} diff --git a/lib/Alchemy/Phrasea/Fractal/TraceableArraySerializer.php b/lib/Alchemy/Phrasea/Fractal/TraceableArraySerializer.php new file mode 100644 index 0000000000..8e35f1bdf5 --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/TraceableArraySerializer.php @@ -0,0 +1,63 @@ +dispatcher = $dispatcher; + } + + public function collection($resourceKey, array $data) + { + /** @var GetSerializationEvent $event */ + $event = $this->dispatcher->dispatch('fractal.serializer.collection', new GetSerializationEvent($resourceKey, $data)); + + $serialization = parent::collection($resourceKey, $data); + + $event->setSerialization($serialization); + + return $serialization; + } + + public function item($resourceKey, array $data) + { + /** @var GetSerializationEvent $event */ + $event = $this->dispatcher->dispatch('fractal.serializer.item', new GetSerializationEvent($resourceKey, $data)); + + $serialization = parent::item($resourceKey, $data); + + $event->setSerialization($serialization); + + return $serialization; + } + + public function null($resourceKey) + { + /** @var GetSerializationEvent $event */ + $event = $this->dispatcher->dispatch('fractal.serializer.null', new GetSerializationEvent($resourceKey, null)); + + $serialization = parent::null($resourceKey); + + $event->setSerialization($serialization); + + return $serialization; + } + +} From 3b965f9d39d13d69739d53e33a365f4208940625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Wed, 18 May 2016 11:42:01 +0200 Subject: [PATCH 21/22] Fixup wrong variable used in check --- lib/Alchemy/Phrasea/Search/SearchResultView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Alchemy/Phrasea/Search/SearchResultView.php b/lib/Alchemy/Phrasea/Search/SearchResultView.php index e4a80edc24..e2c2b2d642 100644 --- a/lib/Alchemy/Phrasea/Search/SearchResultView.php +++ b/lib/Alchemy/Phrasea/Search/SearchResultView.php @@ -49,7 +49,7 @@ class SearchResultView */ public function setStories($stories) { - Assertion::allIsInstanceOf($this->stories, StoryView::class); + Assertion::allIsInstanceOf($stories, StoryView::class); $this->stories = $stories instanceof \Traversable ? iterator_to_array($stories, false) : array_values($stories); } From 96fce57ae349f4e8d62a8dba4af5857f791426ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Wed, 18 May 2016 11:46:10 +0200 Subject: [PATCH 22/22] Make some list Methods private and remove unused one. --- .../Phrasea/Controller/Api/V1Controller.php | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index c80a3e563c..9fb9922644 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1462,7 +1462,7 @@ class V1Controller extends Controller * @param RecordReferenceInterface[]|RecordReferenceCollection $records * @return array */ - public function listRecords(Request $request, $records) + private function listRecords(Request $request, $records) { if (!$records instanceof RecordReferenceCollection) { $records = new RecordReferenceCollection($records); @@ -1488,7 +1488,7 @@ class V1Controller extends Controller * @param \record_adapter $record * @return array */ - public function listRecord(Request $request, \record_adapter $record) + private function listRecord(Request $request, \record_adapter $record) { $technicalInformation = []; foreach ($record->get_technical_infos()->getValues() as $name => $value) { @@ -1524,27 +1524,6 @@ class V1Controller extends Controller return $data; } - /** - * @param Request $request - * @param RecordReferenceInterface[]|RecordReferenceCollection $stories - * @return array - * @throws \Exception - */ - public function listStories(Request $request, $stories) - { - if (!$stories instanceof RecordReferenceCollection) { - $stories = new RecordReferenceCollection($stories); - } - - $data = []; - - foreach ($stories->toRecords($this->getApplicationBox()) as $story) { - $data[] = $this->listStory($request, $story); - } - - return $data; - } - /** * Retrieve detailed information about one story * @@ -1553,7 +1532,7 @@ class V1Controller extends Controller * @return array * @throws \Exception */ - public function listStory(Request $request, \record_adapter $story) + private function listStory(Request $request, \record_adapter $story) { if (!$story->isStory()) { return Result::createError($request, 404, 'Story not found')->createResponse();