I\'m wondering what\'s the best, the cleanest and the most simply way to work with many-to-many relations in Doctrine2.
Let\'s assume that we\'ve got an album like
I was getting from a conflict with join table defined in an association class ( with additional custom fields ) annotation and a join table defined in a many-to-many annotation.
The mapping definitions in two entities with a direct many-to-many relationship appeared to result in the automatic creation of the join table using the 'joinTable' annotation. However the join table was already defined by an annotation in its underlying entity class and I wanted it to use this association entity class's own field definitions so as to extend the join table with additional custom fields.
The explanation and solution is that identified by FMaz008 above. In my situation, it was thanks to this post in the forum 'Doctrine Annotation Question'. This post draws attention to the Doctrine documentation regarding ManyToMany Uni-directional relationships. Look at the note regarding the approach of using an 'association entity class' thus replacing the many-to-many annotation mapping directly between two main entity classes with a one-to-many annotation in the main entity classes and two 'many-to-one' annotations in the associative entity class. There is an example provided in this forum post Association models with extra fields:
public class Person {
/** @OneToMany(targetEntity="AssignedItems", mappedBy="person") */
private $assignedItems;
}
public class Items {
/** @OneToMany(targetEntity="AssignedItems", mappedBy="item") */
private $assignedPeople;
}
public class AssignedItems {
/** @ManyToOne(targetEntity="Person")
* @JoinColumn(name="person_id", referencedColumnName="id")
*/
private $person;
/** @ManyToOne(targetEntity="Item")
* @JoinColumn(name="item_id", referencedColumnName="id")
*/
private $item;
}
Unidirectional. Just add the inversedBy:(Foreign Column Name) to make it Bidirectional.
# config/yaml/ProductStore.dcm.yml
ProductStore:
type: entity
id:
product:
associationKey: true
store:
associationKey: true
fields:
status:
type: integer(1)
createdAt:
type: datetime
updatedAt:
type: datetime
manyToOne:
product:
targetEntity: Product
joinColumn:
name: product_id
referencedColumnName: id
store:
targetEntity: Store
joinColumn:
name: store_id
referencedColumnName: id
I hope it helps. See you.
The solution is in the documentation of Doctrine. In the FAQ you can see this :
http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table
And the tutorial is here :
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
So you do not anymore do a manyToMany
but you have to create an extra Entity and put manyToOne
to your two entities.
ADD for @f00bar comment :
it's simple, you have just to to do something like this :
Article 1--N ArticleTag N--1 Tag
So you create an entity ArticleTag
ArticleTag:
type: entity
id:
id:
type: integer
generator:
strategy: AUTO
manyToOne:
article:
targetEntity: Article
inversedBy: articleTags
fields:
# your extra fields here
manyToOne:
tag:
targetEntity: Tag
inversedBy: articleTags
I hope it helps
What you are referring to is metadata, data about data. I had this same issue for the project I am currently working on and had to spend some time trying to figure it out. It's too much information to post here, but below are two links you may find useful. They do reference the Symfony framework, but are based on the Doctrine ORM.
http://melikedev.com/2010/04/06/symfony-saving-metadata-during-form-save-sort-ids/
http://melikedev.com/2009/12/09/symfony-w-doctrine-saving-many-to-many-mm-relationships/
Good luck, and nice Metallica references!
From $album->getTrackList() you will alwas get "AlbumTrackReference" entities back, so what about adding methods from the Track and proxy?
class AlbumTrackReference
{
public function getTitle()
{
return $this->getTrack()->getTitle();
}
public function getDuration()
{
return $this->getTrack()->getDuration();
}
}
This way your loop simplifies considerably, aswell as all other code related to looping the tracks of an album, since all methods are just proxied inside AlbumTrakcReference:
foreach ($album->getTracklist() as $track) {
echo sprintf("\t#%d - %-20s (%s) %s\n",
$track->getPosition(),
$track->getTitle(),
$track->getDuration()->format('H:i:s'),
$track->isPromoted() ? ' - PROMOTED!' : ''
);
}
Btw You should rename the AlbumTrackReference (for example "AlbumTrack"). It is clearly not only a reference, but contains additional logic. Since there are probably also Tracks that are not connected to an album but just available through a promo-cd or something this allows for a cleaner separation also.
I think I would go with @beberlei's suggestion of using proxy methods. What you can do to make this process simpler is to define two interfaces:
interface AlbumInterface {
public function getAlbumTitle();
public function getTracklist();
}
interface TrackInterface {
public function getTrackTitle();
public function getTrackDuration();
}
Then, both your Album
and your Track
can implement them, while the AlbumTrackReference
can still implement both, as following:
class Album implements AlbumInterface {
// implementation
}
class Track implements TrackInterface {
// implementation
}
/** @Entity whatever */
class AlbumTrackReference implements AlbumInterface, TrackInterface
{
public function getTrackTitle()
{
return $this->track->getTrackTitle();
}
public function getTrackDuration()
{
return $this->track->getTrackDuration();
}
public function getAlbumTitle()
{
return $this->album->getAlbumTitle();
}
public function getTrackList()
{
return $this->album->getTrackList();
}
}
This way, by removing your logic that is directly referencing a Track
or an Album
, and just replacing it so that it uses a TrackInterface
or AlbumInterface
, you get to use your AlbumTrackReference
in any possible case. What you will need is to differentiate the methods between the interfaces a bit.
This won't differentiate the DQL nor the Repository logic, but your services will just ignore the fact that you're passing an Album
or an AlbumTrackReference
, or a Track
or an AlbumTrackReference
because you've hidden everything behind an interface :)
Hope this helps!