POST request on many-to-many association - symfony

I have two entities with a many-to-many association:
class User extends BaseUser
and
class Calendar
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="text")
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="view", type="text")
* #Assert\Choice(choices = {"work week", "week", "month", "year"}, message = "Choose a valid view.")
*/
private $view;
/**
* #ManyToMany(targetEntity="AppBundle\Entity\User")
* #JoinTable(name="calendars_publishers",
* joinColumns={#JoinColumn(name="calendar_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="publisher_id", referencedColumnName="id", unique=true)}
* )
*/
private $publishers;
public function __construct()
{
$this->publishers = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add publishers
*
* #param \AppBundle\Entity\User $publishers
* #return Calendar
*/
public function addPublisher(\AppBundle\Entity\User $publishers)
{
$this->publishers[] = $publishers;
return $this;
}
/**
* Remove publishers
*
* #param \AppBundle\Entity\User $publishers
*/
public function removePublisher(\AppBundle\Entity\User $publishers)
{
$this->publishers->removeElement($publishers);
}
}
And when I do a POST request on my REST API (http://localhost:8000/calendars) with the body:
{
"name": "My calendar",
"view": "week",
"publishers": [
"/users/1",
"/users/2"
]
}
I have the response:
{
"#context": "/contexts/Calendar",
"#id": "/calendars/3",
"#type": "Calendar",
"name": "My calendar",
"view": "week",
"publishers": []
}
So, as you see, my object is recorded well, but I cannot put some users in publishers. Do you have an idea?
I use the bundles:
DunglasApiBundle
NelmioApiDocBundle
NelmioCorsBundle
FOSHttpCacheBundle
FOSUserBundle
LexikJWTAuthenticationBundle
with api-platform.

You should add addPublisher(User $publisher) and removePublisher(User $publisher) methods.
API Platform internally uses the Symfony PropertyAccess component, and this component requires such methods to be able to access to the private property.

It sounds like you should specify fetch="EAGER" in the relationship so that it always gets the related objects.
see http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#manytoone
(and also the next paragraph)
and some good info here: http://www.uvd.co.uk/blog/some-doctrine-2-best-practices/

Related

How can I serialize only the ID of a child

I'm looking for the correct way to serialize a child of my object.
I have the following classes:
class company {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups ({"get"})
*/
private $id;
}
class User {
/**
* #ORM\Column(type="string", length=180, unique=true)
* #Groups ({"get"})
*/
private $email;
/**
* #ORM\ManyToOne(targetEntity=Company::class, inversedBy="users")
* #ORM\JoinColumn(nullable=false)
* #Groups ({"get"})
*/
private $company;
}
As soon as I use the serializer on a User object, I receive the following response.
{
"id": 1,
"email": "email#mydomain.com",
"company": {
"id": 1
}
}
But I prefer the following response, how can I get these?
{
"id": 1,
"email": "email#mydomain.com",
"company": 1
}
You can create a custom normalizer, and summarise the company within the User - as it is being converted from the original object (with sub-entities) to an array, before being json-encoded.
symfony console make:serializer:normalizer [optional name, eg: 'UserNormalizer']
This creates a new class, with in part, the contents:
public function normalize($object, $format = null, array $context = []): array
{
// $object is a User entity at this point
$data = $this->normalizer->normalize($object, $format, $context);
// Here: add, edit, or delete some data
// and we summarise the company entity to just the ID.
$data['company'] = $object->getCompany()->getId();
return $data;
}
When I did this with a slightly more complex entity that referred back to the original one (if company had a reference back to a user), it made 'A circular reference', so I added an annotation to #Ignore the field in the User entity, for serialization purposes. It was still given to the normalizer, to use from the object passed into normalize().
You could also serialize the result of a method instead:
class User {
/**
* #ORM\Column(type="string", length=180, unique=true)
* #Groups ({"get"})
*/
private $email;
/**
* #ORM\ManyToOne(targetEntity=Company::class, inversedBy="users")
* #ORM\JoinColumn(nullable=false)
*/
private $company;
/**
* #Groups ({"get"})
*/
public function getCompanyId()
{
return $this->company->getId();
}
}

Api platform, react admin - create new Entity with relation

I am afraid I might have ran into some sort of XY problem...
I have an entity "Asset" with related "AssetType" entity (One AssetType can have many Asset entities)
When creating new entity with POST method, the request fails with "SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'type_id' cannot be null"
Data posted from react-admin (POST to /api/assets route):
{
"data":{
"type":"assets",
"attributes":{
"name":"asdf",
"description":"LoraWAN enabled sensor"
},
"relationships":{
"asset_type":{
"data":{
"id":"/api/asset_types/a71b47b8-b9fb-11ea-b4d5-e6b986f12daf",
"type":"asset_types"
}
}
}
}
}
I understand that there is data lost somewhere doing deserialization of object, but cannot figure out where. Also I have identical set of entities (Gateway and Location where each Location can have multiple Gateways) and the creation of new entities work as expected...
New to Symfony & api-platform, any help appreciated.
Asset entity is set tup to be visible in api-platform:
/**
* #ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "delete"},
* normalizationContext={"groups"={"read"}},
* denormalizationContext={"groups"={"write"}}
* )
* #ORM\Entity(repositoryClass="App\Repository\AssetRepository")
*/
class Asset
{
/**
* #ORM\Id
* #ORM\Column(type="uuid_binary_ordered_time", nullable=false, unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator")
* #Groups({"read"})
*/
private $uuid;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\AssetType", inversedBy="assets", cascade={"persist"})
* #ORM\JoinColumn(name="type_id", referencedColumnName="uuid", nullable=false)
*
* #Groups({"read", "write"})
*/
private $assetType;
}
AssetType entity:
/**
* #ApiResource(
* normalizationContext={"groups"={"read"}},
* denormalizationContext={"groups"={"write"}}
* )
* #ORM\Entity(repositoryClass="App\Repository\AssetTypeRepository")
*/
class AssetType
{
/**
* #ORM\Id()
* #ORM\Column(name="uuid", type="uuid_binary_ordered_time", nullable=false, unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator")
* #Groups({"read", "write"})
*/
private $uuid;
/**
* #ORM\Column(type="string", length=255, nullable=true)
* #Groups({"read", "write"})
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Asset", mappedBy="assetType")
*/
private $assets;
public function __construct()
{
$this->assets = new ArrayCollection();
}
public function getUuid()
{
return $this->uuid;
}
public function setUuid($uuid): self
{
$this->uuid = $uuid;
return $this;
}
public function getAssets(): Collection
{
return $this->assets;
}
public function addAsset(Asset $asset): self
{
...
}
public function removeAsset(Asset $asset): self
{
...
}
In case anyone sumbles across similar problem - the reason for missing values was property naming and its normalization.
Relationship data posted contains key "asset_type" which needs to be converted to camelCase "assetType" in react-admin's dataProvider (that's the approach I took).

Symfony + JMSSerializer throw 500 - handleCircularReference

I'm trying to use the JMSSerializer with Symfony to build a simple json api.
So i have 2 simple Entities (1 User can have many Cars, each Car belongs to one User):
class Car
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="cars")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
}
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Car", mappedBy="user", orphanRemoval=true)
*/
private $cars;
}
Now i want to get all Cars with their User.
My Controller:
class CarController extends AbstractController
{
/**
* #param CarRepository $carRepository
*
* #Route("/", name="car_index", methods="GET")
*
* #return Response
*/
public function index(CarRepository $carRepository)
{
$cars = $carRepository->findAll();
$serializedEntity = $this->container->get('serializer')->serialize($cars, 'json');
return new Response($serializedEntity);
}
}
This will throw a 500 error:
A circular reference has been detected when serializing the object of
class \"App\Entity\Car\" (configured limit: 1)
Ok, sounds clear. JMS is trying to get each car with the user, and go to the cars and user ....
So my question is: How to prevent this behaviour? I just want all cars with their user, and after this, the iteration should be stopped.
You need to add max depth checks to prevent circular references.
This can be found in the documentation here
Basically you add the #MaxDepth(1) annotation or configure max_depth if you're using XML/YML configuration. Then serialize like this:
use JMS\Serializer\SerializationContext;
$serializer->serialize(
$data,
'json',
SerializationContext::create()->enableMaxDepthChecks()
);
Example Car class with MaxDepth annotation:
class Car
{
/**
* #\JMS\Serializer\Annotation\MaxDepth(1)
*
* [..]
*/
private $user;

JMS Serializer add groups by some condition

I have entity Notification and when my entity contain some condition I want add additional groups for serializer, I create event serializer.pre_serialize for Notification and after check condition add additional groups in serialized context, but in response it's not works. Example I want add some nested to my Notification it would be some another Entity, example Question and this entity have some groups, and after set it when I create context SerializationContext::create() everything work, but when I try add additional group in serializer.pre_serialize it's not work
this my entity
class Notifications
{
use TraitTimestampable;
const GROUP_POST_NOTIFICATION = 'post_notifications';
const GROUP_GET_NOTIFICATIONS = 'get_notifications';
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #Annotation\Groups({
* "get_notifications"
* })
*/
private $id;
/**
* #var object
*
* #Annotation\Groups({
* "get_notifications"
* })
*/
private $providerEntity;
and another entity which I adding in event
class Questions implements NotificationInterface
{
use TraitTimestampable;
const GROUP_POST = 'post_question';
const GROUP_GET = 'get_question';
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #Annotation\Groups({
* "get_question"
* })
*/
private $id;
/**
* #return array
*/
public static function getGetGroup()
{
return [self::GROUP_GET];
}
but in response I have
public function onPreSerializeNotifications(PreSerializeEvent $event)
{
/** #var Notifications $notifications*/
$notifications = $event->getObject();
$provider = $notifications->getProvider(); //this is Questions::class
$this->providerEntity = $this->entityManager->getRepository($provider)
->findOneBy(['id' => $notifications->getProviderId()]);
$notifications->setProviderEntity($this->providerEntity);
$attr = $event->getContext()->attributes;
$groups = array_merge($attr->all()['groups'], $provider::getGetGroup());
$attr->set('groups', $groups);
}
but in response I have
"notifications": [
{
"id": 2,
"provider_entity": {}
}
]
why ?

API Platform Sonata Media Bundle Gallery - Circular Reference

New in SF3, I use API Platform and Sonata Media Bundle.
I'm blocked while getting Gallery entity of Sonata using API Platform GET request.
"A circular reference has been detected when serializing the object of class \"Application\\Sonata\\MediaBundle\\Entity\\Gallery\" (configured limit: 1)"
The admin of the entity works great, I can add a gallery to the entity.
When the entity have a gallery it cause this error, when it does not it's ok.
Entity Technic
GET /technics in API Platform
[
{
"id": 0,
"type": "string",
"comment": "string",
"links": [
"string"
],
"gallery": "string"
}
]
Entity Class
<?php
// src/AppBundle/Entity/Technic.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity
* #ApiResource
*/
class Technic
{
/**
* #var int The id of this evaluation.
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #var string $type TechnicType of the evaluation
*
* #ORM\OneToOne(targetEntity="TechnicType")
* #Assert\NotBlank
*/
public $type;
/**
* #var string $note Note of the evaluation
*
* #ORM\Column(type="string", length=255, nullable=true)
*/
public $comment;
/**
* #var Link[] Link Links of this technic.
*
* #ORM\ManyToMany(targetEntity="Link", cascade={"persist"})
*/
private $links;
/**
* #ORM\OneToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Gallery",cascade={"persist"})
* #ORM\JoinColumn(name="gallery", referencedColumnName="id", nullable=true)
*/
private $gallery;
/**
* Constructor
*/
public function __construct()
{
$this->links = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set type
*
* #param \AppBundle\Entity\TechnicType $type
*
* #return Technic
*/
public function setType(\AppBundle\Entity\TechnicType $type = null)
{
$this->type = $type;
return $this;
}
/**
* Get type
*
* #return \AppBundle\Entity\TechnicType
*/
public function getType()
{
return $this->type;
}
/**
* Add link
*
* #param \AppBundle\Entity\Link $link
*
* #return Technic
*/
public function addLink(\AppBundle\Entity\Link $link)
{
$this->links[] = $link;
return $this;
}
/**
* Remove link
*
* #param \AppBundle\Entity\Link $link
*/
public function removeLink(\AppBundle\Entity\Link $link)
{
$this->links->removeElement($link);
}
/**
* Get links
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getLinks()
{
return $this->links;
}
/**
* Set comment
*
* #param string $comment
*
* #return Technic
*/
public function setComment($comment)
{
$this->comment = $comment;
return $this;
}
/**
* Get comment
*
* #return string
*/
public function getComment()
{
return $this->comment;
}
/**
* Set gallery
*
* #param \Application\Sonata\MediaBundle\Entity\Gallery $gallery
*
* #return Technic
*/
public function setGallery(\Application\Sonata\MediaBundle\Entity\Gallery $gallery = null)
{
$this->gallery = $gallery;
return $this;
}
/**
* Get gallery
*
* #return \Application\Sonata\MediaBundle\Entity\Gallery
*/
public function getGallery()
{
return $this->gallery;
}
}
Thank a lot guys, I'm desesperate I try a lot of things in StackQ/A, annotations, seraliazer config...
You need to configure serialization correctly. Either setup serialization groups, so that on GETting some entity serializer would only pick (for example) IDs of related entities, or set up circualr reference handler in normalizer and inject this normalizer into serializer.
$normalizer = new GetSetMethodNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
There might be more specific answer for api-platform, which I don't know, because serialization of related entities is popular issue.

Resources