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

[feature] exclusion groups #539

Closed
Closed
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
39 changes: 37 additions & 2 deletions doc/cookbook/exclusion_strategies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ expose them via an API that is consumed by a third-party:
``@Until``, and ``@Since`` both accept a standardized PHP version number.

If you have annotated your objects like above, you can serializing different
versions like this::
versions like this:

.. code-block :: php

use JMS\Serializer\SerializationContext;

Expand Down Expand Up @@ -109,7 +111,9 @@ You can achieve that by using the ``@Groups`` annotation on your properties.
private $createdAt;
}

You can then tell the serializer which groups to serialize in your controller::
You can then tell the serializer which groups to serialize in your controller:

.. code-block :: php

use JMS\Serializer\SerializationContext;

Expand All @@ -120,7 +124,38 @@ You can then tell the serializer which groups to serialize in your controller::
$serializer->serialize(new BlogPost(), 'json', SerializationContext::create()->setGroups(array('Default', 'list')));

//will output $id, $title, $nbComments and $createdAt.

In some cases you might want to go the other way around and serialize everything by default but exclude a property in a specific case. You can achive that by defining a exclusion group:

.. code-block :: php

use JMS\Serializer\Annotation\Exclude;
/**
* @ExclusionPolicy("none")
*/
class BlogPost
{
private $id;

private $title;

/**
* @Exclude({"short-list"})
*/
private $content;
}

And then tell the serializer which groups to exclude:

.. code-block :: php

use JMS\Serializer\SerializationContext;

$context = SerializationContext::create()->setExclusionGroups(array('short-list'));
$serializer->serialize(new BlogPost(), 'json', $context);

//will output $id and $title

Limiting serialization depth of some properties
-----------------------------------------------
You can limit the depth of what will be serialized in a property with the
Expand Down
2 changes: 2 additions & 0 deletions src/JMS/Serializer/Annotation/Exclude.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@
*/
final class Exclude
{
/** @var array<string> */
public $groups;
}
19 changes: 17 additions & 2 deletions src/JMS/Serializer/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exclusion\DepthExclusionStrategy;
use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;
use JMS\Serializer\Exclusion\ExclusionGroupsExclusionStrategy;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\Exclusion\VersionExclusionStrategy;
Expand Down Expand Up @@ -72,13 +73,13 @@ public function initialize($format, VisitorInterface $visitor, GraphNavigator $n
if ($this->initialized) {
throw new \LogicException('This context was already initialized, and cannot be re-used.');
}

$this->initialized = true;
$this->format = $format;
$this->visitor = $visitor;
$this->navigator = $navigator;
$this->metadataFactory = $factory;
$this->metadataStack = new \SplStack();
$this->addExclusionStrategy(new ExclusionGroupsExclusionStrategy(array()));
$this->initialized = true;
}

public function accept($data, array $type = null)
Expand Down Expand Up @@ -177,6 +178,20 @@ public function setGroups($groups)
return $this;
}

/**
* @param array $exclusionGroups
*/
public function setExclusionGroups($exclusionGroups)
{
if (empty($exclusionGroups)) {
throw new \LogicException('The exclusion groups must not be empty.');
}

$this->attributes->set('exclusionGroups', (array) $exclusionGroups);
$this->addExclusionStrategy(new ExclusionGroupsExclusionStrategy((array) $exclusionGroups));
return $this;
}

public function enableMaxDepthChecks()
{
$this->addExclusionStrategy(new DepthExclusionStrategy());
Expand Down
63 changes: 63 additions & 0 deletions src/JMS/Serializer/Exclusion/ExclusionGroupsExclusionStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace JMS\Serializer\Exclusion;

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

class ExclusionGroupsExclusionStrategy implements ExclusionStrategyInterface
{
private $groups = array();

public function __construct(array $groups)
{
foreach ($groups as $group) {
$this->groups[$group] = true;
}
}

/**
* Whether the class should be skipped.
*
* @param ClassMetadata $classMetadata
*
* @param Context $context
* @return bool
*/
public function shouldSkipClass(ClassMetadata $classMetadata, Context $context)
{
return $this->shouldSkipGroup($classMetadata->exclusionGroups);
}

/**
* Whether the property should be skipped.
*
* @param PropertyMetadata $propertyMetadata
*
* @param Context $context
* @return bool
*/
public function shouldSkipProperty(PropertyMetadata $propertyMetadata, Context $context)
{
return $this->shouldSkipGroup($propertyMetadata->exclusionGroups);
}

/**
* @param $metadataGroups
* @return bool
*/
private function shouldSkipGroup($metadataGroups) {
if (false === $metadataGroups) {
return false;
}
if (is_array($metadataGroups) && 0 == count($metadataGroups)) {
return true;
}
if (!empty($this->groups) && array_intersect(array_keys($this->groups), $metadataGroups)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

isn't more appropriate use count instead of empty ?

return true;
}

return false;
}
}
3 changes: 3 additions & 0 deletions src/JMS/Serializer/Metadata/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class ClassMetadata extends MergeableClassMetadata
public $accessorOrder;
public $customOrder;
public $handlerCallbacks = array();
public $exclusionGroups = false;

public $discriminatorDisabled = false;
public $discriminatorBaseClass;
Expand Down Expand Up @@ -225,6 +226,7 @@ public function serialize()
$this->accessorOrder,
$this->customOrder,
$this->handlerCallbacks,
$this->exclusionGroups,
Copy link
Collaborator

Choose a reason for hiding this comment

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

$this->discriminatorDisabled,
$this->discriminatorBaseClass,
$this->discriminatorFieldName,
Expand All @@ -246,6 +248,7 @@ public function unserialize($str)
$this->accessorOrder,
$this->customOrder,
$this->handlerCallbacks,
$this->exclusionGroups,
$this->discriminatorDisabled,
$this->discriminatorBaseClass,
$this->discriminatorFieldName,
Expand Down
78 changes: 46 additions & 32 deletions src/JMS/Serializer/Metadata/Driver/AnnotationDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ public function loadMetadataForClass(\ReflectionClass $class)
$propertiesAnnotations = array();

$exclusionPolicy = 'NONE';
$excludeAll = false;
$classAccessType = PropertyMetadata::ACCESS_TYPE_PROPERTY;
$readOnlyClass = false;
foreach ($this->reader->getClassAnnotations($class) as $annot) {
Expand All @@ -86,7 +85,8 @@ public function loadMetadataForClass(\ReflectionClass $class)
} elseif ($annot instanceof XmlNamespace) {
$classMetadata->registerNamespace($annot->uri, $annot->prefix);
} elseif ($annot instanceof Exclude) {
$excludeAll = true;
$classMetadata->exclusionGroups = $annot->groups;
$this->sanitizeGroupNames($classMetadata->exclusionGroups, $classMetadata->name);
} elseif ($annot instanceof AccessType) {
$classAccessType = $annot->type;
} elseif ($annot instanceof ReadOnly) {
Expand Down Expand Up @@ -131,23 +131,22 @@ public function loadMetadataForClass(\ReflectionClass $class)
}
}

if ( ! $excludeAll) {
foreach ($class->getProperties() as $property) {
if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) {
continue;
}
$propertiesMetadata[] = new PropertyMetadata($name, $property->getName());
$propertiesAnnotations[] = $this->reader->getPropertyAnnotations($property);
foreach ($class->getProperties() as $property) {
if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) {
continue;
}
$propertiesMetadata[] = new PropertyMetadata($name, $property->getName());
$propertiesAnnotations[] = $this->reader->getPropertyAnnotations($property);
}

foreach ($propertiesMetadata as $propertyKey => $propertyMetadata) {
$isExclude = false;
$isExpose = $propertyMetadata instanceof VirtualPropertyMetadata;
$propertyMetadata->readOnly = $propertyMetadata->readOnly || $readOnlyClass;
$accessType = $classAccessType;
$accessor = array(null, null);
foreach ($propertiesMetadata as $propertyKey => $propertyMetadata) {
$isExpose = $propertyMetadata instanceof VirtualPropertyMetadata;
$propertyMetadata->readOnly = $propertyMetadata->readOnly || $readOnlyClass;
$accessType = $classAccessType;
$accessor = array(null, null);
$exclude = false;

$propertyAnnotations = $propertiesAnnotations[$propertyKey];
$propertyAnnotations = $propertiesAnnotations[$propertyKey];

foreach ($propertyAnnotations as $annot) {
if ($annot instanceof Since) {
Expand All @@ -158,8 +157,6 @@ public function loadMetadataForClass(\ReflectionClass $class)
$propertyMetadata->serializedName = $annot->name;
} elseif ($annot instanceof Expose) {
$isExpose = true;
} elseif ($annot instanceof Exclude) {
$isExclude = true;
} elseif ($annot instanceof Type) {
$propertyMetadata->setType($annot->name);
} elseif ($annot instanceof XmlElement) {
Expand Down Expand Up @@ -194,17 +191,19 @@ public function loadMetadataForClass(\ReflectionClass $class)
$propertyMetadata->readOnly = $annot->readOnly;
} elseif ($annot instanceof Accessor) {
$accessor = array($annot->getter, $annot->setter);
} elseif ($annot instanceof Exclude) {
if (!empty($annot->groups)) {
$annotationSource = $propertyMetadata->class . '->' . $propertyMetadata->name;
$this->sanitizeGroupNames($annot->groups, $annotationSource);
$propertyMetadata->exclusionGroups = $annot->groups;
} else {
$exclude = true;
}

} elseif ($annot instanceof Groups) {
$propertyMetadata->groups = $annot->groups;
foreach ((array) $propertyMetadata->groups as $groupName) {
if (false !== strpos($groupName, ',')) {
throw new InvalidArgumentException(sprintf(
'Invalid group name "%s" on "%s", did you mean to create multiple groups?',
implode(', ', $propertyMetadata->groups),
$propertyMetadata->class.'->'.$propertyMetadata->name
));
}
}
$annotationSource = $propertyMetadata->class . '->' . $propertyMetadata->name;
$this->sanitizeGroupNames($propertyMetadata->groups, $annotationSource);
} elseif ($annot instanceof Inline) {
$propertyMetadata->inline = true;
} elseif ($annot instanceof XmlAttributeMap) {
Expand All @@ -215,14 +214,29 @@ public function loadMetadataForClass(\ReflectionClass $class)
}


if ((ExclusionPolicy::NONE === $exclusionPolicy && ! $isExclude)
|| (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)) {
$propertyMetadata->setAccessor($accessType, $accessor[0], $accessor[1]);
$classMetadata->addPropertyMetadata($propertyMetadata);
}
if ((ExclusionPolicy::NONE === $exclusionPolicy && ! $exclude)
|| (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)) {
$propertyMetadata->setAccessor($accessType, $accessor[0], $accessor[1]);
$classMetadata->addPropertyMetadata($propertyMetadata);
}
}

return $classMetadata;
}

/**
* @param array $groups
* @param string $annotationSource
*/
private function sanitizeGroupNames($groups, $annotationSource)
{
foreach ((array)$groups as $groupName) {
if (false !== strpos($groupName, ',')) {
throw new InvalidArgumentException(sprintf(
'Invalid group name "%s" on "%s", did you mean to create multiple groups?',
implode(', ', $groups), $annotationSource
));
}
}
}
}
6 changes: 5 additions & 1 deletion src/JMS/Serializer/Metadata/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class PropertyMetadata extends BasePropertyMetadata
public $sinceVersion;
public $untilVersion;
public $groups;
public $exclusionGroups = false;
public $serializedName;
public $type;
public $xmlCollection = false;
Expand Down Expand Up @@ -136,6 +137,7 @@ public function serialize()
parent::serialize(),
'xmlEntryNamespace' => $this->xmlEntryNamespace,
'xmlCollectionSkipWhenEmpty' => $this->xmlCollectionSkipWhenEmpty,
'exclusionGroups' => $this->exclusionGroups,
));
}

Expand Down Expand Up @@ -172,7 +174,9 @@ public function unserialize($str)
if (isset($unserialized['xmlCollectionSkipWhenEmpty'])){
$this->xmlCollectionSkipWhenEmpty = $unserialized['xmlCollectionSkipWhenEmpty'];
}

if (isset($unserialized['exclusionGroups'])){
$this->exclusionGroups = $unserialized['exclusionGroups'];
}

parent::unserialize($parentStr);
}
Expand Down
35 changes: 35 additions & 0 deletions tests/JMS/Serializer/Tests/Fixtures/ExclusionGroupsObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace JMS\Serializer\Tests\Fixtures;

use JMS\Serializer\Annotation\Exclude;
use JMS\Serializer\Annotation\Type;

/**
* @Exclude({"noClass"})
*/
class ExclusionGroupsObject
{
/**
* @Type("string")
* @Exclude({"testExclusionGroup1", "testExclusionGroup2"})
*/
public $foo = 'foo';

/**
* @Type("string")
* @Exclude({"testExclusionGroup1"})
*/
public $foo2 = 'foo2';

/**
* @Type("string")
*/
public $bar = 'bar';

/**
* @Type("string")
* @Exclude()
*/
public $neverShown = 'nevershown';
}
Loading