Je me demande quelle est la meilleure, la plus propre et la plus simple façon de travailler avec des relations plusieurs-à-plusieurs dans Doctrine2.
Supposons que nous ayons un album comme Master of Puppets de Metallica avec plusieurs pistes. Mais veuillez noter le fait qu'une piste peut apparaître dans plus d'un album, comme le fait Battery by Metallica - trois albums présentent cette piste.
Donc, ce dont j'ai besoin, c'est d'une relation plusieurs-à-plusieurs entre les albums et les pistes, en utilisant un troisième tableau 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 atteindre 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;
}
}
Exemples 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 |
+----+----------+----------+----------+------------+
Maintenant, je peux afficher une liste d'albums et de pistes qui leur sont associés:
$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 ce que j'attends, c'est-à-dire: une liste d'albums avec leurs morceaux dans l'ordre approprié et ceux 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 montre ce qui ne va pas:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
renvoie un tableau d' AlbumTrackReference
objets au lieu d' Track
objets. Je ne peux pas créer de méthodes proxy car si les deux, Album
et Track
aurait une getTitle()
méthode? Je pourrais faire un traitement supplémentaire dans la Album::getTracklist()
méthode, mais quelle est la façon 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
ÉDITER
@beberlei a suggéré d'utiliser des méthodes 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()
et $track->getAlbums()[1]->getTitle()
, donc, la getTitle()
méthode devrait renvoyer des données différentes en fonction du contexte de l'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.
$album->getTracklist()[12]
is AlbumTrackRef
object, donc $album->getTracklist()[12]->getTitle()
retournera toujours le titre de la piste (si vous utilisez la méthode proxy). Bien qu'il $track->getAlbums()[1]
soit Album
objet, il $track->getAlbums()[1]->getTitle()
renverra toujours le titre de l'album.
AlbumTrackReference
deux méthodes proxy, getTrackTitle()
et getAlbumTitle
.