diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 761143d6d1..b27d641ec5 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -73,6 +73,7 @@ use Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider; use Alchemy\Phrasea\Core\Provider\WebhookServiceProvider; use Alchemy\Phrasea\Core\Provider\ZippyServiceProvider; use Alchemy\Phrasea\Core\Provider\WebProfilerServiceProvider as PhraseaWebProfilerServiceProvider; +use Alchemy\Phrasea\Databox\Caption\CaptionServiceProvider; use Alchemy\Phrasea\Databox\Subdef\MediaSubdefServiceProvider; use Alchemy\Phrasea\Exception\InvalidArgumentException; use Alchemy\Phrasea\Filesystem\FilesystemServiceProvider; @@ -194,6 +195,7 @@ class Application extends SilexApplication $this->register(new ManipulatorServiceProvider()); $this->register(new TechnicalDataServiceProvider()); $this->register(new MediaSubdefServiceProvider()); + $this->register(new CaptionServiceProvider()); $this->register(new InstallerServiceProvider()); $this->register(new PhraseaVersionServiceProvider()); diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index fd703b6ed0..1eb0333724 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -1159,6 +1159,7 @@ class V1Controller extends Controller ); $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); + if (array_intersect($includes, ['results.metadata', 'results.caption'])) { } @@ -1280,8 +1281,8 @@ class V1Controller extends Controller return [ 'results.subdefs', - //'results.metadata', - //'results.caption', + 'results.metadata', + 'results.caption', 'results.status', ]; } diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php new file mode 100644 index 0000000000..44d214a575 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CachedCaptionDataRepository.php @@ -0,0 +1,100 @@ +decorated = $decorated; + $this->cache = $cache instanceof MultiGetCache && $cache instanceof MultiPutCache + ? $cache + : new MultiAdapter($cache); + $this->baseKey = $baseKey; + } + + /** + * @return int + */ + public function getLifeTime() + { + return $this->lifeTime; + } + + /** + * @param int $lifeTime + */ + public function setLifeTime($lifeTime) + { + $this->lifeTime = (int)$lifeTime; + } + + /** + * @param array $recordIds + * @return \array[] + */ + public function findByRecordIds(array $recordIds) + { + $keys = $this->computeKeys($recordIds); + + $data = $this->cache->fetchMultiple($keys); + + if (count($data) === count($keys)) { + return $data; + } + + $data = $this->decorated->findByRecordIds($recordIds); + + $this->cache->saveMultiple(array_combine(array_values($keys), array_values($data))); + + return $data; + } + + /** + * @param int[] $recordIds + * @return string[] + */ + private function computeKeys(array $recordIds) + { + return array_map(function ($recordId) { + return $this->baseKey . 'caption' . json_encode([(int)$recordId]); + }, $recordIds); + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepository.php new file mode 100644 index 0000000000..826df7b0b2 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionDataRepository.php @@ -0,0 +1,20 @@ +dataRepository = $dataRepository; + $this->captionFactory = $captionFactory; + } + + public function findByRecordIds(array $recordIds) + { + $this->fetchMissing($recordIds); + + $instances = []; + + foreach ($recordIds as $index => $recordId) { + $instances[$index] = $this->idMap[$recordId]; + } + + return $instances; + } + + public function clear() + { + $this->idMap = []; + } + + private function fetchMissing(array $recordIds) + { + $missing = array_diff($recordIds, array_keys($this->idMap)); + + if (!$missing) { + return; + } + + $data = $this->dataRepository->findByRecordIds($missing); + + $factory = $this->captionFactory; + + foreach ($data as $item) { + Assertion::keyIsset($item, 'record_id'); + + $this->idMap[(int)$item['record_id']] = $factory($item); + } + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php new file mode 100644 index 0000000000..d796224bca --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionRepositoryFactory.php @@ -0,0 +1,60 @@ +connectionProvider = $connectionProvider; + $this->cache = $cache; + $this->captionFactoryProvider = $captionFactoryProvider; + } + + public function createRepositoryFor($databoxId) + { + $connection = $this->connectionProvider->getConnection($databoxId); + + $dbalRepository = new DbalCaptionDataRepository($connection); + $dataRepository = new CachedCaptionDataRepository($dbalRepository, $this->cache, sprintf('databox%d:', $databoxId)); + + $provider = $this->captionFactoryProvider; + $factory = $provider($databoxId); + + if (!is_callable($factory)) { + throw new \UnexpectedValueException(sprintf( + 'Caption factory is expected to be callable, got %s', + is_object($factory) ? get_class($factory) : gettype($factory) + )); + } + + return new CaptionRepository($dataRepository, $factory); + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php new file mode 100644 index 0000000000..e27cad063d --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionService.php @@ -0,0 +1,67 @@ +repositoryProvider = $repositoryProvider; + } + + public function findByReferenceCollection($references) + { + $references = $this->normalizeReferenceCollection($references); + + $groups = []; + + foreach ($references->groupPerDataboxId() as $databoxId => $indexes) { + $this->getRepositoryForDatabox($databoxId)->findByRecordIds(array_keys($indexes)); + } + + if ($groups) { + return call_user_func_array('array_merge', $groups); + } + + return []; + } + + /** + * @param RecordReferenceInterface[]|RecordReferenceCollection $references + * @return RecordReferenceCollection + */ + public function normalizeReferenceCollection($references) + { + if ($references instanceof RecordReferenceCollection) { + return $references; + } + + return new RecordReferenceCollection($references); + } + + /** + * @param int $databoxId + * @return CaptionRepository + */ + private function getRepositoryForDatabox($databoxId) + { + return $this->repositoryProvider->getRepositoryForDatabox($databoxId); + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php new file mode 100644 index 0000000000..1163317680 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/CaptionServiceProvider.php @@ -0,0 +1,49 @@ +protect(function ($databoxId) use ($app) { + return function (array $data) use ($app, $databoxId) { + $recordReference = RecordReference::createFromDataboxIdAndRecordId($databoxId, $data['record_id']); + + return new \caption_record($app, $recordReference, $data); + }; + }); + + $app['provider.repo.caption'] = $app->share(function (Application $app) { + $connectionProvider = new DataboxConnectionProvider($app['phraseanet.appbox']); + $factoryProvider = $app['provider.factory.caption']; + + $repositoryFactory = new CaptionRepositoryFactory($connectionProvider, $app['cache'], $factoryProvider); + + return new DataboxBoundRepositoryProvider($repositoryFactory); + }); + + $app['service.caption'] = $app->share(function (Application $app) { + return new CaptionService($app['provider.repo.caption']); + }); + } + + public function boot(Application $app) + { + // no-op + } +} diff --git a/lib/Alchemy/Phrasea/Databox/Caption/DbalCaptionDataRepository.php b/lib/Alchemy/Phrasea/Databox/Caption/DbalCaptionDataRepository.php new file mode 100644 index 0000000000..a8ab78bc93 --- /dev/null +++ b/lib/Alchemy/Phrasea/Databox/Caption/DbalCaptionDataRepository.php @@ -0,0 +1,64 @@ +connection = $connection; + } + + public function findByRecordIds(array $recordIds) + { + if (!$recordIds) { + return []; + } + + $sql = <<<'SQL' +SELECT m.record_id, m.id AS meta_id, s.id AS structure_id, value, VocabularyType, VocabularyId +FROM metadatas m INNER JOIN metadatas_structure s ON s.id = m.meta_struct_id +WHERE m.record_id IN (:recordIds) +ORDER BY m.record_id ASC, s.sorter ASC +SQL; + + $data = $this->connection->fetchAll( + $sql, ['recordIds' => $recordIds], ['recordIds' => Connection::PARAM_INT_ARRAY] + ); + + return $this->mapByRecordId($data, $recordIds); + } + + /** + * @param array $data + * @param int[] $recordIds + * @return array[] + */ + private function mapByRecordId(array $data, array $recordIds) + { + $groups = array_fill_keys($recordIds, []); + + foreach ($data as $item) { + $recordId = $item['record_id']; + + $groups[$recordId][] = $item; + } + + return $groups; + } +} diff --git a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php index 984928167d..0d443d0228 100644 --- a/lib/Alchemy/Phrasea/Media/TechnicalDataService.php +++ b/lib/Alchemy/Phrasea/Media/TechnicalDataService.php @@ -31,7 +31,7 @@ class TechnicalDataService */ public function fetchRecordsTechnicalData($references) { - if (!$references instanceof RecordReferenceCollection) { + if (!$references instanceof RecordReferenceCollection) { $references = new RecordReferenceCollection($references); } diff --git a/lib/classes/caption/record.php b/lib/classes/caption/record.php index c3199f625a..2b90d0a3d3 100644 --- a/lib/classes/caption/record.php +++ b/lib/classes/caption/record.php @@ -29,10 +29,16 @@ class caption_record implements cache_cacheableInterface */ protected $app; - public function __construct(Application $app, RecordReferenceInterface $record) + /** + * @param Application $app + * @param RecordReferenceInterface $record + * @param array[]|null $fieldsData + */ + public function __construct(Application $app, RecordReferenceInterface $record, array $fieldsData = null) { $this->app = $app; $this->record = $record; + $this->fields = null === $fieldsData ? null : $this->mapFieldsFromData($fieldsData); } public function toArray($includeBusinessFields) @@ -61,8 +67,6 @@ class caption_record implements cache_cacheableInterface return $this->fields; } - $databox = $this->getDatabox(); - try { $fields = $this->get_data_from_cache(); } catch (\Exception $e) { @@ -72,70 +76,14 @@ FROM metadatas m INNER JOIN metadatas_structure s ON s.id = m.meta_struct_id WHERE m.record_id = :record_id ORDER BY s.sorter ASC SQL; - $fields = $databox->get_connection() + $fields = $this->getDatabox()->get_connection() ->executeQuery($sql, [':record_id' => $this->record->getRecordId()]) ->fetchAll(PDO::FETCH_ASSOC); $this->set_data_to_cache($fields); } - $rec_fields = array(); - - if ($fields) { - $databox_descriptionStructure = $databox->get_meta_structure(); - $record = $databox->get_record($this->record->getRecordId()); - - // first group values by field - $caption_fields = []; - foreach ($fields as $row) { - $structure_id = $row['structure_id']; - if(!array_key_exists($structure_id, $caption_fields)) { - $caption_fields[$structure_id] = [ - 'db_field' => $databox_descriptionStructure->get_element($structure_id), - 'values' => [] - ]; - } - - if (count($caption_fields[$structure_id]['values']) > 0 && !$caption_fields[$structure_id]['db_field']->is_multi()) { - // Inconsistent, should not happen - continue; - } - - // build an EMPTY caption_Field_Value - $cfv = new caption_Field_Value( - $this->app, - $caption_fields[$structure_id]['db_field'], - $record, - $row['meta_id'], - caption_Field_Value::DONT_RETRIEVE_VALUES // ask caption_Field_Value "no n+1 sql" - ); - - // inject the value we already know - $cfv->injectValues($row['value'], $row['VocabularyType'], $row['VocabularyId']); - - // add the value to the field - $caption_fields[$structure_id]['values'][] = $cfv; - } - - // now build a "caption_field" with already known "caption_Field_Value"s - foreach($caption_fields as $structure_id => $caption_field) { - - // build an EMPTY caption_field - $cf = new caption_field( - $this->app, - $caption_field['db_field'], - $record, - caption_field::DONT_RETRIEVE_VALUES // ask caption_field "no n+1 sql" - ); - - // inject the value we already know - $cf->injectValues($caption_field['values']); - - // add the field to the fields - $rec_fields[$structure_id] = $cf; - } - } - $this->fields = $rec_fields; + $this->fields = $this->mapFieldsFromData($fields); return $this->fields; } @@ -315,4 +263,73 @@ SQL; { return $this->app->findDataboxById($this->record->getDataboxId()); } + + /** + * @param array $data + * @return caption_field[] + */ + protected function mapFieldsFromData($data) + { + if (!$data) { + return []; + } + + $rec_fields = array(); + + $databox = $this->getDatabox(); + $databox_descriptionStructure = $databox->get_meta_structure(); + $record = $databox->get_record($this->record->getRecordId()); + + // first group values by field + $caption_fields = []; + foreach ($data as $row) { + $structure_id = $row['structure_id']; + if (!array_key_exists($structure_id, $caption_fields)) { + $caption_fields[$structure_id] = [ + 'db_field' => $databox_descriptionStructure->get_element($structure_id), + 'values' => [] + ]; + } + + if (count($caption_fields[$structure_id]['values']) > 0 && !$caption_fields[$structure_id]['db_field']->is_multi()) { + // Inconsistent, should not happen + continue; + } + + // build an EMPTY caption_Field_Value + $cfv = new caption_Field_Value( + $this->app, + $caption_fields[$structure_id]['db_field'], + $record, + $row['meta_id'], + caption_Field_Value::DONT_RETRIEVE_VALUES // ask caption_Field_Value "no n+1 sql" + ); + + // inject the value we already know + $cfv->injectValues($row['value'], $row['VocabularyType'], $row['VocabularyId']); + + // add the value to the field + $caption_fields[$structure_id]['values'][] = $cfv; + } + + // now build a "caption_field" with already known "caption_Field_Value"s + foreach ($caption_fields as $structure_id => $caption_field) { + + // build an EMPTY caption_field + $cf = new caption_field( + $this->app, + $caption_field['db_field'], + $record, + caption_field::DONT_RETRIEVE_VALUES // ask caption_field "no n+1 sql" + ); + + // inject the value we already know + $cf->injectValues($caption_field['values']); + + // add the field to the fields + $rec_fields[$structure_id] = $cf; + } + + return $rec_fields; + } }