Symfony ManyToMany with existing entities - symfony

When my requests hits the server, it should add 2 entries to the m:n table,
Core | Prop
1 | 10
1 | 11
Currently, it does add 2 new entities to the Prop table, but those do already exist, it just creates a new primary key and inserts a new item twice - and the assoc table holds then those 2 entities aligned with one core.
The Core Entity holds a m:n with Prop
/**
* #ORM\ManyToMany(targetEntity=Prop::class, inversedBy="cores", cascade="persist")
*/
private Collection $props;
Prop itself is a recursive entity:
class Prop extends BaseEntity
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private int $id;
/**
* #ORM\ManyToOne(targetEntity=Prop::class, inversedBy="children")
* #ORM\JoinColumn(nullable=true)
*/
private ?Prop $parent;
/**
* #ORM\OneToMany(targetEntity=Prop::class, mappedBy="parent")
*/
private Collection $children;
/**
* #ORM\ManyToMany(targetEntity=Core::class, mappedBy="props")
*/
private Collection $cores;
Now this request gets sent:
"name": "a name",
"type": {
"id": 2,
"discr": "accr"
},
"props": [
{
"id": 10
},
{
"id": 11
}
],
When i debug the state before the persist() call, i do have 2 entities in here to be persisted with the correct id - yet it creates two new entities instead. There are no additional calls to it, request comes in - gets converted via the ParamConverter, followed by persist and flush.
Thank you.

Related

Api platform Subresource context when attributes have relation on the same entity

I develop a new api with api-platform/core#2.6.6 and I have a problem with my subresource.
I have 2 Entities Project and User:
// App\Entity\Project
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="managedProjects")
* #ORM\JoinColumn(nullable=false)
* #Groups({"project:read", "project:write"})
*/
#[ApiSubresource(
maxDepth: 1,
)]
private User $manager;
/**
* #ORM\ManyToMany(targetEntity=User::class, inversedBy="projects", fetch="EXTRA_LAZY")
* #Groups({"project:read", "project:write"})
*/
#[ApiSubresource(
maxDepth: 1,
)]
private Collection $developers;
On ApiDoc I can see the subresource url :
/api/projects/{id}/manager
/api/projects/{id}/developers
But when I call /api/projects/{id}/developers the request body contains:
{
"#context": "/api/contexts/User",
"#id": "/api/projects/2/manager",
"#type": "hydra:Collection",
"hydra:member": [//...]
}
In the response you can see #id is not good. When I remove the #ApiSubresource on $manager, all works fine.
Any idea?
Thanks for all and happy new year!

How to filter result of a query builder according condition on join?

I created my query builder, with a join and a condition, but the returned result on the joined data is full and is not filtered according to my join condition (group.id = 3).
How to modify my query or entities to get the affiliations and groups filtered accordingly?
Thank you.
Query in repository
$affiliationsGroup = 3;
$query = $this->createQueryBuilder('u');
$query->join('u.affiliations', 'a');
$query->join('a.group', 'g');
$query->andWhere('g.id = :group');
$query->setParameter('group', $affiliationsGroup);
$query->getQuery();
returned result
[
{
"id": 42,
"name":"user42";
"affiliations": [
{
"id": 1,
"group": {
"id": 1,
"name": "group1",
}
},
{
"id": 94,
"group": {
"id": 3,
"name": "group3"
},
}
]
User Entity
/**
* #ORM\OneToMany(targetEntity=Affiliations::class, mappedBy="user", cascade={"persist"})
* #Serializer\Groups({"student"})
*/
private $affiliations;
Affiliation Entity
/**
* #var \Users
*
* #ORM\ManyToOne(targetEntity="Users", inversedBy="affiliations", cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* })
* #Serializer\Groups({"student"})
*/
private $user;
/**
* #var \Groups
*
* #ORM\ManyToOne(targetEntity="Groups", inversedBy="affiliations")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="group_id", referencedColumnName="id")
* })
* #Serializer\Groups({"public", "student"})
*/
private $group;
Controller User
/**
*
* Get all users
*
* #Rest\Get("/api/admin/users", name="get_users")
* #Rest\View(serializerGroups={"student"})
* #param Request $request
* #return View
*/
public function indexAction(Request $request): View
{
//Access repository and service to call function and my query builder
}
While the results might not be as expected, there is nothing wrong with your code. It filters correctly.
To understand what's happening, you should understand the two steps:
User 42 (I see what you did here) has at least 1 affiliation in group 3 so is part of the result.
Your serializer will serialize the results. And since user 42 has two affiliations, it will serialize them as well.
Since the serializer doesn't know which filters are applied when querying the results, it doesn't apply those filters.
Unfortunately, this is how API Platform works: it doesn't filter nested entities. See this issue for more details/discussion. You can find some dirty tricks to change the behaviour, but when I encountered the same issue in my project, I decided to filter the nested entities in the front end instead.

Api-Platform graphql - create multiple items in one mutation

I have an User entity that can add his/hers contacts from Google using GooglePeopleApi.
The API provides an array of contacts to the front-end as it is (Nextjs). The question is how to insert all those contacts in one single mutation.
I could, of course, have the front-end loop through the array and post the contacts one by one, but that is a bit silly.
It should be possible to create a type array with the Contact input and then, with that, set up a customArgsMutation. ( I've seen some examples of that from Hasura ).
Entity wise, it looks like this for now ( only relevant code ):
User.php
....
/**
* #ORM\OneToMany(targetEntity="App\Entity\Contact", mappedBy="user")
* #Groups({"put-contacts", "get-admin", "get-owner"})
*/
private $contacts;
Contact.php
/**
* #ApiResource(
* attributes={"pagination_enabled"=false},
* graphql={
* "item_query"={
* "normalization_context"={"groups"={"get-admin", "get-owner"}},
* },
* "collection_query"={
* "normalization_context"={"groups"={"get-admin", "get-owner"}},
* },
* "delete"={"security"="is_granted('IS_AUTHENTICATED_FULLY') and object.getUser() == user"},
* "create"={
* "security"="is_granted('IS_AUTHENTICATED_FULLY')",
* "denormalization_context"={"groups"={"post", "put"}},
* "normalization_context"={"groups"={"get-owner", "get-admin"}},
* },
* }
* )
* #ORM\Entity(repositoryClass="App\Repository\ContactRepository")
*/
class Contact
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="contacts")
* #Groups({"post", "get-admin", "get-owner"})
*/
private $user;
/**
* #ORM\Column(type="string", length=180)
* #Groups({"post", "put", "get-admin", "get-owner"})
*/
private $email;
/**
* #ORM\Column(type="string", length=180, nullable=true)
* #Groups({"post", "put", "get-admin", "get-owner"})
*/
private $familyName;
/**
* #ORM\Column(type="string", length=180, nullable=true)
* #Groups({"post", "put", "get-admin", "get-owner"})
*/
private $givenName;
/**
* #ORM\Column(type="string", length=180, nullable=true)
* #Groups({"post", "put", "get-admin", "get-owner"})
*/
private $displayName;
From graphiql, the createContact input looks like this:
user: String
email: String!
familyName: String
givenName: String
displayName: String
clientMutationId: String
You have a couple of options here, depending on the amount of concurrency you want:
1. These should be performed in Serial
The client can make a single HTTP request with multiple mutations as aliases:
mutation CreateUsers {
user1: createUser({ //userInput1 }) { ...userFields }
user2: createUser({ //userInput2 }) { ...userFields }
user3: createUser({ //userInput3 }) { ...userFields }
}
fragment userFields on CreateUserPayload {
firstName
// etc
}
The response will look like this:
{
"data": {
"user1": {...},
"user2": {...},
"user3": {...},
}
}
Pros:
If any single mutation fails, just that one will error out without special handling
Order is maintained. Because the API consumer is specifically labelling the mutations, they know which one has which results.
Cons:
By design, multiple mutations run in Serial, where the first must complete fully before the next one is started. Because of this it's going to be a bit slower.
The client has to add the fields themselves or use a fragment for each mutation (what I showed above)
2. These should be performed in Parallel
It is a common practice (as common as it can be, I guess) to create a "bulk mutation" that allows you to create multiple users.
mutation CreateUsers {
createUsers([
{ //userInput1 },
{ //userInput2 },
{ //userInput3 }
]) {
firstName
// etc
}
}
Pros:
Runs in parallel, so it's faster
The client doesn't have to do string interpolation to build the query. They just need the array of objects to pass in.
Cons:
You have to build the logic yourself to be able to return nulls vs errors on your own.
You have to build the logic to maintain order
The client has to build logic to find their results in the response array if they care about order.
This is how I did it:
Create a custom type:
class CustomType extends InputObjectType implements TypeInterface
{
public function __construct()
{
$config = [
'name' => 'CustomType',
'fields' => [
'id_1' => Type::nonNull(Type::id()),
'id_2' => Type::nonNull(Type::id()),
'value' => Type::string(),
],
];
parent::__construct($config);
}
public function getName(): string
{
return 'CustomType';
}
}
Register CustomType in src/config/services.yaml
(...)
App\Type\Definition\CustomType:
tags:
- { name: api_platform.graphql.type }
Add custom mutation to entity, using CustomType argument as array:
/**
* #ApiResource(
* (...)
* graphql={
* "customMutation"={
* "args"={
* (...)
* "customArgumentName"={"type"=[CustomType!]"}
* }
* }
*)
Then the new customMutation can be called with an array of arguments like this:
mutation myMutation($input: customMutationEntitynameInput!) {
customMutationEntityname(input: $input) {
otherEntity {
id
}
}
}
And graphql variables:
{
"input": {
"customArgumentName": [
{
"id_1": 3,
"id_2": 4
},{
"id_1": 4,
"id_2": 4
}
]
}
}

How to persist entities when one has two parents

Here's my situation:
Framework Symfony 4, using Doctrine(version).
I have three entities : A, B and C.
Entity A has a OneToMany relationship with B, and a OneToMany relationship with C.
Entity B has a OneToMany relationship with C.
In a form, I have a collectionForm about entity C embedded in a collectionForm about entity B.
Adding entities B & C is optional in the form.
However, when trying to add entity C (and therefore entity B), seeing as entity C has two parents (A & B), doctrine fails to know in which order to persist those, and gives the following error :
A new entity was found through the relationship 'App\Entity\EntityC#entityB' that was not configured to cascade persist operations for entity: App\Entity\EntityB#0000000010cd7f460000000077359844. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'App\Entity\EntityB#__toString()' to get a clue.
How do I tell doctrine to persist entity A first, then B and then C in this case ?
So far, I've tried removing the OneToMany relationship between B and C, without any luck, and adding a ManyToOne relationship in entityC for entity B.
I feel if I could bypass this relationship between B and C this could work, but I don't know how.
EntityA :
class entityA extends BaseEntityA {
/**
* #ORM\OneToMany(targetEntity="App\Entity\EntityB", mappedBy="entityA", cascade={"persist"})
* #ORM\JoinColumn(name="id", referencedColumnName="entityA_id", nullable=false)
*/
protected $entitiesB;
/**
* #ORM\OneToMany(targetEntity="App\Entity\EntityC", mappedBy="entityA", cascade={"persist"})
* #ORM\JoinColumn(name="id", referencedColumnName="entityA_id", nullable=false)
*/
protected $entitesC;
...
/**
* Add EntityB entity to collection (one to many).
*
* #param \App\Entity\EntityB $entityB
*
* #return \App\Entity\EntityA
*/
public function addEntityB(\App\Entity\EntityB $entityB)
{
$this->entitiesB[] = $entityB;
$entityB->setEntityA($this);
return $this;
}
/**
* Add EntityC entity to collection (one to many).
*
* #param \App\Entity\EntityC $entityC
*
* #return \App\Entity\EntityA
*/
public function addEntityC(\App\Entity\EntityC $entityC)
{
$this->entitiesC[] = $entityC;
$vehicle->setEntityA($this);
return $this;
}
Entity B :
class EntityB extends BaseEntityB {
/**
* #ORM\OneToMany(targetEntity="App\Entity\EntityC", mappedBy="entityB", cascade={"persist"})
* #ORM\JoinColumn(name="id", referencedColumnName="entityB_id", nullable=false)
*/
protected $entitiesC;
...
/**
* #param Collection $entitiesC
*
* #return $this
*/
public function setEntityC(Collection $entitiesC)
{
$this->entitiesC = $entitiesC;
return $this;
}
/**
* Add EntityC entity to collection (one to many).
*
* #param \App\Entity\EntityC $entityC
*
* #return \App\Entity\EntityB
*/
public function addEntityC(\App\Entity\EntityC $entityC)
{
$this->entitiesC[] = $entityC;
$entityC->setEntityB($this);
$entityC->setEntityA($this->getEntityA());
// Things I tried to remove the relationship between B and C
// $this->getEntityA()->addEntityC($entityC);
// $entityC->setEntityB($this);
return $this;
}
Entity C :
class EntityC extends BaseEntityC {
// Somtehing I tried at some point
// /**
// * #ORM\ManyToOne(targetEntity="EntityB", inversedBy="entitiesC", cascade={"persist"})
// * #ORM\JoinColumn(name="entityB_id", referencedColumnName="id")
// */
// protected $entityB;
...
}
I would like to persist this entities in the right order (A then B then C), using only one form if possible.

Symfony 3.4 entity extra columns in controller response

I have this entity:
<?php
//namespace
//use ...
class Guide
{
private $id;
//private ...
//getters
//setters
}
?>
In a controller I use the entity manager to retrieve the data of this entity.
$guides= $em->getRepository('AppBundle:Guide')
->findAll();
My entity has 4 parameters: id, name, pages, author.
Is there any way to add two extra parameters, that aren´t in the class declaration and I don´t want in the database, if the entity manager returns for example 3 rows, I want o add two extra values to each row and return the data, for example add two boolean values: ok => true, warning => false.
I have tried this:
foreach($guides as $guide){
$guide->ok=true;
$guide->warning=false;
}
If I dump $guides, I see the two parameters like this:
-id:1
-name:'Guide 1'
-pages:12
-author:'John'
+"ok":true
+"warning":false
But when I use this to send a response:
return new Response($serializer->serialize($guides, 'json'));
The two extra parameters aren´t in the response.
You could add a property to entity and do not tag it as a ORM\Column eg:
<?php
//...
/**
* #ORM\Entity
* #ORM\Table(name="guides")
*/
class Guide
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
* #ORM\Column(name="title", type="string")
*/
private $name;
public $myAdditionalProperty;
//...
And then set it in your controller:
foreach($guides as $guide){
$guide->myAdditionalProperty = "my amazing value";
}
Then you can serialize your data without having additional column in your table

Resources