Saya bertanya-tanya apa yang terbaik, paling bersih dan cara paling sederhana untuk bekerja dengan banyak-ke-banyak hubungan di Doctrine2.
Mari kita asumsikan bahwa kita punya album seperti Master of Puppets oleh Metallica dengan beberapa lagu. Tapi tolong perhatikan fakta bahwa satu lagu mungkin muncul di lebih dari satu album, seperti yang dilakukan Battery oleh Metallica - tiga album menampilkan lagu ini.
Jadi yang saya butuhkan adalah hubungan banyak-ke-banyak antara album dan trek, menggunakan tabel ketiga dengan beberapa kolom tambahan (seperti posisi trek di album tertentu). Sebenarnya saya harus menggunakan, seperti yang ditunjukkan oleh doktrin Doktrin, hubungan ganda satu-ke-banyak untuk mencapai fungsi itu.
/** @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;
}
}
Contoh data:
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 |
+----+----------+----------+----------+------------+
Sekarang saya dapat menampilkan daftar album dan trek yang terkait dengannya:
$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!' : ''
);
}
}
Hasilnya adalah apa yang saya harapkan, yaitu: daftar album dengan trek mereka dalam urutan yang sesuai dan yang dipromosikan ditandai sebagai dipromosikan.
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)
Jadi apa yang salah?
Kode ini menunjukkan apa yang salah:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
mengembalikan array AlbumTrackReference
objek, bukan Track
objek. Saya tidak dapat membuat metode proksi karena bagaimana jika keduanya, Album
dan Track
akan memiliki getTitle()
metode? Saya bisa melakukan beberapa pemrosesan tambahan dalam Album::getTracklist()
metode tetapi apa cara paling sederhana untuk melakukan itu? Apakah saya dipaksa menulis sesuatu seperti itu?
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
EDIT
@beberlei menyarankan untuk menggunakan metode proxy:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Itu akan menjadi ide yang bagus tapi saya menggunakan "objek referensi" dari kedua sisi: $album->getTracklist()[12]->getTitle()
dan $track->getAlbums()[1]->getTitle()
, jadi getTitle()
metode harus mengembalikan data yang berbeda berdasarkan konteks doa.
Saya harus melakukan sesuatu seperti:
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();
}
Dan itu bukan cara yang sangat bersih.
$album->getTracklist()[12]
is AlbumTrackRef
object, jadi $album->getTracklist()[12]->getTitle()
akan selalu mengembalikan judul trek (jika Anda menggunakan metode proxy). Sementara $track->getAlbums()[1]
ini Album
objek, sehingga $track->getAlbums()[1]->getTitle()
akan kembali selalu judul album.
AlbumTrackReference
dua metode proxy, getTrackTitle()
dan getAlbumTitle
.