I am really new to Symfony so I apologize in advanced if this sounds stupid and I will really appreciate if anyone to correct my understanding.
I am reading about Databases and Doctrine and while reading I thought why not create a dummy blog app to practice.
The dummy blog app i am working on is very simple just three tables and its Entity
post (where the blog posts go) its Entity is Entity/Post.php,
comments (where to post comments go) its Entity is Entity/Comments.php
category (where the post categories go) its Entity is Entity/Category.php.
I am able to get the post/category/comments to save, show, update, delete all that is working fine.
What i am working on now is when the blog is displayed, its category appears as a number (category id), so i am trying to link the post table with category table to display the category name rather than number.
Question 1, Since the post is also linked with the comments table and i need to link the same post table with category table can we do this inside the Entity/Post.php?
class Post
{
/**
* #ORM\OneToMany(targetEntity="Comments", mappedBy="post")
*/
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="post")
* #ORM\JoinColumn(name="category", referencedColumnName="id")
*/
protected $comment;
protected $categories;
If not then what is the correct way to handle these relationships?
Question 2, While reading "Fetching Related Objects", it seems that I should be able to get the category name my doing the following
$posts = $this->getDoctrine()->getRepository('BlogBundle:Post')->findBy(array('category' => $id), array('id' => 'DESC'));
$category_name = $posts->getCategory();
but this gives me an error
Error: Call to a member function getCategory() on a non-object in
I can confirm that this getCategory() method does exist in Post entity
I will really appreciate any assistance here.
Question 1
The annotations are fine, but you have to write them right on top of the property they belong to, otherwise they are ignored:
class Post
{
/**
* #ORM\OneToMany(targetEntity="Comment", mappedBy="post")
*/
protected $comments;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="posts")
* #ORM\JoinColumn(name="category", referencedColumnName="id")
*/
protected $category;
public function __constructor()
{
$this->comments = new ArrayCollection();
}
// ...
}
Make shure you have the correct counterpart set in the other entities:
class Category
{
/**
* #ORM\OneToMany(targetEntity="Post", mappedBy="category")
*/
protected $posts;
public function __constructor()
{
$this->posts = new ArrayCollection();
}
// ...
}
class Comment
{
/**
* #ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
* #ORM\JoinColumn(name="post", referencedColumnName="id")
*/
protected $post;
// ...
}
Note that I changed some singular / plural of properties and Comments class name. Should be more intuitive like that.
Question 2
findBy returns an array of found objects, even if only one object was found. Use either findOneBy or foreach through your result.
Related
EDIT 2018-05-22: no answer fully fixed issue - can no longer replicate issue as no longer have access. Not removing based on this meta discussion
Please do not spend time/effort creating an answer
Discussion in #Wilt's answer led me to what I know about using discriminators now, it might help future questioners. In my case it helped, but did not provide an answer.
I've got a bit of complex problem where the hydration of data received from client-side gets hydrated incorrectly. I've been trying to fix the problem for close to a week now, so I thought to ask you guys.
We've got this application that allows for the creation of assignments for students. Assignments may contain Questions, Text items, Media items, and more. The problem is with the Questions and associated Answers.
Scenario
Assignment
QuestionSheet 1 (L1)
Question 1 (L1 - V1)
Answer A (L1 - V1 - A1)
Answer B (L1 - V1 - A2)
Question 2 (L1 - V2)
Answer A (L1 - V2 - A1) (just 1 answer)
QuestionSheet 2 (L2)
Question 1 (... and so on)
Answer A
Answer B
Question 2
Answer A
Answer B
Question 3
Answer A
Answer B
The above gets send properly from the client-side. Screenshot of snipped of that data:
Important to note is that, as you can see above, a Question is in fact a GridElements entity. It might've also been Text or Image, this is based on the property type = question which is a Discriminator.
After hydrating the data we get the following Entity structure:
As you can see, the data is no longer correct after hydration. This is done during the $form->isValid(). QuestionSheet 1 contains the first Question for QuestionSheet 2 and that Question has the first Answer from the third Question of the second QuestionSheet.
When reading through the full hydrated dataset, I see that the Answers created for the first QuestionSheet have been dropped. The Answers from the second QuestionSheet have been duplicated and have overwritten the Answers in the first QuestionSheet. In essence, what you see in the picture above.
Worse still
The below is all the data that is saved to the database after the above, with the mentioned scenario of 2 lists, 5 questions and 9 answers.
So of the first Question, no Q&A left. The second QuestionSheet's Questions have been used to overwrite them. Also , only the last 2 Answers are there, filling the space of what should've been 9!.
Btw, the query returning this is completely LEFT JOIN so as to show all empty data as well, this is all there's left.
It seems that it grabs the last set of whatever child entities there are to fill up previous entities, or something. I've gotten lost.
How is this possible?
As I mentioned, I've been at it a good long while, but cannot find a solution. Hope you guys can help.
If you need any info on code I'll do my best to either show it here or explain it as best as possible in case of some proprietary code.
Update - Entities
Assignment entity
/**
* #ORM\Entity(repositoryClass="Wms\Admin\Assignment\Repository\AssignmentRepository" )
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="ass_assignment")
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
*/
class Assignment extends SeoUrl
{
//Traits and properties
/**
* #ORM\OneToMany(targetEntity="Wms\Admin\Assignment\Entity\QuestionSheet", mappedBy="assignment", cascade={"persist", "remove"}, orphanRemoval=true)
**/
protected $questionSheets;
public function __construct()
{
$this->abstractEntity_entityCategories = new ArrayCollection();
$this->questionSheets = new ArrayCollection();
$this->documents = new ArrayCollection();
}
public function __toString()
{
return (string)$this->id;
}
//More getters/setters
}
QuestionSheet entity
/**
* #ORM\Entity(repositoryClass="Wms\Admin\Assignment\Repository\QuestionSheetRepository")
* #ORM\Table(name="ass_questionsheet")
**/
class QuestionSheet extends AbstractEntity
{
/**
* #ORM\ManyToOne(targetEntity="Wms\Admin\Assignment\Entity\Assignment", inversedBy="questionSheets")
* #ORM\JoinColumn(name="assignment_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $assignment;
/**
* #ORM\OneToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Grid", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="grid_id", referencedColumnName="id")
**/
protected $grid;
public function __construct()
{
$this->gridElements = new ArrayCollection();
}
//More getters/setters
}
Grid entity
/**
* #ORM\Entity
* #ORM\Table(name="lg_grid")
**/
class Grid extends AbstractEntity
{
/**
* #ORM\OneToMany(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\AbstractElement", mappedBy="grid", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\OrderBy({"y" = "ASC", "x" = "ASC"})
*/
protected $gridElements;
public function __construct()
{
$this->gridElements = new ArrayCollection();
}
}
Grid Element entity
/**
* #ORM\Table(name="lg_grid_element")
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\HasLifecycleCallbacks
**/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
/**
* #ORM\ManyToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Grid", inversedBy="gridElements")
* #ORM\JoinColumn(name="grid_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $grid;
public $type = ''; //This is a discriminator
}
Question entity
/**
* #ORM\Entity
* #ORM\Table(name="lg_grid_question")
**/
class Question extends AbstractElement
{
/**
* #ORM\OneToMany(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\Answer", mappedBy="question", cascade={"persist"})
*/
protected $answers;
public $type = 'question'; //Inherited property, now filled in with discriminator value
public function __construct()
{
$this->answers = new ArrayCollection();
}
}
Answer entity
/**
* #ORM\Entity
* #ORM\Table(name="lg_grid_answer")
**/
class Answer extends AbstractEntity
{
/**
* #ORM\ManyToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\Question", inversedBy="answers", cascade={"persist"})
* #ORM\JoinColumn(name="question_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $question;
public function __toSting() {
return (string) $this->getId();
}
}
Update 2
Updated AbstractElement entity based on #Wilt's answer.
/**
* #ORM\Table(name="lg_grid_element")
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\HasLifecycleCallbacks
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({
* "abstractElement"="AbstractElement",
* "question"="Question",
* //Others
* })
**/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
//Same as above
}
This update created some problems with the NonUniformCollection which handles getting the correct Entity. This used to be based on the $type property.
However, having a $type property in an Entity which has * #ORM\DiscriminatorColumn(name="type", type="string") as a notation, is not allowed. Therefore all the classes making use of a discriminator have also been updated with the following.
const ELEMENT_TYPE = 'question'; //Overwritten from AbstractElement
/**
* #return string
*/
public function getType() //Overwritten from AbstractElement
{
return self::ELEMENT_TYPE;
}
Alas, the original problem remains.
I am not sure if this is causing your issue, but it seems to me that your inheritance mapping is not setup correctly:
You need to declare your discriminator column inside your entity definitions as written in the docs, they should not be set as properties, doctrine takes care of setting them inside your database:
/**
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap({"element"="AbstractElement", "question"="Question", "text"="TextItem", "media"="MediaItem"})
*/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
//...
}
And is your abstract entity properly mapped as a #MappedSuperClass?
/**
* #MappedSuperclass
*/
class AbstractEntity
{
//...
}
This might be part of your solution, please come back with feedback after you updated accordingly...
I have defined the follow entity in doctrine2 (with symfony).
/**
*
* #ORM\Table(name="order")
* #ORM\Entity
*/
class Order
/**
* #var integer
*
* #ORM\Column(name="personid", type="integer", nullable=false)
*/
private $personid;
/**
* #ORM\OneToOne(targetEntity="People")
* #ORM\JoinColumn(name="personid", referencedColumnName="personid")
*/
private $person;
public function getPersonId()
{
return $this->personid;
}
public function getPerson()
{
return $this->person;
}
}
I realize that if I call $order->getPersonId() it return always an empty value and I have to call the getPerson()->getId() method to get the correct personid.
Could anyone explain me why the variable $personid is not filled?
Should I to delete the column id used for the join if I defined one?
Thanks
Gisella
You should remove private $personid;, it's better to work with objects only in an ORM.
It's not a problem if you get the ID with $order->getPerson()->getId(), because Doctrine won't load the complete entity. The People entity will only be loaded if you call an other field than the join key.
You can still have a getter shortcut like this :
public function getPersonId()
{
return $this->getPerson()->getId();
}
Edit :
You can also still work with "ID" if you use Doctrine references, like this :
$order->setPerson($em->getReference('YourBundle:People', $personId));
With this way, Doctrine won't perform a SELECT query to load data of the person.
You don't need to have the $personid field when you already have the $person field.
$people contains the People object (with all People's attributes including the id).
Moreover, when doctrine translate your object into sql tables, he knows that he have to join with th id so it will create a field (in database) named personid. (It's the name that you defined in your ORM)
/**
* #ORM\OneToOne(targetEntity="People")
* #ORM\JoinColumn(name="personid", referencedColumnName="personid")
*/
private $person;
Sorry for bad english :p
So let's say we use a User and a Ticket class. They are normal entities, nothing fancy.
The User class contains this lines:
/**
* #ORM\ManyToMany(targetEntity="Ticket", mappedBy="collaborateurs")
**/
private $tickets;
The Ticket class contains this:
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="tickets")
* #ORM\JoinTable(name="users_to_tickets")
**/
private $collaborateurs;
To get all ticket's a user has I can just call the getTickets() function created by Symfony. As far as good. The Ticket class has a few additional fields like updated which is a DateTime field or status which is an integer. I would like to sort those tickets by status DESC and updated DESC
I know I could just make a function in the repository like findTicketsByUserOrderedByFooBar($user), but I'm wondering if there isn't a better way.
If you always want your tickets to be in that order you can set and orderBy on the association.
/**
* #ORM\ManyToMany(targetEntity="Ticket", mappedBy="collaborateurs")
* #ORM\OrderBy({"status" = "DESC", "updated" = "DESC"})
**/
private $tickets;
You can add an Helper method to your User entity and sort/filter DIRECTLY on the ArrayCollection with doctrine2 criteria. Something like this:
/**
* this return a ArrayCollection
*/
public function getTicketsByUserOrderedByFooBar()
{
$criteria = Criteria::create()
->orderBy(array('foo'=>'DESC','bar' =>'ASC'))
return $this->tickets->matching($criteria);
}
/**
* this return a ArrayCollection
*/
public function getTicketsByUserOrderedBytitle()
{
$criteria = Criteria::create()
->orderBy(array('title'=>'DESC'))
return $this->tickets->matching($criteria);
}
See also this
Hope this help.
Creating a function the way you suggested would be the suggested approach.
I have one entity, say Person, which contains a list of $pets:
protected $pets;
public function getPets()
{
return $this->pets;
}
Standard Doctrine. Unfortunately, these pets may be of different types, such as cats or dogs, or a mix. So I used Class Table Inheritance:
/**
* #ORM\Entity
* #ORM\Table(name="pets")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="pettype", type="string")
* #ORM\DiscriminatorMap({"cat_animal" = "CatAnimal", "dog_animal" = "DogAnimal"})
*/
class Pet
{
/**
* #ORM\Column(name="eventid", type="integer")
* #ORM\Id
*/
private $id; // protected did not work either
/**
* Get id
*/
public function getId()
{
return $this->id;
}
}
/**
* #ORM\Entity
* #ORM\Table(name="cat_animal")
*/
class CatAnimal extends Pet
{
/**
* #ORM\Column(type="float")
*/
protected $height;
// etc.
}
// DogAnimal class omitted.
This was relatively straightforward using Doctrine's docs.
If I want to get all cats for an individual person, I have discovered I can do this:
public function getCats($person)
{
return $this->getEntityManager()->getRepository('MyBundle:CatAnimal')
->findByPerson($person);
}
However, how do I access the subclasses using a query builder? If I have the Person repository ($repos here), I want to do something like the following:
$repos->createQueryBuilder('person')
->select('pet.height')
->join('person.pets', 'pet')
->where('person = :person')
->setParameter('person', $person);
Except Pet doesn't have height, so this throws an exception. The DQL generated automagically joins to DogAnimal and CatAnimal, so I should be able to access these properties, but I don't know how. I have tried:
$repos->createQueryBuilder('person')
->select('cat.height')
->from('MyBundle:CatAnimal', 'cat)
->join('person.pets', 'pet')
->where('person = :person')
->setParameter('person', $person);
But this seems to do the cartesian product. I can solve that by adding:
->andWhere('person.id = cat.person')
This seems overly complicated for what I want. I have tried looking for the correct way to do this, but resources are limited.
This builds on a previous question, with a similar structure. The names of the tables were changed for clarity and generalisability.
You need to join correctly to Person, adding a field to the Pet class. In my example I named it owner:
$catRepo->createQueryBuilder('cat')
->select('cat.height')
->from('MyBundle:CatAnimal', 'cat')
->join('cat.owner', 'person')
->where('person = :person')
->setParameter('person', $person);
I am developing an application and I came across the following: Lets say I have an entity called Contact, that Contact belongs to a Company and the Company has a Primary Contact and a Secondary Contact and also has the remaining Contacts which I've named Normal.
My question is, what is the best approach for this when talking about entities properties and also form handling. I've though about two things:
Having 2 fields on the Company entity called PrimaryContact and SecondaryContact and also have a one-to-many relationship to a property called contacts.
What I don't like (or I'm not 100% how to do) about this option is that on the Contact entity I would need an inversedBy field for each of the 2 one-to-one properties and also 1 for the one-to-many relationship and my personal thought is that this is kind of messy for the purpose.
Having a property on the Contact entity called Type which would hold if it's primary, secondary or normal and in the Company methods that has to do with Contacts I would modify it and add the getPrimaryContact, getSecondaryContact, etc.
What I don't like about this option is that I would need to have 2 unmapped properties for the Company and I would need to do a lot on the form types in order to get this to work smoothly.
My question is what is the best approach for this structure and how to deal with forms and these dependencies. Let me know if this is not clear enough and I will take time and preparate an example with code and images.
I'm not yet a Symfony expert but i'm currently learning entites manipulation and relations !
And there is not simple way to do relations with attributes.
You have to create an entity that represent your relation.
Let's suppose you have an entity Company and and entity Contact
Then you will have an entity named CompanyContact whick will represent the relation between your objects. (you can have as many attributes as you wish in your relation entity). (Not sure for the Many-to-One for your case but the idea is the same)
<?php
namespace My\Namespace\Entity
use Doctrine\ORM\Mapping as ORM
/**
* #ORM\Entity(repositoryClass="My\Namespace\Entity\CompanyContactRepository")
*/
class CompanyContact
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="contact_type", type="string", length=255)
*/
private $contactType;
/**
* #ORM\ManyToOne(targetEntity="My\Namespace\Entity\Company")
* #ORM\JoinColumn(nullable=false)
*/
private $company;
/**
* #ORM\ManyToOne(targetEntity="My\Namespace\Entity\Contact")
* #ORM\JoinColumn(nullable=false)
*/
private $contact;
}
And in your controller you can do this:
$em = $this->getDoctrine()->getManager();
$company = $em->getRepository('YourBundle:Company')->find($yourCompanyId);
$yourType = "primary";
$companyContacts = $em->getRepository('YourBundle:CompanyContact')
->findBy(array('company' => $company, 'type' => $yourType));
What do you think about this approach ?
If i learn more soon i will get you posted ;)
Thanks to #Cerad this is the following approach I took:
I have a OneToMany property on the Company to hold all the contacts.
Implemented the getPrimaryContact/setPrimaryContact methods and looped through all the contacts and retrieving the one of the type I want. Did the same for the secondary.
On the Form type of the company my issue was that I had the 'mapped' => 'false' option, I removed this since I implemented the getters and setters SF2 knows it has to go to these methods.
`
<?php
namespace XYZ\Entity;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class Company
{
...
/**
* #ORM\OneToMany(targetEntity="\XYZ\Entity\Contact", mappedBy="company", cascade={"persist", "remove"})
*/
private $contacts;
public function getPrimaryContact() { ... }
public function setPrimaryContact(Contact $contact) { //Set the type of $contact and add it $this->addContact($contact) }
public function getSecondaryContact() { ... }
public function setSecondaryContact(Contact $contact) { //Set the type of $contact and add it $this->addContact($contact) }
}`
And for the Form Type I have:
`
class CompanyType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('primaryContact', new ContactType())
->add('secondaryContact', new ContactType())
}
...
}`
With this set everything runs smoothly and I can CRUD without much struggle.