Mi chiedo quale sia il modo migliore, più pulito e più semplice per lavorare con le relazioni molti-a-molti in Doctrine2.
Supponiamo che abbiamo un album come Master of Puppets dei Metallica con diverse tracce. Ma tieni presente che una traccia potrebbe apparire in più di un album, come fa Battery dei Metallica : tre album presentano questa traccia.
Quindi quello di cui ho bisogno è una relazione molti-a-molti tra album e tracce, usando la terza tabella con alcune colonne aggiuntive (come la posizione della traccia nell'album specificato). In realtà devo usare, come suggerisce la documentazione di Doctrine, una doppia relazione uno-a-molti per raggiungere quella funzionalità.
/** @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;
}
}
Dati di esempio:
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 |
+----+----------+----------+----------+------------+
Ora posso visualizzare un elenco di album e tracce ad essi associati:
$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!' : ''
);
}
}
I risultati sono quello che mi aspetto, ovvero: un elenco di album con le loro tracce nell'ordine appropriato e quelli promossi che vengono contrassegnati come promossi.
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)
Quindi cosa c'è che non va?
Questo codice dimostra cosa c'è che non va:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
restituisce una matrice di AlbumTrackReference
oggetti anziché Track
oggetti. Non riesco a creare metodi proxy perché cosa succede se entrambi, Album
e Track
avrebbe getTitle()
metodo? Potrei fare qualche elaborazione extra all'interno del Album::getTracklist()
metodo, ma qual è il modo più semplice per farlo? Sono costretto a scrivere qualcosa del genere?
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
MODIFICARE
@beberlei ha suggerito di utilizzare i metodi proxy:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Sarebbe una buona idea ma sto usando quell'oggetto di riferimento da entrambi i lati: $album->getTracklist()[12]->getTitle()
e $track->getAlbums()[1]->getTitle()
quindi il getTitle()
metodo dovrebbe restituire dati diversi in base al contesto dell'invocazione.
Dovrei fare qualcosa del tipo:
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();
}
E questo non è un modo molto pulito.
$album->getTracklist()[12]
is AlbumTrackRef
object, quindi $album->getTracklist()[12]->getTitle()
restituirà sempre il titolo della traccia (se si utilizza il metodo proxy). While $track->getAlbums()[1]
is Album
object, quindi $track->getAlbums()[1]->getTitle()
restituirà sempre il titolo dell'album.
AlbumTrackReference
due metodi proxy getTrackTitle()
e getAlbumTitle
.