diff --git a/composer.json b/composer.json index c0ea8f156..1cc3e225f 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ }, "require-dev": { "ext-pdo_sqlite": "*", + "composer/semver": "^1.7.4 || ^2 || ^3.2", "doctrine/coding-standard": "^8.1", "doctrine/orm": "~2.1", "doctrine/persistence": "^1.3.3|^2.0|^3.0", diff --git a/doc/cookbook/exclusion_strategies.rst b/doc/cookbook/exclusion_strategies.rst index 90dab33dd..97ce43a6e 100644 --- a/doc/cookbook/exclusion_strategies.rst +++ b/doc/cookbook/exclusion_strategies.rst @@ -57,12 +57,12 @@ expose them via an API that is consumed by a third-party: class VersionedObject { /** - * @Until("1.0.x") + * @Semver("~1.0.0") */ private $name; /** - * @Since("1.1") + * @Semver(">=1.1") * @SerializedName("name") */ private $name2; @@ -70,7 +70,8 @@ expose them via an API that is consumed by a third-party: .. note :: - ``@Until``, and ``@Since`` both accept a standardized PHP version number. + ``@Semver`` accepts `composer 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:: diff --git a/doc/reference/annotations.rst b/doc/reference/annotations.rst index 0808b1366..884ba7567 100644 --- a/doc/reference/annotations.rst +++ b/doc/reference/annotations.rst @@ -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. +@Semver +~~~~~~ +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 +`_. + +.. note :: + + ``composer/semver`` must be installed in your project. + @Groups ~~~~~~~ This annotation can be defined on a property to specify if the property diff --git a/doc/reference/xml_reference.rst b/doc/reference/xml_reference.rst index 239b8310b..afe047324 100644 --- a/doc/reference/xml_reference.rst +++ b/doc/reference/xml_reference.rst @@ -79,6 +79,7 @@ XML Reference serialized-name="foo" since-version="1.0" until-version="1.1" + semver-constraints=">=1.0 <1.2" xml-attribute="true" access-type="public_method" accessor-getter="getSomeProperty" diff --git a/doc/reference/yml_reference.rst b/doc/reference/yml_reference.rst index b7ba44ddc..740a78984 100644 --- a/doc/reference/yml_reference.rst +++ b/doc/reference/yml_reference.rst @@ -51,6 +51,7 @@ YAML Reference serialized_name: foo since_version: 1.0 until_version: 1.1 + semver_constraints: ">=1.0 <1.2" groups: [foo, bar] xml_attribute: true xml_value: true diff --git a/src/Annotation/Semver.php b/src/Annotation/Semver.php new file mode 100644 index 000000000..2ff8602a4 --- /dev/null +++ b/src/Annotation/Semver.php @@ -0,0 +1,24 @@ +semverConstraints) && Semver::satisfies($this->version, $constraints)) { + if ((null !== $constraints = $property->semverConstraints) && !Semver::satisfies($this->version, $constraints)) { + //var_dump('version: '.$this->version.', constraints: '.$constraints.', satisfies: '.var_export(Semver::satisfies($this->version, $constraints), true)); + return true; + } + if ((null !== $version = $property->sinceVersion) && version_compare($this->version, $version, '<')) { return true; } diff --git a/src/Metadata/Driver/AnnotationDriver.php b/src/Metadata/Driver/AnnotationDriver.php index 8cda61bc3..1f364ebfa 100644 --- a/src/Metadata/Driver/AnnotationDriver.php +++ b/src/Metadata/Driver/AnnotationDriver.php @@ -19,6 +19,7 @@ use JMS\Serializer\Annotation\PostSerialize; use JMS\Serializer\Annotation\PreSerialize; use JMS\Serializer\Annotation\ReadOnlyProperty; +use JMS\Serializer\Annotation\Semver; use JMS\Serializer\Annotation\SerializedName; use JMS\Serializer\Annotation\Since; use JMS\Serializer\Annotation\SkipWhenEmpty; @@ -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 Semver) { + $propertyMetadata->semverConstraints = $annot->version; } elseif ($annot instanceof SerializedName) { $propertyMetadata->serializedName = $annot->name; } elseif ($annot instanceof SkipWhenEmpty) { diff --git a/src/Metadata/Driver/XmlDriver.php b/src/Metadata/Driver/XmlDriver.php index e0238f504..e2262888b 100644 --- a/src/Metadata/Driver/XmlDriver.php +++ b/src/Metadata/Driver/XmlDriver.php @@ -219,6 +219,10 @@ protected function loadMetadataFromFile(\ReflectionClass $class, string $path): $pMetadata->untilVersion = (string) $version; } + if (null !== $constraints = $pElem->attributes()->{'semver-constraints'}) { + $pMetadata->semverConstraints = (string) $constraints; + } + if (null !== $serializedName = $pElem->attributes()->{'serialized-name'}) { $pMetadata->serializedName = (string) $serializedName; } diff --git a/src/Metadata/Driver/YamlDriver.php b/src/Metadata/Driver/YamlDriver.php index e7880c6a1..0f1bdbd91 100644 --- a/src/Metadata/Driver/YamlDriver.php +++ b/src/Metadata/Driver/YamlDriver.php @@ -189,6 +189,10 @@ protected function loadMetadataFromFile(ReflectionClass $class, string $file): ? $pMetadata->untilVersion = (string) $pConfig['until_version']; } + if (isset($pConfig['semver_constraints'])) { + $pMetadata->semverConstraints = (string) $pConfig['semver_constraints']; + } + if (isset($pConfig['exclude_if'])) { $pMetadata->excludeIf = $this->parseExpression((string) $pConfig['exclude_if']); } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index f7d0f513c..538bb035a 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -21,6 +21,10 @@ class PropertyMetadata extends BasePropertyMetadata * @var string */ public $untilVersion; + /** + * @var string + */ + public $semverConstraints; /** * @var string[] */ @@ -239,6 +243,7 @@ public function serialize() 'excludeIf' => $this->excludeIf, 'skipWhenEmpty' => $this->skipWhenEmpty, 'forceReflectionAccess' => $this->forceReflectionAccess, + 'semverConstraints' => $this->semverConstraints, ]); } @@ -304,6 +309,10 @@ protected function unserializeProperties(string $str): string $this->forceReflectionAccess = $unserialized['forceReflectionAccess']; } + if (isset($unserialized['semverConstraints'])) { + $this->semverConstraints = $unserialized['semverConstraints']; + } + return $parentStr; } } diff --git a/tests/Fixtures/ObjectWithVersionedVirtualProperties.php b/tests/Fixtures/ObjectWithVersionedVirtualProperties.php index 30acf5dfc..c0148d4a6 100644 --- a/tests/Fixtures/ObjectWithVersionedVirtualProperties.php +++ b/tests/Fixtures/ObjectWithVersionedVirtualProperties.php @@ -5,6 +5,7 @@ namespace JMS\Serializer\Tests\Fixtures; use JMS\Serializer\Annotation\Groups; +use JMS\Serializer\Annotation\Semver; use JMS\Serializer\Annotation\SerializedName; use JMS\Serializer\Annotation\Since; use JMS\Serializer\Annotation\Until; @@ -19,12 +20,18 @@ * options={@Until("8")} * ) * @VirtualProperty( + * "classsemver", + * exp="object.getVirtualValue(61)", + * options={@Semver("^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: [[Semver::class, ['^6.1']]])] #[VirtualProperty(name: 'classhigh', exp: 'object.getVirtualValue(8)', options: [[Since::class, ['6']]])] class ObjectWithVersionedVirtualProperties { @@ -43,6 +50,21 @@ public function getVirtualLowValue() return 1; } + /** + * @Groups({"versions"}) + * @VirtualProperty + * @SerializedName("semver") + * @Semver("6.1") + */ + #[Groups(groups: ['versions'])] + #[VirtualProperty] + #[SerializedName(name: 'semver')] + #[Semver('^6.1')] + public function getVirtualSemverValue() + { + return 61; + } + /** * @Groups({"versions"}) * @VirtualProperty diff --git a/tests/Serializer/BaseSerializationTest.php b/tests/Serializer/BaseSerializationTest.php index ec18bac09..1b7bc659e 100644 --- a/tests/Serializer/BaseSerializationTest.php +++ b/tests/Serializer/BaseSerializationTest.php @@ -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( diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index e76c66599..84d1886e8 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -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"}'; diff --git a/tests/Serializer/xml/virtual_properties_all.xml b/tests/Serializer/xml/virtual_properties_all.xml index 80fb1917f..2cf8ea4fd 100644 --- a/tests/Serializer/xml/virtual_properties_all.xml +++ b/tests/Serializer/xml/virtual_properties_all.xml @@ -1,7 +1,9 @@ 1 + 61 8 1 + 61 8