Symfony Parent Child category relation - php

I have a Product Category table, where I have stored categories if parent_id is null then assume this is parent category if not null then it has child categories. Now the issue is I can't show parent category name in listing page, so can someone have an idea how to make a relation in Product Category entity?
Symfony version: 3.3
Table Structure:(product_category)
id
parent_id
slug
title
I have tried this code in ProductCategory entity but it doesn't show any relational data:
class ProductCategory
{
/**
* #ORM\OneToMany(targetEntity="ProductCategory", mappedBy="parent")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;

I usually use children and parent and this is how I use It:
/**
* One ProductCategory has Many ProductCategories.
* #ORM\OneToMany(targetEntity="ProductCategory", mappedBy="parent")
*/
private $children;
/**
* Many ProductCategories have One ProductCategory.
* #ORM\ManyToOne(targetEntity="ProductCategory", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;
EDIT
/**
* ProductCategory constructor.
*/
public function __construct()
{
$this->children = new ArrayCollection();
}
Getters / Setters and this stuff:
/**
* #param ProductCategory $children
* #return $this
*/
public function addChildren (ProductCategory $children)
{
$this->children[] = $children;
return $this;
}

Using entity with self reference, as Alessandro described, then let's say you have all your records extracted with doctrine using $qb->(bla bla bla)->getResult(); into a variable called $categories. Then you just have to iterate through it like that:
foreach($categories as $category) {
echo $category->getTitle() . ' has parent ' . ($category->getParent() ? $category->getParent()->getTitle() : 'none');
}
To understand more about array collection, read: http://www.doctrine-project.org/api/common/2.3/class-Doctrine.Common.Collections.ArrayCollection.html

Related

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 relation between article and quote symfony

I have a manytomany relation between two entities (product and quote) so that one or more products could be in one or more quotes. For example:
assuming that a customer chooses two products from a specific enterprise and he would like to receive a quote that summarize all the chosen products. Then he decides to choose another product of another enterprise to get at the end another quote. So here we have two quotes of different enterprises and of same user that each quote has its own products.
you will say that it is a onetomany relation between the quote and the product because as I mentioned above that one quote can have many products so in the database you will have a quote_id column in the product table.
However, if another customer chooses the same products, a new quote will be created but when the query will insert these products in the quote by filling the quote_id column of the product table, it finds out that these articles are already have a quote_id of another quote.
That's why it is a manytomany relation so that many articles could be in many quotes.
This part is realized and I can match many products to many quotes without any problems.
Here is the entity of quote:
/**
* #ORM\ManyToMany(targetEntity="ArticleBundle\Entity\Article", mappedBy="quotes")
*/
private $articles;
public function __construct() {
$this->articles = new \Doctrine\Common\Collections\ArrayCollection();
$this->createdAt = new \DateTime();
}
/**
* Set articles
*
* #param \ArticleBundle\Entity\Article $articles
* #return Quote
*/
public function setArticles(\ArticleBundle\Entity\Article $articles = null) {
$this->articles = $articles;
return $this;
}
/**
* Get articles
*
* #return \ArticleBundle\Entity\Article
*/
public function getArticles() {
return $this->articles;
}
/**
* Add articles
*
* #param \QuoteBundle\Entity\Quote$articles
* #return Devis
*/
public function addArticle(\ArticleBundle\Entity\Article $article) {
$article->addQuote($this); // synchronously updating inverse side
$this->articles[] = $article;
return $this;
}
/**
* Remove articles
*
* #param \QuoteBundle\Entity\Quote $articles
*/
public function removeArticle(\ArticleBundle\Entity\Article $article) {
$this->articles->removeElement($article);
}
}
the entity article:
/**
* #ORM\ManyToMany(targetEntity="QuoteBundle\Entity\Quote", inversedBy="articles")
* #ORM\JoinTable(name="quotes_articles")
*/
private $quotes;
/**
* Add devises
*
* #param \DevisBundle\Entity\Quote $quote
* #return Article
*/
public function addQuote(\QuoteBundle\Entity\Quote $quote) {
$this->quotes[] = $quote;
return $this;
}
/**
* Remove quotes
*
* #param \QuoteBundle\Entity\Quote $quote
*/
public function removeQuote(\QuoteBundle\Entity\Quote $quote) {
$this->quotes->removeElement($quote);
}
/**
* Get quotes
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getQuotes() {
return $this->quotes;
}
}
The part that I find it difficult to reach is that when a customer chooses only one product , I would like to let him specify the quantity of the chosen product.
so when I updated the doctrine schema I got these tables :
quote table
product table
quotes_products table
can anyone tell me in which table I must add the quantity column and how can I write the annotation to let it added automatically in the database . I think it will be added to the quotes_products table.
Thank you for helping me.
when adding a quantity column to the join table, you are making of your relationship a non pure manytomany. So, there will be no problem in separating the manyToMany relationships to two oneToMany relationships, and create an Entity Class for the join with additional field quantity.
The idea in pseudo code:
Class Quote
{
private $products;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="quote")
*/
}
Product:
Class Product
{
private $quotes;
/**
* #ORM\OneToMany(targetEntity="Quote", mappedBy="product")
*/
}
ProdcutQuote:
Class ProductQuote
{
/**
* #ORM\ManyToOne(targetEntity="Quote")
* #ORM\JoinColumn(name="quote_id", referencedColumnName="id")
*/
private $quote;
/**
* #ORM\ManyToOne(targetEntity="Product")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
private $quantity;
}

Add brands through company, it's possible? How?

I have this two tables (see pics below) mapped as follow:
class Brand
{
...
/**
* #var Company
*
* #ORM\ManyToOne(targetEntity="Company")
* #ORM\JoinColumn(name="companies_id", referencedColumnName="id")
*/
protected $company;
}
class Company
{
...
}
I need to add support for add a new Brand from Company but I have not idea in how to achieve this. This are handled through SonataAdminBundle but I think I need to add something else to entities in order to create brands from company but I am not sure what this would be, can I get some help? I am stucked
1st attempt
After get an answer this is how I modify Company entity:
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Company
{
...
/**
* #var Brand
* #ORM\OneToMany(targetEntity="Brand", mappedBy="company", cascade={"persist"})
**/
protected $brands;
public function __construct()
{
$this->brands = new ArrayCollection();
}
...
public function getBrands()
{
return $this->brands;
}
/**
* Add brands
*
* #param Brand $brand
* #return Brands
*/
public function addBrand( Brand $brand)
{
$this->brands[] = $brand;
return $this;
}
/**
* Remove brands
*
* #param Brand $brand
*/
public function removeBrand( Brand $brand)
{
$this->brands->removeElement($brand);
}
}
But I am getting this error:
No entity manager defined for class
Doctrine\Common\Collections\ArrayCollection
Why is that?
You could try setting up your entities like this:
class Brand
{
/**
* #var Company
*
* #ORM\ManyToOne(targetEntity="Company", inversedBy="brands")
* #ORM\JoinColumn(name="companies_id", referencedColumnName="id")
*/
protected $company;
}
class Company
{
/**
* #var ArrayCollection
*
* #OneToMany(targetEntity="Brand", mappedBy="company", cascade={"persist"})
**/
protected $brands;
}
What we're defining here is that new Brands can be created from the Company entity with cascade={"persist"}.
It's recommended you implement addBrand and removeBrand in Company for direct interaction with the ArrayCollection.
A simple example of the final functionality:
$company = $service->getCompany(1); // our company entity
$brand = new Brand();
$brand->set...
...
$company->addBrand($brand);
$entityManager->persist($company);
EDIT
This is just an example, you may choose not to add with keys or even implement a remove function, but this is a starting point:
public function addBrand(Brand $brand)
{
// key needs to be something that can uniquely identify the brand
// e.g. name
$this->getBrands()->set(*key*, $brand);
return $this;
}
public function removeBrand($key)
{
$this->getBrands()->remove($key);
return $this;
}

phpdoctrine save collection of differents entities

I have interface:
interface Product
{
function getAmount();
}
and php doctine entities:
/**
* #ORM\Table(name="orders")
* #ORM\Entity
*/
class Order
{
private $products = array();
public function addProduct(Product $product){
$this->products[] = $product;
}
public function getProducts() {
return $this->products;
}
function getAmount() {
$amount = 0;
foreach ($this->products as $product) {
$amount += $product->getAmount();
}
return $amount;
}
}
/**
* #ORM\Table(name="books")
* #ORM\Entity
*/
class Book implements Product
{
function getAmount() {
return 1;
}
}
/**
* #ORM\Table(name="pens")
* #ORM\Entity
*/
class Pen implements Product
{
function getAmount()
{
return 2;
}
}
Book, Pen - are different entities and table. How to implement relationship Order::products with collection of Books, Pens, etc(for save in database)?
I understand that two solutions to this problem.
The first is when saving(and loading) to the database manually convert this relationship to map(array of entities names and ids).
This decision I do not like.
And the second is to correct architecture. I do not know how. Most likely already have a ready-made solution ... Help please.
i'am not sure you can get it exactly that way.
whether you define a separate Entity Poduct and add a column product_type where you tell whether it book, pen or what ever.
in entity Product that property should be defined as enum and it can be tricky(depends on what do you use besides doctrine)
or you make it for each product type (what can be pretty fast to a nightmare). I'd guess you have ManyToMany Relation. most probably it should look smth. like that.
in Order
/**
* #ManyToMany(targetEntity="Book")
* #JoinTable(name="Order_Book",
* joinColumns={#JoinColumn(name="book_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="order_id", referencedColumnName="id")}
* )
*/
protected $book;
/**
* #ManyToMany(targetEntity="Pen")
* #JoinTable(name="Order_Pen",
* joinColumns={#JoinColumn(name="pen_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="order_id", referencedColumnName="id")}
* )
*/
protected $pen;
and in Book:
/*
* #ManyToMany(targetEntity="Order", mappedBy="book")
*/
protected $order;
with Pen and others the same way.
Take a look at inheritance mapping: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html

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