Embedding Relations In Results - doctrine-orm

I'm using API-Platform to deliver content via API. I have a Potential relationship between users and participants (not all users will have participants but all participants will have at least one user). My main goal is to embed the relationship's User data in the result set of Participants as that result will be consumed by a data table and it would be more efficient to have that data already present within the result opposed to performing an additional request for the data.
e.g.:
{
"#context": "/api/contexts/Participants",
"#id": "/api/participants",
"#type": "hydra:Collection",
"hydra:member": [
{
"#id": "/api/participants/1",
"#type": "Participants",
"id": 1,
"name": "Jeffrey Jones",
"users": [
{
"#id": "/api/users/1",
"#type": "User",
"name": "Jenny Jones"
},
{
"#id": "/api/users/2",
"#type": "User",
"name": "Jessie Jones"
}
]
}
],
"hydra:totalItems": 1
}
However, I'm not sure if this is possible. I have looked at https://api-platform.com/docs/core/serialization#embedding-relations but I am not certain it would work for multiple result sets as the example is one book to one author. However, my scenario is one participant to multiple users.
Also (and I may need to go about this in a more direct manner), I'm using a joining table so that I can assign additional metadata to the relationship. So... participants > joint table (containing additional data) > users (and vice versa). Again, I may need to consider having a direct relationship between participants and users and then using a ParticipantUserMeta table to hold the additional metadata. However, at the moment, I'm leaning towards the join table containing the association as well as the additional metadata.
Here are the basics of my entities (most unnecessary data omitted):
User:
/**
* #ApiResource
* ...
*/
class User implements UserInterface, \Serializable
{
/**
* #var int
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
* #ORM\Column(type="string")
* #Assert\NotBlank()
*/
private $name = '';
/**
* #ORM\OneToMany(targetEntity="App\Entity\ParticipantRel", mappedBy="user")
*/
private $participants;
public function __construct()
{
$this->participants = new ArrayCollection();
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
/**
* #return Collection|ParticipantRel[]
*/
public function getParticipants(): Collection
{
return $this->participants;
}
}
ParticipantRel:
/**
* #ApiResource
* ...
*/
class ParticipantRel
{
/**
* #var int The Participant Id
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var int
*
* #ORM\Column(type="boolean")
*/
private $primary_contact;
/**
* #var string Relationship notes
*
* #ORM\Column(type="text", nullable=true)
*/
private $notes;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Participants", inversedBy="users")
* #ORM\JoinColumn(nullable=false)
*/
private $participant;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="participants")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
public function getId (): int
{
return $this->id;
}
public function getPrimaryContact(): ?bool
{
return $this->primary_contact;
}
public function getNotes(): ?string
{
return $this->notes;
}
public function getParticipant(): ?Participants
{
return $this->participant;
}
public function getUser(): ?User
{
return $this->user;
}
}
Participants
/**
* #ApiResource
* ...
*/
class Participants
{
/**
* #var int The Participant Id
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string Participant's first name
*
* #ORM\Column(name="name")
* #Assert\NotBlank
*/
public $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\ParticipantRel", mappedBy="participant")
*/
private $users;
public function __construct() {
$this->users = new ArrayCollection();
}
public function getId (): int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
/**
* #return Collection|ParticipantRel[]
*/
public function getUsers(): Collection
{
return $this->users;
}
}
My question: Is what I'm attempting possible within an entity and if so, what am I missing? I have researched this a lot before coming here but haven't come up w/ any solution as most of the solutions I see involve a Twig tpl but I'm simply sending the data via api-platform. Any positive direction would be greatly appreciated.

So, it turns out that I just needed to experiment more w/ the Groups option (https://api-platform.com/docs/core/serialization#embedding-relations). Associating all related fields w/ the relative groups on all relative enities did end up returning the results in the desired format.

Related

Symfony Many to Many returns empty ArrayCollection

I have two entities Product and Part, connected by a many-to-many relationship.
class Part
{
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Product",
mappedBy="parts", fetch="EAGER")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getProducts()
{
return $this->products;
}
}
class Product
{
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Part", inversedBy="products", cascade={"persist"})
* #ORM\JoinTable(name="products_parts",
* joinColumns={
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="part_id", referencedColumnName="id")
* }
* )
*/
private $parts;
public function __construct()
{
$this->parts = new ArrayCollection();
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getParts()
{
return $this->parts;
}
/**
* #param $parts
*/
public function setParts($parts)
{
$this->parts = $parts;
}
}
When I try get single Product and show all related parts. Symfony returns an empty array. Although the data is correctly added to the database. Where did I go wrong?

Doctrine association persist

I have a problem with doctrine. I have two entities.
FriendRequest
class FriendRequest
{
/** #ORM\Id #ORM\Column(type="integer") #ORM\GeneratedValue **/
protected $id;
/**
* First person from friendship
*
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $from;
/**
* Second person from friendship
*
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="id", referencedColumnName="id")
*/
protected $to;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getFrom()
{
return $this->from;
}
/**
* #param mixed $from
*/
public function setFrom($from)
{
$this->from = $from;
}
/**
* #return mixed
*/
public function getTo()
{
return $this->to;
}
/**
* #param mixed $to
*/
public function setTo($to)
{
$this->to = $to;
}
}
And User
/**
* #ORM\Entity #ORM\Table(name="users")
*/
class User
{
/** #ORM\Id #ORM\Column(type="integer") #ORM\GeneratedValue **/
protected $id;
/** #ORM\Column(type="string") **/
protected $email;
/** #ORM\Column(type="string") **/
protected $password;
/** #ORM\Column(type="string") **/
protected $name;
/** #ORM\Column(type="string") **/
protected $surname;
/** #ORM\Column(type="date") **/
protected $date;
/** #ORM\Column(type="string") **/
protected $sex;
/**
* #return mixed
*/
public function getSex()
{
return $this->sex;
}
/**
* #param mixed $sex
*/
public function setSex($sex)
{
$this->sex = $sex;
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
* #param mixed $email
*/
public function setEmail($email)
{
$this->email = $email;
}
/**
* #return mixed
*/
public function getPassword()
{
return $this->password;
}
/**
* #param mixed $password
*/
public function setPassword($password)
{
$this->password = $password;
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getSurname()
{
return $this->surname;
}
/**
* #param mixed $surname
*/
public function setSurname($surname)
{
$this->surname = $surname;
}
/**
* #return mixed
*/
public function getDate()
{
return $this->date;
}
/**
* #param mixed $date
*/
public function setDate($date)
{
$this->date = $date;
}
}
I tried to create the new friendship request.
$from = $this->entity_manager->getRepository(User::class)->findOneBy(
["id" => $my_id]
);
$to = $this->entity_manager->getRepository(User::class)->findOneBy(
["id" => $target_user_id]
);
/** #var FriendRequest $friend_request */
$friend_request = new FriendRequest();
$friend_request->setFrom($from);
$friend_request->setTo($to);
$this->entity_manager->persist($friend_request);
$this->entity_manager->flush();
But it does not work. Doctrine shows me error:
An exception occurred while executing 'INSERT INTO friendRequest (id) VALUES (?)' with params [186]:
SQLSTATE[HY000]: General error: 1364 Field 'from' doesn't have a default value.
I tried to print $from ad $to, but they are correct. Is there someone who know what does it mean? I spent a lot of time with it, but I dont know...
I think your issue comes from the declaration of the $from and $to attributes in the FriendRequest entity class, if you try to update your database model you are going to get an error like this:
[Doctrine\ORM\Mapping\MappingException]
Duplicate definition of column 'id' on entity 'FriendRequest' in a field or discriminator column mapping.
Since you have assigned the same name 'id' for two different attributes. Probably you have in your database two columns From and To, but they are not mapping with your model, that's why you get an error saying from doesn't have a default value. Since the column name in the database are not sync with the name of the attributes in your entity.
Assuming you already have a FriendRequest table and you are using 'from' and 'to' as the column names this should be the update of the FriendRequest entity class.
class FriendRequest
{
/** #ORM\Id #ORM\Column(type="integer") #ORM\GeneratedValue **/
protected $id;
/**
* First person from friendship
*
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="from", referencedColumnName="id")
*/
protected $from;
/**
* Second person from friendship
*
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="to", referencedColumnName="id")
*/
protected $to;
...
}

Getting the values of one-to-many relationships in the parent entity?

So I have a Doctrine 2 ORM entity, Trust, with several one-to-many relationships. I can't work out how to populate the arrays for these.
class Trust implements \JsonSerializable {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", nullable=true)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="TrustPurpose", mappedBy="trust")
*/
private $purpose;
public function __construct() {
}
public function jsonSerialize() {
return [
'id' => $this->id,
'name' => $this->name,
[...]
'purpose' => $this->purpose,
[...]
];
}
}
In my controller, I might use something like the following.
return new JsonResponse($this->entityManager->find(Trust::class, 1));
And get the following output
{
"id" : 1,
"name" : "Some Trust",
"purpose" : {
}
}
I'm struggling to find documentation on how to edit entity Trust to automatically populate my purpose records?
TrustPurpose
class TrustPurpose implements \JsonSerializable {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", nullable=false)
*/
private $purpose;
/**
* #ORM\ManyToOne(targetEntity="Trust", inversedBy="purpose")
* #ORM\JoinColumn(name="trust_id", referencedColumnName="id")
*/
private $trust;
public function __construct() {
}
public function jsonSerialize() {
return [
'id' => $this->id,
'purpose' => $this->purpose,
'trust' => $this->trust,
];
}
}

Symfony/FosUserBundle - Store Roles in DB

How can I store roles in DB?
I tried this method http://blog.jmoz.co.uk/symfony2-fosuserbundle-role-entities/
USER.php
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Post", mappedBy="author")
*/
public $article;
/**
* #var string
*
* #ORM\Column(name="full_name", type="string", length=255, nullable=true)
*/
public $name;
/**
* #ORM\ManyToMany(targetEntity="Role")
* #ORM\JoinTable(name="users_roles")
*/
protected $roles;
public function __construct()
{
parent::__construct();
$this->roles = new ArrayCollection();
}
....settings...
/**
* Returns an ARRAY of Role objects with the default Role object appended.
* #return array
*/
public function getRoles()
{
return array_merge($this->roles->toArray(), array(new Role(parent::ROLE_DEFAULT)));
}
/**
* Returns the true ArrayCollection of Roles.
* #return ArrayCollection
*/
public function getRolesCollection()
{
return $this->roles;
}
/**
* Pass a string, get the desired Role object or null.
* #param string $role
* #return Role|null
*/
public function getRole($role)
{
foreach ($this->getRoles() as $roleItem) {
if ($role == $roleItem->getRole()) {
return $roleItem;
}
}
return null;
}
/**
* Pass a string, checks if we have that Role. Same functionality as getRole() except returns a real boolean.
* #param string $role
* #return boolean
*/
public function hasRole($role)
{
if ($this->getRole($role)) {
return true;
}
return false;
}
/**
* Adds a Role OBJECT to the ArrayCollection. Can't type hint due to interface so throws Exception.
* #throws Exception
* #param Role $role
*/
public function addRole($role)
{
if (!$role instanceof Role) {
throw new \Exception("addRole takes a Role object as the parameter");
}
if (!$this->hasRole($role->getRole())) {
$this->roles->add($role);
}
}
/**
* Pass a string, remove the Role object from collection.
* #param string $role
*/
public function removeRole($role)
{
$roleElement = $this->getRole($role);
if ($roleElement) {
$this->roles->removeElement($roleElement);
}
}
/**
* Pass an ARRAY of Role objects and will clear the collection and re-set it with new Roles.
* Type hinted array due to interface.
* #param array $roles Of Role objects.
*/
public function setRoles(array $roles)
{
$this->roles->clear();
foreach ($roles as $role) {
$this->addRole($role);
}
}
/**
* Directly set the ArrayCollection of Roles. Type hinted as Collection which is the parent of (Array|Persistent)Collection.
* #param Collection $role
*/
public function setRolesCollection(Collection $collection)
{
$this->roles = $collection;
}
}
ROLE.php
/**
* Role Entity
*
* #ORM\Entity
* #ORM\Table(name="roles")
*/
class Role implements RoleInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer", name="id")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", name="role", unique=true, length=70)
*/
private $role;
/**
* Populate the role field
* #param string $role ROLE_FOO etc
*/
public function __construct($role)
{
$this->role = $role;
}
/**
* Return the role field.
* #return string
*/
public function getRole()
{
return $this->role;
}
/**
* Return the role field.
* #return string
*/
public function __toString()
{
return (string)$this->role;
}
}
I'm always getting this error.
[Doctrine\ORM\Mapping\MappingException]
Property "roles" in "***\Entity\User" was already
declared, but it must be declared only once
Could someone help me please?
Thanks
You need to get rid of everything related to roles inside your User Entity. Roles are supported by the bundle out of the box.
I just setup the bundle and everything is working fine. This is what my User Entity looks like which extends the BaseUser
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User extends BaseUser
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
// your own logic
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
}
Once the bundle was setup, I ran the schema update command.
As you can see the roles column is added automatically via the bundle.
You can play around with bundle using the command is comes with to create the user.
The documentation of the bundle can be found here. Here is another good resource to get you started.

Eager fetching with Sonata list view

I'm using SonataAdmin and SonataDoctrineORMAdmin bundles to manage entities.
The problem is I can't figure out how to eager fetch the related entities in the list view and as the number of listed entities increase the number of queries executed increasing rapidly as well.
I tried adding `fetch="EAGER" to the relation annotations but the profiles show that Sonata executes the separate queries anyway.
Here's one relation worth of code:
Post
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Table()
* #ORM\Entity
*/
class Post
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
**/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Acme\AppBundle\Entity\Tag", fetch="EAGER")
* #ORM\JoinTable(name="join_post_to_tag",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id")}
* )
**/
private $tags;
public function getId()
{
return $this->id;
}
public function setName($names)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
public function __toString()
{
return $this->getName();
}
}
Tag
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table()
* #ORM\Entity
*/
class Tag
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="value", type="string", length=255)
*/
private $value;
public function getId()
{
return $this->id;
}
public function setValue($value)
{
$this->value = $value;
return $this;
}
public function getValue()
{
return $this->value;
}
public function __toString()
{
return ($this->getValue()) ? : '';
}
}
The first related query that is run is fetching all the posts:
SELECT DISTINCT p0_.id AS id0, p0_.id AS id1
FROM Post p0_
LEFT JOIN join_post_to_tag j1_ ON p0_.id = j1_.post_id
LEFT JOIN Tag p1_ ON p1_.id = j1_.target_id
ORDER BY p0_.id ASC
But this does not fetch the related tags or even if it does, it still queries it again:
SELECT t0.id AS id1, t0.value AS value2
FROM Tag t0
INNER JOIN join_post_to_tag ON t0.id = join_post_to_tag.tag_id
WHERE join_post_to_tag.post_id = ?
I tried to mess with the createQuery method in the admin class but could not really find a way to make the related entities fetched correctly.
Is there a way to force the list view to eager fetch the required related entities?
You are on the right track, using the createQuery($context) method.
I have achieved eager loading as following:
public function createQuery($context = 'list')
{
$query = parent::createQuery($context); // let sonata build it's default query for the entity
$rootEntityAlias = $query->getRootAlias(); // get the alias defined by sonata for the root entity
$query->join($rootEntityAlias.'.relationFieldName', 'relationFieldAlias'); // manualy define the join you need
$query->addSelect('relationFieldAlias'); // this is the key line. It is not enough to join a table. You have to also add it to the select list of the query, so that it's actualy fetched
// $query->join(...) // repeat the process of joining and selecting for each relation field you need
// $query->addSelect(...)
return $query; // return the altered query to sonata. this will only work for the "list" action.
}
If you're having trouble using this, let me know:)
Further reads on this topic:
SO question
docs