Contact

FUxCon 2013


Symfony 2.3

Installation

Symfony’s signature feature is that it entirely consists of independent bundles. A package manager is therefore essential for its installation. Symfony’s console tool is app/console. It can be used to clear cache or to generate CRUD code, amongst other functions.

The installation steps a re described in its README.

You can get the code for this implementation from its github repository.

The repository contains a simple install script that loads all required components and sets up a simple demo configuration. Please follow the instructions in the file README.md contained in the repository.

There is also a virtual appliance based on VirtualBox which you can install for free locally on your computer or host on the web. This appliance has a website for each of the four web framworks readily installed. Please ask me about the appliance at the workshop or contact me directly.

Basic configuration

Symfony uses multiple locations for its settings:

FileMajor settings
composer.json Versions of all installed bundles
app/config/config.yml Configuration of all installed bundles and services
app/config/parameters.yml Database credentials
app/config/security.yml Configuration of authentication-related services, path-based access control
app/Kernel.php Registration of all installed bundles

Routes can be configured in multiple locations. Symfony mimics Python decorators based on specially formatted comments in the PHP controller files and uses a special preprocessor to extract these so-called annotations to serialized data structures in a cache directory. These annotations allow to specify routes directly at the controllers/actions which is what I use for this project.

Modeling the problem domain - the models

Symfony manages models through one of its supported database backends. The default is to use the doctrine backend. As with Django, you declare your model fields in either XML, YML or as annotations and use a command line tool to generate the tables in your MySQL database, e.g.

php app/console doctrine:generate:entities FUxCon2013/ProjectsBundle/Entity/Project

Doctrine requires the definition of a getter and a setter for each field. I have opted to use magic methods in a super class to avoid this. Here is the project model from the Symfony implementation in src/FUxCon2013/ProjectsBundle/Entity/Project.php:

<?php
class Project extends GenericAccessors
{
    // The form generator chokes on "protected" attributes  
    public $id;
    public $title;
    public $startDate;
    public $endDate;
    public $about;
    public $created;
    public $modified;

    public $user;
}

The super class makes sure that with this arrangement the code to be written keeps small while there still are getters and setters for each field, e.g.

<?php
$p = new Project();
echo $p->getTitle();

Unfortunately, the form generator we want to use for the edit form does some checking on the access level of attributes directly and does not seem to see the getters inherited from the super class. We cannot declare attributes protected because of this.

In addition, doctrine needs some more information for each field which I provide in a YAMl file, ie.

FUxCon2013\ProjectsBundle\Entity\Project:
    type: entity
    table: projects
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        title:
            type: string
            length: 255
        startDate:
            type: date
            column: start_date
        endDate:
            type: date
            column: end_date
        about:

            type: text
        created:
            type: datetime
        modified:
            type: datetime


     manyToOne:
        user:
            targetEntity: User

Symfony requires a repository class for each model to be able to access collections of model objects. In our implementation, this class contains methods needed for paginated display of projects:

<?php
class ProjectRepository extends EntityRepository
{
    public function count() {
        return $this->getEntityManager()
            ->createQuery('
                SELECT p.title, p.about
                FROM FUxCon2013SitesBundle:Project p'
            )
            ->getResult();
    }

    public function findPaginated($offset, $size) {
        return $this->getEntityManager()
            ->createQuery('
                SELECT p.title, p.about
                FROM FUxCon2013SitesBundle:Project p
                ORDER BY p.title ASC'
            )
            ->setFirstResult($offset)
            ->setMaxResults($size)
            ->getResult();
    }
}

Business logic - the controllers

Controllers in Symfony are classes in the bundle of your application. In our example, this is the file src/FUxCon2013/ProjectsBundle/Controller/ProjectsController.php. Here is an excerpt:

<?php
class ProjectsController extends Controller
{
  const NO_COL = 3;
  const PAGE_SIZE = 5;

  /**
   * @Route("/", defaults={"offset" = 1})
   * @Route("/page:{offset}", name="_projects")
   * @Template()
   */
  function indexAction(Request $request, $offset = 1)
  {
    $repo = $this
        ->getDoctrine()
        ->getManager()
        ->getRepository('FUxCon2013ProjectsBundle:Project');

    $limit = 10;
    $from  = (($offset * $limit) - $limit);

    $totalCount = $repo->count();
    $totalPages = ceil($totalCount / $limit);

    $projects = $repo->findPaginated($from, $limit);

    $columns = array();
    foreach ($projects as $i => $project) {
        $col = $i % self::NO_COL;
        $columns[$col][] = $project;
    }

    $vars = array(
        'columns' => $columns,
        'width' => 12 / self::NO_COL,
        'page' => $offset,
        'totalPages' => $totalPages,
        'body_class' => 'projects-index',
    );

    return $vars;
  }

  // … more action methods
}

We use the option to declare routes as annotations in comments. In this example, the method ProjectsController.indexAction() is accessed through the routes

<?php
/**
 * @Route("/", defaults={"offset" = 1, "tag" = null})
 * @Route("/page:{offset}", name="_projects")
 */

Symfony does not have pagination built in so we use our custom methods from the repository to get at the count and the paginated list of projects.

The annotation @Template() signals to Symfony that we want the variables returned from the controller to be rendered by a template with a standard name, in our case the file src/FUxCon2013/ProjectBundle/Resources/views/Projects/index.html.twig. We show this file below when explaining the views.

Parameter converters

A powerful concept related to controllers are parameter converters. Initially, parameters passed to actions are strings derived from an associated route. In many cases however, they denote domain objects. The process of finding a domain object from an URL parameter can be delegated out if the controller by using a dedicated parameter converter. We make use of this mechanism in our controller. Here is the declaration of our ProjectController.showAction() to display a single project:

<?php
/**
 * Finds and displays a Project entity.
 *
 * @Route("/project/{id}", name="project_show")
 * @Method("GET")
 * @Template()
 *
 * Uses type hint "Project $project" to implicitely invoke ParamConverter
 */
public function showAction(Project $project)
{}

Symfony is even smart enough to derive the need for a parameter converter from the type hint given to the parameter. Even more so, there is a built-in doctrine.orm converter that fetches an object from the database based on its primary key. So, in our simple example, Symfony can get our project completely on its own. If a matching project is not found, a 404 response is generated.

Appearance - the views

Symfony provides its own template language called Twig which very closely resembles the Django template language. Here is an excerpt from the project list in src/FUxCon2013/ProjectsBundle/Resources/views/Projects/index.html.twig:

{% extends 'FUxCon2013ProjectsBundle::layout.html.twig' %}
{% block content %}
     <div class="row-fluid marketing projects">
    {% for column in columns %}
            <div class="span{{ width }}">
          {% for project in column %}
            <div class="project">
                <h4>
                    <a href="{{ path('_project', { 'id': project.id } ) }}">{{ project.title }}</a>
                </h4>
                <a href="{{ path('_project', { 'id': project.id } ) }}">
                    <img src="{{ thumbnail([ '/images/projects/', project.id, '.png' ] | join, '200x200') }}">
                </a>
                <p>{{ project.about | truncate }}</p>
            </div>
          {% endfor %}
          </div>
    {% endfor %}
     </div>
    {% include 'FUxCon2013ProjectsBundle::_paginate.html.twig' with { 'default_page': '_projects' } %}
{% endblock %}

Symfony uses named routes to generate URIs through the path() function. Loop and conditional constructs look like their Python cousins.

This template uses extension to embed its markup into a common layout. The general structure of our layout file src/FUxCon2013/ProjectsBundle/Resources/views/layout.html.twig looks like this:

<!DOCTYPE html>
<html lang="de">
  <head>
    <meta charset="utf-8">
    <title>{% block title %}Projects{% endblock %}</title>
    <link href="{{ asset('bundles/fuxcon2013/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('bundles/fuxcon2013/css/sites.css') }}" rel="stylesheet">
  </head>

  <body>
    <div class="container-narrow">
      {% for flashMessage in app.session.flashbag.get('notice') %}
          <div class="flash-message">{{ flashMessage }}</div>
      {% endfor %}

      {% block content %}
      {% endblock %}

    </div> <!-- /.container-narrow -->
  </body>
</html>

The template views/Projects/index.html.twig provides overwrites for the blocks defined in this layout file.

Twig can be extended with custom tags and functions. We provide a new function thumbnail() and a filter truncate through such an extension. The extension lives in file src/FUxCon2013/ProjectsBundle/Twig/Extension/FUxCon2013Extension.php:

<?php
namespace FUxCon2013\ProjectsBundle\Twig\Extension;

use Symfony\Bundle\TwigBundle\Extension\AssetsExtension;
use Symfony\Component\DependencyInjection\ContainerInterface;

class FUxCon2013Extension extends \Twig_Extension
{
  private $container;

  public function __construct(ContainerInterface $container)
  { $this->container = $container; }

  public function getFunctions()
  {
    return array(new \Twig_SimpleFunction('thumbnail', array($this, 'thumbnail')));
  }

  public function getFilters()
  {
    return array(new \Twig_SimpleFilter('truncate', array($this, 'truncate')));
  }

  /**
   * Provide a new template function that generates a cacheable, derived image
   */
  public function thumbnail($path, $size = null)
  {
    // some hairy code to generate the derived image
  }

  /**
   * Provide a filter that truncates $text at word borders
   */
  public function truncate($text, $length = 150)
  {
    if (strlen($text) < $length) {
      return $text;
    }
   
    $text = substr($text, 0, $length);
    $blank = strrpos($text, ' ');
    if (FALSE === $blank) {
      $text = '';
    }
    else {
      $text = substr($text, 0, $blank);
    }
    return $text . ' ...';
  }

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

To be recognized, it needs to be registered with the system in app/config/config.yml like so:

services:
    fuxcon2013.twig.fuxcon2013_extension:
        class: FUxCon2013\ProjectsBundle\Twig\Extension\FUxCon2013Extension
        tags:
            - { name: twig.extension }
        arguments: [ @service_container ]

Now that we have covered the basic building blocks of a model-view-controller architecture, we go on to describe the features specific to our requested features, namely user accounts, tagging, picture uploading and scaling, creating and updating projects, Markdown formatting, and finally the generation of test data.

User accounts & security

In Symfony, to implement permissions, there are basically two approaches. Access control lists (ACLs) and voters. Access control lists store additional information about individual Access Control Entities (ACEs) and associated information in the database.

In situations where permissions can be derived from existing relationships between domain objects, this overhead can be avoided by implementing a voter.

Our implementation contains such a voter. With the voter, two things are possible:

  • Hide an edit link from the project detail page
  • Prevent unauthorized users from accessing the project edit page.

In case of the edit link, this is the responsible code in our template src/FUxCon2013/ProjectBundle/Resources/views/Project/show.html.twig:

<p class="actions">
   {% if is_granted("MAY_EDIT", project) %}
     <a class="btn" id="edit-project" href="{{ path('project_edit', { 'id': project.id }) }}">edit this project</a>
   {% endif %}
 </p>

This is the check in our method ProjectController.editAction():

<?php
if (!$this->get('security.context')->isGranted('MAY_EDIT', $project)) {
    $this->flash('You are not allowed to edit this project');
    return $this->show($project);
}

In both cases, a method is_granted() rsp. isGranted() is called with a role and the project object. Under the hood, a voter in file FUxCon2013/ProjectsBundle/Security/OwnerVoter.php does the checking:

<?php
function vote(TokenInterface $token, $object, array $attributes)
{
    if (!in_array('MAY_EDIT', $attributes)) {
        return self::ACCESS_ABSTAIN;
    }
    if (!($object instanceof Project)) {
        return self::ACCESS_ABSTAIN;
    }

    $user = $token->getUser();
    $securityContext = $this->container->get('security.context');

    return $securityContext->isGranted('IS_AUTHENTICATED_FULLY')
        && $user->getId() == $object->getUser()->getId()
        ? self::ACCESS_GRANTED
        : self::ACCESS_DENIED;
}

To make this work, some configuration is required. In file app/config/config.yml, we register the voter as a service:

services:
    fuxcon2013.security.owner_voter:
        class:      FUxCon2013\ProjectsBundle\Security\OwnerVoter
        public:     false
        arguments: [ @service_container ]
        tags:
            - { name: security.voter }

In app/config/security.yml, we have to set the strategy of the access decision manager and tell it to allow access if all voters abstain:

security:
    access_control:
        - { path: ^/project/\d+/edit, role: MAY_EDIT }

    access_decision_manager:
        # strategy can be: affirmative, unanimous or consensus
        strategy: unanimous
        allow_if_all_abstain: true

Assiging topics to projects with tagging

We use fpn/tag-bundle and fpn/doctrine-extensions-taggable to implement tagging in our Symfony implementation. The documentation page goes to some length at explaining what you need in terms of configuration and extending your model classes to enjoy tagging.

As we want to use YAML configuration for our Doctrine entities and want to actually enter tags in the edit forms of our projects, we had to do some additional leg work to get the bundle to work.

Our project model needs a setter for tags:

<?php
namespace FUxCon2013\ProjectsBundle\Entity;

use DoctrineExtensions\Taggable\Taggable;
use Doctrine\Common\Collections\ArrayCollection;
// more use clauses

/**
 * Project
 */
class Project implements Taggable
{
  public function getTags()
  {
      $this->tags = $this->tags ?: new ArrayCollection();
      return $this->tags;
  }

  public function getTaggableType()
  { return 'project'; }

  public function getTaggableId()
  { return $this->getId(); }

  // We added this to be able to save tags from a form  
  public function setTags($tags)
  {
      $this->tags = is_array($tags) ? new ArrayCollection($tags) : $tags;
  }
}

Our Doctrine configuration files Tag.orm.yml and Tagging.orm.yml in src/FUxCon2013/ProjectsBundle/Resources/config/doctrine look like this:

FUxCon2013\ProjectsBundle\Entity\Tag:
    type: entity
    table: tag
    id:
            id:
                type: integer
                generator:
                    strategy: AUTO
    oneToMany:
        tagging:
            targetEntity: Tagging
            mappedBy: tag
            fetch: EAGER
FUxCon2013\ProjectsBundle\Entity\Tagging:
    type: entity
    table: tagging
    id:
            id:
                type: integer
                generator:
                    strategy: AUTO
    manyToOne:
        tag:
            targetEntity: Tag

The bundle documentation only explains how to read tags. We want to edit them as well. This requires several extensions. The createAction() and in src/FUxCon2013/ProjectsBundle/Controller/ProjectController.php need to actively save the tags:

<?php
/**
 * Creates a new Project entity.
 *
 * @Route("/", name="project_create")
 * @Method("POST")
 * @Template("FUxCon2013ProjectsBundle:Project:new.html.twig")
 */
public function createAction(Request $request)
{
    $project = new Project();
    $form = $this->createForm(new ProjectType(), $project);
    $form->bind($request);

    if ($form->isValid()) {
        $em = $this->getDoctrine()->getManager();

        $em->persist($project);
        $em->flush();

        $this->get('fpn_tag.tag_manager')->saveTagging($project);
        // more work to save pictures
    }
    // error handling done here
}

The same statement needs to be added to editAction() and updateAction().

To actually be able to edit tags in a form, we need to add them to our edit form. We created a new widget type that converts between comma-separated list and list of tags. First the form in src/FUxCon2013/ProjectsBundle/Form/ProjectType.php:

<?php
namespace FUxCon2013\ProjectsBundle\Form;

class ProjectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('tags', 'tags_entry', array('attr' => array(
                'class' => 'tags-field input-block-level'
            )))
        ;
    }
}

The new widget type tags_entry is created in its own class in src/FUxCon2013/ProjectsBundle/Form/TagsType.php:

<?php
namespace FUxCon2013\ProjectsBundle\Form;

class TagsType extends AbstractType
{
    public function __construct(TagManager $tagManager)
    { $this->tagManager = $tagManager; }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new TagsTransformer($this->tagManager);
        $builder->addModelTransformer($transformer);
    }

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

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

This class needs to be registered in app/config/config.yml to be recognized by Symfony and to have it pass a tag manager as parameter:

services:
    fuxcon2013.form.tags_entry:
        class: FUxCon2013\ProjectsBundle\Form\TagsType
        arguments: [ "@fpn_tag.tag_manager" ]
        tags:
            - { name: form.type, alias: tags_entry }

Actually, the class TagsType doesn’t do do any of the actual work. The conversion between text entry field and lists of tags in turn is handled by yet another class, a TagsTransformer in file src/FUxCon2013/ProjectsBundle/Form/TagsTransformer.php:

<?php
/**
 * @see http://symfony.com/doc/current/cookbook/form/data_transformers.html
 */
namespace FUxCon2013\ProjectsBundle\Form;

use Symfony\Component\Form\DataTransformerInterface;

class TagsTransformer implements DataTransformerInterface
{
    private $tagManager;

    public function __construct($tagManager)
    { $this->tagManager = $tagManager; }

    public function transform($tags)
    { return join(', ', $tags->toArray()); }

    public function reverseTransform($tags)
    {
        return $this->tagManager->loadOrCreateTags(
            $this->tagManager->splitTagNames($tags)
        );
    }
}

The TagsTransformer finally uses some API functions of the tag manager to load or create tags entered by a user.

Picture uploading and scaling

Our picture handling in Symfony mimics the way we have used in our CakePHP implementation. There, we have already mentioned that this approach would need some refinements if used on a production site.

Our model has some barebones methods for getting and setting pictures and for moving them to their proper place after update:

<?php
namespace FUxCon2013\ProjectsBundle\Entity;

class Project
{
  
  private function getPicturePath()
  { return __DIR__ . '/../../../../web/images/project/' . $this->getId() . '.jpg'; }

  public function getPicture()
  {
      if (empty($this->picture)) {
          return null;
      }
      return new \Symfony\Component\HttpFoundation\File\File(
          $this->getPicturePath(), /*checkPath*/false
      );
  }

  public function setPicture($picture)
  {
      $mimeType = $picture->getMimeType();
      if (!in_array($mimeType, array('image/jpeg'))) {
          throw Exception("You may only have images of type JPEG");
      }
      $this->picture = $picture;
  }

  public function processPicture()
  {
      if (! ($this->picture instanceof UploadedFile) ) {
          return false;
      }

      $file = $this->getPicturePath();

      $this->picture->move(
          pathinfo($file, PATHINFO_DIRNAME),
          pathinfo($file, PATHINFO_BASENAME)
      );
      return true;
  }
}

in Symfony, the construct corresponding to a view helper is an extension to the Twig template language. For our thumbnails, we have implemented such an extension. This provides us with a new function that we can then use in templates. Here is an excerpt from src/FUxCon2013/ProjectsBundle/Resources/views/Project/index.html.twig:

<a class="thumbnail" href="{{ path('project_show', { 'id': project.id } ) }}">
    <img src="{{ thumbnail([ '/images/project/', project.id, '.jpg' ] | join, '200x200') }}">
</a>

The function definition lives in file src/FUxCon2013/ProjectsBundle/Twig/Extension/FUxCon2013Extension.php. We have already shown it when introducing views in Symfony.

This is the code we use for creating a new project and saving the image:

<?php
namespace FUxCon2013\ProjectsBundle\Controller;

class ProjectController
{
  /**
   * Creates a new Project entity.
   *
   * @Route("/", name="project_create")
   * @Method("POST")
   * @Template("FUxCon2013ProjectsBundle:Project:new.html.twig")
   */
  public function createAction(Request $request)
  {
      $project = new Project();
      $form = $this->createForm(new ProjectType(), $project);
      $form->bind($request);

      if ($form->isValid()) {
          $em = $this->getDoctrine()->getManager();
          $em->persist($project);
          $em->flush();

          // Picture processed after saving. We need the project ID
          if ($project->getPicture() && $project->processPicture()) {
              $this->flash('The project has been saved', 'success');
          }
      }
      // some more processing and error handling
  }
}

Forms and CRUD - Creating and updating projects

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 }

Text formatting with Markdown

For our Symfony implementation, the knplabs/knp-markdown-bundle bundle provides the required functionality to convert Markdown markup into HTML. Once that is installed and registered with the kernel in app/AppKernel.php:

<?php
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        // ... register all core bundles
        $bundles[] = new Knp\Bundle\MarkdownBundle\KnpMarkdownBundle();
    }
}

This bundle provides a new Twig filter, that we can use in our project detail template, here in an excerpt from file src/FUxCon2013/ProjectsBundle/Resources/views/Project/show.html.twig:

<p>{{ project.about | markdown }}</p>

Shortening of teaser texts does not require an extension. It can directly be used in our template for project list, , here in an excerpt from file src/FUxCon2013/ProjectsBundle/Resources/views/Project/index.html.twig:

<p>{{ project.about | truncate }}</p> 

Test data generation

Our Symfony implementation currently does not have any provision for test data generation. The roll-it-yourself approach from our CakePHP implementation is probably ported easily enough.

There are also some solutions that have a more sophisticated approach which could be applied, namely (gleaned from an answer on Stack Overflow):

Some more work is needed to integrate either of these into automated Behat-driven tests.



comments powered by Disqus