diff --git a/composer.json b/composer.json index 9aee8b75f4..f30a098848 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#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 e52e7ca25b..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": "2f0d3c97831221555b3c1d1a725f897e", - "content-hash": "898155bdbf7333a2634c4b04053469ea", + "hash": "685ed8f8578c211ee14ebc00703ab281", + "content-hash": "acd719252dc8e17e752f2e87f571a7b5", "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": "891856f" }, "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 16:41:03" }, { "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/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/SearchController.php b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php index e1113b3919..04a4f4e742 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/SearchController.php +++ b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php @@ -11,11 +11,16 @@ 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; 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 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/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index c6d3a068a2..9fb9922644 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -29,9 +29,15 @@ 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\Fractal\TraceableArraySerializer; use Alchemy\Phrasea\Model\Entities\ApiOauthToken; use Alchemy\Phrasea\Model\Entities\Basket; use Alchemy\Phrasea\Model\Entities\BasketElement; @@ -53,16 +59,32 @@ 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; +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; 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; @@ -295,7 +317,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']),], @@ -377,7 +399,7 @@ class V1Controller extends Controller 'validationReminder' => $conf->get(['registry', 'actions', 'validation-reminder-days']), 'expirationValue' => $conf->get(['registry', 'actions', 'validation-expiration-days']), ], - ] + ], ]; } @@ -444,7 +466,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(); @@ -523,7 +545,7 @@ class V1Controller extends Controller $ret = [ "document_metadatas" => $this->listDataboxMetadataFields( $this->findDataboxById($databox_id)->get_meta_structure() - ) + ), ]; return Result::create($request, $ret)->createResponse(); @@ -740,7 +762,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, ]; } @@ -788,7 +810,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); @@ -1035,24 +1057,48 @@ class V1Controller extends Controller */ public function searchAction(Request $request) { - list($ret, $search_result) = $this->prepareSearchRequest($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); - $records = []; - $stories = []; + $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); - /** @var SearchEngineResult $search_result */ - foreach ($search_result->getResults() as $record) { - if ($record->isStory()) { - $stories[] = $record; - } else { - $records[] = $record; - } - } + $fractal = new \League\Fractal\Manager(); + $fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher'])); + $fractal->parseIncludes($this->resolveSearchIncludes($request)); - $ret['results'] = [ - 'records' => $this->listRecords($request, $records), - 'stories' => $this->listStories($request, $stories), - ]; + $result = $this->doSearch($request); + $searchView = $this->buildSearchView( + $result, + $includeResolver->resolve($fractal), + $this->resolveSubdefUrlTTL($request) + ); + + $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); return Result::create($request, $ret)->createResponse(); } @@ -1068,33 +1114,330 @@ class V1Controller extends Controller */ public function searchRecordsAction(Request $request) { - list($ret, $search_result) = $this->prepareSearchRequest($request); + $subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer()); + $technicalDataTransformer = new TechnicalDataTransformer(); + $recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer); + $searchTransformer = new V1SearchRecordsResultTransformer($recordTransformer); - /** @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; - } + $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); - $ret['results'][] = $this->listRecord($request, $record); - } + $fractal = new \League\Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $fractal->parseIncludes($this->resolveSearchRecordsIncludes($request)); + + $searchView = $this->buildSearchRecordsView( + $this->doSearch($request), + $includeResolver->resolve($fractal), + $this->resolveSubdefUrlTTL($request) + ); + + $ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray(); return Result::create($request, $ret)->createResponse(); } - private function prepareSearchRequest(Request $request) + /** + * @param SearchEngineResult $result + * @param string[] $includes + * @param int $urlTTL + * @return SearchResultView + */ + private function buildSearchView(SearchEngineResult $result, array $includes, $urlTTL) + { + $references = new RecordReferenceCollection($result->getResults()); + + $records = new RecordCollection(); + $stories = new RecordCollection(); + + foreach ($references->toRecords($this->getApplicationBox()) as $record) { + if ($record->isStory()) { + $stories[$record->getId()] = $record; + } else { + $records[$record->getId()] = $record; + } + } + + $resultView = new SearchResultView($result); + + if ($stories->count() > 0) { + $user = $this->getAuthenticatedUser(); + $children = []; + + 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); + + foreach ($childrenViews as $index => $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->count() > 0) { + $names = in_array('results.records.subdefs', $includes, true) ? null : ['thumbnail']; + $recordViews = $this->buildRecordViews($records); + $subdefViews = $this->buildSubdefsViews($records, $names, $urlTTL); + + $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($records); + + foreach ($recordViews as $index => $recordView) { + $recordView->setSubdefs($subdefViews[$index]); + $recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index])); + } + + if (array_intersect($includes, ['results.records.metadata', 'results.records.caption'])) { + $captions = $this->app['service.caption']->findByReferenceCollection($records); + $canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($records); + + $this->buildCaptionViews($recordViews, $captions, $canSeeBusiness); + } + + $resultView->setRecords($recordViews); + } + + return $resultView; + } + + /** + * @param SearchEngineResult $result + * @param string[] $includes + * @param int $urlTTL + * @return SearchResultView + */ + 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']; + + $recordViews = $this->buildRecordViews($references); + $subdefViews = $this->buildSubdefsViews($references, $names, $urlTTL); + $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); + + foreach ($recordViews as $index => $recordView) { + $recordView->setSubdefs($subdefViews[$index]); + $recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index])); + } + + if (array_intersect($includes, ['results.metadata', 'results.caption'])) { + $captions = $this->app['service.caption']->findByReferenceCollection($references); + $canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($references); + + $this->buildCaptionViews($recordViews, $captions, $canSeeBusiness); + } + + $resultView = new SearchResultView($result); + $resultView->setRecords($recordViews); + + return $resultView; + } + + /** + * @param RecordReferenceInterface[]|RecordReferenceCollection|DataboxGroupable $references + * @param array|null $names + * @param int $urlTTL + * @return SubdefView[][] + */ + private function buildSubdefsViews($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 + * + * @param Request $request + * @return string[] + */ + private function resolveSearchIncludes(Request $request) + { + 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 []; + } + + /** + * Returns requested includes + * + * @param Request $request + * @return string[] + */ + private function resolveSearchRecordsIncludes(Request $request) + { + if ($request->attributes->get('_extended', false)) { + return [ + 'results.subdefs', + 'results.metadata', + 'results.caption', + 'results.status', + ]; + } + + return []; + } + + /** + * @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 + */ + 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 +1454,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; } /** @@ -1137,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); @@ -1163,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) { @@ -1199,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 * @@ -1228,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(); @@ -1398,7 +1702,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), ]; } @@ -1686,7 +1990,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(); @@ -2089,7 +2393,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; } @@ -2110,7 +2414,7 @@ class V1Controller extends Controller $metadatas[] = array( 'meta_struct_id' => $field->get_id(), 'meta_id' => null, - 'value' => $value + 'value' => $value, ); } } @@ -2254,7 +2558,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')) { @@ -2317,7 +2621,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); @@ -2356,7 +2660,7 @@ class V1Controller extends Controller return Result::create($request, [ 'user' => $user, - 'token' => $token + 'token' => $token, ])->createResponse(); } @@ -2614,4 +2918,86 @@ 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 []; + } + + /** + * @param RecordCollection|\record_adapter[] $references + * @return RecordView[] + */ + private function buildRecordViews($references) + { + if (!$references instanceof RecordCollection) { + $references = new RecordCollection($references); + } + + $recordViews = []; + + foreach ($references as $index => $record) { + $recordViews[$index] = new RecordView($record); + } + + return $recordViews; + } + + /** + * @param RecordReferenceInterface[]|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/Databox/Caption/CachedCaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php new file mode 100644 index 0000000000..cdb31aa701 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php @@ -0,0 +1,116 @@ +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 array_combine($recordIds, $data); + } + + $data = $this->decorated->findByRecordIds($recordIds); + + $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([$this, 'computeKey'], array_unique($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/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 @@ +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 new file mode 100644 index 0000000000..e66de9b226 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepository.php @@ -0,0 +1,70 @@ +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 $recordId => $item) { + $this->idMap[(int)$recordId] = $factory($recordId, $item); + } + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php new file mode 100644 index 0000000000..42fa2f4a3e --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php @@ -0,0 +1,96 @@ +repositoryProvider = $repositoryProvider; + } + + public function findByReferenceCollection($references) + { + $references = $this->normalizeReferenceCollection($references); + + $groups = []; + + foreach ($references->getDataboxIds() as $databoxId) { + $recordIds = $references->getDataboxRecordIds($databoxId); + + $groups[$databoxId] = $this->getRepositoryForDatabox($databoxId)->findByRecordIds($recordIds); + } + + return $this->reorderInstances($references, $groups); + } + + /** + * @param RecordReferenceInterface[]|RecordReferenceCollection $references + * @return RecordReferenceCollection + */ + private 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); + } + + /** + * @param RecordReferenceCollection $references + * @param \caption_record[][] $groups + * @return \caption_record[] + */ + private function reorderInstances(RecordReferenceCollection $references, array $groups) + { + $captions = []; + + foreach ($groups as $databoxId => $group) { + $captions[$databoxId] = array_reduce($group, function (array &$carry, \caption_record $caption) { + $carry[$caption->getRecordReference()->getRecordId()] = $caption; + + return $carry; + }, []); + } + + $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/Caption/CaptionServiceProvider.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php new file mode 100644 index 0000000000..3f63153e33 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php @@ -0,0 +1,64 @@ +protect(function ($databoxId) use ($app) { + return function ($recordId, array $data) use ($app, $databoxId) { + $recordReference = RecordReference::createFromDataboxIdAndRecordId($databoxId, $recordId); + + return new \caption_record($app, $recordReference, $data); + }; + }); + + $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) { + 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']; + + return new CaptionRepository( + $dataRepository, + $captionFactoryProvider($databoxId) + ); + }) + ); + }); + + $app['service.caption'] = $app->share(function (Application $app) { + return new CaptionService($app['provider.repo.caption']); + }); + } + + public function boot(Application $app) + { + $app['dispatcher']->addSubscriber(new CaptionCacheInvalider(new LazyLocator($app, 'provider.data_repo.caption'))); + } +} 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/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); + } +} 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/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) : []; } diff --git a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php index 60a3c9d9d8..ae7c75d80b 100644 --- a/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php +++ b/lib/Alchemy/Phrasea/Databox/Subdef/MediaSubdefService.php @@ -30,33 +30,55 @@ 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, - 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) { + if (isset($subdefsByRecordId[$reference->getRecordId()])) { + $carry[$index] = $subdefsByRecordId[$reference->getRecordId()]; + }; + } + + 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 +87,8 @@ class MediaSubdefService return $carry; }, - [] + [], + $names ); if ($groups) { @@ -79,18 +102,22 @@ 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)); + 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/Fractal/ArraySerializer.php b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php new file mode 100644 index 0000000000..264b42803b --- /dev/null +++ b/lib/Alchemy/Phrasea/Fractal/ArraySerializer.php @@ -0,0 +1,54 @@ +callback = $callback; + } + + public function transform() + { + return call_user_func_array($this->callback, func_get_args()); + } +} 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/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/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; + } + +} 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 @@ +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; } - ksort($sets); + $reorder = []; - return $sets; + foreach ($references as $index => $reference) { + $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/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'); } } 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..537a38925f 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 @@ -126,34 +127,16 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess 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); } /** @@ -162,43 +145,43 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess */ public function toRecords(\appbox $appbox) { - $groups = $this->groupPerDataboxId(); + $databoxIds = $this->getDataboxIds(); + $records = array_fill_keys($databoxIds, []); - $records = []; - - foreach ($groups as $databoxId => $recordIds) { + 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; } + /** + * @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 +189,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 +200,74 @@ 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() + { + if (null === $this->groups) { + $this->reorderGroups(); + } + + return $this->groups; + } + + public function reorderGroups() + { + if (null !== $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) + { + // 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) + { + $indexes = []; + + foreach ($this->getDataboxGroup($databoxId) as $index => $references) { + $indexes[$references->getRecordId()] = $index; + } + + return array_flip($indexes); + } + + public function count() + { + return count($this->references); + } } diff --git a/lib/Alchemy/Phrasea/Search/CaptionAware.php b/lib/Alchemy/Phrasea/Search/CaptionAware.php new file mode 100644 index 0000000000..125229245d --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/CaptionAware.php @@ -0,0 +1,35 @@ +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..3a51af0d26 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/CaptionView.php @@ -0,0 +1,61 @@ +caption = $caption; + } + + /** + * @return \caption_record + */ + public function getCaption() + { + 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/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 new file mode 100644 index 0000000000..9fdb3a9847 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/RecordTransformer.php @@ -0,0 +1,138 @@ +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) + { + $fieldData = []; + $values = []; + + foreach ($recordView->getCaption()->getFields() as $field) { + $databox_field = $field->get_databox_field(); + + $fieldData[$field->get_meta_struct_id()] = [ + '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'), + ], + ]; + + $values[] = $field->get_values(); + } + + 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(';'), + ]; + }); + } +} 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 new file mode 100644 index 0000000000..e2c2b2d642 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/SearchResultView.php @@ -0,0 +1,83 @@ +result = $result; + } + + /** + * @return SearchEngineResult + */ + public function getResult() + { + return $this->result; + } + + /** + * @param StoryView[] $stories + * @return void + */ + public function setStories($stories) + { + Assertion::allIsInstanceOf($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..ecd43ff032 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/SubdefTransformer.php @@ -0,0 +1,82 @@ +aclProvider = $aclProvider; + $this->user = $user; + $this->permalinkTransformer = $permalinkTransformer; + } + + public function transform(SubdefView $subdefView) + { + $media = $subdefView->getSubdef(); + + if (!$media->is_physically_present()) { + 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 null; + } + if ($media->get_name() === 'document' + && !$acl->has_right_on_base($record->getBaseId(), 'candwnldhd') + && !$acl->has_hd_grant($record) + ) { + return null; + } + + $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 new file mode 100644 index 0000000000..01064aec89 --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/SubdefView.php @@ -0,0 +1,99 @@ +subdef = $subdef; + } + + /** + * @return \media_subdef + */ + public function getSubdef() + { + 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 new file mode 100644 index 0000000000..6bf89e055f --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/SubdefsAware.php @@ -0,0 +1,56 @@ +subdefs = []; + + foreach ($subdefs as $subdef) { + $this->subdefs[$subdef->getSubdef()->get_name()] = $subdef; + } + } + + /** + * @param string $name + * @return SubdefView + */ + public function getSubdef($name) + { + if (isset($this->subdefs[$name])) { + return $this->subdefs[$name]; + } + + throw new \OutOfBoundsException(sprintf('There are no subdef named "%s"', $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 new file mode 100644 index 0000000000..26c518f2ea --- /dev/null +++ b/lib/Alchemy/Phrasea/Search/V2SearchTransformer.php @@ -0,0 +1,48 @@ + $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(), + ]; + } + + public function includeResults(SearchResultView $searchView) + { + 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(), + ]; + }); + } +} diff --git a/lib/classes/caption/record.php b/lib/classes/caption/record.php index c3199f625a..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; @@ -29,10 +30,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,81 +68,9 @@ class caption_record implements cache_cacheableInterface return $this->fields; } - $databox = $this->getDatabox(); + $data = $this->getDataRepository()->findByRecordIds([$this->getRecordReference()->getRecordId()]); - 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 = $databox->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(array_shift($data)); return $this->fields; } @@ -305,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)); } @@ -315,4 +252,82 @@ 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; + } + + /** + * @return CachedCaptionDataRepository + */ + private function getDataRepository() + { + return $this->app['provider.data_repo.caption'] + ->getRepositoryForDatabox($this->getRecordReference()->getDataboxId()); + } } diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Api/ApiJsonTest.php index 6945e4c151..fd03f65094 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']; @@ -837,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']; @@ -873,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']; 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']); } /**