Doctrine parent, child relationship hydration - php

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

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

Many-To-Many Bidirectional relationships with Doctrine

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

Symfony 2, QueryBuilder, multiple andWhere with same parameter

I have two entities - Lawyer and Category. They are connected with Many to Many association. Assume that example lawyer has 3 categories. I want to create function to search lawyers by a array of categories, and it should return only lawyers who have all categories from array.
class Lawyer {
//...
/**
* #ORM\ManyToMany(targetEntity="Dees\KancelariaBundle\Entity\Category")
* #ORM\JoinTable(name="lawyers_has_categories",
* joinColumns={#ORM\JoinColumn(name="lawyer_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="category_id", referencedColumnName="id")}
* )
*
* #var ArrayCollection
*/
protected $categories = null;
//...
}
class Category {
//...
/**
* #ORM\Column(length=255, nullable=true)
*
* #var string
*/
protected $name;
//...
}
public function searchLawyers(array $categories) {
$queryBuilder = $this->createQueryBuilder('lawyer')
->join('lawyer.categories', 'category');
$queryBuilder->andWhere("category.name = :category1");
$queryBuilder->setParameter("category1", "First category");
$queryBuilder->andWhere("category.name = :category2");
$queryBuilder->setParameter("category2", "Second category");
//...
//won't work, return null, but lawyer with these categories exists.
}
How can I achieve something like that?
I figured it out:
public function searchLawyers(array $categories) {
$queryBuilder = $this->createQueryBuilder('lawyer')
->join('lawyer.categories', 'category');
$queryBuilder->andWhere("category.name in (:categories)");
$queryBuilder->setParameter("categories", $categories);
$queryBuilder->addGroupBy("lawyer.id");
$queryBuilder->andHaving("COUNT(DISTINCT category.name) = :count");
$queryBuilder->setParameter("count", sizeof($categories));
return $queryBuilder->getQuery()->getResult();
}

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