📜 ⬆️ ⬇️

Creating a CRUD Application on Symfony 2

Symfony 2.0


The recently released version of the symfony 2 framework includes many interesting features. In this article I want to tell about the creation of CRUD applications - a very common task of creating a web interface for creating, reading, updating and deleting records in the database.

About the architecture and installation of Symfony 2 has already been on Habré , so we believe that Symfony 2 SE is already installed and the main concepts used (bundles, forms, templates, etc.) are familiar to you.

The main tasks in the development of a standard CRUD application on symfony 2


  1. Data model development
  2. Development of controllers, forms and templates to create, read, update and delete data model entities

In this article, as an example, we will create an application for editing news, each of which is tied to a category, and links can be tied to news.

Data model development


Creating entity descriptions by manually writing yml or xml is quite a tedious task (although of course you can use a plugin for Mysql Workbench or specialized software). To speed up the process in Symfony 2 there is a convenient tool for generating a description of a data model by reverse engineering an existing database .
')
To start, create a TestNewsBundle bundle:

php app/console generate:bundle --namespace=Test/NewsBundle --format=annotation --structure 


Create a database schema in Mysql Workbench (or any other database design tool):



 SET FOREIGN_KEY_CHECKS=0; CREATE TABLE `news` ( `id` INT NOT NULL AUTO_INCREMENT , `news_category_id` INT NOT NULL , `title` VARCHAR(255) NULL , `announce` TEXT NULL , `text` TEXT NULL , `pub_date` DATE NULL , PRIMARY KEY (`id`) , INDEX `pub_date` (`pub_date` ASC) , INDEX `fk_news_news_category` (`news_category_id` ASC) , CONSTRAINT `fk_news_news_category` FOREIGN KEY (`news_category_id` ) REFERENCES `news_category` (`id` ) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB; CREATE TABLE `news_category` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NULL , PRIMARY KEY (`id`) ) ENGINE = InnoDB CREATE TABLE `news_link` ( `id` int(11) NOT NULL AUTO_INCREMENT, `news_id` INT NOT NULL , `url` varchar(255) DEFAULT NULL, `text` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) , INDEX `fk_news_link_news1` (`news_id` ASC) , CONSTRAINT `fk_news_link_news1` FOREIGN KEY (`news_id` ) REFERENCES `news` (`id` ) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB; 

Create doctrine classes:

 php app/console doctrine:mapping:import TestNewsBundle annotation 


As a result of executing this command, one class for each table will be created in the Test / NewsBundle / Entity subdirectory, while the mapping configuration of objects into relational tables for Doctrine ORM is described in class annotations, for example, the News class:

 <?php namespace Test\NewsBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Test\NewsBundle\Entity\News * * @ORM\Table(name="news") * @ORM\Entity */ class News { /** * @var integer $id * * @ORM\Column(name="id", type="integer", nullable=false) * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * @var string $title * * @ORM\Column(name="title", type="string", length=255, nullable=true) */ private $title; /** * @var text $announce * * @ORM\Column(name="announce", type="text", nullable=true) */ private $announce; /** * @var text $text * * @ORM\Column(name="text", type="text", nullable=true) */ private $text; /** * @var date $pubDate * * @ORM\Column(name="pub_date", type="date", nullable=true) */ private $pubDate; /** * @var NewsCategory * * @ORM\ManyToOne(targetEntity="NewsCategory") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="news_category_id", referencedColumnName="id") * }) */ private $newsCategory; } 

To add classes with getters and setters, execute the command:

 php app/console doctrine:generate:entities TestNewsBundle 


Creation of a blank for a CRUD application


We create a newsletter using the doctrine: generate: crud command . The format of the routing - annotations in the controller file (Test / NewsBundle / Controller / NewsController). Routing via annotations in the controller file works through the SensioFrameworkExtra bundle (it is in the Symfony 2 SE package).

 php app/console doctrine:generate:crud --entity=TestNewsBundle:News --route-prefix=news --with-write --format=annotation 

Now, when entering the path specified when generating (if Symfony 2 is unpacked at wwwroot - http: //localhost/Symfony/web/app_dev.php/news /), an empty news list is shown, and when you click on the link “Create new entry”, it opens default entry creation form.

Create the same template for working with news categories and add several categories:

 php app/console doctrine:generate:crud --entity=TestNewsBundle:NewsCategory --route-prefix=newscategory --with-write --format=annotation 


In order for us to have a blank for the form of adding a link to the news in the future, we will similarly generate a blank for the NewsLink entity:

 php app/console doctrine:generate:crud --entity=TestNewsBundle:NewsLink --route-prefix=newslink --with-write --format=annotation 


So that when displaying the list of categories in the add news form, the system knows which field to show in the selector, you need to add a method to the Test / NewsBundle / Entity / NewsCategory class:

 function __toString() { return $this->getName(); } 


Form class modification


Now we will correct the generated form Test / NewsBundle / Form / NewsType. Add headings to the fields (label), enter the necessary field types - text (displayed input type = text), textarea. In the pubDate and newsCategory fields, we leave the field type null - in this case, the Symfony Form Component itself “guesses” what type of field to show.

 <?php namespace Test\NewsBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; class NewsType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder->add('title', 'text', array('label' => '')) ->add('announce', 'textarea', array('label' => '')) ->add('text', 'textarea', array('label' => '')) ->add('pubDate', null, array('label' => ' ')) ->add('newsCategory', null, array('label' => '')); } public function getName() { return 'news'; } } 


Modification of the form template


Details about forms and templating in forms can be found in the sections of the manual Forms and Customization of Forms . In the created form, we will replace the display of the form with a table, for this you need to add an instruction in the template to use a table layout (the default layout can also be changed in the settings).

 {% form_theme form 'form_table_layout.html.twig' %} 

However, we will need to redefine the standard display of the field in the future, so the template for entering the news Test / NewsBundle / Resources / views / News / new.html.twig looks like this:

 {% use 'form_table_layout.html.twig' %} {% form_theme form _self %} <h1> </h1> <form action="{{ path('news_create') }}" method="post" {{ form_enctype(form) }}> {{ form_widget(form) }} <p> <button type="submit"> </button> </p> </form> <ul class="record_actions"> <li> <a href="{{ path('news') }}">    </a> </li> </ul> 


The form is displayed in a table layout:


Editing related entries


Now let's do the most interesting - editing the links to the news. During the reverse engineering only the link “Link -> News” was automatically added to the entity classes. To add the “News -> Links” link, in the Test \ NewsBundle \ Entity \ News class, add:

 /** * @ORM\OneToMany(targetEntity="NewsLink", mappedBy="news", cascade={"all"}) */ protected $newsLinks; function __construct() { $this->newsLinks = new \Doctrine\Common\Collections\ArrayCollection(); } 

Then execute the command (the getter and setter for the $ newsLinks attribute will be generated):

 php app/console doctrine:generate:entities TestNewsBundle 


Now in the Test / NewsBundle / Form / NewsType class we add a “Collection” type field that allows editing a set of related entities:

 $builder->add('title', 'text', array('label' => '')) .... ->add('newsLinks', 'collection', array( 'label' => '  ', 'type' => new NewsLinkType(), 'allow_add' => true, 'allow_delete' => true, 'prototype' => true )); 

In the class Test \ NewsBundle \ Form \ NewsLinkType you must specify the class name of the entity being edited (data_class option):

 <?php namespace Test\NewsBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; class NewsLinkType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder->add('url') ->add('text'); } public function getName() { return 'newsLinkType'; } public function getDefaultOptions(array $options) { return array( 'data_class' => 'Test\NewsBundle\Entity\NewsLink', ); } } 


However, if you now look at the form, then only the title of the “Links to news” field will be displayed. For the form to work, you need to modify the template. At the same time, we will render the definition of the form into a separate template To do this, create a file Test / NewsBundle / Resources / views / News / form.html.twig:
 {% use 'form_table_layout.html.twig' %} {% form_theme form _self %} <script language="JavaScript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> <form action="{{ entity.id ? path('news_update', { 'id': entity.id }) : path('news_create') }}" method="post" {{ form_enctype(form) }}> {{ form_errors(form) }} <table> {{ form_row (form.title) }} {{ form_row (form.announce) }} {{ form_row (form.text) }} {{ form_row (form.pubDate) }} {{ form_row (form.newsCategory) }} <tr> <td valign="top">  </td> <td> <!--      /    --> {% macro linkRow(link) %} <tr> <td>{{ form_widget(link.url) }}</td> <td>{{ form_widget(link.text) }}</td> <td><a href="#" class="deleteRowLink">X</a></td> </tr> {% endmacro %} <!--       /    --> <!--     #addLink     --> <script type="text/html" id="nl">{{ _self.linkRow (form.newsLinks.get('prototype')) }} </script> <!--         --> <table id="linksTable"> <tr><td>Url</td><td> </td></tr> {% for key, link in form.newsLinks %} {{ _self.linkRow(link) }} {% endfor %} </table> <input type="button" id="addLink" value=" "> <script> $(function() { $("#addLink" ).click(function() { $('#linksTable tbody').append($('#nl').html().replace(/\$\$name\$\$/g, $('#linksTable tbody tr').length)); }); $("form a.deleteRowLink").live('click', function() { $(this).closest('tr').remove(); }); }); </script> </td> </tr> </table> {{ form_rest(form) }} <p><button type="submit"></button></p> </form> 


Test record template Test / NewsBundle / Resources / views / News / new.html.twig:

 <h1> </h1> {% include 'TestNewsBundle:News:form.html.twig' %} <ul class="record_actions"> <li> <a href="{{ path('news') }}">    </a> </li> </ul> 


Test / NewsBundle / Resources / views / News / edit.html.twig record editing template:

 <h1> </h1> {% include 'TestNewsBundle:News:form.html.twig' with { 'form' : edit_form } %} <ul class="record_actions"> <li> <a href="{{ path('news') }}"> Back to the list </a> </li> <li> <form action="{{ path('news_delete', { 'id': entity.id }) }}" method="post"> {{ form_widget(delete_form) }} <button type="submit">Delete</button> </form> </li> </ul> 


Of course, you can leave one template for recording and editing records, but in this example, we will leave the structure of the templates and methods of the controller that the CRUD generator generated unchanged.

Next, you need to modify the controller class Test / NewsBundle / Controller / NewsController. Although we specified the cascade = all option in the “News - Link” connection parameters (while saving the entity, the associated entities are saved), however, it is still necessary to define the binding of NewsLink objects to the parent News object:

 public function createAction() { $entity = new News(); $request = $this->getRequest(); $form = $this->createForm(new NewsType(), $entity); $form->bindRequest($request); if ($form->isValid()) { $em = $this->getDoctrine()->getEntityManager(); //     foreach ($entity->getNewsLinks() as $link) { $link->setNews($entity); } $em->persist($entity); $em->flush(); return $this->redirect($this->generateUrl('news_show', array('id' => $entity->getId()))); } return array( 'entity' => $entity, 'form' => $form->createView()); } 


For the updateAction method, it is still necessary to provide for possible deletion of related records. In the html form, there is the deletion of table rows using jQuery, but this is not enough to delete the entries — this must be explicitly implemented in the controller:

  public function updateAction($id) { $em = $this->getDoctrine()->getEntityManager(); $entity = $em->getRepository('TestNewsBundle:News')->find($id); if (!$entity) { throw $this->createNotFoundException('Unable to find News entity.'); } $beforeSaveLinks = $currentLinkIds = array(); foreach ($entity->getNewsLinks() as $link) $beforeSaveLinks [$link->getId()] = $link; $editForm = $this->createForm(new NewsType(), $entity); $deleteForm = $this->createDeleteForm($id); $request = $this->getRequest(); $editForm->bindRequest($request); if ($editForm->isValid()) { foreach ($entity->getNewsLinks() as $link) { $link->setNews($entity); //  -     (   id) if ($link->getId()) $currentLinkIds[] = $link->getId(); } $em->persist($entity); //          -   foreach ($beforeSaveLinks as $linkId => $link) if (!in_array( $linkId, $currentLinkIds)) $em->remove($link); $em->flush(); return $this->redirect($this->generateUrl('news_edit', array('id' => $id))); } return array( 'entity' => $entity, 'edit_form' => $editForm->createView(), 'delete_form' => $deleteForm->createView(), ); } 


Form with the ability to add related entries:

Form with the ability to add related entries

Now our application allows you to add, view, update and delete news. At the same time, work with related links to news occurs without reloading the page. Not to say that without coding at all, but with rather little effort this functionality was developed. The appearance of the form can be customized at your discretion. Using the theme engine in forms, you can override the standard display templates for strings and form fields.

Source: https://habr.com/ru/post/125948/


All Articles