Serializing Entity Relation only to Id with JMS Serializer
Asked Answered
D

6

26

I'm trying to serialize a entity relation with JMS Serializer.

Here is the Entity:

class Ad
{ 

    /**
     * @Type("string")
     * @Groups({"manage"})
     * 
     * @var string
     */
    private $description;

    /**
     * @Type("Acme\SearchBundle\Entity\Country")
     * @Groups({"manage"})
     * 
     * @var \Acme\SearchBundle\Entity\Country
     */
    private $country;

    /**
     * @Type("string")
     * @Groups({"manage"})
     * 
     * @var string
     */
    private $title;

    /**
     * Set description
     *
     * @param string $description
     * @return Ad
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get description
     *
     * @return string 
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Set country
     *
     * @param \Acme\SearchBundle\Entity\Country $country
     * @return Ad
     */
    public function setCountry($country)
    {
        $this->country= $country;

        return $this;
    }

    /**
     * Get country
     *
     * @return string 
     */
    public function getCountry()
    {
        return $this->country;
    }

    /**
     * Set title
     *
     * @param string $title
     * @return Ad
     */
    public function setTituloanuncio($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Get title
     *
     * @return string 
     */
    public function getTitle()
    {
        return $this->title;
    }

}

And the Entity of the relationship:

class Country
{

    /**
     * @Type("string")
     * @Groups("manage")
     * 
     * @var string
     */
    private $id;

    /**
     * @Type("string")
     * @Groups("admin")
     * 
     * @var string
     */
    private $description;

    /**
     * Set description
     * @Groups("")
     *
     * @param string $description
     * @return Country
     */
    public function setDescripcionpais($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get description
     *
     * @return string 
     */
    public function getDescription()
    {
        return $this->description;
    }

    }

    /**
     * Get id
     *
     * @return string 
     */
    public function getId()
    {
        return $this->id;
    }
}

I serialize the entity but I don't know how to convert the country attribute into a simple field.

I get this result in json:

{"description":"foo", "title":"bar", "country":{"id":"en"} }

But I want to get the id field of the country like this:

{"description":"foo", "title":"bar", "country": "en" }

It is possible with JMS Serializer?

Thank you.

[EDIT]

@VirtualProperty doesn't work.

Deneb answered 13/4, 2013 at 22:19 Comment(0)
P
28

Yes, you could use @VirtualProperty annotation:

/**
 * @VirtualProperty
 * @SerializedName("foo")
 */
public function bar()
{
    return $this->country->getCode();
}

But be aware when it comes to deserialization:

@VirtualProperty This annotation can be defined on a method to indicate that the data returned by the method should appear like a property of the object.

> Note: This only works for serialization and is completely ignored during deserialization.

Hope this helps...

Phytology answered 14/4, 2013 at 8:39 Comment(4)
Thank you! Perfect for me. I just needed to serialize.Deneb
I apologize, I was on a vacation and didn't have access to Internet. I noticed that you unaccepted then accepted answer. Did you manage to make it work?Phytology
Sorry, I've gotten it to work. The problem was that I had not saved the Country object in the object ad. Thank you very much.Deneb
YAML config reference also got updated: github.com/schmittjoh/serializer/commit/…Schaller
F
19

Just to follow answered question:

If you don't like writing one method for each relation you have - just write your own handler. It's easy like

final class RelationsHandler
{
    /**
     * @var EntityManagerInterface
     */
    private $manager;

    /**
     * RelationsHandler constructor.
     *
     * @param EntityManagerInterface $manager
     */
    public function __construct(EntityManagerInterface $manager) { $this->manager = $manager; }


    public function serializeRelation(JsonSerializationVisitor $visitor, $relation, array $type, Context $context)
    {
        if ($relation instanceof \Traversable) {
            $relation = iterator_to_array($relation);
        }

        if (is_array($relation)) {
            return array_map([$this, 'getSingleEntityRelation'], $relation);
        }

        return $this->getSingleEntityRelation($relation);
    }

    /**
     * @param $relation
     *
     * @return array|mixed
     */
    protected function getSingleEntityRelation($relation)
    {
        $metadata = $this->manager->getClassMetadata(get_class($relation));

        $ids = $metadata->getIdentifierValues($relation);
        if (!$metadata->isIdentifierComposite) {
            $ids = array_shift($ids);
        }

        return $ids;
    }
}

Register the Handler

  jms_serializer.handler.relation:
      class: MyBundle\RelationsHandler
      arguments:
      - "@doctrine.orm.entity_manager"
      tags:
      - { name: jms_serializer.handler, type: Relation, direction: serialization, format: json, method: serializeRelation}
      - { name: jms_serializer.handler, type: Relation, direction: deserialization, format: json, method: deserializeRelation}
      - { name: jms_serializer.handler, type: Relation<?>, direction: serialization, format: json, method: serializeRelation}
      - { name: jms_serializer.handler, type: Relation<?>, direction: deserialization, format: json, method: deserializeRelation}

This allows you to replace virtual getter methods with `Type("Relation").

If you also want't to deserialize relation - you should tell each @Type("Relation") the classname (@Type("Relation<FQCN>")) which it should deserialize to or wrap the metadata driver with one which do it for you.

    public function deserializeRelation(JsonDeserializationVisitor $visitor, $relation, array $type, Context $context)
    {
        $className = isset($type['params'][0]['name']) ? $type['params'][0]['name'] : null;

        if (!class_exists($className, false)) {
            throw new \InvalidArgumentException('Class name should be explicitly set for deserialization');
        }

        $metadata = $this->manager->getClassMetadata($className);

        if (!is_array($relation)) {
            return $this->manager->getReference($className, $relation);
        }

        $single = false;
        if ($metadata->isIdentifierComposite) {
            $single = true;
            foreach ($metadata->getIdentifierFieldNames() as $idName) {
                $single = $single && array_key_exists($idName, $relation);
            }
        }

        if ($single) {
            return $this->manager->getReference($className, $relation);
        }

        $objects = [];
        foreach ($relation as $idSet) {
            $objects[] = $this->manager->getReference($className, $idSet);
        }

        return $objects;
    }
Ferromagnetic answered 24/3, 2016 at 11:34 Comment(7)
This feels like a far more well rounded answer, I'll try adopting this approach. Thanks!Blackthorn
Accepted answer is correct, but requires a lot manual work and is read-only (does not support deserialization). This one is a bit automated, but requires undestanding of jms internalsFerromagnetic
@Ferromagnetic Entities are still lazy loaded when using this approach. Do you know if that can be avoided? Using the accepted answer with virtual props will not cause the entity to load (assuming you are indeed fetching the id).Guano
@Guano Lazyness is totally up to your configuration and application logic. Entities stay not loaded until you load the field that is not the ID. To awoid lazy loading you can manually prefetch the relations with select('entity', 'realtion')->leftJoin('entity.relation','relation') query builder or configure EAGER fetch globally for the relationFerromagnetic
@Ferromagnetic That's exactly my point, I only serialize the ID, but I still see an additional db query. After some research I suppose it could be related to github.com/schmittjoh/serializer/issues/575. I guess that fix has not made it to a stable release yet.Guano
yeah, try RC release, should be thereFerromagnetic
Works like a charm! For anyone else having issues, note that the option to skip initialization is by default turned off. function __construct($skipVirtualTypeInit = false)in JMS/Serializer/SerializationContext.phpGuano
C
10

I know this has already been answered but you could also use @Accessor. This probably (may, I can't be sure) work with deserialization too.

/**
 * @Type("Acme\SearchBundle\Entity\Country")
 * @Groups({"manage"})
 * 
 * @var \Acme\SearchBundle\Entity\Country
 *
 * @Serializer\Accessor(getter="getCountryMinusId",setter="setCountryWithId")
 */
private $country;

/**
 * @return string|null
 */
public function getCountryMinusId()
{
    if (is_array($this->country) && isset($this->country['id'])) {
        return $this->country['id'];
    }

    return null;
}

/**
 * @param string $country
 * @return $this
 */
public function setCountryWithId($country)
{
    if (!is_array($this->country)) {
        $this->country = array();
    )

    $this->country['id'] = $country;

    return $this;
}
Copenhagen answered 16/2, 2014 at 1:52 Comment(0)
F
10

You can use @Type and @Accessor annotations:

/**
 * @Type("string") 
 * @Accessor(getter="serializeType",setter="setType") 
 */
protected $type;
public function serializeType()
{   
  return $this->type->getId();
}
Favored answered 17/12, 2015 at 14:44 Comment(1)
* you have to return in the function: ` return $this->type->getId();`Maloriemalory
R
1

The author wants to keep the property name, which doesn't apply to the accepted answer. As far as I understood, the answer by ScayTrase would keep the original property name but has another disadvantage according to the comments: The related object will be fetched if you are using Doctrine ORM @ManyToOne, thus decreasing performance.

If you want to keep the original property name, you have to define the @VirtualProperty at class level and @Exclude the original property. Otherwise, the serialized property name will be derived from the getter method (countryId in this case):

/**
 * @Serializer\VirtualProperty(
 *     "country",
 *     exp="object.getCountryId()",
 *     options={@Serializer\SerializedName("country")}
 * )
 */
class Ad {
    /**
     * @Serializer\Exclude
     */
    private $country;

    public function getCountryId() {
        return $this->country === null ? null : $this->country->getId();
    }
}
Richman answered 6/10, 2017 at 8:23 Comment(0)
I
0

Alternatively, you can @inline $country which will serialize its properties into the parent relation. Then you can @Expose the Country $id and set its @SerializedName to "country". Unlike Virtual properties, both serialization and deserialization will work for inline properties.

For this to work, you need to use the @ExclusionPolicy("All") on each class and judiciously @Expose the properties that you need in any of your groups. This is a more secure policy anyways.

/**
 * @ExclusionPolicy("All")
 */
class Ad
{ 

    //...


    /**
     * @Type("Acme\SearchBundle\Entity\Country")
     * 
     * @Expose()
     * @Inline()
     * @Groups({"manage"})
     *
     * @var \Acme\SearchBundle\Entity\Country
     */
    private $country;


    //...

}
/**
 * @ExclusionPolicy("All")
 */
class Country
{

    //...

    /**
     * Get id
     *
     * @Expose()
     * @Groups({"manage"})
     * @SerializedName("country")
     * @return string 
     */
    public function getId()
    {
        return $this->id;
    }
}
Imogene answered 20/4, 2019 at 15:40 Comment(1)
It does not work for me, Invalid data "781f8d11-8407-49bd-b826-d98343f18d0c" (string), expected "AppBundle\Entity\Country"Whirl

© 2022 - 2024 — McMap. All rights reserved.