Add Caption Service and Repositories

This commit is contained in:
Benoît Burnichon
2016-05-09 18:52:08 +02:00
parent da63360865
commit 7aa499716b
11 changed files with 518 additions and 64 deletions

View File

@@ -73,6 +73,7 @@ use Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider;
use Alchemy\Phrasea\Core\Provider\WebhookServiceProvider; use Alchemy\Phrasea\Core\Provider\WebhookServiceProvider;
use Alchemy\Phrasea\Core\Provider\ZippyServiceProvider; use Alchemy\Phrasea\Core\Provider\ZippyServiceProvider;
use Alchemy\Phrasea\Core\Provider\WebProfilerServiceProvider as PhraseaWebProfilerServiceProvider; use Alchemy\Phrasea\Core\Provider\WebProfilerServiceProvider as PhraseaWebProfilerServiceProvider;
use Alchemy\Phrasea\Databox\Caption\CaptionServiceProvider;
use Alchemy\Phrasea\Databox\Subdef\MediaSubdefServiceProvider; use Alchemy\Phrasea\Databox\Subdef\MediaSubdefServiceProvider;
use Alchemy\Phrasea\Exception\InvalidArgumentException; use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Alchemy\Phrasea\Filesystem\FilesystemServiceProvider; use Alchemy\Phrasea\Filesystem\FilesystemServiceProvider;
@@ -194,6 +195,7 @@ class Application extends SilexApplication
$this->register(new ManipulatorServiceProvider()); $this->register(new ManipulatorServiceProvider());
$this->register(new TechnicalDataServiceProvider()); $this->register(new TechnicalDataServiceProvider());
$this->register(new MediaSubdefServiceProvider()); $this->register(new MediaSubdefServiceProvider());
$this->register(new CaptionServiceProvider());
$this->register(new InstallerServiceProvider()); $this->register(new InstallerServiceProvider());
$this->register(new PhraseaVersionServiceProvider()); $this->register(new PhraseaVersionServiceProvider());

View File

@@ -1159,6 +1159,7 @@ class V1Controller extends Controller
); );
$technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references); $technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($references);
if (array_intersect($includes, ['results.metadata', 'results.caption'])) { if (array_intersect($includes, ['results.metadata', 'results.caption'])) {
} }
@@ -1280,8 +1281,8 @@ class V1Controller extends Controller
return [ return [
'results.subdefs', 'results.subdefs',
//'results.metadata', 'results.metadata',
//'results.caption', 'results.caption',
'results.status', 'results.status',
]; ];
} }

View File

@@ -0,0 +1,100 @@
<?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 $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);
}
}

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,74 @@
<?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 Assert\Assertion;
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 $item) {
Assertion::keyIsset($item, 'record_id');
$this->idMap[(int)$item['record_id']] = $factory($item);
}
}
}

View File

@@ -0,0 +1,60 @@
<?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 CaptionRepositoryFactory implements DataboxBoundRepositoryFactory
{
/**
* @var DataboxConnectionProvider
*/
private $connectionProvider;
/**
* @var Cache
*/
private $cache;
/**
* @var callable
*/
private $captionFactoryProvider;
public function __construct(DataboxConnectionProvider $connectionProvider, Cache $cache, callable $captionFactoryProvider)
{
$this->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);
}
}

View File

@@ -0,0 +1,67 @@
<?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->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);
}
}

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\Databox\Caption;
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 (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
}
}

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

@@ -29,10 +29,16 @@ class caption_record implements cache_cacheableInterface
*/ */
protected $app; 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->app = $app;
$this->record = $record; $this->record = $record;
$this->fields = null === $fieldsData ? null : $this->mapFieldsFromData($fieldsData);
} }
public function toArray($includeBusinessFields) public function toArray($includeBusinessFields)
@@ -61,8 +67,6 @@ class caption_record implements cache_cacheableInterface
return $this->fields; return $this->fields;
} }
$databox = $this->getDatabox();
try { try {
$fields = $this->get_data_from_cache(); $fields = $this->get_data_from_cache();
} catch (\Exception $e) { } 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 WHERE m.record_id = :record_id
ORDER BY s.sorter ASC ORDER BY s.sorter ASC
SQL; SQL;
$fields = $databox->get_connection() $fields = $this->getDatabox()->get_connection()
->executeQuery($sql, [':record_id' => $this->record->getRecordId()]) ->executeQuery($sql, [':record_id' => $this->record->getRecordId()])
->fetchAll(PDO::FETCH_ASSOC); ->fetchAll(PDO::FETCH_ASSOC);
$this->set_data_to_cache($fields); $this->set_data_to_cache($fields);
} }
$rec_fields = array(); $this->fields = $this->mapFieldsFromData($fields);
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;
return $this->fields; return $this->fields;
} }
@@ -315,4 +263,73 @@ SQL;
{ {
return $this->app->findDataboxById($this->record->getDataboxId()); 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;
}
} }