Api-Platform: Eager Loading a Subresource - php

Hello guys I just started using Api-Platform and I got stuck on this problem for several hours.
I have a Symfony4 Project and two Entities: Bill & Abo
Bill:
/**
* #ORM\Entity(repositoryClass="App\Repository\BillRepository")
* #ApiResource
*/
class Bill {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="date", nullable=false)
*/
private $date;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Abo", inversedBy="bills")
* #ORM\JoinColumn
* #ApiSubresource
*/
private $abo;
}
Abo:
/**
* #ORM\Entity(repositoryClass="App\Repository\AboRepository")
* #ApiResource
*/
class Abo
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=50, nullable=false)
*/
private $name;
/**
* #var integer
*
* #ORM\Column(name="price", type="integer", nullable=false)
*/
private $price;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Bill", mappedBy="abo")
*/
private $bills;
}
When I now call this url, /api/bills, I get get back this data:
{
"#id": "/api/bills/14",
"#type": "Bill",
"id": 14,
"date": "2018-03-08T00:00:00+00:00",
"abo": "/api/abos/1"
},
...
But instead of this "abo": "/api/abos/1" I want the Abo data already loaded, something like this:
"abo": {
"name": "TestAbo",
"price": 25
}
Is this possible and if yes how can I achieve this?
Thanks for your time and help!

You can use serialization groups for this. Make sure that your relation field $abo and it's member fields groups are exposed.
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity(repositoryClass="App\Repository\BillRepository")
* #ApiResource(attributes={
* "normalization_context"={"groups"={"bill", "bill-abo", "abo"}}
* })
*/
class Bill {
...
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Abo", inversedBy="bills")
* #ORM\JoinColumn
* #ApiSubresource
* #Groups("bill-abo")
*/
private $abo;
...
}
class Abo {
...
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=50, nullable=false)
* #Groups("abo")
*/
private $name;
...
}
You can read more in the docs.

Related

doctrine how to merge incomplete data

I'm creating an REST api with Symfony 4.2 and Doctrine as ORM connected to a MySQL database. I receive data in JSON , deserialize them with JMSSerializer and use the doctrine merge function to attach it. How can I merge the entity I got from the JSON with an already existing entity, updating only the given fields in the JSON?
Event entity
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\Type;
/**
* Event
*
* #ORM\Table(name="Event", indexes={#ORM\Index(name="Organiser", columns={"Organiser"})})
* #ORM\Entity
*/
class Event
{
/**
* #var int
*
* #ORM\Column(name="ID", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue
* #Type("int")
*/
private $id;
/**
* #var \Invited
*
* #ORM\OneToMany(targetEntity="Invited",mappedBy="event",cascade={"persist","merge","remove"})
* #Type("ArrayCollection<App\Entity\Invited>")
*/
private $guests;
/**
* #var string
*
* #ORM\Column(name="Title", type="string", length=255, nullable=false)
* #Type("string")
*/
private $title;
/**
* #var \DateTime
*
* #ORM\Column(name="StartDate", type="datetime", nullable=false)
* #Type("DateTime")
*/
private $startdate;
/**
* #var \DateTime|null
*
* #ORM\Column(name="EndDate", type="datetime", nullable=true)
* #Type("DateTime")
*/
private $enddate;
/**
* #var string
*
* #ORM\Column(name="Place", type="string", length=255, nullable=false)
* #Type("string")
*/
private $place;
/**
* #var string|null
*
* #ORM\Column(name="ImagePath", type="string", length=255, nullable=true)
* #Type("string")
*/
private $imagepath;
/**
* #var \User
*
* #ORM\ManyToOne(targetEntity="User",cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="Organiser", referencedColumnName="ID")
* })
* #Type("App\Entity\User")
*/
private $organiser;
?>
Deserialisation
$json = $request->getContent();
$serializer = SerializerBuilder::create()->build();
$event = $serializer->deserialize($json,'App\Entity\Event','json');
Merging
$feedback = $this->em->merge($newEvent);
$this->em->flush();
Expected vs Actual results:
If I have an already existing and attached event like:
{
id:1,
title: 'oldTitle',
startDate: oldDate,
place: 'oldPlace',
...
}
And I receive :
{
id:1,
title: 'newTitle'
}
Expected result:
{
id:1,
title: 'newTitle',
startDate: oldDate,
place: 'oldPlace',
...
}
Actual result when looking to the database:
{
id:1,
title: 'newTitle',
startDate: null,
place: null,
...
}
How can I achieve that in a efficient way?

Api-Platform: Filter Normalization Context Groups

Hello guys I just started using Api-Platform and I got stuck on this problem for several hours.
I have a Symfony4 Project and two Entities: Bill & Abo
Bill:
/**
* #ORM\Entity(repositoryClass="App\Repository\BillRepository")
* #ApiResource(attributes={
* "normalization_context"={"groups"={"bill-abo"}}
* })
*/
class Bill {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="date", nullable=false)
* #Groups("bill-abo")
*/
private $date;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Abo", inversedBy="bills")
* #ORM\JoinColumn
* #ApiSubresource
* #Groups("bill-abo")
*/
private $abo;
}
Abo:
/**
* #ORM\Entity(repositoryClass="App\Repository\AboRepository")
* #ApiResource
*/
class Abo
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=50, nullable=false)
*/
private $name;
/**
* #var integer
*
* #ORM\Column(name="price", type="integer", nullable=false)
* #Groups("bill-abo")
*/
private $price;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Bill", mappedBy="abo")
*/
private $bills;
}
When I now call this url, /api/bills, I get back this data:
{
"date": "2018-03-14T00:00:00+00:00",
"abo": {
"price": 960
}
},
...
My goal now is to call an url, something like this: /api/bills?abo to get the result above and when I call /api/bills I only want the bill data (without the abo data), like this:
{
"id": 14,
"date": "2018-03-08T00:00:00+00:00",
"abo": "/api/abos/1"
},
...
I read the docs and specifically the parts about the Group Filter and the Property Filter but I coulnd't figure out a solution.
Is this even possible and if yes how can I achieve this?
Thanks for your time and help!
You can leverage the Group Filter.
Add the GroupFilter on the Bill resource class like this #ApiFilter(GroupFilter::class, arguments={"overrideDefaultGroups": true, "whitelist": {"foo"}})
Add an new extra group called "foo" to the Bill::$abo property (don't add to any property in the Abo class)
Try /bills?groups[]=foo

Symfony Rest Controller Limiting serialization depth

I have entity bit and relation ManyToMany developer, for developer I use * #Groups({"for_project"}) and when I use this anotation, in response not see fields developer, I look documentation and see * #MaxDepth(2) and use this but still have null. Why ?
{
"0": {
"id": 501,
"created": "2015-11-27T12:25:11+0200",
"developer_id": {},
"rate": 4,
"comment": "fsefsf"
},
"1": {
"id": 502,
"created": "2015-11-27T12:25:46+0200",
"developer_id": {},
"rate": 3,
"comment": "feasf"
}
}
class Bit
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #Expose()
* #Groups({"for_project"})
*/
private $id;
/**
* #var datetime $created
*
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
* #Expose()
* #Groups({"for_project"})
*/
private $created;
/**
* #var datetime $updated
*
* #Gedmo\Timestampable(on="update")
* #ORM\Column(type="datetime")
* #Groups({"for_project"})
*/
private $updated;
/**
* #var \Artel\ProfileBundle\Entity\Developer
*
* #ORM\ManyToOne(targetEntity="Developer")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="developer_id", referencedColumnName="id", onDelete="CASCADE")
* })
* #Expose()
* #Groups({"for_project"})
* #MaxDepth(2)
*/
private $developerId;
class BitController extends FOSRestController
...anotaion block
public function getBitByProjectAction($id, $token)
{
$this->getDoctrine()->getRepository('ArtelProfileBundle:Users')->findOneBySecuritytoken($token);
if (!empty($user) || $security->isGranted('ROLE_ADMIN') ) {
$bits = $this->getDoctrine()->getManager()
->getRepository('ArtelProfileBundle:Bit')
->findBitByProject($id, $token);
if (!$bits) {
throw new NotFoundHttpException();
}
return View::create()
->setStatusCode(200)
->setData($bits)
->setSerializationContext(
SerializationContext::create()
->enableMaxDepthChecks()
->setGroups(array("for_project"))
);
SOLVED
I understand, need add * #Groups({"for_project"}) for fields entity developer
but when I deleted #MaxDepth I still have fields entity developer, why need #MaxDepth ?
I understand, when use without #MaxDept, we have maximum depth for relation fields, Example I have entity Bit, bit have Developer and developer have User, if I want visible fields entity User I need add #MaxDept(3) for field developer in entity Bit
when use without #MaxDept, we have maximum depth for relation fields, Example I have entity Bit, bit have Developer and developer have User, if I want visible fields entity User I need add #MaxDept(3) for field developer in entity Bit
in action:
return View::create()
->setStatusCode(200)
->setData($bits)
->setSerializationContext(
SerializationContext::create()
->enableMaxDepthChecks()
->setGroups(array("for_project"))
);
and in response
[
{
"id": 501,
"created": "2015-11-30T17:49:19+0200",
"developer_id": {
"id": 201,
"rate": 0,
"user": [
{
"first_name": "Ivan",
"last_name": "Shuba"
}
]
},
"rate": 4,
"comment": "fsefse"
}
]
and Entity
class Bit
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #Expose()
* #Groups({"for_project"})
*/
private $id;
/**
* #var datetime $created
*
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
* #Expose()
* #Groups({"for_project"})
*/
private $created;
/**
* #var datetime $updated
*
* #Gedmo\Timestampable(on="update")
* #ORM\Column(type="datetime")
* #Groups({"for_project"})
*/
private $updated;
/**
* #var \Artel\ProfileBundle\Entity\Developer
*
* #ORM\ManyToOne(targetEntity="Developer")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="developer_id", referencedColumnName="id", onDelete="CASCADE")
* })
* #Expose()
* #Groups({"for_project"})
* #MaxDepth(3)
*/
private $developerId;
class Developer
{
/**
* #ORM\OneToMany(targetEntity="Artel\ProfileBundle\Entity\Users", mappedBy="developer", cascade={"persist", "remove"})
* #Expose()
* #Groups({"for_project"})
*/
protected $user;
class Users implements UserInterface
{
/**
* #var string
*
* #ORM\Column(name="first_name", type="string", length=255, nullable=false)
* #Expose()
* #Assert\Length(min=3, max=255)
* #Groups({"for_project"})
*/
protected $firstName;
/**
* #var string
*
* #ORM\Column(name="last_name", type="string", length=255, nullable=true)
* #Expose()
* #Groups({"for_project"})
*/
protected $lastName;
This works for me using fos rest version: 2* and JMS serializer version: 3*
return $this->view($data)->setContext((new Context())->enableMaxDepth());

Symfony ManyToOne Form add, delete in DB

I have entity developer and comment and relationship Many comment to One developer. And I need form when I see all comment for developer and edit - add, delete in DB . What are the solutions to this problem
entity Comment:
class Comments
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Developer", inversedBy="comments")
* #ORM\JoinColumn(name="talent_id", nullable = true, referencedColumnName="id")
* */
protected $talent;
/**
* #var string
*
* #ORM\Column(name="added_by", type="string", length=10, nullable=true)
*/
private $added_by;
/**
* #var string
*
* #ORM\Column(name="comment", type="string", length=10, nullable=true)
*/
private $comment;
entity Developer:
class Developer extends CustomUser
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/////
/**
* #ORM\OneToMany(targetEntity="Comments", mappedBy="talent", cascade={"persist", "remove"})
*/
protected $comments;
Maybe need form in form but how to do this?
You are looking for field type collection.
Example usage of collection type
class Comments
{
....
/**
*
*#ORM\ManyToOne(targetEntity="Developer", inversedBy="developer_to_comments")
* #ORM\JoinColumn(name="developer_to_comments_id", referencedColumnName="id", nullable=false)
*
*/
private $comments_to_developer;
...
}
And class Developer
class Developer extends CustomUser
{
....
/**
*
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Comments", mappedBy="comments_to_developer", cascade={"remove"})
*/
private $developer_to_comments;
public function __construct()
{
$this->developer_to_comments = new ArrayCollection();
}
....
}
And don't forget use Doctrine\Common\Collections\ArrayCollection

Doctrine Entity extending another Entity

Hi I read this article http://docs.doctrine-project.org/en/latest/reference/inheritance-mapping.html yet I'm not quiet sure how to accomplish the following:
I have a "user"-Table, a "man"-Table and a "woman"-table.
I want my php classes Man and Woman extend the User Object.
Annotationmapping:
namespace Core\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Table(name="user", uniqueConstraints={#ORM\UniqueConstraint(name="email_UNIQUE", columns={"email"}), #ORM\UniqueConstraint(name="username_UNIQUE", columns={"username"})})
* #ORM\Entity
*/
class User
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="first_name", type="string", length=255, nullable=true)
*/
private $firstName;
/**
* #var string
*
* #ORM\Column(name="middle_name", type="string", length=255, nullable=true)
*/
private $middleName;
/**
* #var string
*
* #ORM\Column(name="last_name", type="string", length=255, nullable=true)
*/
private $lastName;
/**
* #var string
*
* #ORM\Column(name="username", type="string", length=255, nullable=false)
*/
private $username;
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=255, nullable=false)
*/
private $email;
/**
* #var string
*
* #ORM\Column(name="password", type="string", length=255, nullable=false)
*/
private $password;
/**
* #var \DateTime
*
* #ORM\Column(name="created_at", type="datetime", nullable=true)
*/
private $createdAt;
/**
* #var \DateTime
*
* #ORM\Column(name="updated_at", type="datetime", nullable=false)
*/
private $updatedAt;
/**
* #var \DateTime
*
* #ORM\Column(name="last_login", type="datetime", nullable=false)
*/
private $lastLogin;
/**
* #var string
*
* #ORM\Column(name="login_hash", type="string", length=255, nullable=true)
*/
private $loginHash;
/**
* #var boolean
*
* #ORM\Column(name="is_premium", type="boolean", nullable=false)
*/
private $isPremium = '0';
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Core\Entity\Bill", inversedBy="user")
* #ORM\JoinTable(name="user_has_bill",
* joinColumns={
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="bill_id", referencedColumnName="id")
* }
* )
*/
private $bill;
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Core\Entity\Picture", inversedBy="user")
* #ORM\JoinTable(name="user_has_picture",
* joinColumns={
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="picture_id", referencedColumnName="id")
* }
* )
*/
private $picture;
/**
* Constructor
*/
public function __construct()
{
$this->bill = new \Doctrine\Common\Collections\ArrayCollection();
$this->picture = new \Doctrine\Common\Collections\ArrayCollection();
}
}
Woman:
namespace Core\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Woman
*
* #ORM\Table(name="woman", indexes={#ORM\Index(name="fk_woman_user1_idx", columns={"user_id"})})
* #ORM\Entity
*/
class Woman
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* #var \Core\Entity\User
*
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
* #ORM\OneToOne(targetEntity="Core\Entity\User")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Core\Entity\Cart", inversedBy="woman")
* #ORM\JoinTable(name="woman_has_cart",
* joinColumns={
* #ORM\JoinColumn(name="woman_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="cart_id", referencedColumnName="id")
* }
* )
*/
private $cart;
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Core\Entity\Interest", inversedBy="woman")
* #ORM\JoinTable(name="woman_has_interest",
* joinColumns={
* #ORM\JoinColumn(name="woman_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="interest_id", referencedColumnName="id")
* }
* )
*/
private $interest;
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Core\Entity\Man", inversedBy="woman")
* #ORM\JoinTable(name="woman_has_man",
* joinColumns={
* #ORM\JoinColumn(name="woman_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="man_id", referencedColumnName="id")
* }
* )
*/
private $man;
/**
* Constructor
*/
public function __construct()
{
$this->cart = new \Doctrine\Common\Collections\ArrayCollection();
$this->interest = new \Doctrine\Common\Collections\ArrayCollection();
$this->man = new \Doctrine\Common\Collections\ArrayCollection();
}
}
The man mapping looks (for now) the same as the woman.
Here a little Snippet from MysqlWorkbench http://s14.directupload.net/images/131013/fbg7okyn.png
The basic idea is this:
Men and Women share some common logic and individual logic. For example take a login. Men and Women need an email and a password to log in. Since it's redundant to implement the same login logic twice I thought of creating a more abstract class, the User, which is where I want to put everything which applies to men and women, like name, email, password, login logic etc...
This is where it gets tricky with doctrine. Men and women will have individual fields to store in the database, yet they still need their "common data"(name, password etc...), so a simple class Man extends User might not work out correctly. I store the corresponding id of the User on men and women. So there is an identification.
What I had in mind is, if I do $men->getPassword() it should use the getPassword()function of the corresponding User object.
I hope I cleared up my intend.
Kind Regards and thank you for digging through.
i have done this what you're looking for in one of my projects once, It's done not too good code wise, but the mapping is fine ;) Please check this link
Item.php.dist would be your User Entity
(Property|Vehicle).php.dist would be your Man / Women Entity
Please notice that the Property Discriminator Mapping is missing within the code examples. I do it differently in the application ;)
Ultimately you wouldn't want to have separate "Tables" on your SQL Server. It all belongs to the Superclass "User" and therefore belongs to the User-Table. You will extends the UserTable and use DiscriminatorMapping to map specific entities.
Note: A Man can not be editted to become a Woman! You'd have to kill the man and give birth to a woman :P
Imagina this Model:
User
*id
-name
-surname
Man extends User
-pc_power
Woman extends User
-nail_color
Your DB-Schema would look like this:
Table User:
*id (pk)
-discriminator (not nullable) (value: man or woman)
-name (not nullable)
-surname (not nullable)
-pc_power (nullable as far as DB is concerned)
-nail_color (nullable as far as DB is concerned)
You do not need 3 tables to mod your models like this. It makes literally no sense to do this. It just slows your Queries down by quite a bit.
Now a Dataset could look like this:
A Man: (1, man, john, doe, 4ghz, null)
A Woman: (2, woman, john, doe, null, pink)
Now on Doctrines side of things you do Queries against the USER-Entity
$entity = $userRepository->find(1);
echo get_class($entity); // returns "Man"
$entity = $userRepository->find(2);
echo get_class($entity); // returns "Woman"
Does that make things more clear, because otherwise i'm simply unable to help you :P

Categories