diff --git a/CHANGELOG.md b/CHANGELOG.md index f1100057..4e47bdf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ CHANGELOG UNRELEASED ---------- +* Feature: Custom Serialization Groups & Access To Root Entity Class On Normalization - PR [#287](https://github.com/algolia/search-bundle/pull/287) + + The feature adds new `rootEntity` to normalization context, which allows distinguishing which index is processed + in a custom normalizer. In addition now it is possible to specify custom normalization groups (by default only + `searchable` group is used). + In order to use custom serialization groups the `enable_serializer_groups` must be enabled. + + 3.4.0 ---------- 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..378d286a 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; @@ -271,13 +286,16 @@ private function forEachChunk(ObjectManager $objectManager, array $entities, $op $searchableEntitiesChunk = []; foreach ($chunk as $entity) { $entityClassName = ClassUtils::getClass($entity); + $groups = $this->getSerializerGroup($entityClassName); $searchableEntitiesChunk[] = new SearchableEntity( $this->getFullIndexName($entityClassName), $entity, $objectManager->getClassMetadata($entityClassName), $this->normalizer, - ['useSerializerGroup' => $this->canUseSerializerGroup($entityClassName)] + $groups === null + ? ['useSerializerGroup' => false] + : ['useSerializerGroup' => true, 'serializerGroups' => $groups] ); } diff --git a/src/SearchableEntity.php b/src/SearchableEntity.php index 3cfbf9c1..217ff062 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,8 +11,21 @@ class SearchableEntity implements SearchableEntityInterface { protected $indexName; protected $entity; + + /** + * @var ClassMetadata + */ protected $entityMetadata; - protected $useSerializerGroups; + + /** + * @var string[] List of groups to use during serialization + */ + protected $serializerGroups = [Searchable::NORMALIZATION_GROUP]; + + /** + * @var bool + */ + protected $useSerializerGroups = false; private $id; private $normalizer; @@ -22,7 +36,14 @@ public function __construct($indexName, $entity, $entityMetadata, $normalizer, a $this->entity = $entity; $this->entityMetadata = $entityMetadata; $this->normalizer = $normalizer; - $this->useSerializerGroups = isset($extra['useSerializerGroup']) && $extra['useSerializerGroup']; + + if (isset($extra['useSerializerGroup']) && $extra['useSerializerGroup']) { + $this->useSerializerGroups = true; + } + + if (isset($extra['serializerGroups']) && \is_array($extra['serializerGroups'])) { + $this->serializerGroups = $extra['serializerGroups']; + } $this->setId(); } @@ -35,11 +56,12 @@ public function getIndexName() public function getSearchableArray() { $context = [ + 'rootEntity' => $this->entityMetadata->name, 'fieldsMapping' => $this->entityMetadata->fieldMappings, ]; if ($this->useSerializerGroups) { - $context['groups'] = [Searchable::NORMALIZATION_GROUP]; + $context['groups'] = $this->serializerGroups; } if ($this->normalizer instanceof NormalizerInterface) { diff --git a/tests/Normalizer/CommentNormalizer.php b/tests/Normalizer/CommentNormalizer.php index b8b6247e..2f94e2d9 100644 --- a/tests/Normalizer/CommentNormalizer.php +++ b/tests/Normalizer/CommentNormalizer.php @@ -10,6 +10,7 @@ class CommentNormalizer implements NormalizerInterface public function normalize($object, $format = null, array $context = array()) { return [ + 'original_class' => \md5($context['rootEntity']), //prevent skewing results with "TestApp" 'content' => $object->getContent(), 'post_title' => $object->getPost()->getTitle(), ]; 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..17e7db6a 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,9 @@ public function testSimpleEntityToSearchableArray() [ "content" => "a great comment", "post_title" => "a simple post", + "original_class" => \md5(Post::class) ] - ], + ] ]; $this->assertEquals($expected, $searchablePost->getSearchableArray()); @@ -114,6 +116,69 @@ public function testEntityWithAnnotationsToSearchableArray() $this->assertEquals($expected, $searchablePost->getSearchableArray()); } + public function annotatedEntityContextProvider() + { + return [ + [ + ['useSerializerGroup' => false], //Grouping disabled -> all properties will be serialized + ['id' => 42, 'url' => 'http://www.example.com', 'customVirtualProperty' => 'here'] + ], + [ + //As in Symfony Serializer empty groups array will return no result + ['useSerializerGroup' => true, 'serializerGroups' => []], + [] + ], + [ + ['useSerializerGroup' => true], //Ensure legacy method still works + ['id' => 42] + ], + [ + //This should work exactly like legacy above + ['useSerializerGroup' => true, 'serializerGroups' => ['searchable']], + ['id' => 42] + ], + + [ + ['useSerializerGroup' => true, 'serializerGroups' => ['unknownGroup']], + [] + ], + [ + ['useSerializerGroup' => true, 'serializerGroups' => ['searchableCustom']], + ['customVirtualProperty' => 'here'] + ], + [ + ['useSerializerGroup' => true, '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 +228,7 @@ public function testDedicatedNormalizer() $expected = [ "content" => "hey, this is a comment", "post_title" => "Another super post", + "original_class" => \md5(Comment::class) ]; $this->assertEquals($expected, $searchableComment->getSearchableArray());