In many-to-many associations, we create an additional entity just to hold relationships in it. As a result we have two one-to-many relationships pointing at this additional entity from parent entities. Our example is based on Student and Subject entities.


Routing


# sport/app/config/routing.yml
football_frontend:
resource: "@FootballFrontendBundle/Controller"
prefix: /
type: annotation

Entities


Student


# src/Football/FrontendBundle/Entity/Student.php
namespace Football\FrontendBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
* @ORM\Entity(repositoryClass="Football\FrontendBundle\Repository\StudentRepository")
* @ORM\Table(name="student")
* @UniqueEntity(fields="student_id", message="StudentID is already taken.")
*/
class Student
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(type="smallint")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;

/**
* @var string
*
* @ORM\Column(name="student_id", type="string", length=15, unique=true)
*/
protected $studentId;

/**
* @var object
*
* @ORM\OneToMany(
* targetEntity="StudentSubject",
* mappedBy="studentMap",
* cascade={"persist", "remove"}
* )
*/
protected $studentInverse;

/**
* Constructor
*/
public function __construct()
{
$this->studentInverse = new ArrayCollection();
}

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

/**
* Set studentId
*
* @param string $studentId
* @return Student
*/
public function setStudentId($studentId)
{
$this->studentId = $studentId;

return $this;
}

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

/**
* Add studentInverse
*
* @param StudentSubject $studentInverse
* @return Student
*/
public function addStudentInverse(StudentSubject $studentInverse)
{
$this->studentInverse[] = $studentInverse;

return $this;
}

/**
* Remove studentInverse
*
* @param StudentSubject $studentInverse
*/
public function removeStudentInverse(StudentSubject $studentInverse)
{
$this->studentInverse->removeElement($studentInverse);
}

/**
* Get studentInverse
*
* @return Collection
*/
public function getStudentInverse()
{
return $this->studentInverse;
}
}

Subject


# src/Football/FrontendBundle/Entity/Subject.php
namespace Football\FrontendBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
* @ORM\Entity(repositoryClass="Football\FrontendBundle\Repository\SubjectRepository")
* @ORM\Table(name="subject")
* @UniqueEntity(fields="subject_code", message="Subject code is already taken.")
*/
class Subject
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(type="smallint")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;

/**
* @var string
*
* @ORM\Column(name="code", type="string", length=5, unique=true)
*/
protected $code;

/**
* @var object
*
* @ORM\OneToMany(
* targetEntity="StudentSubject",
* mappedBy="subjectMap",
* cascade={"persist", "remove"}
* )
*/
protected $subjectInverse;

/**
* Constructor
*/
public function __construct()
{
$this->subjectInverse = new ArrayCollection();
}

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

/**
* Set studentId
*
* @param string $code
* @return Subject
*/
public function setCode($code)
{
$this->code = $code;

return $this;
}

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

/**
* Add subjectInverse
*
* @param StudentSubject $subjectInverse
* @return Subject
*/
public function addSubjectInverse(StudentSubject $subjectInverse)
{
$this->subjectInverse[] = $subjectInverse;

return $this;
}

/**
* Remove subjectInverse
*
* @param StudentSubject $subjectInverse
*/
public function removeSubjectInverse(StudentSubject $subjectInverse)
{
$this->subjectInverse->removeElement($subjectInverse);
}

/**
* Get subjectInverse
*
* @return Collection
*/
public function getSubjectInverse()
{
return $this->subjectInverse;
}
}

StudentSubject


# src/Football/FrontendBundle/Entity/StudentSubject.php
namespace Football\FrontendBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
* @ORM\Entity(repositoryClass="Football\FrontendBundle\Repository\StudentSubjectRepository")
* @ORM\Table(name="student_subject", uniqueConstraints={@ORM\UniqueConstraint(columns={"student", "subject"})})
* @UniqueEntity(fields={"studentMap","subjectMap"}, message="Selected combination exists in database.")
*/
class StudentSubject
{
/**
* @var integer
*
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;

/**
* @var object
*
* @ORM\ManyToOne(targetEntity="Student", inversedBy="studentInverse")
* @ORM\JoinColumn(name="student", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
protected $studentMap;

/**
* @var object
*
* @ORM\ManyToOne(targetEntity="Subject", inversedBy="subjectInverse")
* @ORM\JoinColumn(name="subject", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
protected $subjectMap;

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

/**
* Set studentMap
*
* @param Student $studentMap
* @return StudentSubject
*/
public function setStudentMap(Student $studentMap)
{
$this->studentMap = $studentMap;

return $this;
}

/**
* Get studentMap
*
* @return Student
*/
public function getStudentMap()
{
return $this->studentMap;
}

/**
* Set subjectMap
*
* @param Subject $subjectMap
* @return StudentSubject
*/
public function setSubjectMap(Subject $subjectMap)
{
$this->subjectMap = $subjectMap;

return $this;
}

/**
* Get subjectMap
*
* @return Subject
*/
public function getSubjectMap()
{
return $this->subjectMap;
}
}

Repositories


Student


# sport/src/Football/FrontendBundle/Repository/StudentRepository.php
namespace Football\FrontendBundle\Repository;

use Doctrine\ORM\EntityRepository;

class StudentRepository extends EntityRepository
{
}

Subject


# sport/src/Football/FrontendBundle/Repository/SubjectRepository.php
namespace Football\FrontendBundle\Repository;

use Doctrine\ORM\EntityRepository;

class SubjectRepository extends EntityRepository
{
}

StudentSubject


# sport/src/Football/FrontendBundle/Repository/StudentSubjectRepository.php
namespace Football\FrontendBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;

class StudentSubjectRepository extends EntityRepository
{
public function findAll()
{
$fields = [
'st.id AS stId',
'st.studentId AS stStId',
'sb.id AS sbId',
'sb.code AS sbCode',
];

return
$this
->createQueryBuilder('ss')
->select($fields)
->join('ss.studentMap', 'st')
->join('ss.subjectMap', 'sb')
->addOrderBy('st.studentId', 'ASC')
->addOrderBy('sb.code', 'ASC')
->getQuery()
->getResult(Query::HYDRATE_SCALAR);
}
}

FormTypes


Student


This won't be used in example but just showing as an example.


# sport/src/Football/FrontendBundle/Form/Type/StudentType.php
namespace Football\FrontendBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

class StudentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add(
'studentId',
'text',
[
'error_bubbling' => true,
'constraints' => [
new NotBlank(
[
'message' => 'StudentID is required.'
]
),
new Length(
[
'max' => 15,
'maxMessage' => 'StudentID cannot be longer than {{ limit }} characters.'
]
)
]
]
);
}

public function getName()
{
return 'student';
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'Football\FrontendBundle\Entity\Student']
);
}
}

Subject


This won't be used in example but just showing as an example.


# sport/src/Football/FrontendBundle/Form/Type/SubjectType.php
namespace Football\FrontendBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

class SubjectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add(
'code',
'text',
[
'error_bubbling' => true,
'constraints' => [
new NotBlank(
[
'message' => 'Code is required.'
]
),
new Length(
[
'max' => 5,
'maxMessage' => 'Code cannot be longer than {{ limit }} characters.'
]
)
]
]
);
}

public function getName()
{
return 'subject';
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'Football\FrontendBundle\Entity\Subject']
);
}
}

StudentSubject


# sport/src/Football/FrontendBundle/Form/Type/StudentSubjectType.php
namespace Football\FrontendBundle\Form\Type;

use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints\NotBlank;

class StudentSubjectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add(
'studentMap',
'entity',
[
'error_bubbling' => true,
'class' => 'FootballFrontendBundle:Student',
'property' => 'studentId',
'multiple' => false,
'expanded' => false,
'empty_value' => '',
'query_builder' => function (EntityRepository $repo)
{
return $repo->createQueryBuilder('st')->orderBy('st.studentId', 'ASC');
},
'constraints' => [
new NotBlank(
[
'message' => 'Student is required.'
]
)
]
]
)
->add(
'subjectMap',
'entity',
[
'error_bubbling' => true,
'class' => 'FootballFrontendBundle:Subject',
'property' => 'code',
'multiple' => false,
'expanded' => false,
'empty_value' => '',
'query_builder' => function (EntityRepository $repo)
{
return $repo->createQueryBuilder('sb')->orderBy('sb.code', 'ASC');
},
'constraints' => [
new NotBlank(
[
'message' => 'Subject is required.'
]
)
]
]
);
}

public function getName()
{
return 'studentsubject';
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'Football\FrontendBundle\Entity\StudentSubject']
);
}
}

Controllers


Student


# sport/src/Football/FrontendBundle/Controller/StudentController.php
namespace Football\FrontendBundle\Controller;

use Football\FrontendBundle\Entity\Student;
use Football\FrontendBundle\Form\Type\StudentType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/student")
*/
class StudentController extends Controller
{
/**
* @Route("")
* @Method({"GET"})
*/
public function indexAction()
{
return $this->getTemplate('index');
}

/**
* @Route("/list")
* @Method({"GET"})
*/
public function listAction()
{
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:Student');

return $this->getTemplate(
'list',
[
'students' => $repo->findAll()
]
);
}

/**
* Creates form.
*
* @param Student $entity
* @param string $method
* @param string $action
*
* @return Form
*/
private function getForm(Student $entity, $method, $action)
{
return $this->createForm(
new StudentType(),
$entity,
[
'method' => $method,
'action' => $action
]
);
}

/**
* Creates template.
*
* @param string $template
* @param array $parameters
*
* @return Response
*/
private function getTemplate($template, array $parameters = [])
{
return $this->render(
sprintf('FootballFrontendBundle:StudentSubject\Student:%s.html.twig', $template),
$parameters
);
}
}

Subject


# sport/src/Football/FrontendBundle/Controller/SubjectController.php
namespace Football\FrontendBundle\Controller;

use Football\FrontendBundle\Entity\Subject;
use Football\FrontendBundle\Form\Type\SubjectType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/subject")
*/
class SubjectController extends Controller
{
/**
* @Route("")
* @Method({"GET"})
*/
public function indexAction()
{
return $this->getTemplate('index');
}

/**
* @Route("/list")
* @Method({"GET"})
*/
public function listAction()
{
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:Subject');

return $this->getTemplate(
'list',
[
'subjects' => $repo->findAll()
]
);
}

/**
* Creates form.
*
* @param Subject $entity
* @param string $method
* @param string $action
*
* @return Form
*/
private function getForm(Subject $entity, $method, $action)
{
return $this->createForm(
new SubjectType(),
$entity,
[
'method' => $method,
'action' => $action
]
);
}

/**
* Creates template.
*
* @param string $template
* @param array $parameters
*
* @return Response
*/
private function getTemplate($template, array $parameters = [])
{
return $this->render(
sprintf('FootballFrontendBundle:StudentSubject\Subject:%s.html.twig', $template),
$parameters
);
}
}

StudentSubject


# sport/src/Football/FrontendBundle/Controller/StudentSubjectController.php
namespace Football\FrontendBundle\Controller;

use Doctrine\DBAL\DBALException;
use Doctrine\ORM\ORMException;
use Exception;
use Football\FrontendBundle\Entity\StudentSubject;
use Football\FrontendBundle\Form\Type\StudentSubjectType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/studentsubject")
*/
class StudentSubjectController extends Controller
{
const ROUTER_PREFIX = 'football_frontend_studentsubject_';

/**
* @Route("")
* @Method({"GET"})
*/
public function indexAction()
{
return $this->getTemplate('index');
}

/**
* @Route("/list")
* @Method({"GET"})
*/
public function listAction()
{
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:StudentSubject');

return $this->getTemplate(
'list',
[
'studentsubjects' => $repo->findAll()
]
);
}

/**
* @Route("/create")
* @Method({"GET"})
*/
public function createAction()
{
$form = $this->getForm(
new StudentSubject(),
'POST',
$this->generateUrl(self::ROUTER_PREFIX . 'create')
);

return $this->getTemplate(
'create',
[
'form' => $form->createView()
]
);
}

/**
* @param Request $request
*
* @Route("/create")
* @Method({"POST"})
*
* @return RedirectResponse|Response
* @throws Exception
*/
public function createProcessAction(Request $request)
{
if ($request->getMethod() != 'POST') {
throw new Exception('StudentSubject create: only POST method is allowed.');
}

$form = $this->getForm(
new StudentSubject(),
'POST',
$this->generateUrl(self::ROUTER_PREFIX . 'create')
);
$form->handleRequest($request);

if (!$form->isSubmitted()) {
throw new Exception('StudentSubject create: form is not submitted.');
}

if ($form->isValid() !== true) {
return $this->getTemplate(
'create',
[
'form' => $form->createView()
]
);
}

try {
$data = $form->getData();

$league = new StudentSubject();
$league->setStudentMap($data->getStudentMap());
$league->setSubjectMap($data->getSubjectMap());

$em = $this->getDoctrine()->getManager();
$em->persist($league);
$em->flush();
} catch (DBALException $e) {
$message = sprintf('DBALException [%s]: %s', $e->getCode(), $e->getMessage());
} catch (ORMException $e) {
$message = sprintf('ORMException [%s]: %s', $e->getCode(), $e->getMessage());
} catch (Exception $e) {
$message = sprintf('Exception [%s]: %s', $e->getCode(), $e->getMessage());
}

if (isset($message)) {
throw new Exception($message);
}

return $this->redirect($this->generateUrl(self::ROUTER_PREFIX . 'list'));
}

/**
* Creates form.
*
* @param StudentSubject $entity
* @param string $method
* @param string $action
*
* @return Form
*/
private function getForm(StudentSubject $entity, $method, $action)
{
return $this->createForm(
new StudentSubjectType(),
$entity,
[
'method' => $method,
'action' => $action
]
);
}

/**
* Creates template.
*
* @param string $template
* @param array $parameters
*
* @return Response
*/
private function getTemplate($template, array $parameters = [])
{
return $this->render(
sprintf('FootballFrontendBundle:StudentSubject:%s.html.twig', $template),
$parameters
);
}
}

Twig templates


Student


# sport/src/Football/FrontendBundle/Resources/views/StudentSubject/Student/list.html.twig
{% extends 'FootballFrontendBundle:StudentSubject/Student:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
STUDENT - List
<hr />
{% if students is defined and students|length != 0 %}
<table border="1px">
<tr>
<td>#</td>
<td>ID</td>
<td>STUDENT ID</td>
</tr>
{% for student in students %}
<tr>
<td class="head">{{ loop.index }}</td>
<td>{{ student.id }}</td>
<td>{{ student.studentId }}</td>
</tr>
{% endfor %}
</table>
{% else %}
No student found in database!
{% endif %}
{% endspaceless %}
{% endblock %}

Subject


# sport/src/Football/FrontendBundle/Resources/views/StudentSubject/Subject/list.html.twig
{% extends 'FootballFrontendBundle:StudentSubject/Subject:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
SUBJECT - List
<hr />
{% if subjects is defined and subjects|length != 0 %}
<table border="1px">
<tr>
<td>#</td>
<td>ID</td>
<td>CODE</td>
</tr>
{% for subject in subjects %}
<tr>
<td class="head">{{ loop.index }}</td>
<td>{{ subject.id }}</td>
<td>{{ subject.code }}</td>
</tr>
{% endfor %}
</table>
{% else %}
No subject found in database!
{% endif %}
{% endspaceless %}
{% endblock %}

StudentSubject


# sport/src/Football/FrontendBundle/Resources/views/StudentSubject/list.html.twig
{% extends 'FootballFrontendBundle:StudentSubject:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
STUDENT SUBJECT - List
<hr />
{% if studentsubjects is defined and studentsubjects|length != 0 %}
<table border="1px">
<tr>
<td>#</td>
<td>STUDENT ID</td>
<td>STUDENT STUDENT_ID</td>
<td>SUBJECT ID</td>
<td>SUBJECT CODE</td>
</tr>
{% for studentsubject in studentsubjects %}
<tr>
<td class="head">{{ loop.index }}</td>
<td>{{ studentsubject.stId }}</td>
<td>{{ studentsubject.stStId }}</td>
<td>{{ studentsubject.sbId }}</td>
<td>{{ studentsubject.sbCode }}</td>
</tr>
{% endfor %}
</table>
{% else %}
No student_subject found in database!
{% endif %}
{% endspaceless %}
{% endblock %}

# sport/src/Football/FrontendBundle/Resources/views/StudentSubject/create.html.twig
{% extends 'FootballFrontendBundle:StudentSubject:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
STUDENT COUNTRY - Create
<hr />
{{ form_start(form, { attr: {novalidate: 'novalidate'} }) }}
{% if form_errors(form) != '' %}

{{ form_errors(form) }}
{% endif %}

<p>STUDENT: {{ form_widget(form.studentMap) }}</p>
<p>SUBJECT: {{ form_widget(form.subjectMap) }}</p>

<p><button name="button">Submit</button></p>
{{ form_end(form) }}
{% endspaceless %}
{% endblock %}