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();
}
Related
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;
}
I have a table "table_b" that contain the following details
I would like to use doctrine to query an output with a specific condition. Currently I'm using the block below to query.
$table_a= $em->getRepository('table_a')->findOneBy(['id'=>1]);
foreach($table_a->table_b as $records){
echo $records->name. " : " . $records->value;
}
It will output the entire ArrayCollection. Is there a way to query the record base on latest 'Date Created', that is base on the grouping of column 'Foreign Key Table 2'.
If you want to use native Doctrine query methods, you should use findOneBy with the order byparameter.
findOneBy(['id' => 1], ['DateCreated' => 'desc'])
Then, you says the result is an ArrayCollection, so using the ArrayCollection::first() method, you'll get the last created element
EDIT
Imagine you have a Group entity and a Member entity. groups table is your table_a and members table is your table_b.
Entity description should be something like that :
class Group
{
...
/**
* #ORM\OneToMany(targetEntity="Group", mappedBy="member", cascade={"persist", "remove", "merge"})
* #ORM\OrderBy({"dateCreated"="DESC"})
*/
protected $members;
...
public function __construct()
{
$members = new ArrayCollection();
}
// members handling accessors
/**
* #return ArrayCollection
*/
public function getMembers()
{
return $this->members;
}
/**
* #param $members
*
* #return $this
*/
public function setMembers($members)
{
$this->members = new ArrayCollection();
return $this->addMembers($members);
}
/**
* #param $members
*
* #return $this
*/
public function addMembers($members)
{
foreach ($members as $member)
{
$this->addMember($member);
}
return $this;
}
/**
* #param Member $member
*
* #return $this
*/
public function addMember(Member $member)
{
$this->members->add($member);
$member->setGroup($this);
return $this;
}
/**
* #param Member $member
*
* #return $this
*/
public function removeMember(Member $member)
{
if ($this->members->contains($member))
{
$this->members->removeElement($member);
}
return $this;
}
/**
* #param $members
*
* #return $this
*/
public function removeMembers($members)
{
foreach ($members as $member)
{
$this->removeMember($member);
}
return $this;
}
}
And Member entity :
class Member
{
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="members")
* #ORM\JoinColumn(name="group_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $group;
/**
* #ORM\Column(type="datetime", name="date_created")
*/
protected $dateCreated;
/**
* #return Group
*/
public function getGroup()
{
return $this->group;
}
/**
* #param Group $group
*
* #return $this
*/
public function setGroup(Group $group)
{
$this->group = $group;
return $this;
}
}
Now, we have one group with a dateCreated ordered collection of members.
Example 1 : You want to get the last member created for a given group
$group = $em->getRepository(Group::class)->findOneBy(['id' => 1]);
$lastMember = $group->getMembers()->first();
Example 2 : You want to get all members created on 2014-01-30 :
$members = $group->getMembers()->filter(function (Member $member) {
return ($member->getDateCreated->format('Y-m-d') == '2014-01-30');
});
That's all folk !
PS : I haven't test this code
In TableRepository:
public function getLatestRecord()
{
return $this->getEntityManager()
->createQuery('SELECT t FROM MyBundle:MyEntity t GROUP BY t.table2NameField ORDER BY t.created DESC')
->setMaxResults(1)
->getOneOrNullResult();
}
Iam learning Doctrine.
I have two entities Article and Category in many to many relationship and iam trying to get all categories where isnt specific article.
ArticleEntity:
class Article extends BaseEntity
{
use Identifier;
/**
* #ORM\Column(type="string", nullable = false, unique=TRUE)
* #var string
*/
private $title;
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="articles")
* #ORM\JoinTable(name="article_categories")
*/
private $categories;
public function getCategories()
{
return $this->categories;
}
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
And CategoryEntity:
class Category extends BaseEntity
{
use Identifier;
/**
* #ORM\Column(type="string", nullable = false, unique=true)
* #var string
*/
private $title;
/**
* #ORM\Column(type="string",nullable=false,unique=true)
* #var sting
*/
private $slug;
/**
* #ORM\ManyToMany(targetEntity="Article", mappedBy="categories")
*/
private $articles;
public function __construct()
{
$this->articles = new \Doctrine\Common\Collections\ArrayCollection();
}
And i am trying to get all categories without specific article. In pure MySQL id would be something like this:
SELECT * FROM category LEFT JOIN article_categories ON category.id = article_categories.category_id WHERE article_categories.article_id <> 1(for example) AND article_id IS NOT NULL
And the only solution i could create in my CategoryRepository is this one.
public function findWithoutArticle($article_id)
{
$articleCat = $this->em->find(Article::getClassName(), $article_id);
$qb = $this->em->createQueryBuilder();
$qb->select('c')
->from(Category::getClassName(), 'c')
->where('c.id NOT IN (:article_id)')
->setParameter('article_id', $articleCat->getCategories()->toArray());
return $qb->getQuery()->getResult();
}
And this doesnt look right. Is there any better "Doctrine way" practice?
How about this one?
$qb = $this->em->createQueryBuilder();
$qb->select('c');
$qb->from('Category', 'c');
$qb->leftJoin('c.articles', 'a');
$qb->where($qb->expr()->neq('a.article_id', '?1'));
$qb->setParameter(1, $article_id);
$categories = $qb->getQuery()->getResult();
How about this:
public function findWithoutArticle($article_id)
{
$qb = $this->em->createQueryBuilder()
->select('c')
->from('Category', 'c')
->leftJoin('c.article', 'a')
->where('a.article_id <> :articleId')
->setParameter('articleId', $article_id);
return $qb->getQuery()->getResult();
}
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;
}
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' ) );
?>