From b52e683b362723306157eb4e1cc5d2bc3c5f5ac2 Mon Sep 17 00:00:00 2001 From: kiler129 Date: Wed, 13 Feb 2019 00:16:19 -0600 Subject: [PATCH] #283: Add support for custom serialization groups --- src/DependencyInjection/Configuration.php | 21 +++++++- src/IndexManager.php | 21 ++++++-- src/SearchableEntity.php | 40 +++++++++++--- tests/Normalizer/CommentNormalizer.php | 11 +++- tests/TestApp/Entity/Image.php | 14 ++++- tests/TestCase/ConfigurationTest.php | 22 +++++++- tests/TestCase/SerializationTest.php | 65 +++++++++++++++++++++++ 7 files changed, 179 insertions(+), 15 deletions(-) diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index e4e5b19d..4cbffe11 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -2,6 +2,7 @@ namespace Algolia\SearchBundle\DependencyInjection; +use Algolia\SearchBundle\Searchable; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use function method_exists; @@ -50,15 +51,33 @@ public function getConfigTreeBuilder() ->arrayNode('indices') ->useAttributeAsKey('name') ->arrayPrototype() + ->beforeNormalization() + ->ifTrue(function($v) { + return !empty($v['serializer_groups']) && + (!isset($v['enable_serializer_groups']) || !$v['enable_serializer_groups']); + }) + ->thenInvalid('In order to specify "serializer_groups" you need to enable "enable_serializer_groups"') + ->end() ->children() ->scalarNode('class') ->isRequired() ->cannotBeEmpty() ->end() ->booleanNode('enable_serializer_groups') - ->info('When set to true, it will call normalize method with an extra groups parameter "groups" => [Searchable::NORMALIZATION_GROUP]') + ->info( + 'When set to true, it will call normalize method with an extra groups ' . + 'defined in "serializer_groups" (Searchable::NORMALIZATION_GROUP by default)' + ) ->defaultFalse() ->end() + ->arrayNode('serializer_groups') + ->info('List of serializer groups to use while serializing. This option requires "enable_serializer_groups" set to true.') + ->beforeNormalization() + ->castToArray() + ->end() + ->scalarPrototype()->end() + ->defaultValue([Searchable::NORMALIZATION_GROUP]) + ->end() ->scalarNode('index_if') ->info('Property accessor path (like method or property name) used to decide if an entry should be indexed.') ->defaultNull() diff --git a/src/IndexManager.php b/src/IndexManager.php index 0e8c04bf..843688e5 100644 --- a/src/IndexManager.php +++ b/src/IndexManager.php @@ -20,6 +20,10 @@ class IndexManager implements IndexManagerInterface private $aggregators; private $entitiesAggregators; private $classToIndexMapping; + + /** + * @var array Maps indexed classes to their groups (if configured for the index) + */ private $classToSerializerGroupMapping; private $indexIfMapping; private $normalizer; @@ -176,8 +180,17 @@ public function shouldBeIndexed($entity) return true; } - private function canUseSerializerGroup($className) + /** + * @param string $className + * + * @return string[]|null List of groups or null for entity with groups disabled + */ + private function getSerializerGroup($className) { + if (!isset($this->classToSerializerGroupMapping[$className])) { + return null; + } + return $this->classToSerializerGroupMapping[$className]; } @@ -240,7 +253,9 @@ private function setClassToSerializerGroupMapping() { $mapping = []; foreach ($this->configuration['indices'] as $indexDetails) { - $mapping[$indexDetails['class']] = $indexDetails['enable_serializer_groups']; + if ($indexDetails['enable_serializer_groups']) { + $mapping[$indexDetails['class']] = $indexDetails['serializer_groups']; + } } $this->classToSerializerGroupMapping = $mapping; @@ -277,7 +292,7 @@ private function forEachChunk(ObjectManager $objectManager, array $entities, $op $entity, $objectManager->getClassMetadata($entityClassName), $this->normalizer, - ['useSerializerGroup' => $this->canUseSerializerGroup($entityClassName)] + ['serializerGroups' => $this->getSerializerGroup($entityClassName)] ); } diff --git a/src/SearchableEntity.php b/src/SearchableEntity.php index 3cfbf9c1..fce02711 100644 --- a/src/SearchableEntity.php +++ b/src/SearchableEntity.php @@ -2,6 +2,7 @@ namespace Algolia\SearchBundle; +use Doctrine\ORM\Mapping\ClassMetadata; use JMS\Serializer\ArrayTransformerInterface; use Symfony\Component\Config\Definition\Exception\Exception; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -10,19 +11,41 @@ class SearchableEntity implements SearchableEntityInterface { protected $indexName; protected $entity; + + /** + * @var ClassMetadata + */ protected $entityMetadata; - protected $useSerializerGroups; + + /** + * @var string[]|null List of groups to use during serialization, or null if groups are disabled + */ + protected $serializerGroups = []; private $id; private $normalizer; public function __construct($indexName, $entity, $entityMetadata, $normalizer, array $extra = []) { - $this->indexName = $indexName; - $this->entity = $entity; - $this->entityMetadata = $entityMetadata; - $this->normalizer = $normalizer; - $this->useSerializerGroups = isset($extra['useSerializerGroup']) && $extra['useSerializerGroup']; + $this->indexName = $indexName; + $this->entity = $entity; + $this->entityMetadata = $entityMetadata; + $this->normalizer = $normalizer; + $this->serializerGroups = isset($extra['serializerGroups']) ? $extra['serializerGroups'] : null; + + if (isset($extra['useSerializerGroup']) && $extra['useSerializerGroup']) { + @\trigger_error( + 'Passing "useSerializerGroup" SearchableEntity is deprecated, pass "serializerGroups" with ' . + 'list of groups, or null to disable', + \E_USER_DEPRECATED + ); + + if ($this->serializerGroups === null) { + $this->serializerGroups = [Searchable::NORMALIZATION_GROUP]; + } elseif (!\in_array(Searchable::NORMALIZATION_GROUP, $this->serializerGroups)) { + $this->serializerGroups[] = Searchable::NORMALIZATION_GROUP; + } + } $this->setId(); } @@ -35,11 +58,12 @@ public function getIndexName() public function getSearchableArray() { $context = [ + 'rootEntity' => $this->entityMetadata->name, 'fieldsMapping' => $this->entityMetadata->fieldMappings, ]; - if ($this->useSerializerGroups) { - $context['groups'] = [Searchable::NORMALIZATION_GROUP]; + if ($this->serializerGroups !== null) { + $context['groups'] = $this->serializerGroups; } if ($this->normalizer instanceof NormalizerInterface) { diff --git a/tests/Normalizer/CommentNormalizer.php b/tests/Normalizer/CommentNormalizer.php index b8b6247e..22abbdae 100644 --- a/tests/Normalizer/CommentNormalizer.php +++ b/tests/Normalizer/CommentNormalizer.php @@ -9,10 +9,19 @@ class CommentNormalizer implements NormalizerInterface { public function normalize($object, $format = null, array $context = array()) { - return [ + $out = [ 'content' => $object->getContent(), 'post_title' => $object->getPost()->getTitle(), ]; + + + //This should ALWAYS exists, however it seems that aggregation looses context + // @see https://github.com/algolia/search-bundle/issues/286 + if (isset($context['rootEntity'])) { + $out['originalClass'] = $context['rootEntity']; + } + + return $out; } public function supportsNormalization($data, $format = null) diff --git a/tests/TestApp/Entity/Image.php b/tests/TestApp/Entity/Image.php index bbc23c8b..7f742610 100644 --- a/tests/TestApp/Entity/Image.php +++ b/tests/TestApp/Entity/Image.php @@ -3,6 +3,7 @@ namespace Algolia\SearchBundle\TestApp\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; /** * @ORM\Entity @@ -15,7 +16,7 @@ class Image * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") - * + * @Groups({"searchable"}) */ private $id; @@ -30,6 +31,9 @@ public function __construct(array $attributes = []) $this->url = isset($attributes['url']) ? $attributes['url'] : '/wp-content/uploads/flamingo.jpg'; } + /** + * @Groups({"searchable"}) + */ public function getId() { return $this->id; @@ -49,4 +53,12 @@ public function setUrl($url) { $this->url = $url; } + + /** + * @Groups({"searchableCustom"}) + */ + public function getCustomVirtualProperty() + { + return 'here'; + } } diff --git a/tests/TestCase/ConfigurationTest.php b/tests/TestCase/ConfigurationTest.php index d6ef4f53..dd392070 100644 --- a/tests/TestCase/ConfigurationTest.php +++ b/tests/TestCase/ConfigurationTest.php @@ -60,7 +60,19 @@ public function dataTestConfigurationTree() "prefix" => "sf_", "indices" => [ ['name' => 'posts', 'class' => 'App\Entity\Post', 'index_if' => null], - ['name' => 'tags', 'class' => 'App\Entity\Tag', 'enable_serializer_groups' => true, 'index_if' => null], + [ + 'name' => 'tags', + 'class' => 'App\Entity\Tag', + 'enable_serializer_groups' => true, + 'index_if' => null, + ], + [ + 'name' => 'comments', + 'class' => 'App\Entity\Comment', + 'enable_serializer_groups' => true, + 'serializer_groups' => ['foo', 'bar'], + 'index_if' => null, + ], ], ],[ "prefix" => "sf_", @@ -73,11 +85,19 @@ public function dataTestConfigurationTree() 'posts' => [ 'class' => 'App\Entity\Post', 'enable_serializer_groups' => false, + 'serializer_groups' => ['searchable'], 'index_if' => null, ], 'tags' => [ 'class' => 'App\Entity\Tag', 'enable_serializer_groups' => true, + 'serializer_groups' => ['searchable'], + 'index_if' => null, + ], + 'comments' => [ + 'class' => 'App\Entity\Comment', + 'enable_serializer_groups' => true, + 'serializer_groups' => ['foo', 'bar'], 'index_if' => null, ], ], diff --git a/tests/TestCase/SerializationTest.php b/tests/TestCase/SerializationTest.php index 0af5e0a1..219fd2d9 100644 --- a/tests/TestCase/SerializationTest.php +++ b/tests/TestCase/SerializationTest.php @@ -6,6 +6,7 @@ use Algolia\SearchBundle\Searchable; use Algolia\SearchBundle\SearchableEntity; use Algolia\SearchBundle\TestApp\Entity\Comment; +use Algolia\SearchBundle\TestApp\Entity\Image; use Algolia\SearchBundle\TestApp\Entity\Post; use Algolia\SearchBundle\TestApp\Entity\Tag; use Algolia\SearchBundle\Normalizer\CommentNormalizer; @@ -70,8 +71,10 @@ public function testSimpleEntityToSearchableArray() [ "content" => "a great comment", "post_title" => "a simple post", + "originalClass" => Post::class ] ], + "secretProperty" => "secret" ]; $this->assertEquals($expected, $searchablePost->getSearchableArray()); @@ -114,6 +117,67 @@ public function testEntityWithAnnotationsToSearchableArray() $this->assertEquals($expected, $searchablePost->getSearchableArray()); } + public function annotatedEntityContextProvider() + { + return [ + [ + ['serializerGroups' => null], //Grouping disabled -> all properties will be serialized + ['id' => 42, 'url' => 'http://www.example.com', 'customVirtualProperty' => 'here'] + ], + [ + ['serializerGroups' => []], //As in Symfony Serializer empty groups array will return no result + [] + ], + [ + ['useSerializerGroup' => true], //Ensure legacy method still works + ['id' => 42] + ], + [ + ['serializerGroups' => ['searchable']], //This should work exactly like legacy above + ['id' => 42] + ], + + [ + ['serializerGroups' => ['unknownGroup']], + [] + ], + [ + ['serializerGroups' => ['searchableCustom']], + ['customVirtualProperty' => 'here'] + ], + [ + ['serializerGroups' => ['searchable', 'searchableCustom']], + ['id' => 42, 'customVirtualProperty' => 'here'] + ], + ]; + } + + /** + * @dataProvider annotatedEntityContextProvider + */ + public function testEntityWithCustomSerializationGroupsToSearchableArray($extra, $expectedOutput) + { + $image = new Image( + [ + 'id' => 42, + 'url' => 'http://www.example.com', + ] + ); + $postMeta = $this->get('doctrine') + ->getManager() + ->getClassMetadata(Image::class); + + $searchablePost = new SearchableEntity( + 'images', + $image, + $postMeta, + $this->get('serializer'), + $extra + ); + + $this->assertEquals($expectedOutput, $searchablePost->getSearchableArray()); + } + public function testNormalizableEntityToSearchableArray() { $datetime = new \DateTime(); @@ -163,6 +227,7 @@ public function testDedicatedNormalizer() $expected = [ "content" => "hey, this is a comment", "post_title" => "Another super post", + "originalClass" => Comment::class ]; $this->assertEquals($expected, $searchableComment->getSearchableArray());