i am trying to create the a query for the following situation, but first the used entities:
User:
/**
* Friendships to me (the user)
*
* #OneToMany(targetEntity="Friendship", mappedBy="to", fetch="EAGER")
*/
protected $friendshipsTo;
/**
* Friendships from me (the user)
*
* #OneToMany(targetEntity="Friendship", mappedBy="from", fetch="EAGER")
*/
protected $friendshipsFrom;
Friendship:
/**
* The user who started the friend request
*
* #var \Entity\User
* #ManyToOne(targetEntity="User", inversedBy="friendshipsFrom")
*/
protected $from = null;
/**
* The user who got the friend request
*
* #var \Entity\User
* #ManyToOne(targetEntity="User", inversedBy="friendshipsTo")
*/
protected $to = null;
I hope this pretty much explains it. If I add a friend, a new friendship entity will be created with me as "from" and the other user as "to".
So now I want to select all friends of a user. I already figured that out, here's my solution:
$query = $qb->select('u, ff, ft')
->from('KontakteApp\Api\Entity\User', 'u')
->leftJoin('u.friendshipsTo', 'ft')
->leftJoin('u.friendshipsFrom', 'ff')
->where('ft.from = :user')
->orWhere('ff.to = :user')
->andWhere('u.id != :user')
->getQuery();
So what's the problem now? I realized that in this case if I find a friend (here id = 3) of the user with the id 2 with
{
"id": 3,
"friendshipFrom": [
{
"id": 42,
"from": 3,
"to": 2,
"status": "open"
},
**HERE ARE NO MORE FRIENDSHIPS**
],
"friendshipTo": [
This is array is full of friendships of this user
]
}
there are no more friendships in friendshipsFrom except the one which had the user show up in my select. This happens because of select('u, ff, ft').
Now I would like the same happen to friendshipsTo, so in this case friendshipsTo of the friend should be empty.
To sum up: How do I filter associated entities?
Related
So I have two entites :
NextInfusionMashStepWithoutGrainAdjunct
// [...]
/**
* #ORM\ManyToMany(targetEntity=WaterGrainRatio::class)
* #ORM\JoinTable(name="next_infusion_mash_step_without_grain_adjunct_water_grain_ratio",
* joinColumns={#ORM\JoinColumn(name="next_infusion_mash_step_without_grain_adjunct_water_grain_ratio_id", referencedColumnName="id", nullable=false)},
* inverseJoinColumns={#ORM\JoinColumn(name="water_grain_ratio_id", referencedColumnName="id", nullable=false)}
* )
* #Groups("post:nextInfusionStepWithoutGrainAdjunct")
*/
private ?WaterGrainRatio $waterGrainRatioId;
//[...]
And WaterGrainRatio
//[...]
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
//[...]
I have added an WaterGrainRatioId with id: 1
And when I try to add a NextInfusionStepWithoutGrainAdjunct with postman with this body:
{
"waterGrainRatioId": "/dashboard/water_grain_ratios/1",
"...": "..."
}
I got this error: "The type of the \"waterGrainRatioId\" attribute must be \"array\", \"string\" given.",
I can't figure out why The IRI is not understood and why it needs an array.
What I tried:
#[ApiProperty(readableLink: false, writableLink: false)] but it did not works.
EDIT: I figure it out with this post, I just have to pass an array of IRI like this:
"waterGrainRatioId": [
"/dashboard/water_grain_ratios/1",
"/dashboard/water_grain_ratios/2"
]
Like I said in my edit I solve my problem by passing an array of IRIs:
"waterGrainRatioId": [
"/dashboard/water_grain_ratios/1",
"/dashboard/water_grain_ratios/2"
]
I'm developing an article database with API endpoints using Symfony 3.2 and Doctrine 2.5. In the request body, I have an Author:
{
"article": {
"title": "Some article",
"author": {
"email": "someone#something.com",
"name": "Howard Wedothis"
}
}
}
I'd like to store an Update entity with a datetime, article ID and the Author ID. In order to do this currently, I'm having to post the following:
{
"article": {
"title": "Some article",
"updates": [{
"author": {
"email": "someone#something.com",
"name": "Howard Wedothis"
}
}]
}
}
This isn't as elegant IMO.
First question: What's the best way to mutate/hydrate the simple author fields and store an Update against the article and author? Authors and Articles are ManyToMany but I'd like to avoid posting an array of one for readability reasons mostly.
Second question: Is it possible to add properties to the article entity for created and lastUpdated to display the first and last update respectively from the article without injecting a repository into the entity?
i.e.
$article->getCreated() // returns first Update
$article->getLastUpdated() // returns last Update
In the end I solved this by adding the Author and overriding the Author on updates and using the Lifecycle Events to create a new Update entity on PrePersist and PreUpdated as suggested by #Matko.
Request Data:
{
"article": {
"title": "Some article",
"author": {
"email": "someone#something.com",
"name": "Howard Wedothis"
}
}
}
Article.php:
/**
* #ORM\Entity
* #ORM\Table(name="articles")
* #ORM\HasLifecycleCallbacks
*/
class Article
{
/**
* #return Update
*/
public function getCreated(): Update
{
return $this->updates->first();
}
/**
* #return Update
*/
public function getLastUpdated(): Update
{
return $this->updates->last();
}
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function addUpdate(): void
{
$update = new Update();
$update->setAuthor($this->getAuthor());
$update->setArticle($this);
$this->updates->add($update);
}
}
This method does sort of invalidate the Author of the entity but keeps the article history.
Answer to the second question - use Lifecycle Callbacks
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\HasLifecycleCallbacks
*/
class Author
{
...
/**
* #ORM\Column(type="datetime")
*/
protected $createdAt;
/**
* #ORM\Column(type="datetime")
*/
protected $updatedAt;
/**
* Get id.
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set createdAt.
*
* #param \DateTime $createdAt
*
* #return this
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt.
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set modifiedAt.
*
* #param \DateTime $updatedAt
*
* #return Article
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt.
*
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
...
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updatedTimestamps()
{
$this->setUpdatedAt(new \DateTime(date('Y-m-d H:i:s')));
if ($this->getCreatedAt() == null) {
$this->setCreatedAt(new \DateTime(date('Y-m-d H:i:s')));
}
}
}
http://symfony.com/doc/current/doctrine/lifecycle_callbacks.html
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events
Regarding your request examples, insert request and update request should have the same format. You are complicating your life with that updates key.
Use POST method for inserting article and PUT (you send complete article object) or PATCH (you send only fields that you want to update).
https://knpuniversity.com/screencast/rest/put-versus-post
https://knpuniversity.com/screencast/symfony-rest/patch-programmer
The point of this question is to figure out what technique is better and heard different opinions from some skilled symfony2 coders.
An example will be ilustrated on challenges and "challenge rating" table where many people can rate certain challenge. (something like stackoverflow vote question system).
The tables look like this: (like_dislike is boolean(1= like, 0 = dislike)
The amount of data will be from 10-200+ rates for challenge.
Working with collections
Challenges entity
/**
* Challanges
*
* #ORM\Table(name="challanges")
* #ORM\Entity(repositoryClass="TB\ChallangesBundle\Entity\ChallangesRepository")
*/
class Challanges
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
* #Assert\NotBlank()
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255, unique=true)
*/
private $slug;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
* #Assert\NotBlank()
*/
private $description;
/**
* #var \DateTime
* #ORM\Column(name="start_date", type="datetime", nullable=false)
* #Assert\DateTime()
*/
private $start_date;
/**
* #var \DateTime
* #ORM\Column(name="end_date", type="datetime", nullable=false)
* #Assert\DateTime()
*/
private $end_date;
/**
* #ORM\ManyToOne(targetEntity="TB\UserBundle\Entity\User", fetch="EXTRA_LAZY")
* #ORM\JoinColumn(name="owner_id", referencedColumnName="id", nullable=false)
*/
protected $owner;
/**
* #ORM\OneToMany(targetEntity="TB\ChallangesBundle\Entity\ChallangeRating", mappedBy="challange", cascade={"persist", "remove"})
*/
protected $likes;
/**
* Constructor
*/
public function __construct()
{
$this->likes = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add likes
*
* #param \TB\ChallangesBundle\Entity\ChallangeRating $likes
* #return Challanges
*/
public function addLike(\TB\ChallangesBundle\Entity\ChallangeRating $likes)
{
$this->likes[] = $likes;
return $this;
}
/**
* Remove likes
*
* #param \TB\ChallangesBundle\Entity\ChallangeRating $likes
*/
public function removeLike(\TB\ChallangesBundle\Entity\ChallangeRating $likes)
{
$this->likes->removeElement($likes);
}
/**
* Get likes
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getLikes()
{
return $this->likes;
}
public function filterLikesInChallenge($like_dislike) {
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('like_dislike', $like_dislike));
return $this->likes->matching($criteria);
}
public function checkIfUserRatedAlready(\TB\UserBundle\Entity\User $user)
{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('fan', $user));
return $this->likes->matching($criteria);
}
Challenge rating entity
<?php
namespace TB\ChallangesBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* ChallangeRating
*
* #ORM\Table(name="challange_rating")
* #ORM\Entity(repositoryClass="TB\ChallangesBundle\Entity\ChallangeRatingRepository")
*/
class ChallangeRating
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var boolean
*
* #ORM\Column(name="like_dislike", type="boolean")
*/
private $like_dislike;
/**
* #ORM\ManyToOne(targetEntity="TB\UserBundle\Entity\User", inversedBy="fans")
*/
protected $fan;
/**
* #ORM\ManyToOne(targetEntity="TB\ChallangesBundle\Entity\Challanges", inversedBy="likes")
*/
protected $challange;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get like_dislike
*
* #return boolean
*/
public function getLikeDislike()
{
return $this->like_dislike;
}
/**
* Set like_dislike
*
* #param boolean $like_dislike
* #return ChallangeRating
*/
public function setLikeDislike($like_dislike)
{
$this->like_dislike = $like_dislike;
return $this;
}
/**
* Set fan
*
* #param \TB\UserBundle\Entity\User $fan
* #return ChallangeRating
*/
public function setFan(\TB\UserBundle\Entity\User $fan = null)
{
$this->fan = $fan;
return $this;
}
/**
* Get fan
*
* #return \TB\UserBundle\Entity\User
*/
public function getFan()
{
return $this->fan;
}
/**
* Set challange
*
* #param \TB\ChallangesBundle\Entity\Challanges $challange
* #return ChallangeRating
*/
public function setChallange(\TB\ChallangesBundle\Entity\Challanges $challange = null)
{
$this->challange = $challange;
return $this;
}
/**
* Get challange
*
* #return \TB\ChallangesBundle\Entity\Challanges
*/
public function getChallange()
{
return $this->challange;
}
}
Okay and now i would like:
Display challenge details
Display rate feature (like, dislike) also with numbers of likes and dislikes
Display the list of all users that have rated this challenge
Controller
So classic beginning with obtaining $challenge
// this will take care of point number 1 (display challenge details) (1.)
$challange = $challangesRepo->findOneBy(array('slug'=>$slug));
// display numbers of likes and dislikes for certain challenge (2.)
But now comes a decision...
Question 1
Should i obtain the likes via quering the DB from querybuilder? (classic select count)
OR
Should i use collections and criteria like this ? :
$challangeLikes = $challange->filterLikesInChallenge(1);
$challangeDislikes = $challange->filterLikesInChallenge(0);
**What is better for memory usage? what is better for DB performances? **
If i am not wrong the following two queries are executed by these two methods:
SELECT
t0.id AS id1,
t0.like_dislike AS like_dislike2,
t0.fan_id AS fan_id3,
t0.challange_id AS challange_id4
FROM
challange_rating t0
WHERE
(
t0.like_dislike = ? AND t0.challange_id = ?
)
Parameters: [1, 12]
SELECT
t0.id AS id1,
t0.like_dislike AS like_dislike2,
t0.fan_id AS fan_id3,
t0.challange_id AS challange_id4
FROM
challange_rating t0
WHERE
(
t0.like_dislike = ? AND t0.challange_id = ?
)
And now i can pass the number of likes,dislikes to the view as follow:
'challangeLikes'=>$challangeLikes->count(),
'challangeDislikes'=>$challangeDislikes->count(),
Question 2
What if i want to know if certain user rated this challenge already?
Again...
*Should i use classic querybuilder style with select count *
OR
I should use a method like:
$ratedAlreadyCol = $challange->checkIfUserRatedAlready($user)->first();
That will execute actually another query ? something like classic select count but the collection will do this for me? So it's not a search in some big memory array with allll likes but it's a query to DB ?
SELECT
t0.id AS id1,
t0.like_dislike AS like_dislike2,
t0.fan_id AS fan_id3,
t0.challange_id AS challange_id4
FROM
challange_rating t0
WHERE
(
t0.fan_id = ? AND t0.challange_id = ?
)
Parameters: [25, 12]
Question 3 - probably the most important for performances
I want to display all "fans - people who rated the certain challenge"...
Again...
Should i create a separate querybuilder method in repository with selecting all ratings for certain challenge with inner join to the users table (so i can display profile image and username)
OR
Should i just get all ratings and in twig loop through it like:
$challangeLikesCollection = $challange->getLikes();
{% for bla bla
BUT
If i will do it this way... doctrine will execute a select query to the user table for every "fan" in loop... and when there will be let's say... 200 fans... that's not good right?
BONUS QUESTION
Can somehow please provide his way of dealing with these situations? any suggestions?
Or do you use any other technique?
i care a lot about memory usage and DB load time because this will be used everywhere and every user will have such a list with different challenges. The list will consist of let's say 15 challenges and to connect all the likes,dislikes to every challenge in the list etc etc... performances...
Thank you for your explanations, tips and hints that will help me and other readers to move on another level!
I would do the following:
Denormalize a little and add number of likes and number of dislikes fields to Challenge and update these values in addLike and removeLike
Rename like_dislike to like as it's a boolean field and 1 means like 0 means dislike
Query the list of users with a separate query and use array hydration and INDEXBY username (it must be unique or user id) or maybe create a custom hydrator
SELECT u.username, u.photo FROM User u INNER JOIN u.ratings WITH r.fan = :fan INDEX BY u.username
or something like that. And you can check if the current user's username is in the array or not.
I think this could be performant enough.
Some explanation:
INDEX BY means that the result collection or array key (index) will be the value of a field (this field has to be unique). When you use INDEX BY the result set will contain known keys so you can reach (and e.g. check for existence) individual results directly in constant time (you don't have to search through the whole result set).
Doctrine uses PDO underneath, hydration means how the PDO result set will be processed and transformed into something else. The default object hydration means the result set will be transformed into an object graph, it is a very expensive operation. There are other hydration modes which are less expensive, but you loose some felxibility. E.g. if you use array hydration the result will be an array of arrays so you can't modify it (I mean persist back to the database) so it's just for reading and as the result not entity objects you can't use it's methods, e.g. custom getters. You can create custom hydrators if you want.
I have a form to create a new entity. That entity has a collection of other entities that are also entered in that form.
I want to use the validation options of the entity in the collection to validate those entities but it does not work. The validation rules of the "main" entity (Person) are checked, but the validation rules of the entities in the addressList collection (Address) are not checked. When I input invalid information in the fields, the submitted form is successfully validated.
In this example, the annotation for street is not used on validation.
class Person
{
...
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
*/
protected $addressList;
....
}
class Address
{
...
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="addressList")
* #ORM\JoinColumn(name="person_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $owner;
/**
* #ORM\Column(type="string", length=75)
* #Assert\MinLength(
* limit=3,
* message="Street must have atleast {{ limit }} characters."
* )
*/
protected $street;
...
}
How can I get the form to validate the supplied Address entities?
I had the same problem but was solved with:
/**
* #ORM\OneToMany(
* targetEntity="Entity",
* mappedBy="mappedEntity",
* cascade={"persist" , "remove"}
* )
* #Assert\Valid
*/
I use this:
use Symfony\Component\Validator\ExecutionContextInterface;
class Person
{
...
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
*/
protected $addressList;
....
/**
* #Assert\Callback
*/
public function validate(ExecutionContextInterface $context)
{
if (!$this->getAddressList()->count()) {
$context->addViolationAt(
'addressList',
'You must add at least one address',
array(),
null
);
}
}
}
http://symfony.com/doc/current/reference/constraints/Callback.html
Just add annotation assert like following
/**
* #Assert\Count(
* min = "1",
* minMessage = "You must specify at least one"
* )
* #Assert\Valid
*
*/
protected $name_of_collection_property;
You could also use the "Valid" constraint with the "All" constraint :
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
* #Assert\All({
* #Assert\Valid
* })
*/
protected $addressList;
I'm trying to get some simple CRUD done with doctrine 2 but when it's time to update a record with one property set as an array collection I don't seem to get removeElement() to work as it's supposed to. I even tried doing it in this ridiculously ugly way:
foreach($entity->getCountries() as $c) {
$entity->getCountries()->removeElement($c);
$this->em->persist($entity);
$this->em->flush();
}
and it didn't work... Anyone knows how to handle this? I've asked for a solution to this in many different forms and haven't got a good response so far... seems there's lack of good examples of Doctrine 2 CRUD handling. I'll post more code at request.
Edit
//in user entity
/**
*
* #param \Doctring\Common\Collections\Collection $property
* #OneToMany(targetEntity="Countries",mappedBy="user", cascade={"persist", "remove"})
*/
private $countries;
//in countries entity
/**
*
* #var User
* #ManyToOne(targetEntity="User", inversedBy="id")
* #JoinColumns({
* #JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
I do something similar in a project with Events which have participants not unlike your User/Country relationship. I will just lay out the process and you can see if there's anything you are doing differently.
On the Participant entity
/**
* #ManyToOne(targetEntity="Event", inversedBy="participants", fetch="LAZY")
* #JoinColumn(name="event_id", referencedColumnName="id", nullable="TRUE")
* #var Event
*/
protected $event;
On the Event entity:
/**
* #OneToMany(targetEntity="Participant", mappedBy="event")
* #var \Doctrine\Common\Collections\ArrayCollection
*/
protected $participants;
Also in Event#__constructor I initialize like this:
$this->participants = new \Doctrine\Common\Collections\ArrayCollection();
Here is how I update an event:
public function update(Event $event, Event $changes)
{
// Remove participants
$removed = array();
foreach($event->participants as $participant)
{
if(!$changes->isAttending($participant->person))
{
$removed[] = $participant;
}
}
foreach($removed as $participant)
{
$event->removeParticipant($participant);
$this->em->remove($participant);
}
// Add new participants
foreach($changes->participants as $participant)
{
if(!$event->isAttending($participant->person))
{
$event->addParticipant($participant);
$this->em->perist($participant);
}
}
$event->copyFrom($changes);
$event->setUpdated();
$this->em->flush();
}
The methods on the Event entity are:
public function removeParticipant(Participant $participant)
{
$this->participants->removeElement($participant);
$participant->unsetEvent();
}
public function addParticipant(Participant $participant)
{
$participant->setEvent($this);
$this->participants[] = $participant;
}
The methods on the Participant entity are:
public function setEvent(Event $event)
{
$this->event = $event;
}
public function unsetEvent()
{
$this->event = null;
}
UPDATE: isAttending method
/**
* Checks if the given person is a
* participant of the event
*
* #param Person $person
* #return boolean
*/
public function isAttending(Person $person)
{
foreach($this->participants as $participant)
{
if($participant->person->id == $person->id)
return true;
}
return false;
}
New answer
In your countries entity, should you not have:
#ManyToOne(targetEntity="User", inversedBy="countries")
instead of inversedBy="id"?
Initial answer
You need to set the countries field in your entity as remove cascade. For example, on a bidirectional one to many relationship:
class Entity
{
/**
*
* #OneToMany(targetEntity="Country", mappedBy="entity", cascade={"remove"})
*/
private $countries;
}
This way, when saving your entity, doctrine will also save changes in collections attached to your entity (such as countries). Otherwise you have to explicitly remove the countries you want to remove before flushing, e.g.
$this->em()->remove($aCountry);
This is also valid for persist, merge and detach operations. More information here.