26/05/2014 - DOCTRINE, SYMFONY, TWIG
Example below is based on one-to-many relationship (one Country to many League), uses webforms and all the process are handled in controller although it is not a "good practise". It just gives you a basic idea. The better way is to move logic into custom services. "Thin controller, fat service" is considered as "good practice" when developing applications.
$form->isSubmitted()
and $form->isValid()
checks won't work as expectedly for PUT and PATCH methods because they are not directly be bound handleRequest()
function. To avoid this problem, set methods with setMethod($options['method'])
in form type class.createQueryBuilder()
in repository classes if the targeted entity has associations with others because, depending on what you’re doing doctrine lazy load might kick in and run additional queries for associations later on so it is better to select all associations with leftJoin()
up front.namespace Football\FrontendBundle\Entity;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="Football\FrontendBundle\Repository\CountryRepository")
* @ORM\Table(name="country")
* @UniqueEntity(fields="code", message="Code is already taken.")
* @UniqueEntity(fields="name", message="Name is already taken.")
* @ORM\HasLifecycleCallbacks
*/
class Country
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(type="smallint")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=100, unique=true)
* @Assert\NotBlank(message="Name is required.")
* @Assert\Length(
* max=100,
* exactMessage="Name cannot be longer than {{ limit }} characters."
* )
*/
protected $name;
/**
* @var object
*
* @ORM\OneToMany(
* targetEntity="League",
* mappedBy="country",
* cascade={"persist", "remove"}
* )
*/
protected $league;
/**
* Constructor
*/
public function __construct()
{
$this->league = new ArrayCollection();
}
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* @param string $name
* @return Country
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Add league
*
* @param League $league
* @return Country
*/
public function addLeague(League $league)
{
$this->league[] = $league;
return $this;
}
/**
* Remove league
*
* @param League $league
*/
public function removeLeague(League $league)
{
$this->league->removeElement($league);
}
/**
* Get league
*
* @return Collection
*/
public function getLeague()
{
return $this->league;
}
}
namespace Football\FrontendBundle\Entity;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="Football\FrontendBundle\Repository\LeagueRepository")
* @ORM\Table(
* name="league",
* uniqueConstraints={@ORM\UniqueConstraint(columns={"name", "country_id"})}
* )
* @UniqueEntity(
* fields={"name","country"},
* message="League for given country already exists in database."
* )
* @ORM\HasLifecycleCallbacks
*/
class League
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(type="smallint")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=100)
* @Assert\NotBlank(message="Name is required.")
* @Assert\Length(
* max=100,
* exactMessage="Name cannot be longer than {{ limit }} characters."
* )
*/
protected $name;
/**
* @var datetime
*
* @ORM\Column(name="created_at", type="datetime", nullable=false)
*/
protected $createdAt;
/**
* @var datetime
*
* @ORM\Column(name="updated_at", type="datetime", nullable=true)
*/
protected $updatedAt;
/**
* @var object
*
* @Assert\NotBlank(message="Country is required.")
* @ORM\ManyToOne(
* targetEntity="Country",
* inversedBy="league"
* )
* @ORM\JoinColumn(
* name="country_id",
* referencedColumnName="id",
* onDelete="CASCADE",
* nullable=false
* )
*/
protected $country;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* @param string $name
* @return Country
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @ORM\PrePersist
*/
public function onPrePersist()
{
$this->createdAt = new DateTime('now');
}
/**
* @ORM\PreUpdate
*/
public function onPreUpdate()
{
$this->updatedAt = new DateTime('now');
}
/**
* Get createdAt
*
* @return DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Get updatedAt
*
* @return DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* Set country
*
* @param Country $country
* @return League
*/
public function setCountry(Country $country)
{
$this->country = $country;
return $this;
}
/**
* Get country
*
* @return Country
*/
public function getCountry()
{
return $this->country;
}
}
Country field is represented as an embedded entity.
namespace Football\FrontendBundle\Form\Type;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LeagueType extends AbstractType
{
private $country;
public function __construct()
{
$this->country = [
'error_bubbling' => true,
'class' => 'FootballFrontendBundle:Country',
'property' => 'name',
'multiple' => false,
'expanded' => false,
'required' => false,
'empty_value' => '',
'query_builder' => function (EntityRepository $repo)
{
return $repo->createQueryBuilder('c')->orderBy('c.name', 'ASC');
}
];
}
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
// Update page should not have empty option for country because it is FK to country entity.
if ($options['method'] == 'PATCH') {
$this->country['empty_value'] = false;
}
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add('name', 'text', ['required' => false, 'error_bubbling' => true])
->add('country', 'entity', $this->country);
}
public function getName()
{
return 'league';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'Football\FrontendBundle\Entity\League']
);
}
}
Read facts section above to find out why we're using a repository class although we could simply use built-in find
methods.
namespace Football\FrontendBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
class LeagueRepository extends EntityRepository
{
/**
* Built-in findAll() would run additional queries for Country because
* we are interacting with Country info as parent.
*/
public function findAll()
{
return
$this
->createQueryBuilder('l')
->select('l.id, l.name, l.createdAt, l.updatedAt, c.name as country')
->leftJoin('l.country', 'c')
->getQuery()
->getResult();
}
/**
* Built-in findAll() would run additional queries for Country because
* we are interacting with Country info as parent.
*/
public function findOneById($id, $hydrator = Query::HYDRATE_OBJECT)
{
return
$this
->createQueryBuilder('l')
->select('l.id, l.name, l.createdAt, l.updatedAt, c.name as country')
->leftJoin('l.country', 'c')
->where('l.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult($hydrator);
}
/**
* Removing an entity must return whole objects graph hence reason we must
* select l and c.
*/
public function findOneByIdAsObject($id)
{
return
$this
->createQueryBuilder('l')
->select('l, c')
->leftJoin('l.country', 'c')
->where('l.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
}
namespace Football\FrontendBundle\Controller;
use Doctrine\DBAL\DBALException;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query;
use Exception;
use Football\FrontendBundle\Entity\League;
use Football\FrontendBundle\Exception\LeagueException;
use Football\FrontendBundle\Form\Type\LeagueType;
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("/league")
*/
class LeagueController extends Controller
{
const ROUTER_PREFIX = 'football_frontend_league_';
/**
* Landing page.
*
* @Route("")
* @Method({"GET"})
*
* @return Response
*/
public function listAction()
{
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:League');
return $this->getFormView('list', ['leagueArray' => $repo->findAll()]);
}
/**
* Create page.
*
* @Route("/create")
* @Method({"GET"})
*
* @return Response
*/
public function createAction()
{
$form = $this->getForm(
new League(),
'POST',
$this->generateUrl(self::ROUTER_PREFIX . 'create')
);
return $this->getFormView('create', ['form' => $form->createView()]);
}
/**
* Creates processing.
*
* @param Request $request
*
* @Route("/create")
* @Method({"POST"})
*
* @return RedirectResponse|Response
* @throws LeagueException
*/
public function createProcessAction(Request $request)
{
if ($request->getMethod() != 'POST') {
throw new LeagueException('League create: only POST method is allowed.');
}
$form = $this->getForm(
new League(),
'POST',
$this->generateUrl(self::ROUTER_PREFIX . 'create')
);
$form->handleRequest($request);
if (!$form->isSubmitted()) {
throw new LeagueException('League create: form is not submitted.');
}
if ($form->isValid() !== true) {
return $this->getFormView('create', ['form' => $form->createView()]);
}
try {
$data = $form->getData();
$league = new League();
$league->setName($data->getName());
$league->setCountry($data->getCountry());
$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 LeagueException($message);
}
return $this->redirect($this->generateUrl(self::ROUTER_PREFIX . 'list'));
}
/**
* Update page.
*
* @Route("/update/{id}", requirements={"id"="\d+"})
* @Method({"GET"})
*/
public function updateAction($id)
{
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:League');
$league = $repo->findOneByIdAsObject($id);
if (!$league instanceof League) {
throw new LeagueException(sprintf('League update: league [%s] cannot be found.', $id));
}
$form = $this->getForm(
$league,
'PATCH',
$this->generateUrl(self::ROUTER_PREFIX . 'updateprocess', ['id' => $id])
);
return $this->getFormView(
'update',
[
'form' => $form->createView(),
'id' => $league->getId()
]
);
}
/**
* Update processing.
*
* @param Request $request
* @param int $id
*
* @Route("/update/{id}", requirements={"id"="\d+"})
* @Method({"PATCH"})
*
* @return RedirectResponse|Response
* @throws LeagueException
*/
public function updateProcessAction(Request $request, $id)
{
if ($request->getMethod() != 'PATCH') {
throw new LeagueException('League update: only PATCH method is allowed.');
}
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:League');
$league = $repo->findOneByIdAsObject($id);
if (!$league instanceof League) {
throw new LeagueException(sprintf('League update: league [%s] cannot be found.', $id));
}
$form = $this->getForm(
$league,
'PATCH',
$this->generateUrl(self::ROUTER_PREFIX . 'updateprocess', ['id' => $id])
);
$form->handleRequest($request);
if (!$form->isSubmitted()) {
throw new LeagueException('League update: form is not submitted.');
}
if ($form->isValid() !== true) {
return $this->getFormView('update', ['form' => $form->createView()]);
}
try {
$data = $form->getData();
$league->setName($data->getName());
$league->setCountry($data->getCountry());
$em = $this->getDoctrine()->getManager();
$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 LeagueException($message);
}
return $this->redirect($this->generateUrl(self::ROUTER_PREFIX . 'list'));
}
/**
* Fetches country.
*
* @param int $id
*
* @Route("/{id}", requirements={"id"="\d+"})
* @Method({"GET"})
*
* @return RedirectResponse|Response
* @throws LeagueException
*/
public function readAction($id)
{
try {
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:League');
$league = $repo->findOneById($id, Query::HYDRATE_SCALAR);
if (!count($league)) {
throw new LeagueException(sprintf('League read: league [%s] cannot be found.', $id));
}
return $this->getFormView('read', ['league' => $league]);
} 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 LeagueException($message);
}
return $this->redirect($this->generateUrl(self::ROUTER_PREFIX . 'list'));
}
/**
* Deletes country.
*
* @param int $id
*
* @Route("/delete/{id}", requirements={"id"="\d+"})
* @Method({"GET"})
*
* @return RedirectResponse|Response
* @throws LeagueException
*/
public function deleteAction($id)
{
try {
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('FootballFrontendBundle:League');
$league = $repo->findOneByIdAsObject($id);
if (!$league instanceof League) {
throw new LeagueException(sprintf('League read: league [%s] cannot be found.', $id));
}
$em->remove($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 LeagueException($message);
}
return $this->redirect($this->generateUrl(self::ROUTER_PREFIX . 'list'));
}
/**
* Creates form object.
*
* @param League $league
* @param string $method
* @param string $action
*
* @return Form
*/
private function getForm(League $league, $method, $action)
{
return $this->createForm(
new LeagueType(),
$league,
[
'method' => $method,
'action' => $action
]
);
}
/**
* Creates webform.
*
* @param string $template
* @param array $parameters
*
* @return Response
*/
private function getFormView($template, array $parameters = [])
{
return $this->render(
sprintf('FootballFrontendBundle:League:%s.html.twig', $template),
$parameters
);
}
}
inanzzz-MBP:sport inanzzz$ php app/console router:debug
[router] Current routes
Name Method Scheme Host Path
_wdt ANY ANY ANY /_wdt/{token}
_profiler_home ANY ANY ANY /_profiler/
_profiler_search ANY ANY ANY /_profiler/search
_profiler_search_bar ANY ANY ANY /_profiler/search_bar
_profiler_purge ANY ANY ANY /_profiler/purge
_profiler_info ANY ANY ANY /_profiler/info/{about}
_profiler_phpinfo ANY ANY ANY /_profiler/phpinfo
_profiler_search_results ANY ANY ANY /_profiler/{token}/search/results
_profiler ANY ANY ANY /_profiler/{token}
_profiler_router ANY ANY ANY /_profiler/{token}/router
_profiler_exception ANY ANY ANY /_profiler/{token}/exception
_profiler_exception_css ANY ANY ANY /_profiler/{token}/exception.css
_configurator_home ANY ANY ANY /_configurator/
_configurator_step ANY ANY ANY /_configurator/step/{index}
_configurator_final ANY ANY ANY /_configurator/final
_twig_error_test ANY ANY ANY /_error/{code}.{_format}
football_frontend_default_index GET ANY ANY /
football_frontend_league_list GET ANY ANY /league
football_frontend_league_create GET ANY ANY /league/create
football_frontend_league_createprocess POST ANY ANY /league/create
football_frontend_league_update GET ANY ANY /league/update/{id}
football_frontend_league_updateprocess PATCH ANY ANY /league/update/{id}
football_frontend_league_read GET ANY ANY /league/{id}
football_frontend_league_delete GET ANY ANY /league/delete/{id}
{% extends 'FootballFrontendBundle:League:index.html.twig' %}
{% block body %}
{% spaceless %}
{{ parent() }}
LEAGUE - List
<hr />
{% if leagueArray is defined and leagueArray|length != 0 %}
<table border="1px">
<tr>
<td>#</td>
<td>ID</td>
<td>NAME</td>
<td>CREATED</td>
<td>UPDATED</td>
<td>COUNTRY</td>
<td> </td>
<td> </td>
<td> </td>
</tr>
{% for league in leagueArray %}
<tr>
<td class="head">{{ loop.index }}</td>
<td>{{ league.id }}</td>
<td>{{ league.name }}</td>
<td>{{ league.createdAt|date('d/m/Y H:i:s') }}</td>
<td>{{ league.updatedAt is not null ? league.updatedAt|date('d/m/Y H:i:s') : '' }}</td>
<td>{{ league.country }}</td>
<td><a href="{{ path('football_frontend_league_read', {'id':league.id}) }}">view</a></td>
<td><a href="{{ path('football_frontend_league_delete', {'id':league.id}) }}">delete</a></td>
<td><a href="{{ path('football_frontend_league_update', {'id':league.id}) }}">update</a></td>
</tr>
{% endfor %}
</table>
{% else %}
No league found in database!
{% endif %}
{% endspaceless %}
{% endblock %}
{% extends 'FootballFrontendBundle:League:index.html.twig' %}
{% block body %}
{% spaceless %}
{{ parent() }}
LEAGUE - Read
<hr />
<table border="1px">
<tr>
<td>ID</td>
<td>NAME</td>
<td>CREATED</td>
<td>UPDATED</td>
<td>* COUNTRY</td>
</tr>
<tr>
<td>{{ league.id }}</td>
<td>{{ league.name }}</td>
<td>{{ league.createdAt|date('d/m/Y H:i:s') }}</td>
<td>{{ league.updatedAt is not null ? league.updatedAt|date('d/m/Y H:i:s') : '' }}</td>
<td>{{ league.country }}</td>
</tr>
</table>
{% endspaceless %}
{% endblock %}
{% extends 'FootballFrontendBundle:League:index.html.twig' %}
{% block body %}
{% spaceless %}
{{ parent() }}
LEAGUE - Create
<hr />
{{ form_start(form) }}
{% if form_errors(form) != '' %}
{{ form_errors(form) }}
{% endif %}
<p>NAME: {{ form_widget(form.name) }}</p>
<p>COUNTRY: {{ form_widget(form.country) }}</p>
<p><button name="button">Submit</button></p>
{{ form_end(form) }}
{% endspaceless %}
{% endblock %}
{% extends 'FootballFrontendBundle:League:index.html.twig' %}
{% block body %}
{% spaceless %}
{{ parent() }}
LEAGUE - Update
<hr />
{{ form_start(form) }}
{% if form_errors(form) != '' %}
{{ form_errors(form) }}
{% endif %}
<p>NAME: {{ form_widget(form.name) }}</p>
<p>COUNTRY: {{ form_widget(form.country) }}</p>
<p><button name="button">Submit</button></p>
{{ form_end(form) }}
{% endspaceless %}
{% endblock %}