Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[WIP] [Serializer] Alias support #15200

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

Closed
wants to merge 1 commit into from
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
58 changes: 58 additions & 0 deletions src/Symfony/Component/Serializer/Annotation/Alias.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Serializer\Annotation;

use Symfony\Component\Serializer\Exception\InvalidArgumentException;

/**
* Annotation class for @Alias().
*
* @Annotation
* @Target({"PROPERTY", "METHOD"})
*
* @author Kévin Dunglas <[email protected]>
*/
class Alias
Copy link
Member

Choose a reason for hiding this comment

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

I like the SerializedName from JMS more than alias, which seems to tell me less about what it does. Any reason for using alias specifically?

Copy link
Contributor

Choose a reason for hiding this comment

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

@weaverryan agree with you, but Alias is still understandable

{
/**
* @var string
*/
private $name;

/**
* @param array $data
*
* @throws InvalidArgumentException
*/
public function __construct(array $data)
{
if (!isset($data['value']) || !$data['value']) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" cannot be empty.', get_class($this)));
}

if (!is_string($data['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a string.', get_class($this)));
}

$this->name = $data['value'];
}

/**
* Gets name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
}
31 changes: 30 additions & 1 deletion src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public $groups = array();

/**
* @var string|null
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getAlias()} instead.
*/
public $alias;

/**
* Constructs a metadata for the given attribute.
*
Expand Down Expand Up @@ -72,6 +81,22 @@ public function getGroups()
return $this->groups;
}

/**
* {@inheritdoc}
*/
public function setAlias($alias)
{
$this->alias = $alias;
}

/**
* {@inheritdoc}
*/
public function getAlias()
{
return $this->alias;
}

/**
* {@inheritdoc}
*/
Expand All @@ -80,6 +105,10 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
foreach ($attributeMetadata->getGroups() as $group) {
$this->addGroup($group);
}

if (!$this->alias) {
$this->alias = $attributeMetadata->getAlias();
}
}

/**
Expand All @@ -89,6 +118,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
*/
public function __sleep()
{
return array('name', 'groups');
return array('name', 'groups', 'alias');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ public function addGroup($group);
*/
public function getGroups();

/**
* Sets the normalization alias of this attribute.
*
* @param string|null $alias
*/
public function setAlias($alias);

/**
* Gets the normalization alias of this attribute.
*
* @return string|null
*/
public function getAlias();

/**
* Merges an {@see AttributeMetadataInterface} with in the current one.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Mapping\Loader;

use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Serializer\Annotation\Alias;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
Expand Down Expand Up @@ -55,13 +56,17 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
}

if ($property->getDeclaringClass()->name === $className) {
foreach ($this->reader->getPropertyAnnotations($property) as $groups) {
if ($groups instanceof Groups) {
foreach ($groups->getGroups() as $group) {
foreach ($this->reader->getPropertyAnnotations($property) as $annotation) {
if ($annotation instanceof Groups) {
foreach ($annotation->getGroups() as $group) {
$attributesMetadata[$property->name]->addGroup($group);
}
}

if ($annotation instanceof Alias) {
$attributesMetadata[$property->name]->setAlias($annotation->getName());
}

$loaded = true;
}
}
Expand All @@ -70,25 +75,29 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
foreach ($reflectionClass->getMethods() as $method) {
if ($method->getDeclaringClass()->name === $className) {
foreach ($this->reader->getMethodAnnotations($method) as $groups) {
if (!preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Sometimes the getter name is simply the name of the property without any prefix. I think it would be good to support it too.

continue;
}

$attributeName = lcfirst($matches[2]);

if (isset($attributesMetadata[$attributeName])) {
$attributeMetadata = $attributesMetadata[$attributeName];
} else {
$attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName);
$classMetadata->addAttributeMetadata($attributeMetadata);
}

if ($groups instanceof Groups) {
if (preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches)) {
$attributeName = lcfirst($matches[2]);

if (isset($attributesMetadata[$attributeName])) {
$attributeMetadata = $attributesMetadata[$attributeName];
} else {
$attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName);
$classMetadata->addAttributeMetadata($attributeMetadata);
}

foreach ($groups->getGroups() as $group) {
$attributeMetadata->addGroup($group);
}
} else {
throw new MappingException(sprintf('Groups on "%s::%s" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
foreach ($groups->getGroups() as $group) {
$attributeMetadata->addGroup($group);
}
}

if ($annotation instanceof Alias) {
$attributeMetadata[$property->name]->setAlias($annotation->getName());
}

$loaded = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
foreach ($attribute->group as $group) {
$attributeMetadata->addGroup((string) $group);
}

if ($alias = (string) $attribute->alias) {
$attributeMetadata->setAlias($alias);
}
}

return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
$attributeMetadata->addGroup($group);
}
}

if (isset($data['alias'])) {
$attributeMetadata->setAlias($data['alias']);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@
<xsd:complexType name="attribute">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains serialization groups for a attributes. The name of the attribute should be given in the "name" option.
Contains serialization metadata for a attributes. The name of the attribute should be given in the "name" attribute.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="group" type="xsd:string" maxOccurs="unbounded" />
<xsd:choice>
<xsd:element name="group" type="xsd:string" maxOccurs="unbounded" />
</xsd:choice>
<xsd:element name="alias" minOccurs="0" maxOccurs="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Serializer\NameConverter;

use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;

/**
* Converts name of properties using the alias specified in the attribute metadata.
*
* @author Kévin Dunglas <[email protected]>
*/
class AliasNameConverter implements NameConverterInterface
{
/**
* @var ClassMetadataFactoryInterface
*/
private $classMetadataFactory;

public function __construct(ClassMetadataFactoryInterface $classMetadataFactory)
{
$this->classMetadataFactory = $classMetadataFactory;
}

/**
* {@inheritdoc}
*/
public function normalize($object, $propertyName)
{
$attributesMetadata = $this->getAttributesMetadata($object);

if (isset($attributesMetadata[$propertyName]) && $alias = $attributesMetadata[$propertyName]->getAlias()) {
return $alias;
}

return $propertyName;
}

/**
* {@inheritdoc}
*/
public function denormalize($object, $propertyName)
{
$attributesMetadata = $this->getAttributesMetadata($object);

// TODO: cache that
foreach ($attributesMetadata as $attributeMetadata) {
$alias = $attributeMetadata->getAlias();
if ($propertyName === $alias) {
return $alias;
}
}

return $propertyName;
}

/**
* Gets attributes metadata.
*
* @param string|object $class
*
* @return array
*/
private function getAttributesMetadata($class)
{
if (is_object($class)) {
$class = get_class($class);
}

return $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
namespace Symfony\Component\Serializer\NameConverter;

/**
* CamelCase to Underscore name converter.
* Converts names of properties from CamelCase to Underscore.
*
* @author Kévin Dunglas <[email protected]>
*/
Expand Down Expand Up @@ -40,7 +40,7 @@ public function __construct(array $attributes = null, $lowerCamelCase = true)
/**
* {@inheritdoc}
*/
public function normalize($propertyName)
public function normalize($object, $propertyName)
{
if (null === $this->attributes || in_array($propertyName, $this->attributes)) {
$snakeCasedName = '';
Expand All @@ -63,7 +63,7 @@ public function normalize($propertyName)
/**
* {@inheritdoc}
*/
public function denormalize($propertyName)
public function denormalize($object, $propertyName)
{
$camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ interface NameConverterInterface
/**
* Converts a property name to its normalized value.
*
* @param string $propertyName
* @param string|object $class
* @param string $propertyName
*
* @return string
*/
public function normalize($propertyName);
public function normalize($class, $propertyName);
Copy link
Member Author

Choose a reason for hiding this comment

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

An easy way to limit the BC break is to inverse parameters (same for denormalize).

Copy link
Member

Choose a reason for hiding this comment

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

According to the BC promise, you'd need to make $class the second argument and make it optional. That's not ideal, but I don't think there's another way

Copy link
Member

Choose a reason for hiding this comment

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

@weaverryan even if you make it optional, it is still a BC break: people implementing the interface will have to change their code, otherwise they will get PHP errors about signature mismatches.

Copy link
Member

Choose a reason for hiding this comment

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

I'm reading this from (my recent new best friend) out BC promise: http://symfony.com/doc/current/contributing/code/bc.html#changing-interfaces for non-API interfaces, we are allowed to add optional arguments to methods. Am I reading that wrong? Or perhaps that's not correct?

Copy link
Member

Choose a reason for hiding this comment

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

Well, it says Should be avoided, because it is still a BC break. The Regular column is a column saying "We don't actually provide BC here, but only partial BC".

And actually, I don't think any code added since 2.3 has actually been tagged as @api. Many things are extension points for our users where we care about BC as if they were flagged as @api even though it was not done.
Given that we became better at writing BC layers than a few years back, we may decide to drop the distinction between Regular and API btw. The default behavior of a class should be full BC. The meaningful distinction would be Regular vs Internal (where internal classes not meant to be used by users are less strict on BC)


/**
* Converts a property name to its denormalized value.
*
* @param string $propertyName
* @param string|object $class
* @param string $propertyName
*
* @return string
*/
public function denormalize($propertyName);
public function denormalize($class, $propertyName);
}
Loading