Merge pull request #1831 from bburnichon/improvement/search

Search API
This commit is contained in:
Thibaud Fabre
2016-05-23 14:17:25 +02:00
53 changed files with 3207 additions and 339 deletions

View File

@@ -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",

99
composer.lock generated
View File

@@ -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

View File

@@ -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());

View File

@@ -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;
}
/**

View File

@@ -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<int, bool>
*/
private function retrieveSeeBusinessPerDatabox($references)
{
if (!$references instanceof DataboxGroupable) {
$references = new RecordReferenceCollection($references);
}
$acl = $this->getAclForUser();
$canSeeBusiness = [];
foreach ($references->getDataboxIds() as $databoxId) {
$canSeeBusiness[$databoxId] = $acl->can_see_business_fields($this->findDataboxById($databoxId));
}
$rights = [];
foreach ($references as $index => $reference) {
$rights[$index] = $canSeeBusiness[$reference->getDataboxId()];
}
return $rights;
}
/**
* @param RecordView[] $recordViews
* @param \caption_record[] $captions
* @param bool[] $canSeeBusiness
*/
private function buildCaptionViews($recordViews, $captions, $canSeeBusiness)
{
foreach ($recordViews as $index => $recordView) {
$caption = $captions[$index];
$captionView = new CaptionView($caption);
$captionView->setFields($caption->get_fields(null, isset($canSeeBusiness[$index]) && (bool)$canSeeBusiness[$index]));
$recordView->setCaption($captionView);
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
use Alchemy\Phrasea\Cache\MultiAdapter;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\MultiGetCache;
use Doctrine\Common\Cache\MultiPutCache;
class CachedCaptionDataRepository implements CaptionDataRepository
{
/**
* @var CaptionDataRepository
*/
private $decorated;
/**
* @var Cache|MultiGetCache|MultiPutCache
*/
private $cache;
/**
* @var string
*/
private $baseKey;
/**
* @var int
*/
private $lifeTime = 0;
/**
* CachedCaptionDataRepository constructor.
* @param CaptionDataRepository $decorated
* @param Cache $cache
* @param string $baseKey
*/
public function __construct(CaptionDataRepository $decorated, Cache $cache, $baseKey)
{
$this->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);
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
use Alchemy\Phrasea\Core\Event\Record\MetadataChangedEvent;
use Alchemy\Phrasea\Core\Event\Record\RecordEvents;
use Alchemy\Phrasea\Databox\DataboxBoundRepositoryProvider;
use Assert\Assertion;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CaptionCacheInvalider implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
RecordEvents::METADATA_CHANGED => '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;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
interface CaptionDataRepository
{
/**
* @param int[] $recordIds
* @return array[]
*/
public function findByRecordIds(array $recordIds);
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
use Alchemy\Phrasea\Databox\DataboxBoundRepositoryFactory;
use Alchemy\Phrasea\Databox\DataboxConnectionProvider;
use Doctrine\Common\Cache\Cache;
class CaptionDataRepositoryFactory implements DataboxBoundRepositoryFactory
{
/**
* @var DataboxConnectionProvider
*/
private $connectionProvider;
/**
* @var Cache
*/
private $cache;
public function __construct(DataboxConnectionProvider $connectionProvider, Cache $cache)
{
$this->connectionProvider = $connectionProvider;
$this->cache = $cache;
}
public function createRepositoryFor($databoxId)
{
return new CachedCaptionDataRepository(
new DbalCaptionDataRepository($this->connectionProvider->getConnection($databoxId)),
$this->cache,
sprintf('databox[%d]:', $databoxId)
);
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
class CaptionRepository
{
/**
* @var \caption_record[]
*/
private $idMap = [];
/**
* @var CaptionDataRepository
*/
private $dataRepository;
/**
* @var callable
*/
private $captionFactory;
public function __construct(CaptionDataRepository $dataRepository, callable $captionFactory)
{
$this->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);
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
use Alchemy\Phrasea\Databox\DataboxBoundRepositoryProvider;
use Alchemy\Phrasea\Model\RecordReferenceInterface;
use Alchemy\Phrasea\Record\RecordReferenceCollection;
class CaptionService
{
/**
* @var DataboxBoundRepositoryProvider
*/
private $repositoryProvider;
public function __construct(DataboxBoundRepositoryProvider $repositoryProvider)
{
$this->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;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
use Alchemy\Phrasea\Controller\LazyLocator;
use Alchemy\Phrasea\Databox\ClosureDataboxBoundRepositoryFactory;
use Alchemy\Phrasea\Databox\DataboxBoundRepositoryProvider;
use Alchemy\Phrasea\Databox\DataboxConnectionProvider;
use Alchemy\Phrasea\Record\RecordReference;
use Silex\Application;
use Silex\ServiceProviderInterface;
class CaptionServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['provider.factory.caption'] = $app->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')));
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox\Caption;
use Doctrine\DBAL\Connection;
class DbalCaptionDataRepository implements CaptionDataRepository
{
/**
* @var Connection
*/
private $connection;
public function __construct(Connection $connection)
{
$this->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;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox;
class ClosureDataboxBoundRepositoryFactory implements DataboxBoundRepositoryFactory
{
/**
* @var callable
*/
private $factory;
public function __construct(callable $factory)
{
$this->factory = $factory;
}
public function createRepositoryFor($databoxId)
{
$factory = $this->factory;
return $factory($databoxId);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Databox;
interface DataboxGroupable
{
/**
* Group instance by Databox Id
*
* @return array<int,array>
*/
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();
}

View File

@@ -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) : [];
}

View File

@@ -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;

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use League\Fractal\Pagination\CursorInterface;
use League\Fractal\Pagination\PaginatorInterface;
use League\Fractal\Resource\ResourceInterface;
use League\Fractal\Serializer\SerializerAbstract;
class ArraySerializer extends SerializerAbstract
{
public function collection($resourceKey, array $data)
{
return array_values(array_filter($data));
}
public function item($resourceKey, array $data)
{
return $data;
}
public function null($resourceKey)
{
return null;
}
public function includedData(ResourceInterface $resource, array $data)
{
return $data;
}
public function meta(array $meta)
{
return [];
}
public function paginator(PaginatorInterface $paginator)
{
return [];
}
public function cursor(CursorInterface $cursor)
{
return [];
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use League\Fractal\TransformerAbstract;
class CallbackTransformer extends TransformerAbstract
{
/**
* @var callable
*/
private $callback;
public function __construct(callable $callback = null)
{
if (null === $callback) {
$callback = function () {
return [];
};
}
$this->callback = $callback;
}
public function transform()
{
return call_user_func_array($this->callback, func_get_args());
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use Symfony\Component\EventDispatcher\Event;
class GetSerializationEvent extends Event
{
private $resourceKey;
private $data;
private $serialization;
public function __construct($resourceKey, $data)
{
$this->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;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use League\Fractal\Manager;
class IncludeResolver
{
/**
* @var TransformerResolver
*/
private $transformerResolver;
public function __construct(TransformerResolver $transformerResolver)
{
$this->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));
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use League\Fractal\Resource\ResourceAbstract;
use League\Fractal\TransformerAbstract;
class NullResource extends ResourceAbstract
{
/**
* @param callable|TransformerAbstract $transformer
* @param null|string $resourceKey
*/
public function __construct($transformer, $resourceKey = null)
{
parent::__construct(null, $transformer, $resourceKey);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use League\Fractal\Resource\ResourceInterface;
use League\Fractal\Scope;
use League\Fractal\TransformerAbstract;
class ResourceTransformerAccessibleScope extends Scope
{
/**
* @return TransformerAbstract
*/
public function getResourceTransformer()
{
$transformer = $this->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;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use League\Fractal\TransformerAbstract;
class SearchResultTransformerResolver implements TransformerResolver
{
/**
* @var \ArrayAccess|\League\Fractal\TransformerAbstract[]
*/
private $transformers;
/**
* @param TransformerAbstract[]|\ArrayAccess $transformers
*/
public function __construct($transformers)
{
$this->transformers = $transformers;
}
public function resolve($scopeIdentifier)
{
if (!isset($this->transformers[$scopeIdentifier])) {
throw new \RuntimeException(sprintf('Unknown scope identifier: %s', $scopeIdentifier));
}
return $this->transformers[$scopeIdentifier];
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class TraceableArraySerializer extends ArraySerializer
{
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->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;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Fractal;
use League\Fractal\TransformerAbstract;
interface TransformerResolver
{
/**
* @param string $scopeIdentifier full scope name, empty string for root
* @return TransformerAbstract
*/
public function resolve($scopeIdentifier);
}

View File

@@ -10,6 +10,8 @@
namespace Alchemy\Phrasea\Media;
use Alchemy\Phrasea\Databox\DataboxGroupable;
use Alchemy\Phrasea\Record\PerDataboxRecordId;
use Alchemy\Phrasea\Record\RecordReference;
use Alchemy\Phrasea\Record\RecordReferenceCollection;
@@ -26,27 +28,40 @@ class TechnicalDataService
}
/**
* @param RecordReference[] $references
* @param DataboxGroupable|PerDataboxRecordId|RecordReference[] $references
* @return RecordTechnicalDataSet[]
*/
public function fetchRecordsTechnicalData($references)
{
if (!$references instanceof RecordReferenceCollection) {
if (!($references instanceof DataboxGroupable && $references instanceof PerDataboxRecordId)) {
$references = new RecordReferenceCollection($references);
}
$sets = [];
foreach ($references->groupPerDataboxId() as $databoxId => $indexes) {
foreach ($this->provider->getRepositoryFor($databoxId)->findByRecordIds(array_keys($indexes)) as $set) {
$index = $indexes[$set->getRecordId()];
foreach ($references->getDataboxIds() as $databoxId) {
$recordIds = $references->getDataboxRecordIds($databoxId);
$sets[$index] = $set;
$setPerRecordId = [];
foreach ($this->provider->getRepositoryFor($databoxId)->findByRecordIds($recordIds) as $set) {
$setPerRecordId[$set->getRecordId()] = $set;
}
$sets[$databoxId] = $setPerRecordId;
}
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;
}
}

View File

@@ -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');
}
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Record;
interface PerDataboxRecordId
{
/**
* @param int $databoxId
* @return int[]
*/
public function getDataboxRecordIds($databoxId);
}

View File

@@ -0,0 +1,218 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Record;
use Alchemy\Phrasea\Databox\DataboxGroupable;
use Assert\Assertion;
class RecordCollection implements \IteratorAggregate, \ArrayAccess, \Countable, DataboxGroupable, PerDataboxRecordId
{
/**
* @var \record_adapter[]
*/
private $records = [];
/**
* @var array<int, int|string>
*/
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;
}
}

View File

@@ -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<int|string,array> $records
@@ -126,34 +127,16 @@ class RecordReferenceCollection implements \IteratorAggregate, \ArrayAccess
return new \ArrayIterator($this->references);
}
/**
* @return array<int,array<int,int>>
*/
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);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
trait CaptionAware
{
/**
* @var CaptionView
*/
private $caption;
/**
* @param CaptionView $caption
*/
public function setCaption(CaptionView $caption)
{
$this->caption = $caption;
}
/**
* @return CaptionView
*/
public function getCaption()
{
return $this->caption;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Assert\Assertion;
class CaptionView
{
/**
* @var \caption_record
*/
private $caption;
/**
* @var \caption_field[]
*/
private $fields = [];
public function __construct(\caption_record $caption)
{
$this->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;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use League\Fractal\TransformerAbstract;
class PermalinkTransformer extends TransformerAbstract
{
public function transform(PermalinkView $permalinkView)
{
$permalink = $permalinkView->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(),
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
class PermalinkView
{
/**
* @var \media_Permalink_Adapter
*/
private $permalink;
public function __construct(\media_Permalink_Adapter $permalink)
{
$this->permalink = $permalink;
}
/**
* @return \media_Permalink_Adapter
*/
public function getPermalink()
{
return $this->permalink;
}
}

View File

@@ -0,0 +1,138 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use League\Fractal\TransformerAbstract;
class RecordTransformer extends TransformerAbstract
{
protected $availableIncludes = ['thumbnail', 'technical_informations', 'subdefs', 'metadata', 'status', 'caption'];
protected $defaultIncludes = ['thumbnail', 'technical_informations'];
/**
* @var SubdefTransformer
*/
private $subdefTransformer;
/**
* @var TechnicalDataTransformer
*/
private $technicalDataTransformer;
public function __construct(SubdefTransformer $subdefTransformer, TechnicalDataTransformer $technicalDataTransformer)
{
$this->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(';'),
];
});
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
class RecordView
{
use SubdefsAware;
use CaptionAware;
/**
* @var \record_adapter
*/
private $record;
/**
* @var TechnicalDataView
*/
private $technicalDataView;
public function __construct(\record_adapter $record)
{
$this->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;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\SearchEngine\SearchEngineResult;
use Assert\Assertion;
class SearchResultView
{
/**
* @var SearchEngineResult
*/
private $result;
/**
* @var StoryView[]
*/
private $stories = [];
/**
* @var RecordView[]
*/
private $records = [];
public function __construct(SearchEngineResult $result)
{
$this->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;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\Utilities\NullableDateTime;
use League\Fractal\TransformerAbstract;
class StoryTransformer extends TransformerAbstract
{
protected $availableIncludes = ['thumbnail', 'metadatas', 'records'];
protected $defaultIncludes = ['thumbnail', 'metadatas', 'records'];
/**
* @var SubdefTransformer
*/
private $subdefTransformer;
/**
* @var RecordTransformer
*/
private $recordTransformer;
/**
* @param SubdefTransformer $subdefTransformer
* @param RecordTransformer $recordTransformer
*/
public function __construct(SubdefTransformer $subdefTransformer, RecordTransformer $recordTransformer)
{
$this->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),
];
};
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Assert\Assertion;
class StoryView
{
use SubdefsAware;
use CaptionAware;
/**
* @var \record_adapter
*/
private $story;
/**
* @var RecordView[]
*/
private $children = [];
/**
* @param \record_adapter $story
*/
public function __construct(\record_adapter $story)
{
$this->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;
}
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\Authentication\ACLProvider;
use Alchemy\Phrasea\Model\Entities\User;
use League\Fractal\TransformerAbstract;
class SubdefTransformer extends TransformerAbstract
{
/**
* @var ACLProvider
*/
private $aclProvider;
/**
* @var User
*/
private $user;
/**
* @var PermalinkTransformer
*/
private $permalinkTransformer;
public function __construct(ACLProvider $aclProvider, User $user, PermalinkTransformer $permalinkTransformer)
{
$this->aclProvider = $aclProvider;
$this->user = $user;
$this->permalinkTransformer = $permalinkTransformer;
}
public function transform(SubdefView $subdefView)
{
$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(),
];
}
}

View File

@@ -0,0 +1,99 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Assert\Assertion;
class SubdefView
{
/**
* @var \media_subdef
*/
private $subdef;
/**
* @var PermalinkView
*/
private $permalinkView;
/**
* @var string
*/
private $url;
/**
* @var int
*/
private $urlTTL;
public function __construct(\media_subdef $subdef)
{
$this->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;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Assert\Assertion;
trait SubdefsAware
{
/**
* @var SubdefView[]
*/
private $subdefs = [];
/**
* @param SubdefView[] $subdefs
*/
public function setSubdefs($subdefs)
{
Assertion::allIsInstanceOf($subdefs, SubdefView::class);
$this->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;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\Media\TechnicalData;
use League\Fractal\TransformerAbstract;
class TechnicalDataTransformer extends TransformerAbstract
{
public function transform(TechnicalData $technicalData)
{
return [
'name' => $technicalData->getName(),
'value' => $technicalData->getValue(),
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\Media\TechnicalDataSet;
class TechnicalDataView
{
/**
* @var TechnicalDataSet
*/
private $dataSet;
public function __construct(TechnicalDataSet $dataSet)
{
$this->dataSet = $dataSet;
}
/**
* @return TechnicalDataSet
*/
public function getDataSet()
{
return $this->dataSet;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use League\Fractal\TransformerAbstract;
class V1SearchCompositeResultTransformer extends TransformerAbstract
{
protected $availableIncludes = ['stories', 'records'];
protected $defaultIncludes = ['stories', 'records'];
/**
* @var RecordTransformer
*/
private $recordTransformer;
/**
* @var StoryTransformer
*/
private $storyTransformer;
public function __construct(RecordTransformer $recordTransformer, StoryTransformer $storyTransformer)
{
$this->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);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
class V1SearchRecordsResultTransformer extends V1SearchTransformer
{
/**
* @var RecordTransformer
*/
private $recordTransformer;
public function __construct(RecordTransformer $recordTransformer)
{
$this->recordTransformer = $recordTransformer;
}
public function includeResults(SearchResultView $resultView)
{
return $this->collection($resultView->getRecords(), $this->recordTransformer);
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use League\Fractal\TransformerAbstract;
class V1SearchResultTransformer extends V1SearchTransformer
{
/**
* @var TransformerAbstract
*/
private $transformer;
public function __construct(TransformerAbstract $transformer)
{
$this->transformer = $transformer;
}
public function includeResults(SearchResultView $resultView)
{
return $this->item($resultView, $this->transformer);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\SearchEngine\SearchEngineSuggestion;
use League\Fractal\TransformerAbstract;
abstract class V1SearchTransformer extends TransformerAbstract
{
protected $availableIncludes = ['results'];
protected $defaultIncludes = ['results'];
public function transform(SearchResultView $resultView)
{
$result = $resultView->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);
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Search;
use Alchemy\Phrasea\Model\RecordInterface;
use League\Fractal\TransformerAbstract;
class V2SearchTransformer extends TransformerAbstract
{
protected $availableIncludes = ['results'];
protected $defaultIncludes = ['results'];
public function transform(SearchResultView $searchView)
{
return [
'offset_start' => $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(),
];
});
}
}

View File

@@ -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());
}
}

View File

@@ -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'];

View File

@@ -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']);
}
/**