In the first part we covered module creation and index action of our CRUD module. That action was used to show all the records. In this article we will build the functionality that will allow us to update and delete existing records and create new ones.
Form class
For create and update action we will definitely need a form. In Symfony framework this means that we need to create a class that will render our form. As many things are already predefined we only need to extend from Symfony\Component\Form\AbstractType and define our fields:
src/Dusan/Bundle/SimpleBundle/Form/Type/SimpleCrudType.php
<?php namespace Dusan\Bundle\SimpleBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class SimpleCrudType extends AbstractType { /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add( 'firstname', 'text', [ 'required' => true, 'label' => 'dusan_simple.simple_crud.firstname.label' ] ) ->add( 'lastname', 'text', [ 'required' => true, 'label' => 'dusan_simple.simple_crud.lastname.label', ] ) ->add( 'email', 'text', [ 'required' => true, 'label' => 'dusan_simple.simple_crud.email.label', ] ); } /** * @param OptionsResolverInterface $resolver */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults( [ 'data_class' => 'Dusan\Bundle\SimpleBundle\Entity\SimpleCrud', ] ); } /** * @return string */ public function getName() { return 'dusan_simple_crud_form'; } }
Create and update action
After that, the next thing that we should do is to create two actions createAction and updateAction. Each of these will cover both cases:
- The case when the form should be displayed,
- The case when submtted data should be saved.
The process itself consists of two parts. Receiving request in our controller and then letting the Form Handler class take care of persisting our data or displaying an empty form.
Since we want to make reusable code we will create a protected method inside our controller that will be used for both of these actions. This method should get our SimpleCrud entity and either save it in database or display our form.
Let’s take a look at these actions:
src/Dusan/Bundle/SimpleBundle/Controller/DefaultController.php
/** * @Route("/create", name="dusan_simple_create") * @Template("DusanSimpleBundle:Default:update.html.twig") */ public function createAction() { $formAction = $this->get('oro_entity.routing_helper') ->generateUrlByRequest('dusan_simple_create', $this->getRequest()); $simpleCrud = new SimpleCrud(); return $this->update($simpleCrud, $formAction); } /** * @Route("/update/{id}", name="dusan_simple_update", requirements={"id"="\d+"}) * @Template */ public function updateAction(SimpleCrud $entity) { $formAction = $this->get('router')->generate('dusan_simple_update', ['id' => $entity->getId()]); return $this->update($entity, $formAction); }
Both of these actions use the same template: update.html.twig
The protected method update from our controller is very simple, it just forwards the entity to Form Handler and makes sure that we have everything we need returned so that we can use it in our template.
src/Dusan/Bundle/SimpleBundle/Controller/DefaultController.php
/** * @param SimpleCrud $entity * @param string $formAction * * @return array */ protected function update(SimpleCrud $entity, $formAction) { $saved = false; if ($this->get('dusan_simple.form.handler')->process($entity)) { if (!$this->getRequest()->get('_widgetContainer')) { $this->get('session')->getFlashBag()->add( 'success', $this->get('translator')->trans('dusan_simple.simple_crud.controller.saved.message') ); return $this->get('oro_ui.router')->redirectAfterSave( ['route' => 'dusan_simple_update', 'parameters' => ['id' => $entity->getId()]], ['route' => 'dusan_simple_index'], $entity ); } $saved = true; } return array( 'entity' => $entity, 'saved' => $saved, 'form' => $this->get('dusan_simple.form.handler')->getForm()->createView(), 'formAction' => $formAction ); }
Form handler
Let’s take a look at the form handler. First of all, we need to pass some objects to this form handler as it needs them to process the form.
- Form class instance
- Request object
- Doctrine ORM manager instance
- Oro Routing helper instance
- Form Factory class instance
We will create a new YAML file for this:
src/Dusan/Bundle/SimpleBundle/Resources/config/form.yml
parameters: dusan_simple.form.type.class: Dusan\Bundle\SimpleBundle\Form\Type\SimpleCrudType dusan_simple.form.handler.class: Dusan\Bundle\SimpleBundle\Form\Handler\SimpleCrudHandler services: dusan_simple.form.type: class: %dusan_simple.form.type.class% tags: - { name: form.type, alias: dusan_simple_crud_form } dusan_simple.form.handler: class: %dusan_simple.form.handler.class% scope: request arguments: - "dusan_simple_crud_form" - "dusan_simple_crud_form" - @request - @doctrine.orm.entity_manager - @oro_entity.routing_helper - @form.factory
We also need to include this new YAML file in our Dependancy Injection class.
src/Dusan/Bundle/SimpleBundle/DependencyInjection/DusanSimpleExtension.php
<?php namespace Dusan\Bundle\SimpleBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; /** * This is the class that loads and manages your bundle configuration * * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} */ class DusanSimpleExtension extends Extension { /** * {@inheritDoc} */ public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); $loader->load('form.yml'); } }
These dependencies will be passed to our Form Handler through a constructor:
src/Dusan/Bundle/SimpleBundle/Form/Handler/SimpleCrudHandler.php
<?php namespace Dusan\Bundle\SimpleBundle\Form\Handler; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormFactory; use Symfony\Component\HttpFoundation\Request; use Doctrine\Common\Persistence\ObjectManager; use Oro\Bundle\EntityBundle\Tools\EntityRoutingHelper; use Dusan\Bundle\SimpleBundle\Entity\SimpleCrud; class SimpleCrudHandler { /** @var FormInterface */ protected $form; /** @var string */ protected $formName; /** @var string */ protected $formType; /** @var Request */ protected $request; /** @var ObjectManager */ protected $manager; /** @var EntityRoutingHelper */ protected $entityRoutingHelper; /** @var FormFactory */ protected $formFactory; /** * @param string $formName * @param string $formType * @param Request $request * @param ObjectManager $manager * @param EntityRoutingHelper $entityRoutingHelper * @param FormFactory $formFactory */ public function __construct( $formName, $formType, Request $request, ObjectManager $manager, EntityRoutingHelper $entityRoutingHelper, FormFactory $formFactory ) { $this->formName = $formName; $this->formType = $formType; $this->request = $request; $this->manager = $manager; $this->entityRoutingHelper = $entityRoutingHelper; $this->formFactory = $formFactory; } /*...*/ }
The process method is responsible for, as its name suggests, processing the form. We first need to create a form using form factory. For this we need to pass form name and form type (which is an instance of our Form Type class), our entity and options. We then need to set SimpleCrud entity on our form.
Since we want to reuse this method we will update our record if the HTTP method was POST or PUT. Maybe there are better ways for doing this because in our case this method depends heavily on request object which is often thought of as an anti pattern. OroCRM developers did something similar in CallBundle so I am sure that there are reasons for doing it this way.
Form is then validated and the entity is persisted if validation is successful.
src/Dusan/Bundle/SimpleBundle/Form/Handler/SimpleCrudHandler.php
/** * Process form * * @param SimpleCrud $entity * * @return bool True on successful processing, false otherwise */ public function process(SimpleCrud $entity) { $options = []; $this->form = $this->formFactory->createNamed($this->formName, $this->formType, $entity, $options); $this->form->setData($entity); if (in_array($this->request->getMethod(), array('POST', 'PUT'))) { $this->form->submit($this->request); if ($this->form->isValid()) { $this->onSuccess($entity); return true; } } return false; } /** * "Success" form handler * * @param SimpleCrud $entity */ protected function onSuccess(SimpleCrud $entity) { $this->manager->persist($entity); $this->manager->flush(); } /** * Get form, that build into handler, via handler service * * @return FormInterface */ public function getForm() { return $this->form; }
Templates
Let’s add “new” button to our index page template:
src/Dusan/Bundle/SimpleBundle/Resources/views/Default/index.html.twig
{% extends 'OroUIBundle:actions:index.html.twig' %} {% import 'OroDataGridBundle::macros.html.twig' as dataGrid %} {% set pageTitle = 'dusan_simple.simple_crud.entity_plural_label'|trans %} {% set gridName = 'dusan-simple-grid' %} {% block navButtons %} <div class="btn-group"> {{ UI.addButton({ 'path' : path('dusan_simple_create'), 'title' : 'dusan_simple.simple_crud.new'|trans, 'label' : 'dusan_simple.simple_crud.new'|trans }) }} </div> {% endblock %}
We also need to create a template for the update action:
src/Dusan/Bundle/SimpleBundle/Resources/views/Default/update.html.twig
{% extends 'OroUIBundle:actions:update.html.twig' %} {% oro_title_set({params : {"%subject%": form.vars.value.subject|default('N/A') } }) %} {% block content_data %} {% set id = 'simplecrud-log' %} {% set title = 'oro.ui.edit_entity'|trans({'%entityName%': 'dusan_simple.simple_crud.entity_label'|trans}) %} {% set formFields = [] %} {% set formFields = formFields|merge([ form_row(form.firstname), form_row(form.lastname), form_row(form.email), ]) %} {% set dataBlocks = [{ 'title': title, 'class': 'active', 'subblocks': [ { 'title': title, 'data': formFields } ] }] %} {% set data = { 'formErrors': form_errors(form)? form_errors(form) : null, 'dataBlocks': dataBlocks } %} {{ parent() }} {% endblock content_data %} {% block navButtons %} {{ UI.cancelButton(path('dusan_simple_index')) }} {% set html = UI.saveAndCloseButton() %} {% if form.vars.value.id or resource_granted('dusan_simple_update') %} {% set html = html ~ UI.saveAndStayButton() %} {% endif %} {{ UI.dropdownSaveButton({'html': html}) }} {% endblock %} {% block pageHeader %} {% if form.vars.value.id %} {% set breadcrumbs = { 'entity': form.vars.value, 'indexPath': path('dusan_simple_index'), 'indexLabel': 'dusan_simple.simple_crud.entity_plural_label'|trans, 'entityTitle': form.vars.value.subject|default('N/A') } %} {% else %} {% set breadcrumbs = { 'entity': form.vars.value, 'indexPath': path('dusan_simple_index'), 'indexLabel': 'dusan_simple.simple_crud.entity_plural_label'|trans, 'entityTitle': 'dusan_simple.simple_crud.edit'|trans } %} {% endif %} {{ parent() }} {% endblock pageHeader %}
If we did everything correctly at this point it should look like this:

Validation
At the end let’s create some Javascript validation, we won’t do anything special here, we’ll just make sure that each of the fields is filled with some value.
src/Dusan/Bundle/SimpleBundle/Resources/config/validation.yml
Dusan\Bundle\SimpleBundle\Entity\SimpleCrud: properties: firstname: - NotBlank: ~ lastname: - NotBlank: ~ email: - NotBlank: ~ - Email: ~
Delete action
The only thing left to do now is to implement deleteAction. OroCRM has a generic way of doing this usi API methods. Since that would be beyond scope of this article, I came up with this approach:
src/Dusan/Bundle/SimpleBundle/Controller/DefaultController.php
/** * @Route("/delete/{id}", name="dusan_simple_delete", requirements={"id"="\d+"}) * @Template */ public function deleteAction($id) { $manager = $this->getDoctrine()->getManager(); $entity = $manager->find('Dusan\Bundle\SimpleBundle\Entity\SimpleCrud', $id); if (!$entity) { $this->get('session')->getFlashBag()->add( 'error', $this->get('translator')->trans('dusan_simple.simple_crud.controller.notfound.message') ); return $this->redirect($this->generateUrl('dusan_simple_index')); } $manager->remove($entity); $manager->flush(); $this->get('session')->getFlashBag()->add( 'success', $this->get('translator')->trans('dusan_simple.simple_crud.controller.deleted.message') ); return $this->redirect($this->generateUrl('dusan_simple_index')); }
Wrap up
In this two-part tutorial we covered a simple OroCRM CRUD bundle creation. You can see all the code on my Github page. The goal was to get you started so that you can explore more.
I think that OroCRM looks like an interesting project so far and that you should definitely pay attention to its progress. It is also a good way to improve your Symfony skills.
Would you like to recommend this article to your friends or colleagues?
Feel free to share.
Article "OroCRM – Creating a simple CRUD (part 2)" has 2 responses
Večeras implementujem oba dela, odlično objašnjeno!
Odlicno, javi kako je proslo :)