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.

escrichov
  • 559
  • 2
  • 6
  • 14

6 Answers6

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...

Roman Newaza
  • 11,405
  • 11
  • 58
  • 89
Jovan Perovic
  • 19,846
  • 5
  • 44
  • 85
  • Thank you! Perfect for me. I just needed to serialize. – escrichov Apr 14 '13 at 17:44
  • 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? – Jovan Perovic Apr 19 '13 at 08:36
  • 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. – escrichov Apr 19 '13 at 21:18
  • YAML config reference also got updated: https://github.com/schmittjoh/serializer/commit/e104873368b8ca22ac057f76086c6a4fce5c7033 – Denes Papp Mar 09 '14 at 21:19
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;
    }
Ryall
  • 12,010
  • 11
  • 53
  • 77
ScayTrase
  • 1,810
  • 23
  • 36
  • 1
    This feels like a far more well rounded answer, I'll try adopting this approach. Thanks! – Kevin Herrera May 06 '16 at 15:16
  • 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 internals – ScayTrase May 08 '16 at 06:20
  • 1
    @ScayTrase 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). – bblue Feb 07 '17 at 22:37
  • @bblue 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 relation – ScayTrase Feb 08 '17 at 04:45
  • @ScayTrase 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 https://github.com/schmittjoh/serializer/issues/575. I guess that fix has not made it to a stable release yet. – bblue Feb 08 '17 at 09:13
  • yeah, try RC release, should be there – ScayTrase Feb 08 '17 at 09:41
  • 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.php` – bblue Feb 08 '17 at 15:15
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;
}
qooplmao
  • 17,622
  • 2
  • 44
  • 69
10

You can use @Type and @Accessor annotations:

/**
 * @Type("string") 
 * @Accessor(getter="serializeType",setter="setType") 
 */
protected $type;
public function serializeType()
{   
  return $this->type->getId();
}
Tarjei Huse
  • 1,231
  • 11
  • 14
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();
    }
}
fishbone
  • 3,140
  • 2
  • 37
  • 50
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;
    }
}
Eric Amshukov
  • 223
  • 3
  • 9