Doctrine2 - add to product entity parameter with value - php

Please, could you help me?
I´m looking for the best way, how to add to entity (Product) some parametrs with values.
For example:
Product T-Shirt would have parametrs: size: XXL, color: red, material: cotton. How to make tables to have the best result - easy adding parametrs to product and easy filtering products by parametrs.
Thank you for your opinions.

You have two options:
A OneToMany relationship with another entity (recommended)
You may create a new entity called ProductProperty and declare a OneToMany relationship from Product to ProductProperty, like this:
The Product entity
/**
* #ORM\Entity
*/
class Product
{
/**
* #ORM\OneToMany(targetEntity="ProductProperty", mappedBy="product", cascade={"remove"})
*/
public $properties;
}
The ProductProperty entity
/**
* #ORM\Entity
*/
class ProductProperty
{
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="properties")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
public $product;
}
Create an array property in entity Product
Doctrine 2 supports arrays (it serializes the array into a TEXT column). Create a property that behaves like an array:
/**
* #ORM\Entity
*/
class Product
{
/** #ORM\Column(type="array") */
public $properties;
public function __construct()
{
$this->properties = []; //new PHP array notation, if using older PHP use array()
}
}

Related

Incorrect Doctrine2 hydration when received data is correct

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...

Get subclass from repository using Class Table Inheritance

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);

How to handle Primary/Secondary/Normal statuses for entity in Symfony2

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.

symfony2 doctrine manytoone relationship

I believe it's that there must be a simple solution for my issue but I can't get that to work and I'm not sure why.
Scenario:
I've got 2 entities: Bike and Review, relationship is OneToMany:
/**
* Bike
*
* #ORM\Entity(repositoryClass="BikeRepository")
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="bike")
*/
class Bike
{
...
/**
* #var string
*
* #ORM\OneToMany(targetEntity="Review", mappedBy="bike", cascade={"persist"}, fetch="EXTRA_LAZY")
*/
private $reviews;
...
}
/**
* Review
*
* #ORM\Entity(repositoryClass="ReviewRepository")
* #ORM\Table(name="review")
* #ORM\HasLifecycleCallbacks()
*/
class Review
{
...
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="Bike", inversedBy="reviews", fetch="EXTRA_LAZY")
* #ORM\JoinColumn(name="bike_id", referencedColumnName="id")
*/
private $bike;
....
}
Getters and setters has been generated by doctrine.
On the frontend I've got a form where bike is as a hidden field because there is an autocomplete field with ajax request. So in the controller if $form->isValid() I'm getting bike id from the hidden field, search database for that bike and set bike as below:
if ($form->isValid()) {
if ($bikeId) {
$bike = $this->em->getRepository('BikeBundle:Bike')->findOneBy(array('id' => $bikeId));
$review->setBike($bike);
}
$this->em->persist($review);
$this->em->flush();
}
And this is always giving me NULL in the database for bike_id. Any idea what could be wrong? I have tried to dump $review before persist and I can get the bike details so I don't know what's going wrong. In the entity setBike() method I can get any value of that Bike object except getId() is returning NULL.
Thanks for any help.
you don't need to persist an existing entity, so you can just save your entity like
$review->setBike($bike);
$this->em->flush();
try to check if doctrine really returns any entities..I'm afraid if it doesn't return Bike entity
UPD: Also try to check the namespace of Bike Entity class. It seems like it is wrong in your case, because usually it looks like "AcmeBikeBundle:Bike"

Load table values into objects variable without mapping

I have the following data model
Product: id, description, price
ProductExtraField: id, name
ProductExtraFieldData: product_id, extra_field_id, value
I want to use this datamodel so users of my application can add extra properties to a product on the fly. Let's say the add the property 'Color'. When they do so a row will be created in the ProductExtraField table with a value like this: 6, 'Color'. Now the can add values for this property to there products. When the do so a new row will be created in the ProductExtraFieldData table with a value like this: 4, 6, 'Green'. Now I want to represent this model in a class called Product. I use the following code:
<?php
use Doctrine\Common\Collections\ArrayCollection;
/** #Entity #Table(name="product") **/
class Product
public function __construct() {
$this->extraFieldData = new ArrayCollection();
$this->extraField = new ArrayCollection();
}
/** #Id #Column(name="id", type="integer") #GeneratedValue **/
private $id;
/** #Column(name="description", type="string") **/
private $description;
/** #Column(name="price", type="float") **/
private $price;
/**
* #OneToMany(targetEntity="ProductExtraFieldData", mappedBy="product", cascade={"persist"})
* #JoinColumn(name="product_id", referencedColumnName="id")
* #var extraFieldData[]
**/
protected $extraFieldData = null;
/**
* #OneToMany(targetEntity="ProductExtraField", mappedBy="??????")
* #var extraField[]
**/
protected $extraField = null;
Offcourse the last couple of lines are not correct. I don't know how to load this data. I want the names of the extra fields to be available with every product. I think $extraField should even be static. How can I configure Doctrine to load these values? Persisting the $extraField values from the Product class is not requirement.
Perhaps a structure like the following:
Product
id
description
etc...
ProductExtraField
product_id
extra_field_id
value
ExtraField
id
label
(anything else? type?)
So your Product will have a OneToMany mapping on ProductExtraField, which will have a ManyToOne mapping to ExtraField. Allowing you to use code such as...
$extraValues = array();
foreach ($product->getExtraFields() as $productExtraField) {
$extraValues[$productExtraField->getExtraField()->getLabel()] = $productExtraField->getValue();
}
// builds array such as array('colour' => 'blue', 'size' => 'small)

Categories