Many-To-Many Bidirectional relationships with Doctrine - php

I have got a many-to-many bidirectional relationship in Doctrine. It associates items with categories. The issue is that at the beginning I am assigning a category to a item correctly, but when I am trying to update an item's category then it fails, with a duplicate primary key.
These are some snippets from the code that might be helpful:
/**
* #ORM\Table(name="item")
* #ORM\Entity(repositoryClass="SomeBundle\Entity\Repository\ItemRepository")
*
*/
class Item
{
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="items", cascade={"persist"})
**/
private $categories;
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #param Item $item
*/
public function addItem(Item $item)
{
$this->items[] = $item;
}
and
/**
* Category
*
* #ORM\Table(name="category", indexes={#ORM\Index(name="category_parent", columns={"parent_id"})})
* #ORM\Entity(repositoryClass="SomeBundle\Entity\Repository\CategoryRepository")
* #ORM\HasLifecycleCallbacks
*/
class Category
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Item", mappedBy="categories", cascade={"persist"})
* #ORM\JoinTable(name="item_category")
**/
private $items;
public function __construct()
{
$this->items = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #param Category $category
*/
public function addCategory(Category $category)
{
$this->categories[] = $category;
$category->addItem($this);
}
UPDATE
public function saveItem(Request $request)
{
$editMode = false;
$itemId = $request->request->get('item_id');
if (isset($itemId) && $itemId > 0) {
$editMode = true;
}
$itemName = $request->request->get('itemName');
$itemShortName= $request->request->get('itemShortName');
$itemRepo = $this->getItemRepository();
$item = $itemRepo->find($itemId);
// get last Item Id
if (!$editMode) {
$newItem = new Item();
$newItemId = rand(1000, 6000); // TODO
$newItem->setId($newItemId);
$newItem->setSection('ar');
// by default the item is inactive
$newItem->setActive(0);
}
//store the Item Type
$itemType = new ItemType();
$itemType->setTypeId($request->request->get('itemType'));
if (!$editMode) {
$itemType->setItemId($newItemId);
}
// store the data into the ItemTranslation
if (!$editMode) {
$newItemTranslation = new ItemTranslation();
$newItemTranslation->setItemId($newItemId);
$newItemTranslation->setLanguageId('1');
$newItemTranslation->setItemName($itemName);
$newItemTranslation->ItemShortname($itemShortName);
$newItemTranslation->setTimestampAdd(new \DateTime());
$this->em->persist($newItemTranslation);
}
//assign the respective Categories to the item
$selectedCategoriesIds = $request->request->get('itemCategories');
$categoryRepo = $this->getCategoryRepository();
if (count($selectedCategoriesIds) > 0) {
foreach ($selectedCategoriesIds as $selectedCategoryId) {
$category = $categoryRepo->find($selectedCategoryId);
if (is_object($item)) { //TODO
$item->addCategory($category);
$category->addItem($item);
} else {
$newItem->addCategory($category);
$category->addItem($newItem);
}
if (!$editMode) {
$this->em->persist($newItem);
}
}
}
$this->em->flush();
}
Error Message
An exception occurred while executing 'INSERT INTO item_category (item_id, category_id) VALUES (?, ?)' with params [2117, 1]:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '2117-1' for key 'PRIMARY'
** SECOND UPDATE **
I have added this
if (is_object($item)) {
$item->removeExistingCategories();
}
just before:
if (count($selectedCategoriesIds) > 0) {
foreach ($selectedCategoriesIds as $selectedCategoryId) {
$category = $categoryRepo->find($selectedCategoryId);
and it seems that it works fine, with the exception that now the categories are being appeared twice in the UI, although the item_category table has been correctly populated.
OK, the last issue seems to have been sorted. I have made a mistake in itemsCategories iteration. :)

Try removing cascade persist on Category Entity...you should do that only on the owning side of the relationship (Item in your case).

Related

Symfony / Doctrine - Get rows from related entity in entity

I'm trying to create cart in doctrine. Now I'm stuck with "quantity".
I'm trying to achieve that if product is already in cart, update quantity(quantity + 1).
Here are my entities:
Cart.php
class Cart
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
* #ORM\Column(type="guid")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity="Order", inversedBy="cart", cascade={"persist"})
* #ORM\JoinColumn()
*/
private $order;
/**
* #ORM\OneToMany(targetEntity="CartItem", mappedBy="cart", cascade={"persist"})
*/
private $cartItems;
public function __construct()
{
$this->cartItems = new ArrayCollection();
}
...
public function getItems()
{
return $this->cartItems;
}
public function addItem(CartItem $cartItem, Product $product, int $quantity = 1)
{
if ($this->cartItems->contains($cartItem))
return;
$cartItem->setProduct($product);
$cartItem->setQuantity($quantity);
$cartItem->setBoughtPrice($product->getBoughtPrice());
$cartItem->setPrice($product->getPrice());
$this->cartItems[] = $cartItem;
// set the *owning* side!
$cartItem->setCart($this);
}
public function removeItem(CartItem $cartItem)
{
$this->cartItems->removeElement($cartItem);
// set the owning side to null
$cartItem->setCart(null);
}
}
CartItem.php
class CartItem
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
* #ORM\Column(type="guid")
*/
private $id;
...
/**
* #ORM\ManyToOne(targetEntity="Cart", inversedBy="cartItems")
* #ORM\JoinColumn(name="cart_id", referencedColumnName="id")
*/
private $cart;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Product\Product", inversedBy="cartItems")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
public function getId()
{
return $this->id;
}
...
public function getCart()
{
return $this->cart;
}
public function setCart(Cart $cart)
{
$this->cart = $cart;
}
public function getProduct()
{
return $this->product;
}
public function setProduct(Product $product)
{
$this->product = $product;
}
...
}
I think most important method is addItem() in Cart.php.
Is it possible to access all rows from related entity and compare if product already exist?
Or should I do it in the controller?
Try with the following code:
public function addItem(CartItem $cartItem, Product $product, int $quantity = 1)
{
if ($this->cartItems->contains($cartItem))
return;
// Looking for an item with the same product
foreach ($this->cartItems as $item) {
// Suppose the product are equals comparing it by id
if ($item->getProduct()->getId() === $product->getId()) {
// We find an existing cart item for the product
// Update the cart item info:
$cartItem->setQuantity( $cartItem->getQuantity() + $quantity );
// NB: should we take care of the quantity ?
$cartItem->setBoughtPrice($cartItem->getBoughtPrice() + $product->getBoughtPrice());
// NB: should we take care of the quantity ?
$cartItem->setPrice($cartItem->getPrice() + $product->getPrice());
return;
}
}
$cartItem->setProduct($product);
$cartItem->setQuantity($quantity);
$cartItem->setBoughtPrice($product->getBoughtPrice());
$cartItem->setPrice($product->getPrice());
$this->cartItems[] = $cartItem;
// set the *owning* side!
$cartItem->setCart($this);
}
Hope this help

Delete a 3-entity (one-to-many-to-one) association with Symfony 3 using Doctrine

This is my very first question!
I have two entities that I want to relate: Product and Category. A product may have multiple categories and a category may correspond to many products. I've decided to implement this relationship as a 3-class association, having an intermediate ProductCategory entity, as shown in the image below. This give me flexibility to add properties to the association in the future.
Representation of my tree-class association
I want to assign existing categories to existing products. I want to establish the relationship from within the entities themselves. I am able to do that within the Product entity, using a setter method that receives an array of Category entities, and creates a new ProductCategory entity for each category passed. The procedure is as follows:
//Product.php
/**
* #param \Doctrine\Common\Collections\ArrayCollection $categories
* #return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
Note that I clear the ProductCategory collection before adding new ones; in this way only those categories selected in the form are saved to the database.
My problem is that Doctrine doesn't delete the records from the database before inserting the new ones. This is fine when no categories were assigned to the product but I get an Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'PRIMARY' when trying to update the association. I've checked the Symfony debug panel, in the Doctrine section, and no DELETE statement is ever executed prior to the INSERTs.
Is it possible to delete related entities from within an entity? If not, then why is it possible to add new ones? Thanks in advance.
My entities are as follows:
Product.php:
namespace TestBundle\Entity;
/**
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="TestBundle\Repository\ProductRepository")
*/
class Product {
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var \Doctrine\Common\Collections\ArrayCollection
* #ORM\OneToMany(targetEntity="ProductCategory", mappedBy="product", cascade={"persist"})
*/
private $productCategory;
/**
* Constructor
*/
public function __construct() {
$this->productCategory = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #param \TestBundle\Entity\ProductCategory $productCategory
* #return Product
*/
public function addProductCategory(\TestBundle\Entity\ProductCategory $productCategory) {
$this->productCategory[] = $productCategory;
return $this;
}
/**
* #param \TestBundle\Entity\ProductCategory $productCategory
*/
public function removeProductCategory(\TestBundle\Entity\ProductCategory $productCategory) {
$this->productCategory->removeElement($productCategory);
}
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getProductCategory() {
return $this->productCategory;
}
/**
* #param \Doctrine\Common\Collections\ArrayCollection $categories
* #return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
/**
* #return \Doctrine\Common\Collections\ArrayCollection
*/
public function getCategories() {
$categories = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($this->getProductCategory() as $pc) {
$categories[] = $pc->getCategory();
}
return $categories;
}
}
Category.php:
namespace TestBundle\Entity;
/**
* #ORM\Table(name="category")
* #ORM\Entity(repositoryClass="TestBundle\Repository\CategoryRepository")
*/
class Category {
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var \Doctrine\Common\Collections\ArrayCollection
* #ORM\OneToMany(targetEntity="ProductCategory", mappedBy="category", cascade={"persist"})
*/
private $productCategory;
}
ProductCategory.php
namespace TestBundle\Entity;
/**
* #ORM\Table(name="product_category")
* #ORM\Entity(repositoryClass="TestBundle\Repository\ProductCategoryRepository")
*/
class ProductCategory {
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Product", inversedBy="productCategory")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Category", inversedBy="productCategory")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
}
My Product form is generated as follows:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')
->add('categories', EntityType::class, array(
'class' => 'TestBundle:Category',
'choice_label' => 'name',
'expanded' => true,
'multiple' => true,
));
}
Note that I use a categories field name that will be populated with categories taken from Category entity. The form returns an array of Category objects that I use to generate ProductCategory entities in the setCategories() method within Product.php.
/**
* #param \Doctrine\Common\Collections\ArrayCollection $categories
* #return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
EDIT 1:
I don't have a categories field in Product, I only have a getCategories() and setCategories() methods. As shown in my form type code, I add an EntityType field of class Categories, that maps to the categories property (that doesn't actually exist). In this way I'm able to show existing categories as checkboxes an the product's categories are checked correctly.
EDIT 2: POSSIBLE SOLUTION
I ended up following Sam Jenses's suggestion. I created a service as follows:
File: src/TestBundle/Service/CategoryCleaner.php
namespace TestBundle\Service;
use Doctrine\ORM\EntityManagerInterface;
use TestBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Request;
class CategoryCleaner {
/**
*
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
public function cleanCategories(Product $product, Request $request) {
if ($this->em == null) {
throw new Exception('Entity manager parameter must not be null');
}
if ($request == null) {
throw new Exception('Request parameter must not be null');
}
if ($request->getMethod() == 'POST') {
$categories = $this->em->getRepository('TestBundle:ProductCategory')->findByProduct($product);
foreach ($categories as $category) {
$this->em->remove($category);
}
$this->em->flush();
}
}
}
In the cleanCategories method, which receives the current Product and Request as parameters, all entries of ProductCategory which correspond to Product are removed, only in case of a POST request.
The service is registered as follows:
File app/config/services.yml
services:
app.category_cleaner:
class: TestBundle\Service\CategoryCleaner
arguments: ['#doctrine.orm.entity_manager']
The service must be called from the controller before handleRequest($request), that is, before the new categories are added. If not, we get a duplicate entry exception.
Edit method from file TestBundle/Controller/ProductController.php
public function editAction(Request $request, Product $product) {
$deleteForm = $this->createDeleteForm($product);
$editForm = $this->createForm('TestBundle\Form\ProductType', $product);
$this->container->get('app.category_cleaner')->cleanCategories($product, $request);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('product_edit', array('id' => $product->getId()));
}
return $this->render('product/edit.html.twig', array(
'product' => $product,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
Please validate my approach.
create an intermediate service, in which you can also use doctrine to remove the existing entities
I suppose that you have inside your entity some methods like:
addCategory
removeCategory
getCategory
and also
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
So inside your function you can do:
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($this->categories as $category) {
$this->removeCategory($category);
}
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}

OneToMany or OneToOne, I'm in the right or wrong path?

I have this DB model:
Then I made this entities (I just leave the relation part since the other isn't relevant on the topic):
Orders.php
class Orders {
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="orders")
* #ORM\JoinColumn(name="person_id", referencedColumnName="id")
* */
protected $person;
public function setPerson(Person $person)
{
$this->person = $person;
return $this;
}
public function getPerson()
{
return $this->person;
}
}
Person.php
class Person {
/**
* #ORM\OneToMany(targetEntity="NaturalPerson", mappedBy="person")
* */
private $naturals;
/**
* #ORM\OneToMany(targetEntity="LegalPerson", mappedBy="person")
* */
private $legals;
/**
* #ORM\OneToMany(targetEntity="Orders", mappedBy="person")
* */
private $orders;
public function __construct()
{
$this->naturals = new ArrayCollection();
$this->legals = new ArrayCollection();
$this->orders = new ArrayCollection();
}
public function getNaturals()
{
return $this->naturals;
}
public function getLegals()
{
return $this->legals;
}
public function getOrders()
{
return $this->orders;
}
}
NaturalPerson.php
class NaturalPerson {
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Person", inversedBy="naturals")
* #ORM\JoinColumn(name="person_id", referencedColumnName="id")
*/
protected $person;
/**
* #ORM\Column(name="identification_type", type="ci_type", nullable=false)
* #DoctrineAssert\Enum(entity="Tanane\FrontendBundle\DBAL\Types\CIType")
*/
protected $identification_type;
/**
* #ORM\Column(name="ci", type="integer", nullable=false)
*/
protected $ci;
public function setPerson(Person $person)
{
$this->person = $person;
return $this;
}
public function getPerson()
{
return $this->person;
}
public function setIdentificationType($identification_type)
{
$this->identification_type = $identification_type;
return $this;
}
public function getIdentificationType()
{
return $this->identification_type;
}
public function setCI($ci)
{
$this->ci = $ci;
return $this;
}
public function getCI()
{
return $this->ci;
}
}
I omitted LegalPerson since it's pretty much the same as NaturalPerson so here is the problem. The mapping looks good but how I do get related records from Orders?
The idea behind this is for each Orders I need to know to which Person belongs too (the Orders) and also the extra information stored at NaturalPerson or LegalPerson depending on person.type.
See this code:
public function getOrdersAction()
{
$response = array();
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository("FrontendBundle:Orders")->findAll();
if (!$entities)
{
$response['message'] = "No se encontraron resultados";
}
$orders = array();
foreach ($entities as $entity)
{
$personType = $entity->getPerson()->getPersonType();
$order = array();
$order[] = $entity->getNickname();
// Here I'm trying to access to `Naturals` methods from `Orders`
if ($personType == 1)
{
$order[] = $entity->getPerson()->getNaturals()[0]->getIdentificationType() . $entity->getPerson()->getNaturals()[0]->getCI();
}
elseif ($personType == 2)
{
$order[] = $entity->getPerson()->getLegals()[0]->getIdentificationType() . $entity->getPerson()->getLegals()[0]->getRIF();
}
$orders[] = $order;
}
$response['data'] = $orders;
return new JsonResponse($response);
}
But I get this error:
Error: Call to a member function getIdentificationType() on a
non-object in
/var/www/html/tanane/src/Tanane/BackendBundle/Controller/OrderController.php
line 115
Maybe my mapping is wrong since I should have OneToOne between Person and NaturalPerson (and that sounds wrong to my logic as DER shows) or maybe is not, but then I don't know how to fetch related properties for just one record, I read docs here and also in here but they didn't talk about this part or I don't see it, any advice? ideas? tips?
Trying to use Repositories and DQL to solve the problem
I'm building a function in a Repository class to fetch the data and not get to complicated as apparently my problem is, so I did this:
public function getOrders($person_type = 1)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('ord.*, ps.*')
->from("FrontendBundle:Orders", "ord")
->join('FrontendBUndle:Person', 'ps', 'WITH', 'ps.id = ord.person_id')
->orderBy('ord.created', 'DESC');
if ($person_type == 1)
{
$qb
->select('np.*')
->join('FrontendBundle:NaturalPerson', 'np', 'WITH', 'ps.id = np.person'); // Join NaturalPerson table
}
elseif ($person_type == 2)
{
$qb
->select('lp.*')
->join('FrontendBundle:LegalPerson', 'lp', 'WITH', 'ps.id = lp.person'); // Join NaturalPerson table
}
return $qb->getQuery()->getResult();
}
I'm not tested yet so maybe it won't works but, if the idea is to get the extra information for both tables, then using this DQL I made how I pass the $person_type which is inside Person table? This is getting a little complicated, at least for me
Running a raw query to see if columns are NULL
I build this simple query just for test if results are NULL:
SELECT
ord.id,
ord.person_id as ord_person_id,
ord.nickname,
ps.id,
ps.description,
np.person_id as natural_person_id,
np.identification_type,
np.ci
FROM
orders ord
LEFT JOIN person ps ON ord.person_id = ps.id
LEFT JOIN natural_person np ON np.person_id = ps.id
WHERE
ps.person_type = 1;
And this what query returns:
So there is not NULL columns in there
CRUD for create new Orders
// Set Person entity
$entityPerson = new Person();
$person_type === 1 ? $entityPerson->setDescription($orders['nat']['person']['description']) : $entityPerson->setDescription($orders['leg']['person']['description']);
$person_type === 1 ? $entityPerson->setContactPerson($orders['nat']['person']['contact_person']) : $entityPerson->setContactPerson($orders['leg']['person']['contact_person']);
$entityPerson->setPersonType($person_type);
$em->persist($entityPerson);
$em->flush();
...
if ($person_type === 1)
{
// Set NaturalPerson entity
$entityNatural = new NaturalPerson();
$entityNatural->setIdentificationType($orders['nat']['identification_type']);
$entityNatural->setCI($orders['nat']['ci']);
$em->persist($entityNatural);
$em->flush();
}
elseif ($person_type === 2)
{
// Set LegalPerson entity
$entityLegal = new LegalPerson();
$entityLegal->setIdentificationType($orders['leg']['identification_type']);
$entityLegal->setRIF($orders['leg']['rif']);
$em->persist($entityLegal);
$em->flush();
}
Since LegalPerson and NaturalPerson are specializations of Person I would recommend using what Doctrine calls Class Table Inheritance (documentation).
You would have:
Person.php
/**
* #ORM\Table(name="person")
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({
* "natural" = "NaturalPerson",
* "legal" = "LegalPerson",
* })
*/
class Person {
/**
* #ORM\OneToMany(targetEntity="Orders", mappedBy="person")
* */
private $orders;
public function __construct()
{
$this->orders = new ArrayCollection();
}
public function getOrders()
{
return $this->orders;
}
}
NaturalPerson.php
/**
* #ORM\Table(name="natural_person")
* #ORM\Entity
*/
class NaturalPerson extends Person {
/**
* #ORM\Column(name="identification_type", type="ci_type", nullable=false)
* #DoctrineAssert\Enum(entity="Tanane\FrontendBundle\DBAL\Types\CIType")
*/
protected $identification_type;
/**
* #ORM\Column(name="ci", type="integer", nullable=false)
*/
protected $ci;
public function setIdentificationType($identification_type)
{
$this->identification_type = $identification_type;
return $this;
}
public function getIdentificationType()
{
return $this->identification_type;
}
public function setCI($ci)
{
$this->ci = $ci;
return $this;
}
public function getCI()
{
return $this->ci;
}
}
Order.php stays the same.
As you can see, now both NaturalPerson and LegalPerson extend Person. Since you've changed your entities definition, you'll have to update your database schema.
Now, in your Controller you only have to do this:
foreach ($entities as $entity)
{
$person = $entity->getPerson();
$order = array();
$order[] = $entity->getNickname();
if ($person instanceof NaturalPerson)
{
$order[] = $person->getIdentificationType() . $person->getCI();
}
else // it has to be LegalPerson
{
$order[] = $person->getIdentificationType() . $person->getRIF();
}
$orders[] = $order;
}
Don't forget to add the use statement for NaturalPerson!
This way you only work with instances of either NaturalPerson or LegalPerson. I'm sure you can further improve this.
Lastly, you will have to change your CRUD for this. You don't work directly with Person anymore (in fact, it should be abstract), so now you need to handle CRUD for NaturalPerson and for LegalPerson separately. Each will have its Type, Controller, views, etc.
Your code would now look like this:
if ($person_type === 1)
{
$entityPerson = new NaturalPerson();
$entityPerson->setDescription($orders['nat']['person']['description']);
$entityPerson->setContactPerson($orders['nat']['person']['contact_person']);
$entityPerson->setIdentificationType($orders['nat']['identification_type']);
$entityPerson->setCI($orders['nat']['ci']);
$em->persist($entityPerson);
$em->flush();
}
elseif ($person_type === 2)
{
$entityPerson = new LegalPerson();
$entityPerson->setDescription($orders['leg']['person']['description']);
$entityPerson->setContactPerson($orders['leg']['person']['contact_person']);
$entityPerson->setIdentificationType($orders['leg']['identification_type']);
$entityPerson->setRIF($orders['leg']['rif']);
$em->persist($entityPerson);
$em->flush();
}
Perhaps, a problem in other. You can forget to assign NaturalPerson or LegalPerson to Person entity. So you need to check it before calling getIdentificationType():
if($personType == 1){
if(null !== $natural = $entity->getPerson()->getNaturals()[0]){
$order[] = $natural->getIdentificationType() . $natural->getCI();
}
}elseif($personType == 2){
if(null !== $legal = $entity->getPerson()->getLegals()[0]){
$order[] = $legal->getIdentificationType() . $legal->getRIF();
}
}

Doctrine parent, child relationship hydration

I am having a problem and I am afraid I am missing something stupid. But I am struggling with searching for a similar problem to see what I am missing.
Anyway, I have a product entity:
<?php
class Product {
private $productid;
private $name;
/**
* #ORM\ManyToOne(targetEntity="MyNamespace\CategoryBundle\Entity\Category")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="categoryid", referencedColumnName="categoryid")
* })
*/
private $category;
public function getCategoryPath()
{
$category = $this->category;
$items = array($category);
var_dump($category->getParent());
while (null !== $category->getParent()) {
$category = $category->getParent();
$items[] = $category;
}
return $items;
}
}
class Category {
private $categoryid;
private $name;
/**
* #var \Category
*
* #ORM\ManyToOne(targetEntity="MyNamespace\CategoryBundle\Entity\Category")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="parentid", referencedColumnName="categoryid")
* })
*/
private $parent;
public function getParent()
{
return $this->parent;
}
}
?>
And I try:
<?php
$product = $entityManager->find('MyNamespaceProductBundle:Product', 10);
$categories = $product->getCategoryPath();
?>
The problem is, that the categories array only contains the directly linked category. It doesn't seem that doctrine fetches the parent ones so $category->getParent() will always return null and if I look to the mysql-general log I don't see a query raised for the parent category.
What am I doing wrong?
My guess is that you should call the getCategoryPath() method inside the Category class like getCategoryid() and just return the categoryid value. Then:
<?php
$product = $entityManager->find( 'MyNamespaceProductBundle:Product', 10 );
$category = $product->getCategoryid();
$categoryProducts = $entityManager->find( 'MyNamespaceCategoryBundle:Product',array( 'categoryid' => '$category' ) );
?>

Problems saving Doctrine2 record with cascade={"persist"}, null PK is attempted inserted event after specifying it explicitly

I have two models, Product and Category. Each product can be part of several categories with a weight property. This gives three tables; product, category and product_category. Here are my models:
/** #Entity #Table(name="product") **/
class Product
{
/** #Id #Column(type="integer") #GeneratedValue **/
protected $id = null;
/** #OneToMany(targetEntity="ProductCategory", mappedBy="product", orphanRemoval=true, cascade={"persist","remove"}) #var ProductCategory[] **/
protected $productCategories = null;
public function __construct ()
{
$this->productCategories = new ArrayCollection();
}
// Take an array of category_ids of which the product should be part of. The first category gets weight=1, next weight=2 etc.
public function saveCategories ($category_ids)
{
$weight = 1;
$this->productCategories = new ArrayCollection();
foreach ($category_ids as $category_id)
$this->productCategories[] = new ProductCategory($this->id, $category_id, $weight++);
}
}
/** #Entity #Table(name="category") **/
class Category
{
/** #Id #Column(type="integer") #GeneratedValue **/
protected $id = null;
/** #Column(type="string",length=200,nullable=false) #var string **/
protected $title = null;
/** #OneToMany(targetEntity="ProductCategory", mappedBy="category") #var ProductCategory[] **/
protected $productCategories = null;
public function __construct()
{
$this->productCategories = new ArrayCollection();
}
}
/** #Entity #Table(name="product_category") **/
class ProductCategory
{
/** #Id #Column(type="integer",nullable=false) **/
protected $product_id = null;
/** #Id #Column(type="integer",nullable=false) **/
protected $attraction_id = null;
/** #Column(type="integer",nullable=false) **/
protected $weight = null;
/** #ManyToOne(targetEntity="Product",inversedBy="productCategories") #JoinColumn(name="product_id",referencedColumnName="id",onDelete="CASCADE") #var Product **/
protected $product;
/** #ManyToOne(targetEntity="Category",inversedBy="productCategories") #JoinColumn(name="category_id",referencedColumnName="id",onDelete="CASCADE") #var Category **/
protected $category;
public function __construct ($product_id, $category_id, $weight)
{
$this->product_id = $product_id;
$this->attraction_id = $attraction_id;
$this->weight = $weight;
}
}
The problem is that when I try to save the categories, I get an error message stating that product_id cannot be null - and the MySQL log confirms that Doctrine attempts to insert a row into product_category with both product_id and category_id set to 0, despite me setting them in the ProductCategory constructor.
Any suggestions where I might have done wrong?
You are doing it wrong. In Doctrine2, there is no such thing as product_id nor category_id. You only deal with Product and Category entities, column values are handled by doctrine itself.
Instead of
....
foreach ($category_ids as $category_id)
$this->productCategories[] = new ProductCategory($this->id, $category_id, $weight++);
You should have something like
public function saveCategories ($categories)
{
foreach ($categories as $category)
$this->productCategories[] = new ProductCategory($this, $category)
Fix the constructor of ProductCategory to reflect these and also remove category_id and product_id definitions.

Categories