Contact

FUxCon 2013


Forms

CakePHP

Forms in CakePHP closely cooperate with models. Controllers provide the glue between the two. Controller code can be generated by the CakePHP console:

cd app
../lib/Cake/Console/cake bake

The generated controller does have methods for Create, Read, Update and Delete (CRUD) operations. Here is the generated method to add a project:

<?php
class ProjectsController
{
  /**
   * edit method
   *
   * @throws NotFoundException
   * @param string $id
   * @return void
   */
	public function edit($id = null) {
		if (!$this->Project->exists($id)) {
			throw new NotFoundException(__('Invalid project'));
		}
		if ($this->request->is('post') || $this->request->is('put')) {
      unset($this->request->data['user_id']);
			if ($this->Project->save($this->request->data)) {
				$this->Session->setFlash(__('The project has been saved'), 'message-success');
				$this->redirect(array('action' => 'view', $id));
			} else {
				$this->Session->setFlash(__('The project could not be saved. Please, try again.'), 'message-error');
			}
		} else {
			$options = array('conditions' => array('Project.' . $this->Project->primaryKey => $id));
			$this->request->data = $this->Project->find('first', $options);
		}
		$users = $this->Project->User->find('list');
		$tags = $this->Project->Tag->find('list');
		$this->set(compact('users', 'tags'));

		$this->set('pageTitle', __('Edit project'));
	}
}

We have rearranged the edit template somewhat:

CakePHP edit form

Here is a part of the file app/View/Projects/edit.ctp that creates this form:

<div class="pull-right thumbnail">
	<?php
  echo $this->Thumbnail->render('project/' . $form->field('id') . '.jpg', array(
    'width' => 160, 
    'height' => 160, 
    'resizeOption' => 'auto',
  )); 
  ?>
</div>
<?php
echo $this->Form->input('picture', array(
  'type' => 'file',
  'id' => 'picture-field'
));
?>

Django

In our Django implementation, the models already provide an API to read, create & update, and delete domain objects, projects in our case.

URL patterns link to the responsible views:

urlpatterns = patterns('',
  url(r'^project/(?P<pk>\d+)/$', ProjectDetailView.as_view(), name='project'),
  url(r'^project/add/$', ProjectAddView.as_view(), name='add_project'),
  url(r'^project/edit/(?P<pk>\d+)/$', ProjectEditView.as_view(), name='edit_project'),

These views are defined in file projects/views.py. Actually, these classes are only concerned with permissions. They inherit all the CRUD functionality directly fron built-in views in Django:

class ProjectAddView(ProjectMixin, CreateView):
  pass

class ProjectEditView(ProjectMixin, UpdateView):
  pass

The ProjectMixin keeps non-owners from editing projects as explained elsewhere.

With the support of this rather short template in templates/projects/project_form.html:

{% extends 'layout.html' %}

{% block content %}
{# http://blog.headspin.com/?p=541 #}
<form method="POST" enctype="multipart/form-data">
  <h3>Add new project</h3>
  {{ form }}{% csrf_token %}
  <div class="submit">
    <button id="save-button" class="btn" type="submit">Submit</button>
  </div>
</form>
{% endblock content %}

… the edit form looks like this

Django edit form

Drupal

Drupal has edit forms built-in. It will take some more styling than we have invested up to now (none to be honest) to make them look similar to those of the other frameworks, but it is definitely possible:

Drupal edit form

Symfony

In Symfony, basic CRUD code can be generated by the command

php app/console generate:doctrine:crud --entity="Project"

We started with this, and added the special handling for tags and pictures for our project. Our ProjectController in file src/FUxCon2013/ProjectsBundle/Controller/ProjectBundle has two helper methods:

<?php
namespace FUxCon2013\ProjectsBundle\Controller;

class ProjectController extends Controller
{
    /**
     * Send flash message
     */
    private function flash($message, $type = 'error')
    { $this->get('session')->getFlashBag()->add($type, $message); }

    /**
     * Redirect to the project detail page
     */
    private function show($project)
    {
        return $this->redirect(
            $this->generateUrl('project_show', array('id' => $project->getId()))
        );
    }
}

With these, the updateAction() looks like this:

<?php
namespace FUxCon2013\ProjectsBundle\Controller;

class ProjectController extends Controller
{
      /**
       * Edits an existing Project entity.
       *
       * @Route("/project/{id}", name="project_update")
       * @Method("PUT")
       * @Template("FUxCon2013ProjectsBundle:Project:edit.html.twig")
       *
       * Uses type hint "Project $project" to implicitely invoke ParamConverter
       */
      public function updateAction(Request $request, Project $project)
      {
          if (!$this->get('security.context')->isGranted('MAY_EDIT', $project)) {
              $this->flash('You are not allowed to edit this project');
              return $this->show($project);
          }

          $editForm = $this->createForm(new ProjectType(), $project);
          $editForm->bind($request);

          if ($editForm->isValid()) {
              if (!$project->getPicture() || $project->processPicture()) {
                  $em = $this->getDoctrine()->getManager();
                  $em->persist($project);
                  $em->flush();

                  $this->get('fpn_tag.tag_manager')->saveTagging($project);

                  $this->flash('The project has been saved', 'success');
                  return $this->show($project);
              }
              else {
                  $this->flash('Picture could not be saved. Please try again.');
              }
          }
          else {
              $this->flash('The project could not be saved. Please, try again.');
          }

          return array(
              'project'      => $project,
              'edit_form'   => $editForm->createView(),
          );
      }
  }
}

This method handles submitted for data. The form itself is displayed by this code:

<?php
namespace FUxCon2013\ProjectsBundle\Controller;

class ProjectController extends Controller
{
  /**
   * Displays a form to edit an existing Project entity.
   *
   * @Route("/project/{id}/edit", name="project_edit")
   * @Method("GET")
   * @Template()
   *
   * Uses type hint "Project $project" to implicitely invoke ParamConverter
   */
  public function editAction(Project $project)
  {
      if (!$this->get('security.context')->isGranted('MAY_EDIT', $project)) {
          $this->flash('You are not allowed to edit this project');
          return $this->show($project);
      }

      $this->get('fpn_tag.tag_manager')->loadTagging($project);

      $editForm = $this->createForm(new ProjectType(), $project);

      return array(
          'project'      => $project,
          'edit_form'   => $editForm->createView(),
      );
  }
}

The template for this lives in in file src/FUxCon2013/ProjectsBundle/Resources/views/Project/edit.html.twig:

{% extends 'FUxCon2013ProjectsBundle::layout.html.twig' %}

{% block content -%}
<div class="row">
    <form action="{{ path('project_update', { 'id': project.id }) }}" method="post" {{ form_enctype(edit_form) }} class="span6 offset1">
        <h1>Edit project</h1>

        <input type="hidden" name="_method" value="PUT" />
        {{ form_widget(edit_form) }}
        <p>
            <button class="btn" id="save-button" type="submit">Save</button>
        </p>
    </form>
</div>
{% endblock %}

The form itself is constructed neither here nore in the controller but rather in a separate class in file src/FUxCon2013/ProjectsBundle/Form/ProjectType.php:

<?php
namespace FUxCon2013\ProjectsBundle\Form;

class ProjectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', null, array('attr' => array(
                'class' => 'title-field input-block-level'
            )))
            ->add('picture', 'file', array('required' => false))
            ->add('startDate', 'date_entry')
            ->add('endDate', 'date_entry')
            ->add('about', null, array('attr' => array(
                'class' => 'about-field input-block-level'
            )))
            ->add('tags', 'tags_entry', array('attr' => array(
                'class' => 'tags-field input-block-level'
            )))
        ;
    }
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'FUxCon2013\ProjectsBundle\Entity\Project'
        ));
    }

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

In this form, the fields with there desired front end properties are listed. It is not possible to set CSS IDs as Symfony uses these for injecting Javascript validation code into the form.

For dates and tags, we have created new widget types date_entry and tags_entry. The widget type for tags is described elsewhere. The date_entry is much simpler, but still requires another class and some configuration. Here is the widget class in src/FUxCon2013/ProjectsBundle/Form/DateType.php

<?php
namespace FUxCon2013\ProjectsBundle\Form;

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

class DateType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    { $builder->addModelTransformer(new DateTransformer()); }

    public function getParent()
    { return 'text'; }

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

This class needs to be registered in app/config/config.yml for Symfony to recognize it:

services:
    fuxcon2013.form.date_entry:
        class: FUxCon2013\ProjectsBundle\Form\DateType
        tags:
            - { name: form.type, alias: date_entry }


comments powered by Disqus