At some point while reading the doctrine 2.x documentation about association mapping there is a mention about association classes at the bottom of the section about unidirectional many-to-many associations.

Why are many-to-many associations less common? Because frequently you want to associate additional attributes with an association, in which case you introduce an association class. Consequently, the direct many-to-many association disappears and is replaced by one-to-many/many-to-one associations between the 3 participating classes.

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#many-to-many-unidirectional

So while these associations are less common, no real example is given of an implementation of 2 entities and an association class.

In the next section I will try to give an example implementation of such a construct.

We will use the easy example of a recipe website that needs to associate users to recipes. So each user can be associated with multiple recipes and each recipe can be associated with multiple users. The catch is that the association between the user and the recipe must indicate if a user owns a recipe or if a user likes another user’s recipe.

Let’s start with the user Entity. I like using FOSUserBundle for my user implementations so that is what I will be using in this example.

User Entity

namespace NVC\UserBundle\Entity;

use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="User")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\OneToMany(targetEntity="UserRecipeAssociation", mappedBy="user")
     */
    protected $user_recipe_associations;

    public function __construct()
    {
        parent::__construct();

        $this->user_recipe_associations = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Add user_recipe_associations
     *
     * @param \NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations
     * @return User
     */
    public function addUserRecipeAssociation(\NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations)
    {
        $this->user_recipe_associations[] = $userRecipeAssociations;

        return $this;
    }

    /**
     * Remove user_recipe_associations
     *
     * @param \NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations
     */
    public function removeUserRecipeAssociation(\NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations)
    {
        $this->user_recipe_associations->removeElement($userRecipeAssociations);
    }

    /**
     * Get user_recipe_associations
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getUserRecipeAssociations()
    {
        return $this->user_recipe_associations;
    }
}

We have introduced a member with a target entity of the type UserRecipeAssociation. This type will be explained in the last section of this post

    /**
     * @ORM\OneToMany(targetEntity="UserRecipeAssociation", mappedBy="user")
     */
    protected $user_recipe_associations;

Now the Recipe entity. For the sake of clarity we keep it simple and implement just four fields: The unique id, a name, a description and an integer for the cooking time.

Recipe Entity

namespace NVC\RecipeBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Recipe
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="NVC\RecipeBundle\Entity\RecipeRepository")
 */
class Recipe
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /**
     * @var string
     *
     * @ORM\Column(name="description", type="text")
     */
    private $description;

    /**
     * @var integer
     *
     * @ORM\Column(name="cooking_time", type="integer")
     */
    private $cooking_time;

    /**
     * @ORM\OneToMany(targetEntity="NVC\UserBundle\Entity\UserRecipeAssociation", mappedBy="recipe")
     */
    protected $user_recipe_associations;

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return Recipe
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set description
     *
     * @param string $description
     * @return Recipe
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get description
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Set cooking_time
     *
     * @param integer $cookingTime
     * @return Recipe
     */
    public function setCookingTime($cookingTime)
    {
        $this->cooking_time = $cookingTime;

        return $this;
    }

    /**
     * Get cooking_time
     *
     * @return integer
     */
    public function getCookingTime()
    {
        return $this->cooking_time;
    }

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->user_recipe_associations = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Add user_recipe_associations
     *
     * @param \NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations
     * @return Recipe
     */
    public function addUserRecipeAssociation(\NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations)
    {
        $this->user_recipe_associations[] = $userRecipeAssociations;

        return $this;
    }

    /**
     * Remove user_recipe_associations
     *
     * @param \NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations
     */
    public function removeUserRecipeAssociation(\NVC\UserBundle\Entity\UserRecipeAssociation $userRecipeAssociations)
    {
        $this->user_recipe_associations->removeElement($userRecipeAssociations);
    }

    /**
     * Get user_recipe_associations
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getUserRecipeAssociations()
    {
        return $this->user_recipe_associations;
    }
}

Now what we want to do is introduce a third entity class that will hold the associations between the User entity and the Recipe entity.This entity will be called ‘UserRecipeAssociation’ and reside in the NVC\UserBundle\Entity namespace.

UserRecipeAssociation Entity

namespace NVC\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * UserRecipeAssociation
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="NVC\UserBundle\Entity\UserRecipeAssociationRepository")
 */
class UserRecipeAssociation
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     **/

    /**
     *
     * @ORM\ManyToOne(targetEntity="User", inversedBy="user_recipe_associations")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     *
     */
    private $user;

    /**
     *
     * @ORM\ManyToOne(targetEntity="NVC\RecipeBundle\Entity\Recipe", inversedBy="user_recipe_associations")
     * @ORM\JoinColumn(name="recipe_id", referencedColumnName="id")
     */
    private $recipe;

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set user
     *
     * @param \NVC\UserBundle\Entity\User $user
     * @return UserRecipeAssociation
     */
    public function setUser(\NVC\UserBundle\Entity\User $user = null)
    {
        $this->user = $user;

        return $this;
    }

    /**
     * Get user
     *
     * @return \NVC\UserBundle\Entity\User
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * Set recipe
     *
     * @param \NVC\RecipeBundle\Entity\Recipe $recipe
     * @return UserRecipeAssociation
     */
    public function setRecipe(\NVC\RecipeBundle\Entity\Recipe $recipe = null)
    {
        $this->recipe = $recipe;

        return $this;
    }

    /**
     * Get recipe
     *
     * @return \NVC\RecipeBundle\Entity\Recipe
     */
    public function getRecipe()
    {
        return $this->recipe;
    }
}

With these three code examples you should be able to produce your own association classes.

{ 9 comments }

Ajaxify your Symfony2 forms with jQuery

by Niki on January 30, 2013

Let’s start by creating a form type for a fictive blogging app

namespace Acme\BlogBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class PostType extends AbstractType
{
  public function buildForm( FormBuilderInterface $builder, 
                                            array $options )
  {
    $builder->add( 'title', 'text' );
    $builder->add( 'body',  'textarea' );
  }

  function getName() {
    return 'PostType';
  }
}

Now we need a controller that will use this form type to create a formview for use in our templates.

namespace Acme\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

use Acme\BlogBundle\Form\Type\PostType;

class PostController extends Controller
{
  /**
   * @Route( "/post/new", name="create_post" )
   * @Template()
   */
  public function createAction( Request $request )
  {
    $postform = $this->createForm( new PostType( ) );

    return array(
      'postform' => $postform->createView( );
    );
  }
}

Don’t forget to include the form type namespace in your controller!

Next we create a view to render our form in.

<form action="{{ path('create_post') }}" method="post" {{ form_enctype(form) }}>
    {{ form_widget(postform) }}
    <p>
        <button type="submit">Create</button>
    </p>
</form>

This wel generate something along the lines of following markup

<form action="/post/new" method="post">
  <div id="acme_blogbundle_posttype">
    <div>
      <label for="acme_blogbundle_posttype_title" class="required">
          Name
      </label>
      <input type="text" id="acme_blogbundle_posttype_title" name="acme_blogbundle_posttype[title]" required="required" maxlength="255">
    </div>
      <div>
      <label for="acme_blogbundle_posttype_body" class="required">
        Description
      </label>
      <textarea id="acme_blogbundle_posttype_body" name="acme_blogbundle_posttype[body]" required="required">
      </textarea>
    </div>
    <input type="hidden" id="acme_blogbundle_posttype__token" name="acme_blogbundle_posttype[_token]" value="82c82d6a2f52564f23c2437970f6192aa7102c08">
  </div>
  <p>
    <button type="submit">
        Create
    </button>
  </p>
</form>

Now the magic sits in the following 2 snippets of JavaScript code. First we create a function capable of reading out a form and submitting all the form data as a a regular form submit.

function postForm( $form, callback ){

  /*
   * Get all form values
   */
  var values = {};
  $.each( $form.serializeArray(), function(i, field) {
    values[field.name] = field.value;
  });

  /*
   * Throw the form values to the server!
   */
  $.ajax({
    type        : $form.attr( 'method' ),
    url         : $form.attr( 'action' ),
    data        : values,
    success     : function(data) {
      callback( data );
    }
  });

}

With that function in place the following code will bind the ajax submit to every form in the forms array. Notice I’m using twig variables in the script to get the name of the form.

$(document).ready(function(){

  var forms = [
    '[ name="{{ postform.vars.full_name }}"]'
  ];

  $( forms.join(',') ).submit( function( e ){
    e.preventDefault();

    postForm( $(this), function( response ){
    });

    return false;
  });

});

In the controller we can now handle the data as if it was a regular form submit

namespace Acme\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

use Acme\BlogBundle\Form\Type\PostType;

class PostController extends Controller
{
  /**
   * @Route( "/post/new", name="create_post" )
   * @Template()
   */
  public function createAction( Request $request )
  {
    $postform = $this->createForm( new PostType( ) );

    if ( $request->isMethod( 'POST' ) ) {

      $form->bind( $request );

      if ( $form->isValid( ) ) {

        /*
         * $data['title']
         * $data['body']
         */
        $data = $form->getData();

        $response['success'] = true;

      }else{

        $response['success'] = false;
        $response['cause'] = 'whatever';

      }

      return new JsonResponse( $response );
    }

    return array(
      'postform' => $postform->createView( );
    );
  }
}

Isn’t it nice how easy life can be ?

{ 3 comments }

How To: PHP Autoloading classes with multi platform namespace support

January 5, 2012

While doing some research about the autoloading features in PHP it soon became clear that the build in support aka a non parameterized call to spl_autoload_register(); had little to offer. What this call does is tell PHP that we want to use the build in spl_autoload function for autoloading. While this is nice in theory, […]

Read the full article →

How To: PHP SOAP client in WSDL mode over SSL (private key) with HTTP authentication

November 10, 2011

Whoa, what a title. Don’t be discouraged though, this post is here to make it (look) easy. The first thing to know is how to construct a SOAP client in PHP. Since PHP5 SOAP is baked in natively so this should be fairly easy right? Right. $client = new SoapClient($wsdl, $soapclient_options); The SoapClient constructor needs […]

Read the full article →

Why small businesses need pentests too

November 10, 2011

1. They ARE important A common misconception I have heard dozens of times from different kind of small business owners is that they think they’re not important enough. This phrase I have heard many times over and over again: “Why would anyone want to hack me? I don’t have any valuable information”. WRONG. Customer information, […]

Read the full article →

My .vimrc and vim plugins

November 10, 2011

My .vimrc . Very simple yet super effective ( especially for a web developer) set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cindent “Setting how the statusline must look set statusline=%F%m%r%h%w\ [FORMAT=%{&ff}]\ [TYPE=%Y]\ [ASCII=\%03.3b]\ [HEX=\%02.2B]\ [POS=%04l,%04v][%p%%]\ [LEN=%L] “Always show the statusline set laststatus=2 “Make working with tabs more usefull(this calls a script defined further in this file) set […]

Read the full article →

Why I use Vim

November 9, 2011

1. I can do everything with only my keyboard All editing, copy-paste, navigation, etc are done with the keyboard. No time wasted having to grab a mouse and target GUI elements. This not only saves me a lot of time, it also reduces what I call “context switching” . In computer science a context switch […]

Read the full article →