Skip to content

[Serializer] Add a @Mapping Annotation #39048

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

Open
wants to merge 2 commits into
base: 7.4
Choose a base branch
from

Conversation

bertrandseurot
Copy link

@bertrandseurot bertrandseurot commented Nov 10, 2020

Q A
Branch? 5.x
Bug fix? no
New feature? yes
Deprecations? no
Tickets
License MIT
Doc PR None yet
  • update CHANGELOG
  • PR for documentation

Add a @mapping annotation applicable to a class to configure the groups, the maxDepth, and the serializedName of a set of attributes.

It allows you to configure the serialization of attributes from traits, when specific to each class, or when the trait comes from a third party library (or from an hexagone of your code you don't want to depend on the serializer)

The "ignore" metadata is currently not supported, simply for readability reasons (an @IgnoreAttributes annotation would probably be better) but could easily implemented.

The "maxDepth" parameter on the root of the annotation could also be removed, to avoid unnecessary metadata storage for attributes not using it.

This annotation also allows you to configure the class attributes at the same place, even if yaml or xml would be a better choice for that in a fresh project.

Let me know if there is any chance this feature to be merged, so I will start working on the documentation.
Note : the fabbot does not accept the php8 attribute, is it too soon for that ?

Main use case example

Let say you want to use a vendor trait, or a very common behavior trait from your application. Sometimes you want some properties to be serialized, sometimes not. Here an example with BlameableEntity from Gedmo DoctrineExtension (you probably want to write your own trait, it just illustrates the confidentiality need):

<?php
namespace Gedmo\Blameable\Traits;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * Blameable Trait, usable with PHP >= 5.4
 *
 * @author David Buchmann <mail@davidbu.ch>
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */
trait BlameableEntity
{
    /**
     * @var string
     * @Gedmo\Blameable(on="create")
     * @ORM\Column(nullable=true)
     */
    protected $createdBy;

    /**
     * @var string
     * @Gedmo\Blameable(on="update")
     * @ORM\Column(nullable=true)
     */
    protected $updatedBy;

   // ...
}

For a BlogPost, you may want expose thoses properties

/**
 * @Mapping(
 *     attributes={"createdBy", "updatedBy"},
 *     groups="BlogPost:read"
 * )
*/
class BlogPost{
    use BlameableEntity;

    // ...
}

For an AnonymousSuggestion, you probably don't want to leak those informations:

class AnonymousSuggestion{
    use BlameableEntity;

    // ...
}

For a DisruptiveIdeaDraft, you may want to expose updatedBy, but createdBy only for the managers (because in a first time, you want your team not to be influenced by who created the thing)

/**
 * @Mapping(
 *     attributes={ "updatedBy"},
 *     groups="DisruptiveIdeaDraft:read"
 * )
 * @Mapping(
 *     attributes={"createdBy"},
 *     groups="DisruptiveIdeaDraft:managers:read"
 * )
*/
class DisruptiveIdeaDraft{
    use BlameableEntity;

    // ...
}

or

/**
 * @Mapping(
 *     attributes={
 *        {"name": "updatedBy", "groups"="DisruptiveIdeaDraft:read"},
 *        {"name": "createdBy", "groups"="DisruptiveIdeaDraft:managers:read"},
 *     }
 * )
*/
class DisruptiveIdeaDraft{
    use BlameableEntity;

    // ...
}

How it works

Full usage example to configure groups :

/**
 * @Mapping(
 *     attributes={"foo", "bar", {"name": "baz", "groups"="foobar:special"}},
 *     groups="fooBar:read"
 * )
 * @Mapping(
 *     attributes={"qux", "quux", "quuz", "propertyFromSomeTrait"},
 *     groups={"fooBar:read", "foobar:write"}
 * )
*/
class Foobar{
    use SomeTrait;

    // ...
}

you can configure maxDepth generally, or per attribute :

/**
 * @Mapping(
 *     attributes={"foo", "bar", {"name": "baz", "groups"="foobar:special", maxDepth=3}},
 *     groups="fooBar:read",
 *     maxDepth=2
 * )
*/

you can specify a serializedName per attribute :

/**
 * @Mapping(
 *     attributes={"foo", "bar", {"name": "baz", "groups"="foobar:special", maxDepth=3, serializedName="fromFooBar"}},
 *     groups="fooBar:read",
 *     maxDepth=2
 * )
*/

Annotations priorities

Currently, how it works :

For maxDepth & serializedName:
method annotation > property annotation > method annotation from parent class > property annotation from parent class > method annotation from interfaces

For groups:
groups from any annotation are cumulated, not replaced

How it works with this annotation:

For maxDepth & serializedName:
method annotation > property annotation > class annotation (attribute specific) > class annotation (root parameter for maxDepth) > method annotation from parent class > property annotation from parent class > class annotation from parent class (attribute specific) > class annotation from parent class (root parameter for maxDepth) > method annotation from interface > class annotation from interface (attribute specific) > class annotation from interface (root parameter for maxDepth)

NOTE : looks complicated, but it's the same logic sequence

For groups:
groups from any annotation are cumulated, not replaced

@carsonbot
Copy link

Hey!

I see that this is your first PR. That is great! Welcome!

Symfony has a contribution guide which I suggest you to read.

In short:

  • Always add tests
  • Keep backward compatibility (see https://symfony.com/bc).
  • Bug fixes must be submitted against the lowest maintained branch where they apply (see https://symfony.com/releases)
  • Features and deprecations must be submitted against the 5.x branch.

Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change.

When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor!
If this PR is merged in a lower version branch, it will be merged up to all maintained branches within a few days.

I am going to sit back now and wait for the reviews.

Cheers!

Carsonbot

@bertrandseurot
Copy link
Author

@ro0NL This annotation has not be created to replace per property annotations, but to to fill the lack of possibility to configure properties/method of a trait from the using class. This is possible in yaml, in xml but not yet with annotations (encouraged by the documentation). When you have to deal with this lack in a project with dozen of entities configured with annotations, you can : switch all your configuration to yaml / xml (ouch !), switch ONLY the concerned class to yaml / xml (yuck) or find a hack (a little less yuck) or a solution. I think this feature, as a lower-level API, could help. The "readability" argument in my description has finaly no importance (It's a side-effect which could be a bonus for whose like it).

Is it mutually exclusive?

Currently, how it works :

For maxDepth & serializedName:
method annotation > property annotation > method annotation from parent class > property annotation from parent class > method annotation from interfaces

For groups:
groups from any annotation are cumulated, not replaced

How it works with this annotation:

For maxDepth & serializedName:
method annotation > property annotation > class annotation (attribute specific) > class annotation (root parameter for maxDepth) > method annotation from parent class > property annotation from parent class > class annotation from parent class (attribute specific) > class annotation from parent class (root parameter for maxDepth) > method annotation from interface > class annotation from interface (attribute specific) > class annotation from interface (root parameter for maxDepth)

For groups:
groups from any annotation are cumulated, not replaced

So I think it is consistent with existing codebase.

i suggest yaml or xml, for clarity and separating concerns.

Thanks for your suggestion, I just thinked that the three options should be a developper choice, and equivalent in terms of possibilities.

You made me realized this PR description was worst that I thinked (emphasizing minor subjective benefits instead of emphazing technical possibilities additions), thanks for that.

Add a @mapping annotation applicable to a class to configure the groups, the maxDepth, and the serializedName of a set of attributes. This could be used to ease readability when a class own a lot of attributes and the same groups are applied on a recurring basis. This also ease to configure attributes embedded in the traits used by this class, since you can apply some class-specific groups to them (currently a little bit tricky id you don't use the yaml / xml format).

The "ignore" metadata is currently not supported, simply for readability reasons (an @IgnoreAttributes annotation would probably be better) bet could easily implemented.

The "maxDepth" parameter on the root of the annotation could also be removed, to avoid unnecessary metadata storage for attributes not using it.
@bertrandseurot bertrandseurot force-pushed the serializer/annotations-for-classes branch from db38013 to 3b5d86f Compare November 12, 2020 08:56
@bertrandseurot
Copy link
Author

You may want to use traits from tierce-party bundle, and serialize its properties. The only way to achieve that via annotations is to get a class annotation. Totally open to any other form of annotation allowing that (maybe decline all existing annotation for class usage ? Isn't it more complex to maintain / compute ?)

@bertrandseurot
Copy link
Author

You suggest to redeclare all the wanted properties of the trait in each class using it ? (BTW, according to this snippet, it is nice to discover you have to redeclare all the attributes you want to keep.) Could be a life-saver, but it limits the interest of using the trait.

@carsonbot carsonbot changed the title [WIP] [Serializer] Add a @Mapping Annotation [Serializer] [WIP] Add a @Mapping Annotation Nov 12, 2020
@bertrandseurot bertrandseurot changed the title [Serializer] [WIP] Add a @Mapping Annotation [Serializer] Add a @Mapping Annotation Nov 13, 2020
@bertrandseurot
Copy link
Author

it also saves you from upstream bc breaks :')

You live only once, @ro0NL , you live only once !

More seriously, I updated the description, thank you for your comments which helped make it more relevant.

@fabpot fabpot modified the milestones: 5.4, 6.1 Oct 30, 2021
@fabpot fabpot modified the milestones: 6.1, 6.2 May 20, 2022
@nicolas-grekas nicolas-grekas modified the milestones: 6.2, 6.3 Nov 5, 2022
@nicolas-grekas nicolas-grekas modified the milestones: 6.3, 6.4 May 23, 2023
@nicolas-grekas nicolas-grekas modified the milestones: 6.4, 7.1 Nov 15, 2023
@xabbuh xabbuh modified the milestones: 7.1, 7.2 May 15, 2024
@fabpot fabpot removed this from the 7.2 milestone Nov 20, 2024
@fabpot fabpot added this to the 7.3 milestone Nov 20, 2024
@fabpot fabpot modified the milestones: 7.3, 7.4 May 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants