Merge pull request #413 from damienalexandre/serializer-integration

Serializer support in providers and mapping configuration fixes
This commit is contained in:
Tim Nagel 2013-12-09 12:35:26 -08:00
commit fc64078575
11 changed files with 453 additions and 19 deletions

View file

@ -433,6 +433,10 @@ class Configuration implements ConfigurationInterface
}
foreach ($index['types'] as $type) {
if (empty($type['mappings'])) {
continue;
}
$nestings = array_merge_recursive($nestings, $this->getNestingsForType($type['mappings'], $nestings));
}
}

View file

@ -16,6 +16,7 @@ class FOSElasticaExtension extends Extension
protected $indexConfigs = array();
protected $typeFields = array();
protected $loadedDrivers = array();
protected $serializerConfig = array();
public function load(array $configs, ContainerBuilder $container)
{
@ -40,8 +41,8 @@ class FOSElasticaExtension extends Extension
}
$clientIdsByName = $this->loadClients($config['clients'], $container);
$serializerConfig = isset($config['serializer']) ? $config['serializer'] : null;
$indexIdsByName = $this->loadIndexes($config['indexes'], $container, $clientIdsByName, $config['default_client'], $serializerConfig);
$this->serializerConfig = isset($config['serializer']) ? $config['serializer'] : null;
$indexIdsByName = $this->loadIndexes($config['indexes'], $container, $clientIdsByName, $config['default_client']);
$indexRefsByName = array_map(function($id) {
return new Reference($id);
}, $indexIdsByName);
@ -94,7 +95,7 @@ class FOSElasticaExtension extends Extension
* @throws \InvalidArgumentException
* @return array
*/
protected function loadIndexes(array $indexes, ContainerBuilder $container, array $clientIdsByName, $defaultClientName, $serializerConfig)
protected function loadIndexes(array $indexes, ContainerBuilder $container, array $clientIdsByName, $defaultClientName)
{
$indexIds = array();
foreach ($indexes as $name => $index) {
@ -129,7 +130,7 @@ class FOSElasticaExtension extends Extension
if (!empty($index['settings'])) {
$this->indexConfigs[$name]['config']['settings'] = $index['settings'];
}
$this->loadTypes(isset($index['types']) ? $index['types'] : array(), $container, $name, $indexId, $typePrototypeConfig, $serializerConfig);
$this->loadTypes(isset($index['types']) ? $index['types'] : array(), $container, $name, $indexId, $typePrototypeConfig);
}
return $indexIds;
@ -171,7 +172,7 @@ class FOSElasticaExtension extends Extension
* @param $indexId
* @param array $typePrototypeConfig
*/
protected function loadTypes(array $types, ContainerBuilder $container, $indexName, $indexId, array $typePrototypeConfig, $serializerConfig)
protected function loadTypes(array $types, ContainerBuilder $container, $indexName, $indexId, array $typePrototypeConfig)
{
foreach ($types as $name => $type) {
$type = self::deepArrayUnion($typePrototypeConfig, $type);
@ -180,12 +181,12 @@ class FOSElasticaExtension extends Extension
$typeDef = new Definition('%fos_elastica.type.class%', $typeDefArgs);
$typeDef->setFactoryService($indexId);
$typeDef->setFactoryMethod('getType');
if ($serializerConfig) {
$callbackDef = new Definition($serializerConfig['callback_class']);
if ($this->serializerConfig) {
$callbackDef = new Definition($this->serializerConfig['callback_class']);
$callbackId = sprintf('%s.%s.serializer.callback', $indexId, $name);
$typeDef->addMethodCall('setSerializer', array(array(new Reference($callbackId), 'serialize')));
$callbackDef->addMethodCall('setSerializer', array(new Reference($serializerConfig['serializer'])));
$callbackDef->addMethodCall('setSerializer', array(new Reference($this->serializerConfig['serializer'])));
if (isset($type['serializer']['groups'])) {
$callbackDef->addMethodCall('setGroups', array($type['serializer']['groups']));
}
@ -198,6 +199,11 @@ class FOSElasticaExtension extends Extension
$typeDef->addMethodCall('setSerializer', array(array(new Reference($callbackId), 'serialize')));
}
$container->setDefinition($typeId, $typeDef);
$this->indexConfigs[$indexName]['config']['mappings'][$name] = array(
"_source" => array("enabled" => true), // Add a default setting for empty mapping settings
);
if (isset($type['_id'])) {
$this->indexConfigs[$indexName]['config']['mappings'][$name]['_id'] = $type['_id'];
}
@ -210,7 +216,7 @@ class FOSElasticaExtension extends Extension
if (isset($type['_routing'])) {
$this->indexConfigs[$indexName]['config']['mappings'][$name]['_routing'] = $type['_routing'];
}
if (isset($type['mappings'])) {
if (isset($type['mappings']) && !empty($type['mappings'])) {
$this->indexConfigs[$indexName]['config']['mappings'][$name]['properties'] = $type['mappings'];
$typeName = sprintf('%s/%s', $indexName, $name);
$this->typeFields[$typeName] = $type['mappings'];
@ -324,8 +330,14 @@ class FOSElasticaExtension extends Extension
return $typeConfig['model_to_elastica_transformer']['service'];
}
if ($this->serializerConfig) {
$abstractId = sprintf('fos_elastica.model_to_elastica_identifier_transformer');
} else {
$abstractId = sprintf('fos_elastica.model_to_elastica_transformer');
}
$serviceId = sprintf('fos_elastica.model_to_elastica_transformer.%s.%s', $indexName, $typeName);
$serviceDef = new DefinitionDecorator('fos_elastica.model_to_elastica_transformer');
$serviceDef = new DefinitionDecorator($abstractId);
$serviceDef->replaceArgument(0, array(
'identifier' => $typeConfig['identifier']
));
@ -336,12 +348,26 @@ class FOSElasticaExtension extends Extension
protected function loadObjectPersister(array $typeConfig, Definition $typeDef, ContainerBuilder $container, $indexName, $typeName, $transformerId)
{
$arguments = array(
$typeDef,
new Reference($transformerId),
$typeConfig['model'],
);
if ($this->serializerConfig) {
$abstractId = 'fos_elastica.object_serializer_persister';
$callbackId = sprintf('%s.%s.serializer.callback', $this->indexConfigs[$indexName]['index'], $typeName);
$arguments[] = array(new Reference($callbackId), 'serialize');
} else {
$abstractId = 'fos_elastica.object_persister';
$arguments[] = $this->typeFields[sprintf('%s/%s', $indexName, $typeName)];
}
$serviceId = sprintf('fos_elastica.object_persister.%s.%s', $indexName, $typeName);
$serviceDef = new DefinitionDecorator('fos_elastica.object_persister');
$serviceDef->replaceArgument(0, $typeDef);
$serviceDef->replaceArgument(1, new Reference($transformerId));
$serviceDef->replaceArgument(2, $typeConfig['model']);
$serviceDef->replaceArgument(3, $this->typeFields[sprintf('%s/%s', $indexName, $typeName)]);
$serviceDef = new DefinitionDecorator($abstractId);
foreach ($arguments as $i => $argument) {
$serviceDef->replaceArgument($i, $argument);
}
$container->setDefinition($serviceId, $serviceDef);
return $serviceId;

View file

@ -0,0 +1,105 @@
<?php
namespace FOS\ElasticaBundle\Persister;
use Elastica\Document;
use Elastica\Type;
use FOS\ElasticaBundle\Transformer\ModelToElasticaTransformerInterface;
/**
* Inserts, replaces and deletes single objects in an elastica type, making use
* of elastica's serializer support to convert objects in to elastica documents.
* Accepts domain model objects and passes them directly to elastica
*
* @author Lea Haensenberber <lea.haensenberger@gmail.com>
*/
class ObjectSerializerPersister extends ObjectPersister
{
protected $serializer;
public function __construct(Type $type, ModelToElasticaTransformerInterface $transformer, $objectClass, $serializer)
{
parent::__construct($type, $transformer, $objectClass, array());
$this->serializer = $serializer;
}
/**
* Insert one object into the type
* The object will be transformed to an elastica document
*
* @param object $object
*/
public function insertOne($object)
{
$document = $this->transformToElasticaDocument($object);
$this->type->addDocument($document);
}
/**
* Replaces one object in the type
*
* @param object $object
* @return null
*/
public function replaceOne($object)
{
$document = $this->transformToElasticaDocument($object);
$this->type->deleteById($document->getId());
$this->type->addDocument($document);
}
/**
* Deletes one object in the type
*
* @param object $object
* @return null
**/
public function deleteOne($object)
{
$document = $this->transformToElasticaDocument($object);
$this->type->deleteById($document->getId());
}
/**
* Deletes one object in the type by id
*
* @param mixed $id
*
* @return null
**/
public function deleteById($id)
{
$this->type->deleteById($id);
}
/**
* Inserts an array of objects in the type
*
* @param array $objects array of domain model objects
**/
public function insertMany(array $objects)
{
$docs = array();
foreach ($objects as $object) {
$docs[] = $this->transformToElasticaDocument($object);
}
$this->type->addDocuments($docs);
}
/**
* Transforms an object to an elastica document
* with just the identifier set
*
* @param object $object
* @return Document the elastica document
*/
public function transformToElasticaDocument($object)
{
$document = $this->transformer->transform($object, array());
$data = call_user_func($this->serializer, $object);
$document->setData($data);
return $document;
}
}

View file

@ -193,6 +193,8 @@ If you do this wrong, you will see a `RoutingMissingException` as elasticsearch
### Declaring `nested` or `object`
Note that object can autodetect properties
fos_elastica:
clients:
default: { host: localhost, port: 9200 }
@ -213,6 +215,12 @@ If you do this wrong, you will see a `RoutingMissingException` as elasticsearch
properties:
date: { boost: 5 }
content: ~
user:
type: "object"
approver:
type: "object"
properties:
date: { boost: 5 }
#### Doctrine ORM and `object` mappings

View file

@ -49,6 +49,13 @@
<argument /> <!-- transformer -->
</service>
<service id="fos_elastica.object_serializer_persister" class="FOS\ElasticaBundle\Persister\ObjectSerializerPersister" abstract="true">
<argument /> <!-- type -->
<argument /> <!-- model to elastica transformer -->
<argument /> <!-- model -->
<argument /> <!-- serializer -->
</service>
<service id="fos_elastica.model_to_elastica_transformer" class="FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer" public="false" abstract="true">
<argument /> <!-- options -->
<call method="setPropertyAccessor">
@ -56,6 +63,13 @@
</call>
</service>
<service id="fos_elastica.model_to_elastica_identifier_transformer" class="FOS\ElasticaBundle\Transformer\ModelToElasticaIdentifierTransformer" public="false" abstract="true">
<argument /> <!-- options -->
<call method="setPropertyAccessor">
<argument type="service" id="fos_elastica.property_accessor" />
</call>
</service>
<service id="fos_elastica.elastica_to_model_transformer.collection" class="%fos_elastica.elastica_to_model_transformer.collection.class%" public="true" abstract="true">
<argument type="collection" /> <!-- transformers -->
</service>

View file

@ -0,0 +1,131 @@
<?php
namespace FOS\ElasticaBundle\Tests\ObjectSerializerPersister;
use FOS\ElasticaBundle\Persister\ObjectPersister;
use FOS\ElasticaBundle\Persister\ObjectSerializerPersister;
use FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer;
use FOS\ElasticaBundle\Transformer\ModelToElasticaIdentifierTransformer;
use Symfony\Component\PropertyAccess\PropertyAccess;
class POPO
{
public $id = 123;
public $name = 'popoName';
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
}
class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
if (!class_exists('Elastica\Type')) {
$this->markTestSkipped('The Elastica library classes are not available');
}
}
public function testThatCanReplaceObject()
{
$transformer = $this->getTransformer();
/** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\Elastica\Type */
$typeMock = $this->getMockBuilder('Elastica\Type')
->disableOriginalConstructor()
->getMock();
$typeMock->expects($this->once())
->method('deleteById')
->with($this->equalTo(123));
$typeMock->expects($this->once())
->method('addDocument');
$serializerMock = $this->getMockBuilder('FOS\ElasticaBundle\Serializer\Callback')->getMock();
$serializerMock->expects($this->once())->method('serialize');
$objectPersister = new ObjectSerializerPersister($typeMock, $transformer, 'SomeClass', array($serializerMock, 'serialize'));
$objectPersister->replaceOne(new POPO());
}
public function testThatCanInsertObject()
{
$transformer = $this->getTransformer();
/** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\Elastica\Type */
$typeMock = $this->getMockBuilder('Elastica\Type')
->disableOriginalConstructor()
->getMock();
$typeMock->expects($this->never())
->method('deleteById');
$typeMock->expects($this->once())
->method('addDocument');
$serializerMock = $this->getMockBuilder('FOS\ElasticaBundle\Serializer\Callback')->getMock();
$serializerMock->expects($this->once())->method('serialize');
$objectPersister = new ObjectSerializerPersister($typeMock, $transformer, 'SomeClass', array($serializerMock, 'serialize'));
$objectPersister->insertOne(new POPO());
}
public function testThatCanDeleteObject()
{
$transformer = $this->getTransformer();
/** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\Elastica\Type */
$typeMock = $this->getMockBuilder('Elastica\Type')
->disableOriginalConstructor()
->getMock();
$typeMock->expects($this->once())
->method('deleteById');
$typeMock->expects($this->never())
->method('addDocument');
$serializerMock = $this->getMockBuilder('FOS\ElasticaBundle\Serializer\Callback')->getMock();
$serializerMock->expects($this->once())->method('serialize');
$objectPersister = new ObjectSerializerPersister($typeMock, $transformer, 'SomeClass', array($serializerMock, 'serialize'));
$objectPersister->deleteOne(new POPO());
}
public function testThatCanInsertManyObjects()
{
$transformer = $this->getTransformer();
/** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\Elastica\Type */
$typeMock = $this->getMockBuilder('Elastica\Type')
->disableOriginalConstructor()
->getMock();
$typeMock->expects($this->never())
->method('deleteById');
$typeMock->expects($this->never())
->method('addObject');
$typeMock->expects($this->never())
->method('addObjects');
$typeMock->expects($this->once())
->method('addDocuments');
$serializerMock = $this->getMockBuilder('FOS\ElasticaBundle\Serializer\Callback')->getMock();
$serializerMock->expects($this->exactly(2))->method('serialize');
$objectPersister = new ObjectSerializerPersister($typeMock, $transformer, 'SomeClass', array($serializerMock, 'serialize'));
$objectPersister->insertMany(array(new POPO(), new POPO()));
}
/**
* @return ModelToElasticaIdentifierTransformer
*/
private function getTransformer()
{
$transformer = new ModelToElasticaIdentifierTransformer();
$transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor());
return $transformer;
}
}

View file

@ -102,6 +102,11 @@ class POPO
);
}
public function getObj()
{
return array('foo' => 'foo', 'bar' => 'foo', 'id' => 1);
}
public function getUpper()
{
return (object) array('id' => 'parent', 'name' => 'a random name');
@ -118,7 +123,6 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
if (!class_exists('Elastica\Document')) {
;
$this->markTestSkipped('The Elastica library classes are not available');
}
}
@ -273,6 +277,25 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase
), $data['sub']);
}
public function testObjectDoesNotRequireProperties()
{
$transformer = $this->getTransformer();
$document = $transformer->transform(new POPO(), array(
'obj' => array(
'type' => 'object'
)
));
$data = $document->getData();
$this->assertTrue(array_key_exists('obj', $data));
$this->assertInternalType('array', $data['obj']);
$this->assertEquals(array(
'foo' => 'foo',
'bar' => 'foo',
'id' => 1
), $data['obj']);
}
public function testParentMapping()
{
$transformer = $this->getTransformer();

View file

@ -0,0 +1,65 @@
<?php
namespace FOS\ElasticaBundle\Tests\Transformer\ModelToElasticaIdentifierTransformer;
use FOS\ElasticaBundle\Transformer\ModelToElasticaIdentifierTransformer;
use Symfony\Component\PropertyAccess\PropertyAccess;
class POPO
{
protected $id = 123;
protected $name = 'Name';
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
}
class ModelToElasticaIdentifierTransformerTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
if (!class_exists('Elastica\Document')) {
$this->markTestSkipped('The Elastica library classes are not available');
}
}
public function testGetDocumentWithIdentifierOnly()
{
$transformer = $this->getTransformer();
$document = $transformer->transform(new POPO(), array());
$data = $document->getData();
$this->assertInstanceOf('Elastica\Document', $document);
$this->assertEquals(123, $document->getId());
$this->assertCount(0, $data);
}
public function testGetDocumentWithIdentifierOnlyWithFields()
{
$transformer = $this->getTransformer();
$document = $transformer->transform(new POPO(), array('name' => array()));
$data = $document->getData();
$this->assertInstanceOf('Elastica\Document', $document);
$this->assertEquals(123, $document->getId());
$this->assertCount(0, $data);
}
/**
* @return ModelToElasticaIdentifierTransformer
*/
private function getTransformer()
{
$transformer = new ModelToElasticaIdentifierTransformer();
$transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor());
return $transformer;
}
}

View file

@ -71,11 +71,11 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf
$value = $this->propertyAccessor->getValue($object, $key);
if (isset($mapping['type']) && in_array($mapping['type'], array('nested', 'object'))) {
if (isset($mapping['type']) && in_array($mapping['type'], array('nested', 'object')) && isset($mapping['properties'])) {
/* $value is a nested document or object. Transform $value into
* an array of documents, respective the mapped properties.
*/
$document->add($key, $this->transformNested($value, $mapping['properties']));
$document->set($key, $this->transformNested($value, $mapping['properties']));
continue;
}
@ -89,7 +89,7 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf
continue;
}
$document->add($key, $this->normalizeValue($value));
$document->set($key, $this->normalizeValue($value));
}
return $document;

View file

@ -0,0 +1,26 @@
<?php
namespace FOS\ElasticaBundle\Transformer;
use Elastica\Document;
/**
* Creates an Elastica document with the ID of
* the Doctrine object as Elastica document ID
*/
class ModelToElasticaIdentifierTransformer extends ModelToElasticaAutoTransformer
{
/**
* Creates an elastica document with the id of the doctrine object as id
*
* @param object $object the object to convert
* @param array $fields the keys we want to have in the returned array
*
* @return Document
**/
public function transform($object, array $fields)
{
$identifier = $this->propertyAccessor->getValue($object, $this->options['identifier']);
return new Document($identifier);
}
}

32
UPGRADE-3.0.md Normal file
View file

@ -0,0 +1,32 @@
UPGRADE FROM 2.1 to 3.0
=======================
### Serialization
* you can now define a Serializer service and callback for indexing. All providers and listeners will use it.
```yml
serializer:
callback_class: FOS\ElasticaBundle\Serializer\Callback
serializer: serializer
```
### Mapping
* you do not have to setup any mapping anymore if you use a Serializer, properties are no more indexed only if
they are mapped. So this kind of configuration became valid:
```yml
serializer:
callback_class: FOS\ElasticaBundle\Serializer\Callback
serializer: serializer
indexes:
acme:
client: default
types:
Article:
persistence:
driver: orm
model: Acme\Bundle\CoreBundle\Entity\Article
provider: ~
```