Doctrine : many-to-many with extra columns in reference table - symfony

I have a problem with Symfony3/doctrine2 and many-to-many with extra columns. I just start to develop with it
Yes, I see the post : Doctrine2: Best way to handle many-to-many with extra columns in reference table
But honestly, I don't understand why it's not working with my code...
I'm stuck since 2 days, looking on Google (he's not my friend), reading documentations... my brain doesn't understand...
Please, help me to understand why.
Problem
I have member and address entities. I would like to have all addresses by member OR all members by address. So, ManyToMany in Doctrine.
On my join Table, I need to have an extra column favorite_address
So, I need to have 3 entities : Member, Address, MemberAddress
Entity Member :
class Member implements AdvancedUserInterface
{
....
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Member\MemberAddress", mappedBy="member", cascade={"all"})
* #ORM\JoinTable(name="member_address",
* joinColumns={#ORM\JoinColumn(name="member_id", referencedColumnName="id")})
*/
private $addresses;
....
public function __construct(){
$this->addresses = new ArrayCollection();
}
/**
* Add an address
* #param Address $address
*/
public function addAddress(\AppBundle\Entity\Address\Address $address)
{
// Ici, on utilise l'ArrayCollection vraiment comme un tableau
$this->addresses[] = $address;
}
/**
* Remove an address
* #param Address $address
*/
public function removeAddress(\AppBundle\Entity\Address\Address $address)
{
// Ici on utilise une méthode de l'ArrayCollection, pour supprimer la catégorie en argument
$this->addresses->removeElement($address);
}
/**
* Get all addresses
* #return ArrayCollection
*/
public function getAddresses()
{
return $this->addresses;
}
Entity Address :
class Address
{
....
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Member\MemberAddress", mappedBy="address", cascade={"all"})
* #ORM\JoinTable(name="member_address",
* joinColumns={#ORM\JoinColumn(name="address_id", referencedColumnName="id")})
*/
private $members;
....
/**
* Add a member
* #param Member $member
*/
public function addMember(\AppBundle\Entity\Member\Member $member)
{
// Ici, on utilise l'ArrayCollection vraiment comme un tableau
$this->members[] = $member;
}
/**
* Remove a member
* #param Member $member
*/
public function removeMember(\AppBundle\Entity\Member\Member $member)
{
// Ici on utilise une méthode de l'ArrayCollection, pour supprimer la catégorie en argument
$this->members->removeElement($member);
}
/**
* Get all members
* #return ArrayCollection
*/
public function getMembers()
{
return $this->members;
}
And the last Entity : MemberAddressReference
class MemberAddress
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #ORM\ManyToOne(targetEntity="AppBundle\Entity\Member\Member", inversedBy="addresses")
* #ORM\JoinColumn(name="member_id", referencedColumnName="id")
*/
protected $member;
/** #ORM\ManyToOne(targetEntity="AppBundle\Entity\Address\Address", inversedBy="members")
* #ORM\JoinColumn(name="address_id", referencedColumnName="id")
*/
protected $address;
/** #ORM\Column(type="boolean") */
protected $isFavorite;
To finish, the controller
class MemberAddressController extends Controller
{
public function createAction(Request $request){
....
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$currentDate = new \DateTime("now");
$em = $this->getDoctrine()->getManager();
$address = new Address();
$memberAddress = new MemberAddress();
$address->setType($form['type']->getData());
$address->setCreated($currentDate);
$address->setModified($currentDate);
$memberAddress->setMember($member);
$memberAddress->setAddress($address);
$memberAddress->setFavorite(1);
$em->persist($member);
$em->persist($address);
$em->persist($memberAddress);
$member->addAddress($address);
$em->flush();
dump($member);
die();
}
So, what's wrong
I get this error :
Expected value of type "Doctrine\Common\Collections\Collection|array" for association field "AppBundle\Entity\Member\Member#$addresses", got "AppBundle\Entity\Address\Address" instead.
Yup, type is not good, I understand, but why he's not good ?
public function addAddress(\AppBundle\Entity\Address\Address $address)
{
// Ici, on utilise l'ArrayCollection vraiment comme un tableau
$this->addresses[] = $address;
}
addAddress take Address object, no ? So why he's waiting an array ?
Please help me, I'm going crazy...

I don't understand the goal of your memberAdress entity. If you want to create a OneToMany bidirectional relationship, you don't need to create a third entity.
According to doctrine documentation :
/** #Entity */
class Product
{
// ...
/**
* One Product has Many Features.
* #OneToMany(targetEntity="Feature", mappedBy="product")
*/
private $features;
// ...
public function __construct() {
$this->features = new ArrayCollection();
}
}
/** #Entity */
class Feature
{
// ...
/**
* Many Features have One Product.
* #ManyToOne(targetEntity="Product", inversedBy="features")
* #JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
// ...
}
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html
EDIT
If you want to use a third entity, you don't have a relationship between your adress and your member. They are just linked by the third party.
So you should do the mapping this way :
Member Entity :
class Member implements AdvancedUserInterface
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Member\MemberAddress", mappedBy="member", cascade={"all"})
*/
private $addresses;
Adress Entity
class Address
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Member\MemberAddress", mappedBy="address", cascade={"all"})
*/
private $members;
If you want all the adresses of a member you will need to create a custom repository to join your adress table with the two other tables.

Related

How to order by entity property in symfony?

I'm trying to get the "demands" of a user.
User can have some demands and a demand have only one user (OneToMany)
This is my User entity (Utilisateur in french) :
class Utilisateur extends AbstractEntity implements UserInterface, PasswordAuthenticatedUserInterface
{
/**
* #ORM\Id
* #ORM\Column(type="ulid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class=UlidGenerator::class)
*/
private Ulid $id;
/**
* #ORM\OneToMany(targetEntity=DemandeTransport::class, mappedBy="utilisateur", orphanRemoval=true)
*/
private Collection $demandeTransports;
And my demands entity :
class DemandeTransport extends AbstractEntity
{
/**
* #ORM\Id
* #ORM\Column(type="ulid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class=UlidGenerator::class)
*/
private Ulid $id;
/**
* #ORM\ManyToOne(targetEntity=Utilisateur::class, inversedBy="demandeTransports")
* #ORM\JoinColumn(nullable=false)
*/
private Utilisateur $utilisateur;
My controller receiving the request :
/**
* #throws Exception
*/
#[Route('/liste_propositions_transporteur', name: 'liste_propositions_transporteur', methods: ['GET'])]
public function listePropositionsTransporteur(Request $request): Response
{
return match ($request->getMethod()) {
'GET' => new Response(json_encode(['success' => true, 'data' => $this->propositionsTransportService->handleListePropositionsByUser($this->getUser())])),
default => new Response(404),
};
}
The service handling the request and retreiving the demands :
/**
* #param UserInterface $user
* #return array
*/
public function handleListePropositionsByUser(UserInterface $user) : array
{
$propositions = [];
foreach ($this->propositionTransportRepository->findPropositionsByUtilisateur($user) as $propositionTransport) {
$propositions[] = DemandeTransportHelper::serializePropositionDemande($propositionTransport);
}
return $propositions;
}
And the DQL :
/**
* #param UserInterface $user
* #return mixed
*/
public function findPropositionsByUtilisateur(UserInterface $user) : mixed
{
$q = $this->createQueryBuilder('p')
->where('p.utilisateur = :utilisateur')
->setParameters([
'utilisateur' => $user
])
->orderBy('p.dateCreation', 'DESC');
return $q->getQuery()->getResult();
}
So :
When i'm doing $utilisateur->getDemandesTransports() : it works by showing me all the demands.
Well, but when I'm trying to get them by DQL (cause I want them orderd by), it returns me 0 results...
Solved by setting the parameter type :
->setParameter('utilisateur', $utilisateur->getId(), 'ulid')
I'm using ULID (UUID like) on IDs.
https://symfony.com/doc/current/components/uid.html#working-with-ulids
With annotations
You can order your data by specifying sorting in your $demandeTransports property annotations.
/**
* #ORM\OneToMany(targetEntity=DemandeTransport::class, mappedBy="utilisateur", orphanRemoval=true)
* #ORM\OrderBy({"dateCreation" = "DESC"})
*/
private Collection $demandeTransports;
So when you call $utilisateur->getDemandesTransports() you will get ordered data.
With DQL
Also if you still want to use DQL then you should change your query to this as you need to join the Utilisateur entity then you can use the desired properties
$q = $this->createQueryBuilder('p')
->join('p.utilisateur', 'u')
->where('u.utilisateur = :utilisateur')
->setParameters([
'utilisateur' => $user
])
->orderBy('u.dateCreation', 'DESC');

Doctrine JoinColumn id is Null

I'm stuck with my relation.
Here's my entity :
class Orderproduct
/**
* #ORM\OneToMany(targetEntity="ProductBundle\Entity\Product", mappedBy="orderproduct", cascade={"persist"})
*/
private $product;
/**
* #ORM\OneToMany(targetEntity="ProductBundle\Entity\Machining", mappedBy="orderproduct", cascade={"persist"})
*/
private $machining;
And my two others entity :
class Product
/**
* #ORM\ManyToOne(targetEntity="ProductBundle\Entity\Orderproduct", inversedBy="product")
* #ORM\JoinColumn(name="orderproduct_id", referencedColumnName="id", nullable=false)
*/
private $orderproduct;
class Machining
/**
* #ORM\ManyToOne(targetEntity="ProductBundle\Entity\Orderproduct", inversedBy="machining")
* #ORM\JoinColumn(name="orderproduct_id", referencedColumnName="id", nullable=false)
*/
private $orderproduct;
And I've got this error :
SQLSTATE[23000]: Integrity constraint violation: 1048 Le champ 'orderproduct_id' ne peut être vide (null)
Here's my simple add function
public function addOrderproductAction(Request $request)
{
$orderproduct = new Orderproduct();
$formorderproduct = $this->createForm(OrderproductType::class, $orderproduct);
if($formorderproduct->handleRequest($request)->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->persist($orderproduct);
$em->flush();
return $this->redirect($this->generateUrl('product_bundle_listorderproduct'));
}
return $this->render('ProductBundle:Default:neworderproduct.html.twig', array(
'formorderproduct' => $formorderproduct->createView(),
));
}
And i got this with a dump just before the flush :
Any idea ?
Thx for your help!
Edit : After put $product->getOrderproduct($this) and $machining->getOrderproduct($this).
Edit :
I'm changing my model but still have the same problem. So I have a relation between product and machining
Product
/**
* #ORM\OneToMany(targetEntity="ProductBundle\Entity\Machining", mappedBy="product", cascade={"persist"})
*/
private $machining;
Machining
/**
* #ORM\ManyToOne(targetEntity="ProductBundle\Entity\Product", inversedBy="machining")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
This is Machining table in my Db : product_id is null.
I already try to modify setProduct in Machining but it still the same.
try this:
in Orderproduct' addMachining($machining) method:
$this->machining[] = $machining;
$machining->setOrderproduct($this);
return $this;
the same for the other entity(of course different method name and properties). If this does not work try to persist all entity separately.
So I found a solution :
In the buildForm simply add
'by_reference => false'
Now it's work !

Symfony Doctrine relationships - delete entity on the inverse side

my question is how to delete entity on the inverse side without going through every association and delete it manually.
<?php
/** #Entity */
class User
{
// ...
/**
* #OneToMany(targetEntity="Address", mappedBy="user")
*/
private $addresses;
// ...
public function __construct() {
$this->addresses = new ArrayCollection();
}
}
/** #Entity */
class Address
{
// ...
/**
* #ManyToOne(targetEntity="User", inversedBy="features")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
// ...
}
In this example, Address is the owning side, so I can't delete User because foreign key validation will fail. I have to delete Address and then delete User. If I have 10 relationships like these the delete process is painful.
I can create ManyToMany relationship, but this way the Address entity will have users not user and I want addresses to have only one user.
What is the best way to do this?
I hope it's helpful.
Just add cascade to inverse side entity.
/** #Entity */
class User
{
// ...
/**
* #OneToMany(targetEntity="Address", mappedBy="user", cascade={"persist", "remove"})
*/
private $addresses;
// ...
public function __construct() {
$this->addresses = new ArrayCollection();
}
/*
* #return ArrayCollection
*/
public function getAddresses (){
return $this->addresses:
}
/*
* #pram Address $address
*/
public function setAddresses (Address $address){
$this->addresses->add ($address);
}
}

Doctrine one to many - persisting multiple files from owning side not working

I have 2 entities Submission and Documents. 1 Submission can have Multiple documents.
Submission Entity:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*/
protected $document;
/**
* #return mixed
*/
public function getDocument()
{
return $this->document->toArray();
}
public function setDocument(Document $document)
{
$this->document[] = $document;
return $this;
}
Document Entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Submission", inversedBy="document")
* #ORM\JoinColumn(name="submission_id", referencedColumnName="id",onDelete="cascade", nullable=true)
*/
protected $submission;
public function getSubmission()
{
return $this->submission;
}
/**
* #param mixed $submission
*/
public function setSubmission($submission)
{
$this->submission = $submission;
}
After receiving files dropzonejs - I'm saving them into Document object, and then, i'm try to save this object into Submission, and persist.
$document = new Document();
$em = $this->getDoctrine()->getManager();
$media = $request->files->get('file');
foreach($media as $req){
$document->setFile($req);
$document->setPath($req->getPathName());
$document->setName($req->getClientOriginalName());
$em->persist($document);
}
$submission->setSubmissionStatus(true);
foreach($document as $item){
$submission->setDocument($item);
}
$submission->setUser($user);
$em = $this->getDoctrine()->getManager();
$em->persist($submission);
$em->flush();
Problem is that all the time, i'm receiving error that submission_title is not set, but that's not true, because i have set this field before. I haven't got idea, what is wrong.
I think you'll get some mileage out of following the tutorial over at http://symfony.com/doc/current/doctrine/associations.html, if you haven't already.
I can see that your getters / setters aren't optimal for associating more than one Document with your Submission.
As they write in the Symfony docs, where they want to associate one category with many products, they have the following code:
// src/AppBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
}
From the docs:
The code in the constructor is important. Rather than being
instantiated as a traditional array, the $products property must be of
a type that implements Doctrine's Collection interface. In this case,
an ArrayCollection object is used. This object looks and acts almost
exactly like an array, but has some added flexibility. If this makes
you uncomfortable, don't worry. Just imagine that it's an array and
you'll be in good shape.
So, you'll want to be sure the constructor for your Document entity has something like $this->submissions = new ArrayCollection();. I've changed the property to a plural name, because I think it's more semantically correct. But you can keep your $submission property name, if you like.
Next is to add a addSubmission, removeSubmission, and a getSubmissions method.
Then, your class might end up looking like this:
<?php
// src/AppBundle/Entity/Submission.php
namespace AppBundle\Entity
use Doctrine\Common\Collections\ArrayCollection;
class Submission
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*
* #var ArrayCollection()
*/
protected $documents;
...
/**
* Instantiates the Submission Entity
*
* #return void
*/
public function __construct()
{
$this->documents = new ArrayCollection();
}
/**
* Returns all documents on the Submission
*
* #return mixed
*/
public function getDocuments()
{
return $this->documents;
}
/**
* Add document to this Submission
*
* #param Document $document The object to add to the $documents collection.
*
* #return Submission
*/
public function setDocument(Document $document)
{
$this->documents[] = $document;
return $this;
}
/**
* Remove a document from this Submission
*
* #param Document $document The object to remove from the $documents collection.
*
* #return Submission
*/
public function removeDocument(Document $document)
{
$this->documents->removeElement($document);
return $this;
}
}

Doctrine - ManyToMany Self Referencing Association + nested toArray() on child elements

I'm trying to perform a ManyToMany self referencing association in my Symfony 2.1 project by following the Doctrine docs: http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-many-self-referencing
My use-case is that I'm working on a CMS and I'm adding the ability to have related items of content. For example: I could have a sidebar on a website which would say that this piece of content X is related to Y and Z. Similarly on pages where content Y appears it says that it is related to content item X.
In my tests using this to add a new relation between content items fails because it reaches PHP's maximum nesting level of 100 because it is running toArray() on the current content item and then again on the related content item and so on and so on.
I've seen many similar questions on SO about Many-to-Many Self referential Doctrine associations but none with enough complete code to be able to see how others have managed this. Can anybody help?
My Content entity:
/**
* #ORM\MappedSuperclass
* #ORM\Table(name="content")
* #ORM\Entity(repositoryClass="CMS\Bundle\Common\ContentBundle\Entity\ContentRepository")
* #ORM\InheritanceType("JOINED")
*/
abstract class content implements ContentInterface
{
/**
* #var int $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $title
*
* #ORM\Column(name="title", type="string", length=255)
* #Assert\NotBlank()
*/
private $title;
// Other class properties
/**
* #var array
*
* #ORM\ManyToMany(targetEntity="Content", cascade={"persist"})
* #ORM\JoinTable(name="content_relation",
* joinColumns={#ORM\JoinColumn(name="relation_id", referencedColumnName="id")},
* inverseJoinColumns={
* #ORM\JoinColumn(name="related_content_id", referencedColumnName="id")
* })
**/
private $related;
public function __construct()
{
$this->related = new ArrayCollection();
}
// Other getters & setters for class properties
/**
* #return array
*/
public function getRelated()
{
return $this->related;
}
/**
* #param Content $relation
*/
public function addRelation(Content $relation)
{
$this->related->add($relation);
$this->related->add($this);
}
/**
* #return array
*/
public function toArray()
{
$related = array();
foreach($this->getRelated() as $relatedItem) {
$related[] = $relatedItem->toArray();
}
return array(
'type' => static::getType(),
'id' => $this->id,
'title' => $this->title,
....
'related' => $related
);
}
In my RelationsController for managing the related content data I use it like this:
/**
* Creates a new relation to a content item
*
* #Route("{_locale}/content/{id}/related", name="relation_add")
* #Method("POST")
*/
public function addAction(Request $request, $id)
{
// Validation and error checking
// $entity is loaded by the repository manager doing a find on the passed $id
$entity->addRelation($relation);
$em = $this->getEntityManager();
$em->persist($entity);
$em->persist($relation);
$em->flush();
$response = $relation->toArray();
return new JsonResponse($response, 201);
}
The fix for this was to use the JMSSerializerBundle to encode the entity to JSON instead of using a toArray method and change the addRelation function to:
/**
* #param Content $relation
*/
public function addRelation(Content $relation)
{
$this->related[] = $relation;
if (! $relation->getRelated()->contains($this)) {
$relation->addRelation($this);
}
}

Resources