Je me demande quel est le meilleur moyen, le plus propre et le plus simple de travailler avec des relations many-to-many dans Doctrine2.
Supposons que nous ayons un album comme Le maître des marionnettes par Metallica avec plusieurs titres. Mais veuillez noter qu'une piste peut apparaître dans plus d'un album, par exemple Batterie par Metallica fait - trois albums comportent ce titre.
J'ai donc besoin d'une relation many-to-many entre les albums et les pistes, en utilisant une troisième table avec quelques colonnes supplémentaires (comme la position de la piste dans l'album spécifié). En fait, je dois utiliser, comme le suggère la documentation de Doctrine, une double relation un-à-plusieurs pour réaliser cette fonctionnalité.
/** @Entity() */
class Album {
/** @Id @Column(type="integer") */
protected $id;
/** @Column() */
protected $title;
/** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="album") */
protected $tracklist;
public function __construct() {
$this->tracklist = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getTitle() {
return $this->title;
}
public function getTracklist() {
return $this->tracklist->toArray();
}
}
/** @Entity() */
class Track {
/** @Id @Column(type="integer") */
protected $id;
/** @Column() */
protected $title;
/** @Column(type="time") */
protected $duration;
/** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="track") */
protected $albumsFeaturingThisTrack; // btw: any idea how to name this relation? :)
public function getTitle() {
return $this->title;
}
public function getDuration() {
return $this->duration;
}
}
/** @Entity() */
class AlbumTrackReference {
/** @Id @Column(type="integer") */
protected $id;
/** @ManyToOne(targetEntity="Album", inversedBy="tracklist") */
protected $album;
/** @ManyToOne(targetEntity="Track", inversedBy="albumsFeaturingThisTrack") */
protected $track;
/** @Column(type="integer") */
protected $position;
/** @Column(type="boolean") */
protected $isPromoted;
public function getPosition() {
return $this->position;
}
public function isPromoted() {
return $this->isPromoted;
}
public function getAlbum() {
return $this->album;
}
public function getTrack() {
return $this->track;
}
}
Exemple de données :
Album
+----+--------------------------+
| id | title |
+----+--------------------------+
| 1 | Master of Puppets |
| 2 | The Metallica Collection |
+----+--------------------------+
Track
+----+----------------------+----------+
| id | title | duration |
+----+----------------------+----------+
| 1 | Battery | 00:05:13 |
| 2 | Nothing Else Matters | 00:06:29 |
| 3 | Damage Inc. | 00:05:33 |
+----+----------------------+----------+
AlbumTrackReference
+----+----------+----------+----------+------------+
| id | album_id | track_id | position | isPromoted |
+----+----------+----------+----------+------------+
| 1 | 1 | 2 | 2 | 1 |
| 2 | 1 | 3 | 1 | 0 |
| 3 | 1 | 1 | 3 | 0 |
| 4 | 2 | 2 | 1 | 0 |
+----+----------+----------+----------+------------+
Je peux maintenant afficher une liste des albums et des pistes qui leur sont associées :
$dql = '
SELECT a, tl, t
FROM Entity\Album a
JOIN a.tracklist tl
JOIN tl.track t
ORDER BY tl.position ASC
';
$albums = $em->createQuery($dql)->getResult();
foreach ($albums as $album) {
echo $album->getTitle() . PHP_EOL;
foreach ($album->getTracklist() as $track) {
echo sprintf("\t#%d - %-20s (%s) %s\n",
$track->getPosition(),
$track->getTrack()->getTitle(),
$track->getTrack()->getDuration()->format('H:i:s'),
$track->isPromoted() ? ' - PROMOTED!' : ''
);
}
}
Les résultats sont ceux que j'attendais, c'est-à-dire une liste d'albums avec leurs pistes dans l'ordre approprié et les albums promus étant marqués comme promus.
The Metallica Collection
#1 - Nothing Else Matters (00:06:29)
Master of Puppets
#1 - Damage Inc. (00:05:33)
#2 - Nothing Else Matters (00:06:29) - PROMOTED!
#3 - Battery (00:05:13)
Alors, qu'est-ce qui ne va pas ?
Ce code démontre ce qui ne va pas :
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
renvoie un tableau de AlbumTrackReference
au lieu de Track
objets. Je ne peux pas créer de méthodes de proxy car que faire si les deux, Album
y Track
aurait getTitle()
méthode ? Je pourrais faire un traitement supplémentaire dans Album::getTracklist()
mais quelle est la manière la plus simple de le faire ? Suis-je obligé d'écrire quelque chose comme ça ?
public function getTracklist() {
$tracklist = array();
foreach ($this->tracklist as $key => $trackReference) {
$tracklist[$key] = $trackReference->getTrack();
$tracklist[$key]->setPosition($trackReference->getPosition());
$tracklist[$key]->setPromoted($trackReference->isPromoted());
}
return $tracklist;
}
// And some extra getters/setters in Track class
EDITAR
@beberlei a suggéré d'utiliser des méthodes de proxy :
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Ce serait une bonne idée, mais j'utilise cet "objet de référence" des deux côtés : $album->getTracklist()[12]->getTitle()
y $track->getAlbums()[1]->getTitle()
donc getTitle()
doit renvoyer des données différentes en fonction du contexte d'invocation.
Je devrais faire quelque chose comme :
getTracklist() {
foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
}
// ....
getAlbums() {
foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
}
// ...
AlbumTrackRef::getTitle() {
return $this->{$this->context}->getTitle();
}
Et ce n'est pas une façon très propre.
2 votes
Comment gérez-vous la référence AlbumTrackReference ? Par exemple $album->addTrack() ou $album->removeTrack() ?
0 votes
Je n'ai pas compris votre commentaire sur le contexte. A mon avis, les données ne dépendent pas du contexte. A propos de
$album->getTracklist()[12]
estAlbumTrackRef
donc$album->getTracklist()[12]->getTitle()
retournera toujours le titre de la piste (si vous utilisez la méthode proxy). Alors que$track->getAlbums()[1]
esAlbum
donc$track->getAlbums()[1]->getTitle()
retournera toujours le titre de l'album.0 votes
Une autre idée consiste à utiliser sur
AlbumTrackReference
deux méthodes de procuration,getTrackTitle()
ygetAlbumTitle
.