Мне интересно, какой самый лучший, самый чистый и самый простой способ работать с отношениями «многие ко многим» в Doctrine2.
Давайте предположим, что мы у нас есть альбом типа Master of Puppets Metallica с несколькими треками. Но, пожалуйста, обратите внимание на тот факт, что один трек может появляться в более чем одном альбоме, как это делает Battery Metallica - три альбома содержат этот трек.
Так что мне нужно много-много много связей между альбомами и треками, используя третью таблицу с некоторыми дополнительными столбцами (например, положение трека в указанном альбоме). На самом деле я должен использовать, как предполагает документация Doctrine, двойное отношение «один ко многим» для достижения этой функциональности.
/** @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;
}
}
Пример данных:
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 |
+----+----------+----------+----------+------------+
Теперь я могу отобразить список альбомов и связанных с ними треков:
$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!' : ''
);
}
}
Результаты - это то, чего я ожидаю, а именно: список альбомов с их треками в соответствующем порядке, а продвигаемые помечаются как продвинутые.
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)
Этот код демонстрирует, что не так:
foreach ($album->getTracklist() as $track) {
echo $track->getTrack()->getTitle();
}
Album :: getTracklist ()
возвращает массив объектов AlbumTrackReference
вместо объектов Track
. Я не могу создать прокси-методы, потому что если оба метода Album
и Track
будут иметь метод getTitle ()
? Я мог бы выполнить дополнительную обработку в методе Album :: getTracklist ()
, но как проще всего это сделать? Я вынужден написать что-то подобное?
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
@beberlei предложил использовать прокси-методы:
class AlbumTrackReference {
public function getTitle() {
return $this->getTrack()->getTitle()
}
}
Это было бы неплохо, но я использую этот «ссылочный объект» с обеих сторон: $ album-> getTracklist () [12] -> getTitle ()
и $ track-> getAlbums () [1] -> getTitle ()
, поэтому метод getTitle ()
должен возвращать разные данные в зависимости от контекста вызова.
Мне нужно сделать что-то вроде:
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();
}
И это не очень чистый способ.
Из $album->getTrackList() вы всегда получите объекты "AlbumTrackReference", так что насчет добавления методов из Track и прокси?
class AlbumTrackReference
{
public function getTitle()
{
return $this->getTrack()->getTitle();
}
public function getDuration()
{
return $this->getTrack()->getDuration();
}
}
Так ваш цикл значительно упрощается, как и весь остальной код, связанный с циклом. треки альбома, так как все методы просто проксируются внутри AlbumTrakcReference:
foreach ($album->getTracklist() as $track) {
echo sprintf("\t#%d - %-20s (%s) %s\n",
$track->getPosition(),
$track->getTitle(),
$track->getDuration()->format('H:i:s'),
$track->isPromoted() ? ' - PROMOTED!' : ''
);
}
Btw Вы должны переименовать AlbumTrackReference (например, "AlbumTrack"). Он явно не только отсылка, но и содержит дополнительную логику. Так как, вероятно, также есть треки, которые не связаны с альбомом, а просто доступны через промо-диск или что-то еще, это также позволяет более четкое разделение.
Вы просите "лучший способ", но лучшего пути не существует. Способов много, и некоторые из них вы уже открыли. То, как вы хотите управлять и/или инкапсулировать управление ассоциациями при использовании классов ассоциации, полностью зависит от вас и вашего конкретного домена, и, боюсь, никто не может показать вам «лучший способ».
Кроме того, вопрос можно было бы значительно упростить, исключив из уравнения Doctrine и реляционные базы данных. Суть вашего вопроса сводится к вопросу о том, как поступать с классами ассоциаций в простом ООП.