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

Added default_skip_when_empty option as config using the exclusionStrategy #1257

Open
wants to merge 4 commits 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
20 changes: 20 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,23 @@ a serialization context from your callable and use it.
You can also set a default DeserializationContextFactory with
``->setDeserializationContextFactory(function () { /* ... */ })``
to be used with methods ``deserialize()`` and ``fromArray()``.
Setting a default behaviour for skipping properties with empty values - `SkipWhenEmpty`_ annotation
---------------------------------------------------------------------------------------------------
To avoid to specifying the annotation ``@skipWhenEmpty`` for each property
it is possible to enable this behaviour in configuration by calling ``enableSkipWhenEmpty``

Example using with the SerializerBuilder::

use JMS\Serializer\SerializationContext;

$serializer = JMS\Serializer\SerializerBuilder::create()
->setSerializationContextFactory(function () {

return SerializationContext::create()
->enableSkipWhenEmpty();

})
->build();

.. _SkipWhenEmpty: reference/annotations.html#skipwhenempty
34 changes: 22 additions & 12 deletions src/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\Exclusion\SkipWhenEmptyExclusionStrategy;
use JMS\Serializer\Exclusion\ValueExclusionStrategyInterface;
use JMS\Serializer\Exclusion\VersionExclusionStrategy;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
Expand All @@ -18,6 +20,7 @@

abstract class Context
{
public const ATTR_SKIP_WHEN_EMPTY = 'default_skip_when_empty';
/**
* @var array
*/
Expand Down Expand Up @@ -83,6 +86,10 @@ public function initialize(string $format, VisitorInterface $visitor, GraphNavig
$this->addExclusionStrategy(new DepthExclusionStrategy());
}

if (!empty($this->attributes[self::ATTR_SKIP_WHEN_EMPTY])) {
$this->addExclusionStrategy(new SkipWhenEmptyExclusionStrategy());
}

$this->initialized = true;
}

Expand All @@ -101,7 +108,7 @@ public function getNavigator(): GraphNavigatorInterface
return $this->navigator;
}

public function getExclusionStrategy(): ?ExclusionStrategyInterface
public function getExclusionStrategy(): ?DisjunctExclusionStrategy
vasilake-v marked this conversation as resolved.
Show resolved Hide resolved
{
return $this->exclusionStrategy;
}
Expand Down Expand Up @@ -142,28 +149,21 @@ final protected function assertMutable(): void
}

/**
* @param ExclusionStrategyInterface|ValueExclusionStrategyInterface $strategy
*
* @return $this
*/
public function addExclusionStrategy(ExclusionStrategyInterface $strategy): self
public function addExclusionStrategy($strategy): self
vasilake-v marked this conversation as resolved.
Show resolved Hide resolved
{
$this->assertMutable();

if (null === $this->exclusionStrategy) {
$this->exclusionStrategy = $strategy;

return $this;
}

if ($this->exclusionStrategy instanceof DisjunctExclusionStrategy) {
$this->exclusionStrategy->addStrategy($strategy);

return $this;
}

$this->exclusionStrategy = new DisjunctExclusionStrategy([
$this->exclusionStrategy,
$strategy,
]);
$this->exclusionStrategy = new DisjunctExclusionStrategy([$strategy]);

return $this;
}
Expand Down Expand Up @@ -204,6 +204,16 @@ public function enableMaxDepthChecks(): self
return $this;
}

/**
* @return $this
*/
public function enableSkipWhenEmpty(): self
{
$this->attributes[self::ATTR_SKIP_WHEN_EMPTY] = true;

return $this;
}

public function getFormat(): string
{
return $this->format;
Expand Down
40 changes: 34 additions & 6 deletions src/Exclusion/DisjunctExclusionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
* @author Johannes M. Schmitt <[email protected]>
*/
final class DisjunctExclusionStrategy implements ExclusionStrategyInterface
final class DisjunctExclusionStrategy implements ExclusionStrategyInterface, ValueExclusionStrategyInterface
{
/**
* @var ExclusionStrategyInterface[]
Expand All @@ -30,8 +30,24 @@ public function __construct(array $delegates = [])
$this->delegates = $delegates;
}

public function addStrategy(ExclusionStrategyInterface $strategy): void
/**
* @param ExclusionStrategyInterface|ValueExclusionStrategyInterface $strategy
*/
public function addStrategy($strategy): void
{
if (
!($strategy instanceof ExclusionStrategyInterface)
|| !($strategy instanceof ValueExclusionStrategyInterface)
) {
throw new \InvalidArgumentException(
sprintf(
'Strategy should be one of %s, %s instances',
ExclusionStrategyInterface::class,
ValueExclusionStrategyInterface::class
)
);
}

$this->delegates[] = $strategy;
}

Expand All @@ -41,8 +57,7 @@ public function addStrategy(ExclusionStrategyInterface $strategy): void
public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool
{
foreach ($this->delegates as $delegate) {
\assert($delegate instanceof ExclusionStrategyInterface);
if ($delegate->shouldSkipClass($metadata, $context)) {
if ($delegate instanceof ExclusionStrategyInterface && $delegate->shouldSkipClass($metadata, $context)) {
return true;
}
}
Expand All @@ -56,8 +71,21 @@ public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool
public function shouldSkipProperty(PropertyMetadata $property, Context $context): bool
{
foreach ($this->delegates as $delegate) {
\assert($delegate instanceof ExclusionStrategyInterface);
if ($delegate->shouldSkipProperty($property, $context)) {
if ($delegate instanceof ExclusionStrategyInterface && $delegate->shouldSkipProperty($property, $context)) {
return true;
}
}

return false;
}

/**
* Whether the property should be skipped.
*/
public function shouldSkipPropertyWithValue(PropertyMetadata $property, Context $context, $value): bool
{
foreach ($this->delegates as $delegate) {
if ($delegate instanceof ValueExclusionStrategyInterface && $delegate->shouldSkipPropertyWithValue($property, $context, $value)) {
return true;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Exclusion/ExpressionLanguageExclusionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
* @author Asmir Mustafic <[email protected]>
*/
final class ExpressionLanguageExclusionStrategy
final class ExpressionLanguageExclusionStrategy implements ExclusionStrategyInterface
vasilake-v marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* @var ExpressionEvaluatorInterface
Expand Down
34 changes: 34 additions & 0 deletions src/Exclusion/SkipWhenEmptyExclusionStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Metadata\PropertyMetadata;

class SkipWhenEmptyExclusionStrategy implements ValueExclusionStrategyInterface
{
/**
* @inheritDoc
*/
public function shouldSkipPropertyWithValue(PropertyMetadata $property, Context $context, $value): bool
{
if (
$property->skipWhenEmpty
|| (
$context->hasAttribute(Context::ATTR_SKIP_WHEN_EMPTY)
&& $context->getAttribute(Context::ATTR_SKIP_WHEN_EMPTY)
)
) {
if ($value instanceof \ArrayObject || \is_array($value) && 0 === count($value)) {
return true;
}

// This would be used for T object types, later, in the visitor->visitProperty
$property->skipWhenEmpty = true;
}

return false;
}
}
27 changes: 27 additions & 0 deletions src/Exclusion/ValueExclusionStrategyInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
* Interface for exclusion strategies including the values
*
* @author Veaceslav vasilache <[email protected]>
*/
interface ValueExclusionStrategyInterface
{
/**
* Whether the property should be skipped, using the value also
*
* @param PropertyMetadata $property
* @param Context $context
* @param mixed $value
*
* @return bool
*/
public function shouldSkipPropertyWithValue(PropertyMetadata $property, Context $context, $value): bool;
}
4 changes: 2 additions & 2 deletions src/GraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace JMS\Serializer;

use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;

/**
* Handles traversal along the object graph.
Expand Down Expand Up @@ -32,7 +32,7 @@ abstract class GraphNavigator implements GraphNavigatorInterface
*/
protected $format;
/**
* @var ExclusionStrategyInterface
* @var DisjunctExclusionStrategy
*/
protected $exclusionStrategy;

Expand Down
4 changes: 4 additions & 0 deletions src/GraphNavigator/SerializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ public function accept($data, ?array $type = null)
continue;
}

if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipPropertyWithValue($propertyMetadata, $this->context, $v)) {
continue;
}

$this->context->pushPropertyMetadata($propertyMetadata);
$this->visitor->visitProperty($propertyMetadata, $v);
$this->context->popPropertyMetadata();
Expand Down
44 changes: 44 additions & 0 deletions tests/Fixtures/ObjectWithEmptyArrayAndHashNotAnnotated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures;

use JMS\Serializer\Annotation as Serializer;

class ObjectWithEmptyArrayAndHashNotAnnotated
{
/**
* @Serializer\Type("array<string,string>")
*/
private $hash = [];
/**
* @Serializer\Type("array<string>")
*/
private $array = [];

private $object = [];

/**
* @Serializer\SkipWhenEmpty()
*/
private $objectAnnotated = [];

/**
* @Serializer\Type("array<string>")
* @Serializer\SkipWhenEmpty()
*/
private $someEmptyAnnotatedProp = [];

/**
* @Serializer\Type("array<string>")
*/
private $someEmptyNonAnnotatedProp = [];

private $someNonEmptyProp = 'test-value';

public function __construct()
{
$this->object = new InlineChildEmpty();
}
}
3 changes: 2 additions & 1 deletion tests/Handler/ArrayCollectionHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace JMS\Serializer\Tests\Handler;

use Doctrine\Common\Collections\ArrayCollection;
use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;
use JMS\Serializer\Handler\ArrayCollectionHandler;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\SerializationContext;
Expand Down Expand Up @@ -48,7 +49,7 @@ public function testSerializeArraySkipByExclusionStrategy()
$factoryMock = $this->getMockBuilder(MetadataFactoryInterface::class)->getMock();
$factoryMock->method('getMetadataForClass')->willReturn(new ClassMetadata(ArrayCollection::class));

$context->method('getExclusionStrategy')->willReturn(new AlwaysExcludeExclusionStrategy());
$context->method('getExclusionStrategy')->willReturn(new DisjunctExclusionStrategy([new AlwaysExcludeExclusionStrategy()]));
vasilake-v marked this conversation as resolved.
Show resolved Hide resolved
$context->method('getMetadataFactory')->willReturn($factoryMock);

$type = ['name' => 'ArrayCollection', 'params' => []];
Expand Down
21 changes: 21 additions & 0 deletions tests/Serializer/BaseSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
use JMS\Serializer\Tests\Fixtures\Node;
use JMS\Serializer\Tests\Fixtures\ObjectUsingTypeCasting;
use JMS\Serializer\Tests\Fixtures\ObjectWithArrayIterator;
use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyArrayAndHashNotAnnotated;
use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyHash;
use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyNullableAndEmptyArrays;
use JMS\Serializer\Tests\Fixtures\ObjectWithGenerator;
Expand Down Expand Up @@ -1731,6 +1732,26 @@ public function testMaxDepthWithOneDepthObject()
self::assertEquals($this->getContent('maxdepth_1'), $serialized);
}

public function testSkipWhenEmptyByDefaultEnabled()
{
$data = new ObjectWithEmptyArrayAndHashNotAnnotated();

$context = SerializationContext::create()->enableSkipWhenEmpty();
$serialized = $this->serialize($data, $context);

self::assertEquals($this->getContent('default_skip_when_empty_enabled_object'), $serialized);
}

public function testSkipWhenEmptyByDefaultDisabled()
{
$data = new ObjectWithEmptyArrayAndHashNotAnnotated();

$context = SerializationContext::create();
$serialized = $this->serialize($data, $context);

self::assertEquals($this->getContent('default_skip_when_empty_disabled_object'), $serialized);
}

public function testDeserializingIntoExistingObject()
{
if (!$this->hasDeserializer()) {
Expand Down
Loading