Symfony2 Doctrine correctly removing a ManyToMany relationship from object - php

So I have two objects Product and Subcategory with a ManyToMany relationship. The relationship is saved in a third table called ProductSubcategory.
The problem is that if I try to remove a subcategory from the product I get foreign constraint error (I can't remove from ProductSubcategory because it has values mapped to existing objects).
So what I did is added onDelete=SET NULL to my ManyToMany table. Now I can remove the subcategory, but what it does is it just sets the field product to NULL instead of deleting the whole thing.
In product I have:
/**
* #var \Mp\ShopBundle\Entity\ProductSubcategory
* #ORM\OneToMany(targetEntity="\Mp\ShopBundle\Entity\ProductSubcategory", mappedBy="product", cascade={"persist"}, orphanRemoval=true)
*/
private $subcategory;
and a function to remove a subcategory:
/**
* Remove subcategory
*
* #param \Mp\ShopBundle\Entity\ProductSubcategory $subcategory
*/
public function removeSubcategory(\Mp\ShopBundle\Entity\ProductSubcategory $subcategory) {
foreach ($this->subcategory as $k => $s) {
if ($s->getId() == $subcategory->getId()) {
unset($this->subcategory[$k]);
}
}
}
ProductsSubcategory is a ManyToMany table where I save the relationship:
/**
* #var \Mp\ShopBundle\Entity\Product
* #ORM\ManyToOne(targetEntity="\Mp\ShopBundle\Entity\Product", inversedBy="subcategory")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="SET NULL")
* })
*/
private $product;
So if i do this:
$product->removeSubcategory($subcategory);
in database it just makes it null:
id|subcategory_id|product_id|
1 |2 | NULL|
instead of completely removing it.
What am I doing wrong?

Related

Select entities using an array of values that represents the another field (`in`)

First, sorry for the cryptic title, but I really don't know how to write one more clear.
This is not a question about join but is a question about using in in conjunction with join.
I'm going to explain my situation.
I have an array that contains a list of slugged names of cities:
$cities = ['new_york', 'rome', 'hong_kong'];
These cities are in a table cities and have each one their own ID:
------------------
| ID | slug |
| 1 | new_york |
| 2 | rome |
| 3 | hong_kong |
------------------
Then I have another table listings that contain the listing of various venues.
Each Listing has a field city that relates the Listing with the City in which it is.
The relation is a ManyToOne:
# Listing
/**
* #ORM\Entity(repositoryClass=ListingRepository::class)
* #ORM\Table(name="listings", schema="app")
*/
class Listing
{
/**
* #ORM\Id
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private int $id;
/** #ORM\Column(type="string", length=255, nullable=false) */
private string $name;
/** #ORM\ManyToOne(targetEntity=City::class, inversedBy="listings") */
private City $city;
...
}
and then
# City
/**
* #ORM\Entity(repositoryClass=CityRepository::class)
* #ORM\Table(name="cities", schema="app")
*/
class City
{
/**
* #ORM\Id
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private int $id;
/** #ORM\Column(type="string", length=255, nullable=false) */
private string $name;
/** #ORM\Column(type="string", length=255, nullable=false) */
private string $slug;
/** #ORM\OneToMany(targetEntity=Listing::class, mappedBy="city") */
private Collection $listings;
...
}
WHAT I WANT TO ACHIEVE
Passing an array of slugs of the City(ies), I'd like to select all the Listings that belong to each passed City.
POSSIBLE SOLUTIONS
One possible solution is to
First select all cities by their slugs in the array,
Get the list of entities City and cycle them to get their IDs
Pass the list of IDs to the ListingRepository and use in
Something like this:
# Not tested, but it should work (may require some bug fixing)
...
/**
* #param City[] $cities
*
* #return Listing[]
*/
public function findAllByCities(array $cities):array
{
$qb = $this->createQueryBuilder('l');
$citiesIds = $this->getCitiesIds($cities);
return $qb
->select('l')
->where($qb->expr()->in('l.city', ':cities'))
->setParameter('cities', $citiesIds, Connection::PARAM_STR_ARRAY)
->getQuery()
->getResult();
}
/**
* #param City[] $cities
*
* #return int[]
*/
private function getCitiesIds(array $cities):array
{
return array_map(static fn(City $city): int => $city->getId(), $cities);
}
...
The drawback of this approach are that:
I have to perform a query to get the list of cities
I have to cycle each returned city to get its ID
This may become heavy if the cities in the list are numerous.
QUESTION
Is there a wy to select all the Listings in each passed City but using the slugs instead of first retrieving their ID?
You simply can join the associated table.
public function findAllByCities(array $cities):array
{
$qb = $this->createQueryBuilder('l');
return $qb
->select('l')
->join('l.city', 'c')
->where($qb->expr()->in('c.slug', ':cities'))
->setParameter('cities', $cities, Connection::PARAM_STR_ARRAY)
->getQuery()
->getResult();
}

Symfony2 Get entities of an entity in another entity

This question is about Symfony2 table relationships using ORM. I have three tables/entities that are related to each other. The relationship is very similar to Wordpress Posts, Categories and Categories relationship tables.
Table 1 contains posts.
Table 2 contains categories
Table 3 contains relationships between the categories and posts.
I want to be able to have the categories property in the posts table and a posts property in the categories table. So that when I call.
Categories->posts : I should get posts in that category.
Posts->categories : I should get the categories the post belongs to.
I want to have unique categories per table and I want all posts to point to a category without having to create a new entry for the category that already exists which is what ManyToOne or OneToMany is offering this is why the third table I think is necessary.
For example here is the relationships
class Category_relationship
{
/**
* #var integer
*
* #ORM\Column(name="object_id", type="bigint")
*
* #ORM\ManyToOne(targetEntity="Worksheet", inversedBy="category_relationships")
* #ORM\JoinColumn(name="worksheet_id", referencedColumnName="id", nullable=FALSE)
*/
private $objectId;
/**
* #var integer
*
* #ORM\Column(name="category_id", type="bigint")
*
* #ORM\ManyToOne(targetEntity="Category", inversedBy="categories")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=FALSE)
*/
private $categoryId;
}
Here is the Category class:
class Category
{
/**
* #ORM\OneToMany(targetEntity="Category_relationship", mappedBy="categoryId", cascade={"persist", "remove"}, orphanRemoval=TRUE)
*/
protected $posts;
}
Here is the Category class:
class Posts
{ /**
* #ORM\OneToMany(targetEntity="Category_relationship", mappedBy="objectId", cascade={"persist", "remove"}, orphanRemoval=TRUE)
*/
protected $categories;
}
I want to create a system where I can assign posts to a category but the category table can only contain 1 entry about the category. I also want to be able to use expressions link;
Post->categories
Category->posts
or
Post->AddCategory()
Category->AddPost()
Thanks for your help.
It seems that you want a simple many-to-many relationship.
Every post can have multiple categories, and every category have list of related posts. Many to many handles pivot table by itself.
So, in Post entity you have to declare relationship that way:
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="posts")
* #ORM\JoinTable(name="PostsCategories",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="category_id", referencedColumnName="id")}
* )
**/
protected $categories;
Remember about using Doctrine\ORM\Mapping with ORM alias (you don't have to import all subclasses separately):
use Doctrine\ORM\Mapping as ORM;
After that, you need to create a new ArrayCollection in class constructor:
public function __construct()
{
$this->categories = new ArrayCollection();
}
And add proper methods, like addCategory:
public function addCategory(Category $category)
{
$this->categories[] = $category;
return $this;
}
You can also add them automatically with:
php app/console doctrine:generate:entities BundleName:EntityName
Same thing in Category entity, but with a little different definiton:
/**
* #ORM\ManyToMany(targetEntity="Post", mappedBy="categories")
**/
protected $posts;
You can find all of these information in Doctrine docs

Doctrine - Entity loaded without existing associations

I have two entity types: \Model\News and \Model\News\Category.
\Model\News: (without couple of fields)
namespace Model;
/**
* #Entity
* #Table(name="news")
*/
class News extends \Framework\Model {
/**
* #Id
* #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #ManyToOne(targetEntity="\Model\User", inversedBy="news")
*/
protected $author;
/**
* #ManyToOne(targetEntity="\Model\News\Category", inversedBy="news")
*/
protected $category;
}
\Model\News\Category:
namespace Model\News;
/**
* #Entity
* #Table(name="news_category")
*/
class Category extends \Framework\Model {
/**
* #Id
* #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #Column(type="string", length=50, unique=TRUE)
*/
protected $name;
/**
* #OneToMany(targetEntity="\Model\News", mappedBy="category")
*/
protected $news;
/**
* Constructor
*/
public function __construct() {
parent::__construct();
$this->news = new \Doctrine\Common\Collections\ArrayCollection;
}
}
Table data from \Model\News:
id | category_id | author_id
------------------------------- ...
4 | 1 | NULL
Table data from \Model\News\Category:
id | name
---------------
1 | General
---------------
2 | Other
While I'm loading News type Entity with this particular code and doing dump with \Kint class:
$sId = '4';
$sModel = 'Model\News';
$oObject = $entity_manager->find($sModel, $sId);
d($oObject);
It returns me this:
My question is, why category property from $oObject variable has NULL values despite the fact that the category with id = 1 exists in database?
UPDATE:
After above code, I want to load this category (with ID=1) separately. Same thing. But... when I'm loading a category with other ID (for example, 2) it's loading with no problems:
$oObject2 = $entity_manager->('\Model\News\Category', '2');
I have no idea what to do now...
If you know that the category entity will be accessed almost each time you load news, you might want to eager load it (force doctrine to load it when News is loaded instead of when a Category property is called).
In order to do so, just add the fetch="EAGER" annotation on your association
/**
* #ManyToOne(targetEntity="\Model\News\Category", inversedBy="news", fetch="EAGER")
*/
protected $category;
So... I finally came to solve the problem thanks to #asm89 from #doctrine IRC chat in freenode server.
Doctrine is creating a proxy class which uses "lazy loading". Data is loaded only when one of getters is used.
So, after using $oObject->getCategory()->getName(), my category object available from $oObject->getCategory() was filled up with proper data.

Symfony2 duplicate the id in 2 fields (id and idbis) with strategy="AUTO"

I would like symfony/doctrine to duplicate the id in two different field every time a record is created.
Is it possible in one shot (I use Stragegy="AUTO") ?
If yes how can I do this?
For instence I would like my entity CATEGORY to have two attribut id (auto).
on named id, the other idbis.
(In my exemple I work on a entity which contains parent and children records, so I also have idparent to link the children categories with their parent category)
if the record is a parent Category, then id and idbis get the same integer:
=> id=2, idbis=2, idParent = NULL
if the record is a child Category (let say its parent is Category with id=2) then:
=> id=3, idParent=2, idCategory1=2
That would be great because then I could then easily retrieve all categories (parent and children) who are linked to category which id is 2.
You should take a look to Nested Set structures. It's made to easily retrieve sub elements in a simple query by using bounds.
Otherwise you can do that by creating a trigger on insert/update.
You can use a relation with the entity itself:
<?php
/** #Entity **/
class Category
{
// ...
/**
* #OneToMany(targetEntity="Category", mappedBy="parent")
**/
private $children;
/**
* #ManyToOne(targetEntity="Category", inversedBy="children")
**/
private $parent;
// ...
}
This is just a complete version of the good idea presented by #Markus. Basically parent_id is null for a main category, otherwise has as value the id of parent.
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\OneToMany(targetEntity="path\to\Entity\Categories", mappedBy="parent", cascade={"persist"})
*/
private $children;
/**
* #var path\to\Entity\Categories
*
* #ORM\ManyToOne(targetEntity="path\to\Entity\Categories", inversedBy="children")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
* })
*/
private $parent;

Doctrine multiple composite foreign key

I am trying to construct an object with two composite foreign keys pointing out to the same object, but they seem to have the same data, like doing the join only on one column, product_id.
class PostpaidProduct extends Product {
/**
* #ManyToOne(targetEntity="Bundle", fetch="EAGER", cascade={"persist"})
* #JoinColumn(name="bundle_voice_id", referencedColumnName="id")
*/
private $bundleVoice;
/**
* #ManyToOne(targetEntity="Bundle", fetch="EAGER", cascade={"persist"})
* #JoinColumn(name="bundle_data_id", referencedColumnName="id")
*/
private $bundleData;
/**
* #OneToMany(targetEntity="BundlePromo", mappedBy="product", fetch="EAGER", cascade={"persist"})
* #JoinColumns({
* #JoinColumn(name="id", referencedColumnName="product_id"),
* #JoinColumn(name="bundle_voice_id", referencedColumnName="bundle_id")
* })
*/
private $bundleVoicePromos;
/**
* #OneToMany(targetEntity="BundlePromo", mappedBy="product", fetch="EAGER", cascade={"persist"})
* #JoinColumns({
* #JoinColumn(name="id", referencedColumnName="product_id"),
* #JoinColumn(name="bundle_data_id", referencedColumnName="bundle_id")
* })
*/
private $bundleDataPromos;
}
What would be wrong with my mapping?
Is it possible to have composite foreign keys but without being primary keys?
I have talked to one of the developers of Doctrine and he said that the #JoinColumns field in #OneToMany relationships is ignored. The alternative would be having just one foreign key and to use matching criterias in an entity method, filtering for the entries needed based on the other key. Another solution would be having repository methods specific for getting these values.
Also, in OneToMany relationships eager fetching does not work, so it does separate queries for all children. So if you have a product with multiple prices, when fetching a product it will do separate queries for fetching the prices.

Categories