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

Exclude properties based on semver constraints #1361

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
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@
"phpstan/phpdoc-parser": "^0.4 || ^0.5 || ^1.0"
},
"suggest": {
"composer/semver": "Required if you like to exclude properties from serialization based on version constraints.",
"doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.",
"symfony/cache": "Required if you like to use cache functionality.",
"symfony/yaml": "Required if you'd like to use the YAML metadata format."
},
"require-dev": {
"ext-pdo_sqlite": "*",
"composer/semver": "^1.7.4 || ^2.0 || ^3.0",
"doctrine/coding-standard": "^8.1",
"doctrine/orm": "~2.1",
"doctrine/persistence": "^1.3.3|^2.0|^3.0",
Expand Down
7 changes: 4 additions & 3 deletions doc/cookbook/exclusion_strategies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,21 @@ expose them via an API that is consumed by a third-party:
class VersionedObject
{
/**
* @Until("1.0.x")
Copy link
Collaborator

@goetas goetas Nov 8, 2021

Choose a reason for hiding this comment

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

I would not abandon @Until and @Since , mainly for performance reasons. my guess is that composer/semver is way slower that the php's version_compare function, in a large object graph in it can make the difference.

I think that it could be also mentioned in the documentation the potential performance impact.

BTW, i would be curious to see a benchmark about it

* @VersionConstraints("<1.1")
*/
private $name;

/**
* @Since("1.1")
* @VersionConstraints(">=1.1")
* @SerializedName("name")
*/
private $name2;
}

.. note ::

``@Until``, and ``@Since`` both accept a standardized PHP version number.
``@VersionConstraints`` accepts `composer version constraints <https://getcomposer.org/doc/articles/versions.md#writing-version-constraints>`_.
``composer/semver`` must be installed in your project.

If you have annotated your objects like above, you can serializing different
versions like this::
Expand Down
12 changes: 12 additions & 0 deletions doc/reference/annotations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ property was available. If a later version is serialized, then this property is
excluded automatically. The version must be in a format that is understood by
PHP's ``version_compare`` function.

@VersionConstraints
~~~~~~
This annotation can be defined on a property to specify the version constraints
for which this property is available. This property is excluded automatically if
a version is serialized which does not satisfy the constraints. The constraints
must be in a format that is understood by `composer
<https://getcomposer.org/doc/articles/versions.md#writing-version-constraints>`_.

.. note ::

``composer/semver`` must be installed in your project.

@Groups
~~~~~~~
This annotation can be defined on a property to specify if the property
Expand Down
1 change: 1 addition & 0 deletions doc/reference/xml_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ XML Reference
serialized-name="foo"
since-version="1.0"
until-version="1.1"
version-constraints=">=1.0 <1.2"
xml-attribute="true"
access-type="public_method"
accessor-getter="getSomeProperty"
Expand Down
1 change: 1 addition & 0 deletions doc/reference/yml_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ YAML Reference
serialized_name: foo
since_version: 1.0
until_version: 1.1
version_constraints: ">=1.0 <1.2"
groups: [foo, bar]
xml_attribute: true
xml_value: true
Expand Down
24 changes: 24 additions & 0 deletions src/Annotation/VersionConstraints.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

use Composer\Semver\Semver;

/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class VersionConstraints extends Version
{
public function __construct($values = [], ?string $version = null)
{
if (!class_exists(Semver::class)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

what if this check is done in the metadata drivers? so we avoid to call it at runtime each time the annotation is instantiated?

throw new \LogicException(sprintf('composer/semver must be installed to use "%s".', self::class));
}

parent::__construct($values, $version);
}
}
5 changes: 5 additions & 0 deletions src/Exclusion/VersionExclusionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace JMS\Serializer\Exclusion;

use Composer\Semver\Semver;
use JMS\Serializer\Context;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
Expand All @@ -27,6 +28,10 @@ public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorConte

public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext): bool
{
if ((null !== $constraints = $property->versionConstraints) && !Semver::satisfies($this->version, $constraints)) {
return true;
}

if ((null !== $version = $property->sinceVersion) && version_compare($this->version, $version, '<')) {
return true;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Metadata/Driver/AnnotationDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use JMS\Serializer\Annotation\SkipWhenEmpty;
use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\Until;
use JMS\Serializer\Annotation\VersionConstraints;
use JMS\Serializer\Annotation\VirtualProperty;
use JMS\Serializer\Annotation\XmlAttribute;
use JMS\Serializer\Annotation\XmlAttributeMap;
Expand Down Expand Up @@ -182,6 +183,8 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat
$propertyMetadata->sinceVersion = $annot->version;
} elseif ($annot instanceof Until) {
$propertyMetadata->untilVersion = $annot->version;
} elseif ($annot instanceof VersionConstraints) {
$propertyMetadata->versionConstraints = $annot->version;
} elseif ($annot instanceof SerializedName) {
$propertyMetadata->serializedName = $annot->name;
} elseif ($annot instanceof SkipWhenEmpty) {
Expand Down
4 changes: 4 additions & 0 deletions src/Metadata/Driver/XmlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path):
$pMetadata->untilVersion = (string) $version;
}

if (null !== $constraints = $pElem->attributes()->{'version-constraints'}) {
$pMetadata->versionConstraints = (string) $constraints;
}

if (null !== $serializedName = $pElem->attributes()->{'serialized-name'}) {
$pMetadata->serializedName = (string) $serializedName;
}
Expand Down
4 changes: 4 additions & 0 deletions src/Metadata/Driver/YamlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ protected function loadMetadataFromFile(ReflectionClass $class, string $file): ?
$pMetadata->untilVersion = (string) $pConfig['until_version'];
}

if (isset($pConfig['version_constraints'])) {
$pMetadata->versionConstraints = (string) $pConfig['version_constraints'];
}

if (isset($pConfig['exclude_if'])) {
$pMetadata->excludeIf = $this->parseExpression((string) $pConfig['exclude_if']);
}
Expand Down
9 changes: 9 additions & 0 deletions src/Metadata/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class PropertyMetadata extends BasePropertyMetadata
* @var string
*/
public $untilVersion;
/**
* @var string
*/
public $versionConstraints;
/**
* @var string[]
*/
Expand Down Expand Up @@ -239,6 +243,7 @@ public function serialize()
'excludeIf' => $this->excludeIf,
'skipWhenEmpty' => $this->skipWhenEmpty,
'forceReflectionAccess' => $this->forceReflectionAccess,
'versionConstraints' => $this->versionConstraints,
]);
}

Expand Down Expand Up @@ -304,6 +309,10 @@ protected function unserializeProperties(string $str): string
$this->forceReflectionAccess = $unserialized['forceReflectionAccess'];
}

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

return $parentStr;
}
}
22 changes: 22 additions & 0 deletions tests/Fixtures/ObjectWithVersionedVirtualProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\Since;
use JMS\Serializer\Annotation\Until;
use JMS\Serializer\Annotation\VersionConstraints;
use JMS\Serializer\Annotation\VirtualProperty;

/**
Expand All @@ -19,12 +20,18 @@
* options={@Until("8")}
* )
* @VirtualProperty(
* "classsemver",
* exp="object.getVirtualValue(61)",
* options={@VersionConstraints("^6.1")}
* )
* @VirtualProperty(
* "classhigh",
* exp="object.getVirtualValue(8)",
* options={@Since("6")}
* )
*/
#[VirtualProperty(name: 'classlow', exp: 'object.getVirtualValue(1)', options: [[Until::class, ['8']]])]
#[VirtualProperty(name: 'classsemver', exp: 'object.getVirtualValue(61)', options: [[VersionConstraints::class, ['^6.1']]])]
#[VirtualProperty(name: 'classhigh', exp: 'object.getVirtualValue(8)', options: [[Since::class, ['6']]])]
class ObjectWithVersionedVirtualProperties
{
Expand All @@ -43,6 +50,21 @@ public function getVirtualLowValue()
return 1;
}

/**
* @Groups({"versions"})
* @VirtualProperty
* @SerializedName("semver")
* @VersionConstraints("6.1")
*/
#[Groups(groups: ['versions'])]
#[VirtualProperty]
#[SerializedName(name: 'semver')]
#[VersionConstraints('^6.1')]
public function getVirtualSemverValue()
{
return 61;
}

/**
* @Groups({"versions"})
* @VirtualProperty
Expand Down
2 changes: 1 addition & 1 deletion tests/Serializer/BaseSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ public function testVirtualVersions()

self::assertEquals(
$this->getContent('virtual_properties_all'),
$serializer->serialize(new ObjectWithVersionedVirtualProperties(), $this->getFormat(), SerializationContext::create()->setVersion('7'))
$serializer->serialize(new ObjectWithVersionedVirtualProperties(), $this->getFormat(), SerializationContext::create()->setVersion('6.1'))
);

self::assertEquals(
Expand Down
2 changes: 1 addition & 1 deletion tests/Serializer/JsonSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ protected function getContent($key)
$outputs['virtual_properties'] = '{"exist_field":"value","virtual_value":"value","test":"other-name","typed_virtual_property":1}';
$outputs['virtual_properties_low'] = '{"classlow":1,"low":1}';
$outputs['virtual_properties_high'] = '{"classhigh":8,"high":8}';
$outputs['virtual_properties_all'] = '{"classlow":1,"classhigh":8,"low":1,"high":8}';
$outputs['virtual_properties_all'] = '{"classlow":1,"classsemver":61,"classhigh":8,"low":1,"semver":61,"high":8}';
$outputs['nullable'] = '{"foo":"bar","baz":null,"0":null}';
$outputs['nullable_skip'] = '{"foo":"bar"}';
$outputs['person_secret_show'] = '{"name":"mike","gender":"f"}';
Expand Down
2 changes: 2 additions & 0 deletions tests/Serializer/xml/virtual_properties_all.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<result>
<classlow>1</classlow>
<classsemver>61</classsemver>
<classhigh>8</classhigh>
<low>1</low>
<semver>61</semver>
<high>8</high>
</result>