Doctrine 2 Many To Many follower relationship not working - doctrine-orm

I have followed the example here doctrine 2 documentation and made the entity
<?php
namespace Account\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Zend\Filter\Null;
/**
* #ORM\Entity
* #ORM\Table(name="accounts")
*/
class Account
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
* #ORM\Column(length=11)
*/
private $id;
// ......
/**
* #ORM\ManyToMany(targetEntity="Account\Entity\Account", mappedBy="following")
*/
private $followers;
/**
* #ORM\ManyToMany(targetEntity="Account\Entity\Account", inversedBy="followers")
* #ORM\JoinTable(name="followers",
* joinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="follower_id", referencedColumnName="id")}
* )
*/
private $following;
public function __construct(){
$this->followers = new ArrayCollection();
$this->following = new ArrayCollection();
}
/**
* #param mixed $followers
*/
public function setFollowers($followers)
{
$this->followers[] = $followers;
}
/**
* #return mixed
*/
public function getFollowers()
{
return $this->followers;
}
public function addFollowers($followers){
foreach($followers as $follower)
$this->followers->add($follower);
}
public function removeFollowers($followers){
$this->followers->removeElement($followers);
}
/**
* #param mixed $following
*/
public function setFollowing($following)
{
$this->following[] = $following;
}
/**
* #return mixed
*/
public function getFollowing()
{
return $this->following;
}
public function addFollowing($followers){
foreach($followers as $follower)
$this->following->add($follower);
}
public function removeFollowing($followers){
$this->following->removeElement($followers);
}
/**
* #param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
}
So I have 2 accounts (ids 1 and 2) and made it so that 1 follows (is friend to) 2.
The column is something like
user_id follower_id
2 1
By using the following code, I'm not getting any results as I should
$user = $this->entityManager()->getRepository('Account/Entity/Account')->find(1);
$followers = $user->getFollowers();
var_dump($followers);
It returns something like:
object(Doctrine\ORM\PersistentCollection)#357 (9) { ["snapshot":"Doctrine\ORM\PersistentCollection":private]=> array(0) { } ["owner":"Doctrine\ORM\PersistentCollection":private]=> NULL ["association":"Doctrine\ORM\PersistentCollection":private]=> NULL ["em":"Doctrine\ORM\PersistentCollection":private]=> NULL ["backRefFieldName":"Doctrine\ORM\PersistentCollection":private]=> NULL ["typeClass":"Doctrine\ORM\PersistentCollection":private]=> NULL ["isDirty":"Doctrine\ORM\PersistentCollection":private]=> bool(false) ["initialized":"Doctrine\ORM\PersistentCollection":private]=> bool(false) ["coll":"Doctrine\ORM\PersistentCollection":private]=> object(Doctrine\Common\Collections\ArrayCollection)#358 (1) { ["_elements":"Doctrine\Common\Collections\ArrayCollection":private]=> array(0) { } } }
The same happens if I use getFollowing and all the combinations I've tried. Am I missing something? I mean it's pretty much like the documentation code, please help me out!
I'm using Zend Framework 2, if that's of any help.

All associations are LAZY by default, which means it is populated when you first access it. PersistentCollection actually is an iterator and a var_dump will not trigger iteration, that's why you see _intialized property set to false and the count of _elements is 0.
You can use getArrayCopy or simply iterate through the collection.
var_dump($followers->getArrayCopy());
or:
foreach ($followers as $follower) {
var_dump($follower);
}

Related

Unexpected non-iterable value for to-many relation

When I run the phpunit,
$this->assertResponseIsSuccessful();
show the error:
'hydra:description' => 'Unexpected non-iterable value for to-many relation'
Failed asserting that the Response is successful.
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, private
Content-Type: application/ld+json; charset=utf-8
Date: Fri, 09 Jul 2021 13:06:48 GMT
Link: <http://example.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Robots-Tag: noindex
I think this problem have relation with null value in Entity.
/**
* #return Collection|null<int, Falta>
*/
public function getFalta(): ?Collection
{
return $this->falta;
}
It makes sense? And how fix this?
My complete entity:
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ORM\Table(schema="db_automacao_sti", name="tb_pessoa")
*
* #ApiResource(
* normalizationContext={"groups"={"pessoa:read"}},
* denormalizationContext={"groups"={"pessoa:write"}},
* collectionOperations={
* "get"
* },
* itemOperations={
* "get"
* }
* )
* #ApiFilter(SearchFilter::class, properties={"ordemServicoContratoItemPessoa.itemContrato.contrato": "exact","ordemServicoContratoItemPessoa.itemContrato": "exact"})
*/
class Pessoa
{
/**
* #Groups({"pessoa:read"})
*
* #ORM\Id
* #ORM\Column(name="pk_pessoa", type="integer")
* #ORM\GeneratedValue(strategy="SEQUENCE")
* #ORM\SequenceGenerator(sequenceName="sq_pessoa", initialValue=1, allocationSize=100)
*/
private ?int $id = null;
/**
* #Groups({"pessoa:read"})
*
* #ORM\Column(name="dh_criado_em", type="datetime")
*/
private ?\DateTimeInterface $criadoEm = null;
/**
* #Groups({"pessoa:read"})
*
* #ORM\Column(name="dh_atualizado_em", type="datetime")
*/
private ?\DateTimeInterface $atualizadoEm = null;
/**
* #Assert\NotBlank
* #Assert\Type("\DateTimeInterface")
*
* #Groups({"pessoa:read", "pessoa:write"})
*
* #ORM\Column(name="dh_contratado_em", type="datetime")
*/
private ?\DateTimeInterface $contratadoEm = null;
/**
* #Assert\NotBlank
* #Assert\Length(max=255)
*
* #Groups({"pessoa:read", "pessoa:write"})
*
* #ORM\Column(name="no_pessoa", type="string", length=255)
*/
private ?string $nome = null;
/**
* #Assert\NotBlank
* #Assert\Length(11)
*
* #Groups({"pessoa:read", "pessoa:write"})
*
* #ORM\Column(name="nu_cpf", type="string", length=11)
*/
private ?string $cpf = null;
/**
* #Assert\NotBlank
* #Assert\Length(max=255)
* #Assert\Email
*
* #Groups({"pessoa:read", "pessoa:write"})
*
* #ORM\Column(name="ds_email", type="string", length=255)
*/
private ?string $email = null;
/**
* #Assert\NotBlank
* #Assert\Length(min=10, max=11)
* #Assert\Type("digit")
*
* #Groups({"pessoa:read", "pessoa:write"})
*
* #ORM\Column(name="nu_telefone", type="string", length=11)
*/
private ?string $numeroTelefone = null;
/**
* #var Collection<int, OrdemServicoContratoItemPessoa> Coleção de ordemServicoContratoItemPessoa
* #ORM\OneToMany(targetEntity="OrdemServicoContratoItemPessoa", mappedBy="pessoa")
*/
private $ordemServicoContratoItemPessoa;
/**
* #var Collection<int, Falta> Coleção de falta
* #ORM\OneToMany(targetEntity="Falta", mappedBy="pessoa")
* #Groups({"item:read"})
*/
private $falta;
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function onPrePersistOrUpdate(): void
{
$this->setAtualizadoEm(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
if (null === $this->getCriadoEm()) {
$this->setCriadoEm(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
}
}
public function getId(): ?int
{
return $this->id;
}
public function getCriadoEm(): ?\DateTimeInterface
{
return $this->criadoEm;
}
public function setCriadoEm(\DateTimeInterface $criadoEm): self
{
$this->criadoEm = $criadoEm;
return $this;
}
public function getAtualizadoEm(): ?\DateTimeInterface
{
return $this->atualizadoEm;
}
public function setAtualizadoEm(\DateTimeInterface $atualizadoEm): self
{
$this->atualizadoEm = $atualizadoEm;
return $this;
}
public function getContratadoEm(): ?\DateTimeInterface
{
return $this->contratadoEm;
}
public function setContratadoEm(\DateTimeInterface $contratadoEm): self
{
$this->contratadoEm = $contratadoEm;
return $this;
}
public function getNome(): ?string
{
return $this->nome;
}
public function setNome(string $nome): self
{
$this->nome = $nome;
return $this;
}
public function getCpf(): ?string
{
return $this->cpf;
}
public function setCpf(string $cpf): self
{
$this->cpf = $cpf;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getNumeroTelefone(): ?string
{
return $this->numeroTelefone;
}
public function setNumeroTelefone(string $numeroTelefone): self
{
$this->numeroTelefone = $numeroTelefone;
return $this;
}
/**
* #return Collection<int, OrdemServicoContratoItemPessoa>
*/
public function getOrdemServicoContratoItemPessoa(): Collection
{
return $this->ordemServicoContratoItemPessoa;
}
/**
* #return Collection|null<int, Falta>
*/
public function getFalta(): ?Collection
{
return $this->falta;
}
}
You are having an error because a Collection (ArrayCollection) is not initialized.
Add a constructor to your entity and set every collection value as a new ArrayCollection();
It should look like:
use Doctrine\Common\Collections\ArrayCollection;
public function __construct()
{
$this->ordemServicoContratoItemPessoa = new ArrayCollection();
$this->falta = new ArrayCollection();
}
It makes sense?
Yes, I'd say you've found the culprit that is causing the error.
And how fix this?
It needs to become anything iterable, null does not suffice (it is commonly not seen as an iterable, albeit this could be questionable in a one to many relationship if it allows one to none, it seems an oversight to not handle the case of null but this is a purely theoretical discussion).
Practically given your getter:
/**
* #return Collection|null<int, Falta>
*/
public function getFalta(): ?Collection
{
return $this->falta;
}
you can use the error to make it more obvious by making the return-type non-null:
/**
* #return Collection<int, Falta>
*/
public function getFalta(): Collection
{
return $this->falta;
}
(btw. null<XXX> would have been wrong anyway - null is not of any other type, that type template does not exist.)
Now PHP will directly highlight the illegal access. You won't need an additional assertion (as currently it is done when you run the tests).
TypeError: ...
Sounds counter-productive? No, first of all the return-type-hint you use in your production code is fixed. It exactly shows and documents which type is intended for the code to work - in its public interface (one of the best places to document such things).
In some languages this called "testing with the compiler", that is to make errors or flaws in the code visible early (we embrace errors). As in PHP there is no compiler, Unit-Tests are important so all code is at least executed once and then the type constraints in the runtime are applied. So to say "testing with the interpreter".
This should also make a concrete fix more obvious: If at that place and in time $this->falta is null, you need an empty collection:
/**
* #return Collection<int, Falta>
*/
public function getFalta(): Collection
{
return $this->falta ?? new ArrayCollection();
}
The default Collection Doctrine\Common\Collections\ArrayCollection is empty, so it can well represent that there is no data (previously null).
( Dylan Kas also pointed to this basic Collection which is easily available at hand in his answer that initializes it already in the constructor)
And you have "solved" the type problem.
How good is this fix you still need to ask yourself, so far it only solves the type problem. But the type problem might have hinted an initialization (or other) problem.
Fixing the problem too early might hide the actual problem (!). This is again why we embrace errors and provoke them until we have understood what we're doing here. And then in a glimpse of the eye suddenly things are clear, fixes are easy and we can throw away code we've been written previously (or well, this can happen).
I can not tell you if at the moment you get the error in test (or would get the error with the first fix suggestion for the return-type only - then in production) this is actually due to the fact that $this->falta must be non-null already. If so, let it fail and fix the actual reason.
This is what in the end should the testing give you.

Relationship between two tables in doctrine. Data dosen't save in database

I have a problem with saving data in Database.I have two tables which are created with Doctrine Entities. My Entities are:
<?php
namespace App\Http\Entities\Cic;
use \Doctrine\ORM\Mapping AS ORM;
use \Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="cic_case_files")
*/
class CicCaseFile {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="CicCase", inversedBy="cicFiles")
* #ORM\JoinColumn(name="case_id", referencedColumnName="id")
*/
protected $case;
/**
* One Product has One Shipment.
* #ORM\OneToOne(targetEntity="File")
* #ORM\JoinColumn(name="file_id", referencedColumnName="id")
*/
protected $file;
/**
* #ORM\Column(name="case_id", type="integer", nullable=false)
*/
protected $case_id;
/**
* #ORM\Column(name="file_id", type="integer", nullable=false)
*/
protected $file_id;
public function __construct() {
//$this->mailboxes = new ArrayCollection();
}
public function setFileId($fileId){
$this->file_id = $fileId;
}
public function getFileId(){
return $this->file_id;
}
public function setCaseId($caseId){
$this->case_id = $caseId;
}
public function getCaseId(){
return $this->case_id;
}
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getCase(){
return $this->case;
}
public function setCase($case){
$this->case = $case;
}
public function getFile(){
return $this->file;
}
public function setFile($file){
$this->file = $file;
}
}
<?php
namespace App\Http\Entities\Cic;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="\App\Http\Repositories\Cic\FileRepository")
* #ORM\Table(name="files")
*/
class File
{
/**
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
* #var int
*/
protected $id;
/**
* #ORM\Column(type="string")
* #var string
*/
protected $path;
/**
* #ORM\Column(type="string")
* #var string
*/
protected $originalName;
/**
* #ORM\Column(type="string")
* #var string
*/
protected $hashName;
/**
* #ORM\Column(type="string")
* #var string
*/
protected $extension;
public function getId(){
return $this->id;
}
public function setId($id){
$this->id = $id;
}
public function getPath(){
return $this->path;
}
public function setPath($path){
$this->path = $path;
}
public function getOriginalName(){
return $this->originalName;
}
public function setOriginalName($originalName){
$this->originalName = $originalName;
}
public function getHashName(){
return $this->hashName;
}
public function setHashName($hashName){
$this->hashName = $hashName;
}
public function getExtension(){
return $this->extension;
}
public function setExtension($extension){
$this->extension = $extension;
}
}
My tables:
files:
cic_case_file:
I'd like to save data in table CicCaseFile by code:
$cicFile = new CicCaseFile();
cicFile->setCaseId($caseId);
$cicFile->setFileId($fileId);
$this->entityManager->persist($cicFile);
$this->entityManager->flush();
Something is wrong with my Entities, but I don't know what. Could someone help me with that? I would be very greatful. Best regards.
This is what I used to follow if I am in your position.
Your File Entity must only have id, path, extension, original_name, hash_name properties and methods, you can internally use any conversion methods
Your CicCaseFile Entity must only have id, case_id, file_id properties and methods, you can internally use any conversion methods
So what must be your approach while inserting the data is, first you need to insert the data for your parent table ie file table then its children table ie cic_case_file table
$em = $this->getDoctrine()->getManager(); /* In Symfony Framework */
$fileObj = new File();
$fileObj->setPath($path);
$fileObj->setExtension($extension);
$fileObj->setOriginalName,($originalName);
$fileObj->setHashName($hashName);
$em->persist($fileObj);
$em->flush();
/* So once you flush here it fileObj will be having the last inserted object which you can use to store in child table */
$cicCaseFileObj = new CicCaseFile();
$cicCaseFileObj->setFile($fileObj); /* $fileObj which was inserted above */
$cicCaseFileObj->setCaseId($caseObj); /* Same as $fileObj as inserted above in case if this is from different table */
$em->persist($cicCaseFileObj);
$em->flush();
if caseId is inserted in different saveMethod then first you need to fetch the object and then give it to the above object as follows
$caseId = 10; /* This you may get from else where */
$caseObj = $em->getRepository('BundleName:Case')->find($caseId);
/* Make sure your case_id is nullable or not */
$cicCaseFileObj = new CicCaseFile();
$cicCaseFileObj->setFile($fileObj);
if($caseObj){
$cicCaseFileObj->setCaseId($caseObj);
}
$em->persist($cicCaseFileObj);
$em->flush();
Happy Coding!

Doctrine retrieve entity data with one to many association

i want to fetch the data of a menu including its categories in a custom repository by a dql-statement, but it doesn't return the associated entities. I need it as json data, so i added the hydration mode Query::HYDRATE_ARRAY to the function call.
/**
* #ORM\Entity(repositoryClass="Company\Repository\Doctrine\MenuRepository")
* #ORM\Table(name="menu")
*/
class Menu {
/**
* #var \Company\Entity\MenuCategory[]
*
* #ORM\OneToMany(targetEntity="Company\Entity\MenuCategory", mappedBy="menu")
*/
protected $categories;
}
/**
* #ORM\Entity(repositoryClass="Company\Repository\Doctrine\MenuCategoryRepository")
* #ORM\Table(name="menu_category")
*/
class MenuCategory {
/**
* #var \Company\Entity\Menu
*
* #ORM\ManyToOne(targetEntity="Company\Entity\Menu", inversedBy="categories")
* #ORM\JoinColumn(nullable=false)
*/
protected $menu;
}
class MenuRepository extends EntityRepository implements MenuRepositoryInterface {
public function findById($id, $hydration = Query::HYDRATE_OBJECT) {
$queryBuilder = $this->createQueryBuilder('menu')
->leftJoin('menu.categories', 'categories')
->where('menu.id = :menuId')
->setParameter('menuId', $id);
return $queryBuilder->getQuery()->getSingleResult($hydration);
}
}
The result looks like that:
array(4) {
["id"]=>int(1)
["name"]=> string(6) "Test"
}

Two one-to-many relationship with reference table (Doctrine 2, ZF2)

I've a problem with my many-to-many relation. I want to have access to the reference table for a querybuilder query. With a many-to-many relation I don't have access to my reference table, so I've set up two one-to-many relationships. My structure look likes:
User ---> UserUserCategory <--- UserCategory
The above structure has two one-to-many relationships and are working fine with the database. When I have a user with the following data in the database (in UserUserCategory):
Table User
ID | Name
1 | Bart
2 | Gerard
Table Category
ID | Name
1 | Officer
2 | Medic
Table UserUserCategory
User | Category
1 | 1
2 | 2
So Bart is an Officer and Gerard is a Medic. But when I want to retrieve the data, it said that Bart is the Medic, and Gerard has a "null" value in the category.
My User-entity:
/**
* Entity Class representing a post of our User module.
*
* #ORM\Entity
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="User\Repository\UserRepository")
*
*/
class User extends zfcUser implements UserInterface
{
/**
* Categories from user
*
* #ORM\OneToMany(targetEntity="User\Entity\UserUserCategory", mappedBy="user_id", cascade={"remove", "persist"})
* #var UserUserCategory
* #access protected
*/
protected $user_usercategories;
//name & user_id comes here
/**
* Constructor to make a new ArrayCollection for addresses
*
*
*/
public function __construct()
{
$this->user_usercategories = new ArrayCollection();
}
/**
* #param Collection $categories
*/
public function addUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setUser($this);
$this->user_usercategories->add($user_usercategorie);
}
}
/**
* #param Collection $categories
*/
public function removeUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setUser(null);
$this->user_usercategories->removeElement($user_usercategorie);
}
}
/**
* #return Collection
*/
public function getUserUserCategories()
{
return $this->categories;
}
}
My UserCategory-entity:
/**
* A User category entity.
* #ORM\Entity
* #ORM\Table(uniqueConstraints={#ORM\UniqueConstraint(name="unique_name_parentId", columns={"name", "parent_id"})})
* #ORM\HasLifecycleCallbacks
*/
class UserCategory extends Category
{
/**
* User_usercategories
*
* #ORM\OneToMany(targetEntity="User\Entity\UserUserCategory", mappedBy="category_id")
* #var UserUserCategory
* #access protected
*/
protected $user_usercategories;
/**
* Constructor
*/
public function __construct()
{
$this->user_usercategories = new ArrayCollection();
}
/**
* #param Collection $categories
*/
public function addUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setCategory($this);
$this->user_usercategories->add($user_usercategorie);
}
}
/**
* #param Collection $categories
*/
public function removeUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setCategory(null);
$this->user_usercategories->removeElement($user_usercategorie);
}
}
/**
* #return Collection
*/
public function getUserUserCategories()
{
return $this->categories;
}
}
My UserUserCategory-entity:
/**
* Entity Class representing a post of our User_UserCategory entity.
*
* #ORM\Entity
* #ORM\Table(name="user_usercategory")
*
*/
class UserUserCategory
{
/**
* User with a category
*
* #ORM\ManyToOne(targetEntity="User\Entity\User", inversedBy="user_usercategories")
* #ORM\JoinColumn(name="user_id", referencedColumnName="user_id", nullable=false, onDelete="CASCADE")
* #ORM\Id
*
* #var User
* #access protected
*/
protected $user_id;
/**
* Category from user
*
* #ORM\ManyToOne(targetEntity="User\Entity\UserCategory", inversedBy="user_usercategories")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false, onDelete="CASCADE")
* #ORM\Id
*
* #var Category
* #access protected
*/
protected $category_id;
public function getUser()
{
return $this->user;
}
/**
* Set User
*
* #param User $user
* #return User
*/
public function setUser(User $user = null)
{
//die('setUser');
$this->user = $user;
return $this;
}
public function getCategory()
{
return $this->category;
}
/**
* Set Category
*
* #param Category $category
* #return Category
*/
public function setCategory(Category $category = null)
{
$this->category = $category;
return $this;
}
}
When I execute the following line, it gives back the wrong result. The wrong category pops up:
\Doctrine\Common\Util\Debug::dump($this->getEntityManager()->find('User\Entity\User', '49')->user_usercategories);
die;
Result:
array(1) {
[0]=>
object(stdClass)#452 (3) {
["__CLASS__"]=>
string(28) "User\Entity\UserUserCategory"
["user_id"]=>
string(16) "User\Entity\User"
["category_id"]=>
string(24) "User\Entity\UserCategory"
}
}
In the category_id is the medic printed, I expect the officer to get back.
In my other user, (id=60) the category_id field is "null". So it look likes Doctrine skips the first input in my UserUserCategory, starts with the second and can't get the last category anymore.
No offence, but I find your code very hard to read. I would suggest you to do few corrections and that might even help you in solving the problem.
1: Naming: Instead of UserCategory, rename it to Category. If your Category will have different types, create new column "type" with values from constansts like
class Category
{
const TYPE_USER = 1 ;
....
2: Instead of addCategories(Collection $array), do singular version like
public function addCategory(Category $category)
{
$reference = new UserCategory() ;
$reference->setUser($this) ;
$reference->setCategory($category) ;
$this->user_categories->add($reference) ;
}
public function removeCategory(Category $category)
{
foreach($this->user_categories as $reference) {
if ( $reference->getCategory() === $category )
$this->user_categories->removeElement($reference) ;
}
}
Symfony2 automaticaly recognizes methods like this. Even if your relation is plural (like categories), s2 will find singularified addCategory and removeCategory methods.
To get array of categories, use this:
public function getCategories()
{
$categories = new ArrayCollection() ;
foreach($this->user_categories as $reference) {
$categories->add( $reference->getCategory() ) ;
}
return $categories ;
}
If you do it like this, you will probably solve the problem you have.

Doctrine2: Many-To-Many with extra columns in reference table (add record)

Spoiler: I think I found the answer but I'm not 100% sure ;)
I've been looking at this question for a while but I cannot manage to make it work. So I've create dummies Entities to test the relation and here they are:
A Product can be in many Cart
A Cart can contains several Product
The Product in a Cart are order by a position
Product
<?php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="demo_product")
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="product", cascade={"all"})
*/
protected $productCarts;
/**
* Constructor
*/
public function __construct()
{
$this->productCarts = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Add productCarts
*
* #param \Acme\DemoBundle\Entity\CartHasProduct $productCarts
* #return Product
*/
public function addProductCart(\Acme\DemoBundle\Entity\CartHasProduct $productCarts)
{
$this->productCarts[] = $productCarts;
return $this;
}
/**
* Remove productCarts
*
* #param \Acme\DemoBundle\Entity\CartHasProduct $productCarts
*/
public function removeProductCart(\Acme\DemoBundle\Entity\CartHasProduct $productCarts)
{
$this->productCarts->removeElement($productCarts);
}
/**
* Get productCarts
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getProductCarts()
{
return $this->productCarts;
}
}
Cart
<?php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="demo_cart")
*/
class Cart
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="cart", cascade={"all"})
*/
protected $cartProducts;
/**
* Constructor
*/
public function __construct()
{
$this->cartProducts = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Add cartProducts
*
* #param \Acme\DemoBundle\Entity\CartHasProduct $cartProducts
* #return Cart
*/
public function addCartProduct(\Acme\DemoBundle\Entity\CartHasProduct $cartProducts)
{
$this->cartProducts[] = $cartProducts;
return $this;
}
/**
* Remove cartProducts
*
* #param \Acme\DemoBundle\Entity\CartHasProduct $cartProducts
*/
public function removeCartProduct(\Acme\DemoBundle\Entity\CartHasProduct $cartProducts)
{
$this->cartProducts->removeElement($cartProducts);
}
/**
* Get cartProducts
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCartProducts()
{
return $this->cartProducts;
}
}
and finally CartHasProduct reference table
<?php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="demo_cartHasProduct")
*/
class CartHasProduct
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Cart", inversedBy="productCarts")
*/
protected $cart;
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="cartProducts")
*/
protected $product;
/**
* #ORM\Column(type="integer")
*/
protected $position;
public function __construct(Cart $cart, Product $product, $position=0) {
$this->cart = $cart;
$this->product = $product;
$this->setPosition($position);
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set position
*
* #param integer $position
* #return CartHasProduct
*/
public function setPosition($position)
{
$this->position = $position;
return $this;
}
/**
* Get position
*
* #return integer
*/
public function getPosition()
{
return $this->position;
}
/**
* Set cart
*
* #param \Acme\DemoBundle\Entity\Cart $cart
* #return CartHasProduct
*/
public function setCart(\Acme\DemoBundle\Entity\Cart $cart = null)
{
$this->cart = $cart;
return $this;
}
/**
* Get cart
*
* #return \Acme\DemoBundle\Entity\Cart
*/
public function getCart()
{
return $this->cart;
}
/**
* Set product
*
* #param \Acme\DemoBundle\Entity\Product $product
* #return CartHasProduct
*/
public function setProduct(\Acme\DemoBundle\Entity\Product $product = null)
{
$this->product = $product;
return $this;
}
/**
* Get product
*
* #return \Acme\DemoBundle\Entity\Product
*/
public function getProduct()
{
return $this->product;
}
}
I've created the Entities manually, adding the #ORM annotations to setup the relationship and then I've used app/console generate:doctrine:entities AcmeDemoBundle to populate the getter, setter and __construct
Now I a controller I have to following code:
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class WelcomeController extends Controller
{
public function indexAction()
{
// Create a Cart Entity
$cart = new \Acme\DemoBundle\Entity\Cart();
// Create a Product Entity
$product = new \Acme\DemoBundle\Entity\Product();
// Add the Product into the Cart
$cart->getCartProducts()->add($product);
// Save the Cart
$em = $this->getDoctrine()->getManager();
$em->persist($cart);
$em->flush();
return $this->render('AcmeDemoBundle:Welcome:index.html.twig');
}
}
Doing so I have the following error coming up:
Found entity of type Acme\DemoBundle\Entity\Product on association Acme\DemoBundle\Entity\Cart#cartProducts, but expecting Acme\DemoBundle\Entity\CartHasProduct
So my question is how to add a Product into a Cart? Do I need to create the relation Object manually (CartHasProduct)? I would think Doctrine would have done it. I looked everywhere on Doctrine documentation and I could not find an exemple of relationship with extra field.
I've also looked into the tests in the vendor, there is plenty of model (very interesting) but nothing with extra field in relationship.
I was thinking the create my own method in Cart like this:
public function addProduct(Product $product, $position=0) {
$relation = new CartHasProduct($this, $product, $position);
if (!$this->cartProducts->contains($relation)) {
$this->cartProducts->add($relation);
}
}
But I'd like to know if I need to implement it or if it's meant to be handled automatically?
#### UPDATE 1 ####
So I ended up adding this method addProduct. The problem is that contains() is not working as expected. So I tried to delete all Product from the Cart and add a new one.
Here is my function to delete the products:
/**
* Reset the product for the cart
*
* #return bool
*/
public function resetCart() {
foreach ($this->getCartProducts() as $relation) {
$relation->getProduct()->removeProductCart($relation);
$this->removeCartProducts($relation);
}
}
and here is how I call it:
$em = $this->getDoctrine()->getManager();
$cart->resetCart();
$em->persist($cart);
$em->flush();
But the records are not deleted in CartHasProduct table.
UPDATE 2
I found what was the problem, you need to add orphanRemoval=true in the OneTwoMany relation (on both side) if you want to delete the relationship between the 2 main Entity (Cart and Product):
/**
* #ORM\Entity
* #ORM\Table(name="demo_product")
*/
class Product
{
...
/**
* #ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="product", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $productCarts;
And
/**
* #ORM\Entity
* #ORM\Table(name="demo_cart")
*/
class Cart
{
...
/**
* #ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="cart", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $cartProducts;
...
/**
* Reset the product for the cart
*/
public function resetCart() {
$this->getCartProducts()->clear();
}
Cheers,
Maxime
Well many to many association with extra parameters can be implemented by using a third intermediate entity. You have the right approach, but wrong associations defined. Here's how it should be.
Taking your 3 entities Product, Cart, CartProducts
Cart should have a one-to-many relationship with CartProducts
CartProducts should have many-to-one relationship to Product and Many-to-one association with Cart
So you first initialize a Cart, and add products to Cart like this:
For every Product
initialize a CartProduct with the Product and Cart and other extra parameters you need.
Add it to the Cart