Tôi đang tự hỏi đâu là cách tốt nhất, sạch nhất và đơn giản nhất để làm việc với các mối quan hệ nhiều-nhiều trong Doctrine2.
Giả sử rằng chúng ta đã có một album như Master of Puppets của Metallica với nhiều bản nhạc. Nhưng xin lưu ý thực tế là một bản nhạc có thể xuất hiện trong nhiều album hơn, như Battery by Metallica - ba album được trình bày bản nhạc này.
Vì vậy, điều tôi cần là mối quan hệ nhiều-nhiều giữa album và bản nhạc, sử dụng bảng thứ ba với một số cột bổ sung (như vị trí của bản nhạc trong album được chỉ định). Trên thực tế tôi phải sử dụng, như tài liệu của Doctrine gợi ý, mối quan hệ một-nhiều-nhiều để đạt được chức năng đó.
/** @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;
}
}
Dữ liệu mẫu:
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 |
+----+----------+----------+----------+------------+
Bây giờ tôi có thể hiển thị danh sách các album và bản nhạc liên quan đến chúng:
$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!' : ''
);
}
}
Kết quả là những gì tôi mong đợi, tức là: một danh sách các album với các bài hát của họ theo thứ tự phù hợp và những album được quảng cáo sẽ được đánh dấu là quảng cáo.
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)
Vì vậy những gì là sai?
Mã này cho thấy những gì sai:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album::getTracklist()
trả về một mảng các AlbumTrackReference
đối tượng thay vì các Track
đối tượng. Tôi không thể tạo phương thức proxy gây ra điều gì nếu cả hai Album
và Track
sẽ có getTitle()
phương thức? Tôi có thể thực hiện một số xử lý bổ sung trong Album::getTracklist()
phương thức nhưng cách đơn giản nhất để làm điều đó là gì? Tôi có bị buộc phải viết một cái gì đó như thế không?
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
BIÊN TẬP
@beberlei đề xuất sử dụng các phương thức proxy:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Đó sẽ là một ý tưởng tốt nhưng tôi đang sử dụng "đối tượng tham chiếu" đó từ cả hai phía: $album->getTracklist()[12]->getTitle()
và $track->getAlbums()[1]->getTitle()
, vì vậy getTitle()
phương thức sẽ trả về dữ liệu khác nhau dựa trên bối cảnh của lệnh gọi.
Tôi sẽ phải làm một cái gì đó như:
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();
}
Và đó không phải là một cách rất sạch sẽ.
$album->getTracklist()[12]
là AlbumTrackRef
đối tượng, vì vậy $album->getTracklist()[12]->getTitle()
sẽ luôn trả về tiêu đề của bản nhạc (nếu bạn đang sử dụng phương thức proxy). Trong khi $track->getAlbums()[1]
là Album
đối tượng, vì vậy $track->getAlbums()[1]->getTitle()
sẽ luôn luôn trả lại tiêu đề của album.
AlbumTrackReference
hai phương thức proxy getTrackTitle()
và getAlbumTitle
.