Unexpected non-iterable value for to-many relation - unit-testing

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.

Related

API Platform - PATCH and ArrayCollection

I'm using API-Platform and faced an issue with updating many-to-many with an empty value.
Here is the small example:
/**
* Many Organizations have Many Followers.
* #ORM\ManyToMany(targetEntity="App\Entity\User\User", inversedBy="organizations")
* #ORM\JoinTable(name="organizations_followers",
* joinColumns={#ORM\JoinColumn(name="organization_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", unique=true)}
* )
*/
protected $followers;
/**
* #return Collection
*/
public function getFollowers(): Collection
{
return $this->followers;
}
/**
* #param array $followers
*/
public function setFollowers(array $followers): void
{
$this->followers = $followers;
}
/**
* Organization constructor.
*/
public function __construct()
{
$this->id = Uuid::uuid4();
$this->followers = new ArrayCollection();
}
So, when I'm trying to delete all followers (PATCH request with empty followers in the relationships field) I always get one undeleted record. What am I doing wrong? Any Ideas?

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

Doctrine 2 Many To Many follower relationship not working

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

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