Friday, April 13, 2012

Many-to-many (or not) with additional fields with Symfony2

Since I'm learning Symfony2 these days, I thought it would be interesting to see, how you would solve the "many-to-many with additional fields" problem. To be more clear of what I exactly mean, I'll give you an example:

We have albums, and songs. An album can have many songs, and a song can be found on multiple albums. So far that sounds like a normal many-to-many relationship. What if we want to store some additional information about the relation of album and song, let's say the position of the song on the album, maybe some text or whatever which is just meant to be displayed on that album-song relation.

To transfrom this into code, specifically Symfony2 code we will need the following three classes:
  • Album
  • Song
  • AlbumSong
AlbumSong is the relation between Album and Song. That's where we are going to store our additional fields. This is an overview of the actual ORM config files (*.orm.yml).

Your\ExampleBundle\Entity\Album:
    type: entity
    table: album
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        name:
            type: string
            length: 100
        description:
            type: text
            nullable: true
    oneToMany:
        songs:
            targetEntity: AlbumSong
            mappedBy: song
            cascade: [persist]

Your\ExampleBundle\Entity\Song:
    type: entity
    table: song
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        name:
            type: string
            length: 100
        description:
            type: text
            nullable: true
    oneToMany:
        albums:
            targetEntity: AlbumSong
            mappedBy: album
            cascade: [persist]

Your\ExampleBundle\Entity\AlbumSong:
    type: entity
    table: album_song
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        label:
            type: string
            length: 100
        whatever:
            type: boolean
    manyToOne:
        album:
            targetEntity: Album
            cascade: [persist]
            inversedBy: songs
            joinColumn:
                name: album_id
                referencedColumnName: id
        song:
            targetEntity: Song
            cascade: [persist]
            inversedBy: albums
            joinColumn:
                name: song_id
                referencedColumnName: id

I have read the doctrine documentation about bidirectional relations, but I don't yet understand what role the key "inversedBy" plays, so if anybody could enlight, or point me to an example that would be awesome.

Based on this, you can now generate your classes via app/console doctrine:generate:entities YourExampleBundle. Then you can fill up your database with fixtures like that for example:

    $album = new Album();
    $album->setDescription("Description of Party Rocking");
    $album->setName("SorryForPartyRocking");

    $song = new Song();
    $song->setName("Sexy and I Know It");

    $song2 = new Song();
    $song2->setName("Sorry for Party Rocking");

    $album_song = new AlbumSong();
    $album_song->setAlbum($album);
    $album_song->setSong($song);
    $album_song->setExtraInfo("Extra Info to Sexy and I Know it. Only available on the album SorryForPartyRocking");

    $album_song2 = new AlbumSong();
    $album_song2->setAlbum($album);
    $album_song2->setSong($song2);
    $album_song2->setExtraInfo("Extra Info to Sorry for Party Rocking. Only available on the album SorryForPartyRocking");

    $manager->persist($album_song);
    $manager->persist($album_song2);

    $manager->flush();

That's the way I think is correct, but maybe there are better ways to do that. If you can point me to a well documented, better solution, I would be very happy about that.

Otherwise I hope that helps a bit.