Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #283: Add support for custom serialization groups #287

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------

Expand Down
21 changes: 20 additions & 1 deletion src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't have ->defaultFalse()?

Copy link
Contributor Author

@kiler129 kiler129 Feb 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a reason why this node should (or can it even?) get a default value of false?
This is just to prevent putting custom groups and wondering why this doesn't work (so really an DX improvement here).

Am I missing something?

->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()
Expand Down
24 changes: 21 additions & 3 deletions src/IndexManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class IndexManager implements IndexManagerInterface
private $aggregators;
private $entitiesAggregators;
private $classToIndexMapping;

/**
* @var array<string, string[]> Maps indexed classes to their groups (if configured for the index)
*/
private $classToSerializerGroupMapping;
private $indexIfMapping;
private $normalizer;
Expand Down Expand Up @@ -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];
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
kiler129 marked this conversation as resolved.
Show resolved Hide resolved
$groups === null
? ['useSerializerGroup' => false]
: ['useSerializerGroup' => true, 'serializerGroups' => $groups]
);
}

Expand Down
28 changes: 25 additions & 3 deletions src/SearchableEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -10,8 +11,21 @@ class SearchableEntity implements SearchableEntityInterface
{
protected $indexName;
protected $entity;

/**
* @var ClassMetadata
*/
protected $entityMetadata;
protected $useSerializerGroups;
kiler129 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @var string[] List of groups to use during serialization
*/
protected $serializerGroups = [Searchable::NORMALIZATION_GROUP];

/**
* @var bool
*/
protected $useSerializerGroups = false;

private $id;
private $normalizer;
Expand All @@ -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();
}
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions tests/Normalizer/CommentNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
Expand Down
14 changes: 13 additions & 1 deletion tests/TestApp/Entity/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Algolia\SearchBundle\TestApp\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
* @ORM\Entity
Expand All @@ -15,7 +16,7 @@ class Image
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*
* @Groups({"searchable"})
*/
private $id;

Expand All @@ -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;
Expand All @@ -49,4 +53,12 @@ public function setUrl($url)
{
$this->url = $url;
}

/**
* @Groups({"searchableCustom"})
*/
public function getCustomVirtualProperty()
{
return 'here';
}
}
22 changes: 21 additions & 1 deletion tests/TestCase/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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_",
Expand All @@ -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,
kiler129 marked this conversation as resolved.
Show resolved Hide resolved
],
'comments' => [
'class' => 'App\Entity\Comment',
'enable_serializer_groups' => true,
'serializer_groups' => ['foo', 'bar'],
'index_if' => null,
],
],
Expand Down
68 changes: 67 additions & 1 deletion tests/TestCase/SerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down