Example below 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.


Facts



Entity


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(type="string", length=2, unique=true)
* @Assert\NotBlank(message="Code is required.")
* @Assert\Regex(pattern="/^[A-Z]{2}$/",
* message="Code must be exactly 2 character long upper case letters."
* )
*/
protected $code;

/**
* @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 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;

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

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

return $this;
}

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

/**
* 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;
}
}

FormType


namespace Football\FrontendBundle\Form\Type;

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

class CountryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add('code', 'text', ['required' => false, 'error_bubbling' => true])
->add('name', 'text', ['required' => false, 'error_bubbling' => true]);
}

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

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

Controller


namespace Football\FrontendBundle\Controller;

use Doctrine\DBAL\DBALException;
use Doctrine\ORM\ORMException;
use Exception;
use Football\FrontendBundle\Entity\Country;
use Football\FrontendBundle\Exception\CountryException;
use Football\FrontendBundle\Form\Type\CountryType;
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("/country")
*/
class CountryController extends Controller
{
const ROUTER_PREFIX = 'football_frontend_country_';

/**
* Landing page.
*
* @Route("")
* @Method({"GET"})
*
* @return Response
*/
public function listAction()
{
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:Country');

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

/**
* Create page.
*
* @Route("/create")
* @Method({"GET"})
*
* @return Response
*/
public function createAction()
{
$form = $this->getForm(
new Country(),
'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 CountryException
*/
public function createProcessAction(Request $request)
{
if ($request->getMethod() != 'POST') {
throw new CountryException('Country create: only POST method is allowed.');
}

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

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

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

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

$country = new Country();
$country->setCode($data->getCode());
$country->setName($data->getName());

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

if (isset($message)) {
throw new CountryException($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:Country');
$country = $repo->findOneById($id);
if (!$country instanceof Country) {
throw new CountryException(sprintf('Country update: country [%i] cannot be found.', $id));
}

$form = $this->getForm(
$country,
'PATCH',
$this->generateUrl(self::ROUTER_PREFIX . 'updateprocess', ['id' => $id])
);

return $this->getFormView(
'update',
[
'form' => $form->createView(),
'id' => $country->getId()
]
);
}

/**
* Update processing.
*
* @param Request $request
* @param int $id
*
* @Route("/update/{id}", requirements={"id"="\d+"})
* @Method({"PATCH"})
*
* @return RedirectResponse|Response
* @throws CountryException
*/
public function updateProcessAction(Request $request, $id)
{
if ($request->getMethod() != 'PATCH') {
throw new CountryException('Country update: only PATCH method is allowed.');
}

$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:Country');
$country = $repo->findOneById($id);
if (!$country instanceof Country) {
throw new CountryException(sprintf('Country update: country [%i] cannot be found.', $id));
}

$form = $this->getForm(
$country,
'PATCH',
$this->generateUrl(self::ROUTER_PREFIX . 'updateprocess', ['id' => $id])
);
$form->handleRequest($request);

if (!$form->isSubmitted()) {
throw new CountryException('Country update: form is not submitted.');
}

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

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

$country->setCode($data->getCode());
$country->setName($data->getName());

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

if (isset($message)) {
throw new CountryException($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 CountryException
*/
public function readAction($id)
{
try {
$repo = $this->getDoctrine()->getRepository('FootballFrontendBundle:Country');
$country = $repo->findOneById($id);
if (!$country instanceof Country) {
throw new CountryException(sprintf('Country read: country [%i] cannot be found.', $id));
}

return $this->getFormView('read', ['country' => $country]);
} catch (DBALException $e) {
$message = sprintf('DBALException [%i]: %s', $e->getCode(), $e->getMessage());
} catch (ORMException $e) {
$message = sprintf('ORMException [%i]: %s', $e->getCode(), $e->getMessage());
} catch (Exception $e) {
$message = sprintf('Exception [%i]: %s', $e->getCode(), $e->getMessage());
}

if (isset($message)) {
throw new CountryException($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 CountryException
*/
public function deleteAction($id)
{
try {
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('FootballFrontendBundle:Country');

$country = $repo->findOneById($id);
if (!$country instanceof Country) {
throw new CountryException(sprintf('Country delete: country [%i] cannot be found.', $id));
}

$em->remove($country);
$em->flush();
} catch (DBALException $e) {
$message = sprintf('DBALException [%i]: %s', $e->getCode(), $e->getMessage());
} catch (ORMException $e) {
$message = sprintf('ORMException [%i]: %s', $e->getCode(), $e->getMessage());
} catch (Exception $e) {
$message = sprintf('Exception [%i]: %s', $e->getCode(), $e->getMessage());
}

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

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

/**
* Creates form object.
*
* @param Country $country
* @param string $method
* @param string $action
*
* @return Form
*/
private function getForm(Country $country, $method, $action)
{
return $this->createForm(
new CountryType(),
$country,
[
'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:Country:%s.html.twig', $template),
$parameters
);
}
}

Routers


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_country_list GET ANY ANY /country
football_frontend_country_create GET ANY ANY /country/create
football_frontend_country_createprocess POST ANY ANY /country/create
football_frontend_country_update GET ANY ANY /country/update/{id}
football_frontend_country_updateprocess PATCH ANY ANY /country/update/{id}
football_frontend_country_read GET ANY ANY /country/{id}
football_frontend_country_delete GET ANY ANY /country/delete/{id}
football_frontend_default_index GET ANY ANY /

List template


{% extends 'FootballFrontendBundle:Country:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
COUNTRY - List
<hr />
{% if countryArray is defined and countryArray|length != 0 %}
<table border="1px">
<tr>
<td>#</td>
<td>ID</td>
<td>CODE</td>
<td>NAME</td>
<td>CREATED</td>
<td>UPDATED</td>
<td> </td>
<td> </td>
<td> </td>
</tr>
{% for country in countryArray %}
<tr>
<td class="head">{{ loop.index }}</td>
<td>{{ country.id }}</td>
<td>{{ country.code }}</td>
<td>{{ country.name }}</td>
<td>{{ country.createdAt|date('d/m/Y H:i:s') }}</td>
<td>{{ country.updatedAt is not null ? country.updatedAt|date('d/m/Y H:i:s') : '' }}</td>
<td><a href="{{ path('football_frontend_country_read', {'id':country.id}) }}">view</a></td>
<td><a href="{{ path('football_frontend_country_delete', {'id':country.id}) }}">delete</a></td>
<td><a href="{{ path('football_frontend_country_update', {'id':country.id}) }}">update</a></td>
</tr>
{% endfor %}
</table>
{% else %}
No country found in database!
{% endif %}
{% endspaceless %}
{% endblock %}

Read template


{% extends 'FootballFrontendBundle:Country:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
COUNTRY - Read
<hr />
<table border="1px">
<tr>
<td>ID</td>
<td>CODE</td>
<td>NAME</td>
<td>CREATED</td>
<td>UPDATED</td>
</tr>
<tr>
<td>{{ country.id }}</td>
<td>{{ country.code }}</td>
<td>{{ country.name }}</td>
<td>{{ country.createdAt|date('d/m/Y H:i:s') }}</td>
<td>{{ country.updatedAt is not null ? country.updatedAt|date('d/m/Y H:i:s') : '' }}</td>
</tr>
</table>
{% endspaceless %}
{% endblock %}

Create template


{% extends 'FootballFrontendBundle:Country:index.html.twig' %}

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

{{ form_errors(form) }}

{% endif %}

<p>CODE: {{ form_widget(form.code) }}</p>
<p>NAME: {{ form_widget(form.name) }}</p>

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

Update template


{% extends 'FootballFrontendBundle:Country:index.html.twig' %}

{% block body %}
{% spaceless %}
{{ parent() }}
COUNTRY - Update
<hr />
{{ form_start(form) }}
{% if form_errors(form) != '' %}

{{ form_errors(form) }}

{% endif %}

<p>CODE: {{ form_widget(form.code) }}</p>
<p>NAME: {{ form_widget(form.name) }}</p>

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