Ich frage mich, was die beste, sauberste und einfachste Art ist, mit vielen-zu-vielen-Beziehungen in Doctrine2 zu arbeiten.
Nehmen wir an, wir haben ein Album wie Master of Puppets von Metallica mit mehreren Tracks. Bitte beachten Sie jedoch, dass ein Titel möglicherweise in mehr als einem Album enthalten ist, wie dies bei Battery by Metallica der Fall ist - drei Alben enthalten diesen Titel.
Was ich also brauche, ist eine Viele-zu-Viele-Beziehung zwischen Alben und Titeln, wobei eine dritte Tabelle mit einigen zusätzlichen Spalten verwendet wird (z. B. die Position des Titels im angegebenen Album). Tatsächlich muss ich, wie aus der Dokumentation von Doctrine hervorgeht, eine doppelte Eins-zu-Viele-Beziehung verwenden, um diese Funktionalität zu erreichen.
/** @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;
}
}
Beispieldaten:
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 |
+----+----------+----------+----------+------------+
Jetzt kann ich eine Liste der damit verbundenen Alben und Titel anzeigen:
$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!' : ''
);
}
}
Die Ergebnisse sind das, was ich erwarte, dh: eine Liste von Alben mit ihren Titeln in der richtigen Reihenfolge und beworbenen, die als beworben markiert sind.
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)
Also, was ist falsch?
Dieser Code zeigt, was falsch ist:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
Gibt ein Array von AlbumTrackReference
Objekten anstelle von Track
Objekten zurück. Ich kann keine Proxy-Methoden erstellen, was wäre, wenn beide, Album
und Track
hätte getTitle()
Methode? Ich könnte eine zusätzliche Verarbeitung innerhalb der Album::getTracklist()
Methode durchführen, aber wie geht das am einfachsten? Bin ich gezwungen, so etwas zu schreiben?
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
BEARBEITEN
@beberlei schlug vor, Proxy-Methoden zu verwenden:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Das wäre eine gute Idee, aber ich verwende dieses "Referenzobjekt" von beiden Seiten: $album->getTracklist()[12]->getTitle()
und $track->getAlbums()[1]->getTitle()
, daher getTitle()
sollte die Methode je nach Aufrufkontext unterschiedliche Daten zurückgeben.
Ich müsste so etwas tun wie:
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();
}
Und das ist kein sehr sauberer Weg.
$album->getTracklist()[12]
ist ein AlbumTrackRef
Objekt, gibt also $album->getTracklist()[12]->getTitle()
immer den Titel des Tracks zurück (wenn Sie die Proxy-Methode verwenden). Während $track->getAlbums()[1]
es sich um ein Album
Objekt handelt, $track->getAlbums()[1]->getTitle()
wird immer der Titel des Albums zurückgegeben.
AlbumTrackReference
zwei Proxy-Methoden getTrackTitle()
und getAlbumTitle
.