A fast introduction to Symfony2

You can read the step by step Symfony 2 Jobeet Tutorial that describes the creation of a web application with the Symfony2 framework,  from the specifications to the implementation. It is targeted at beginners who want to learn Symfony2, understand how it works, and also learn about the best web development practices.

Symfony2 requires at least PHP 5.3.2. You can download from http://symfony.com/download.

In Symfony2 you will not create frontend and backend applications like in Symfony 1.

The Symfony2 project is organized in bundles (in the src directory: src/Dbla/MyappBundle).

To generate a bundle run:


php app/console generate:bundle --namespace=Dbla/MyappBundle --format=yml

To clear the cache:


php app/console cache:clear --env=prod --no-debug

Controllers

Controllers are located in /src/Dbla/MyappBundle/Controller/

<?php
// src/Dbla/MyappBundle/Controller/HelloController.php
namespace Dbla\MyappBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
  public function indexAction($name)
  {
    // ...
  }
}

We have to declare the namespace of the classes we create. We have to “use” the classes we need.

Views

Views should be saved in /src/Dbla/MyappBundle/Resources/views/

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Symfony!</title>
  </head>
  <body>
    <h1>{{ page_title }}</h1>

    <ul id="navigation">
      {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
      {% endfor %}
    </ul>
  </body>
</html>

Twig

{{ }} - Says something (php echo)
{% %} - Does something (for in, if else, etc)
{# #} - Comment

Twig filters:

{{ title|upper }}
{{ body|raw }}

Including teplates or controllers (partials and components):


{% include 'AcmeArticleBundle:Article:articleDetails.html.twig' with {'article': article} %}

{% render "AcmeArticleBundle:Article:recentArticles" with {'max': 3} %}

Twig blocks:

{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
  <head>...</head>
  <body>
  <div id="content">
    {% block body %}some default content{% endblock %}
  </div>
  </body>
</html>

The default content from {% block body %} will be replaced in the template that extends the aboove:

{# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #}
{% extends '::base.html.twig' %}

{% block body %}
  {% for entry in blog_entries %}
    <h2>{{ entry.title }}</h2>
    <p>{{ entry.body }}</p>
  {% endfor %}
{% endblock %}

Assets:


<img src="{{ asset('images/logo.png') }}" alt="Symfony!" />

<link href="{{ asset('css/blog.css') }}" rel="stylesheet" type="text/css" />

Including Stylesheets and Javascripts in Twig:

{# app/Resources/views/base.html.twig #}
<html>
  <head>
    {# ... #}
    {% block stylesheets %}
      <link href="{{ asset('/css/main.css') }}" type="text/css" rel="stylesheet" />
    {% endblock %}
  </head>
  <body>
    {# ... #}
    {% block javascripts %}
      <script src="{{ asset('/js/main.js') }}" type="text/javascript"></script>
    {% endblock %}
  </body>
</html>

Including new stylesheet in the template:

{# src/Acme/DemoBundle/Resources/views/Contact/contact.html.twig #}
{% extends '::base.html.twig' %}

{% block stylesheets %}
  {{ parent() }}

  <link href="{{ asset('/css/contact.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}

{# ... #}

Routing

# app/config/routing.yml
blog_show:
  pattern:   /blog/{slug}
  defaults:  { _controller: AcmeBlogBundle:Blog:show }

Database

Connection config file: app/config/parameters.ini

Creating ORM mapping file (similar to schema.yml):

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
  type: entity
  table: product
  id:
    id:
      type: integer
      generator: { strategy: AUTO }
  fields:
    name:
      type: string
      length: 100
    price:
      type: decimal
      scale: 2
    description:
      type: text

To generate entities (models) (src/Acme/StoreBundle/Entity/Product.php) run:

php app/console doctrine:generate:entities AcmeStoreBundle

To create/update database tables:

php app/console doctrine:schema:update --force

Generate crud:

php app/console doctrine:generate:crud

To have Symfony2 generate repository classes (similar to Propel Peer classes) specify the repositoryClass key in the ORM mapping file:

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
  type: entity
  repositoryClass: Acme\StoreBundle\Repository\ProductRepository
  # ...

Database relations: one to many

# src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.yml
Acme\StoreBundle\Entity\Category:
  type: entity
  # ...
  oneToMany:
    products:
      targetEntity: Product
      mappedBy: category

Database relations: many to one

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
  type: entity
  # ...
  manyToOne:
    category:
      targetEntity: Category
      inversedBy: products
      joinColumn:
        name: category_id
        referencedColumnName: id

Lifecycle callbacks (auto created_at/updated_at columns):

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
  type: entity
  # ...
  lifecycleCallbacks:
    prePersist: [ setCreatedValue ]
    postPersit:
    preUpdate: [ setUpdatedAtValue ]
    postUpdate:
    preRemove:
    postRemove:
    postLoad:
    # ...

Saving (persisting) objects:

$product = new Product();
$product->setName('A Foo Bar');
$product->setPrice('19.99');
$product->setDescription('Lorem ipsum dolor');

$em = $this->getDoctrine()->getEntityManager();
$em->persist($product);
$em->flush();

Fetching objects:

$repo = $this->getDoctrine()->getRepository('AcmeStoreBundle:Product');

$product = $repo->findOneById($id);
$product = $repo->findOneByName($foo);
$product = $repo->findByPrice(19.99);
$product = $repo->findAll();
$product = $repo->findBy(array('name' => 'foo', 'price' => 19.99));

//ordered
$product = $repo->findBy(array('price' => 19.99), array('name' => 'ASC'));

Updating objects:

$em = $this->getDoctrine()->getEntityManager();
$product = $em->getRepository('AcmeStoreBundle:Product')->find($id);

$product->setName('New product name!');
$em->flush();

Deleting objects:

$em->remove($product);
$em->flush();

Querying for objects using DQL:

$em = $this->getDoctrine()->getEntityManager();
$query = $em->createQuery(
  'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC'
);
$query->setParameter('price', '19.99');

$products = $query->getResult();
$product = $query->getSingleResult();

Join:

$em = $this->getDoctrine()->getEntityManager();
$query = $em->createQuery(
  'SELECT DISTINCT p FROM DblaLemontageBundle:Promotion p JOIN p.promotion_categories pc WHERE pc.category_id = :category_id'
);

// promotion_categories – relation name from Promotion.orm.yml

Forms

Creating forms in controller:

// create a task and give it some dummy data
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));

$form = $this->createFormBuilder($task)
  ->add('task', 'text')
  ->add('dueDate', 'date')->getForm();

return $this->render('AcmeTaskBundle:Default:new.html.twig', array('form' => $form->createView()));

Form classes:

// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class TaskType extends AbstractType
{
  public function buildForm(FormBuilder $builder, array $options)
  {
    $builder->add('task');
    $builder->add('dueDate', null, array('widget' => 'single_text'));
  }
  public function getName()
  {
    return 'task';
  }
  public function getDefaultOptions(array $options)
  {
    return array(
      'data_class' => 'Acme\TaskBundle\Entity\Task',
    );
  }
}

Using the form in controller:

// src/Acme/TaskBundle/Controller/DefaultController.php
use Acme\TaskBundle\Form\Type\TaskType;

public function newAction()
{
  $task = // ...
  $form = $this->createForm(new TaskType(), $task);
  // ...
}

Handling the form submission:

if ($request->getMethod() == 'POST')
{
  $form->bindRequest($request);

  if ($form->isValid())
  {
    // perform some action, such as saving the task to the database
    return $this->redirect($this->generateUrl('task_success'));
  }
}

Form validation:

We validate the underlying object using yml:

# Acme/TaskBundle/Resources/config/validation.yml
Acme\TaskBundle\Entity\Task:
  properties:
    task:
      - NotBlank: ~
    dueDate:
      - NotBlank: { message: Date is required. }
      - Type:
    type: \DateTime
    message: The value {{ value }} is not a valid {{ type }}.

Validating a form with no underlying object (in controller):

// import the namespaces above your controller class
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\Collection;

$collectionConstraint = new Collection(array('name' => new MinLength(5),
  'email' => new Email(array('message' => 'Invalid email address'))));

// create a form, no default values, pass in the constraint option
$form = $this->createFormBuilder(null, array(
  'validation_constraint' => $collectionConstraint,
))->add('email', 'email')    // ...
;

Validating a value in the controller:

$emailConstraint = new Email();
$emailConstraint->message = 'Invalid email address';
$errorList = $this->get('validator')->validateValue($email, $emailConstraint);
if (count($errorList) == 0)
{
  // this IS a valid email address, do something
}
else
{
  // this is *not* a valid email address
  $errorMessage = $errorList[0]->getMessage();

  // do something with the error
}

Form rendering:

<form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
  {{ form_widget(form) }}
  <input type="submit" />
</form>

Rendering each field by hand:

{{ form_errors(form) }}

<div>
  {{ form_label(form.task) }}
  {{ form_errors(form.task) }}
  {{ form_widget(form.task) }}
</div>

<div>
  {{ form_label(form.dueDate, 'Custom label') }}
  {{ form_errors(form.dueDate) }}
  {{ form_widget(form.dueDate, {'attr' : {'class' => 'custom_class'} }) }}
</div>

{{ form_rest(form) }

Security

Security is a two step process: authentication (firewalls) & authorization (access control)

# app/config/security.yml
security:
  firewalls: # authentication
    secured_area:
      pattern: ^/ # everything is secured,
      anonymous: ~ # even if anonymous users have access
    http_basic:
      realm: "Secured Demo Area"

  access_control: # authorization
    - { path: ^/admin, roles: ROLE_ADMIN }

  providers:
    in_memory:
      users:
        ryan:  { password: ryanpass, roles: 'ROLE_USER' }
        admin: { password: kitten, roles: 'ROLE_ADMIN' }

  encoders:
    Symfony\Component\Security\Core\User\User: plaintext

Using a login form:

# app/config/security.yml
security:
  firewalls:
    secured_area:
      pattern:    ^/
      anonymous: ~
      form_login:
        login_path:  /login
        check_path:  /login_check

 

# app/config/routing.yml
login:
  pattern:   /login
  defaults:  { _controller: AcmeSecurityBundle:Security:login }
login_check:
  pattern:   /login_check

We only have to display the login form and errors (no login validation is required, this is taken care by Symfony2):

// src/Acme/SecurityBundle/Controller/Main;
namespace Acme\SecurityBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class SecurityController extends Controller
{
  public function loginAction()
  {
    $request = $this->getRequest();
    $session = $request->getSession();

    // get the login error if there is one
    if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR))
    {
      $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
    }
    else
    {
      $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
    }

    return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
      // last username entered by the user
      'last_username' => $session->get(SecurityContext::LAST_USERNAME),
      'error'         => $error )
    );
  }
}

The view:

{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
  <div>{{ error.message }}</div>
{% endif %}

<form action="{{ path('login_check') }}" method="post">
  <label for="username">Username:</label>
  <input type="text" id="username" name="_username" value="{{ last_username }}" />

  <label for="password">Password:</label>
  <input type="password" id="password" name="_password" />

  {#
    If you want to control the URL the user is redirected to on success
    <input type="hidden" name="_target_path" value="/account" />
  #}

  <input type="submit" name="login" />
</form>

User providers:

# app/config/security.yml
security:
  # ...
  providers:
    default_provider: # in memory provider
      users:
        ryan:  { password: ryanpass, roles: 'ROLE_USER' }
        admin: { password: kitten, roles: 'ROLE_ADMIN' }

 

# app/config/security.yml
security:
  providers:
    main: # database provider
      entity: { class: Acme\UserBundle\Entity\User, property: username }

Database user provider – the entity class has to implement Symfony\Component\Security\Core\User\UserInterface:

// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class User implements UserInterface
{
  /**
  * @ORM\Column(type="string", length="255")
  */
  protected $username;

  // ...
}

Password encoders:

# app/config/security.yml
security:
  # ...
  providers:
    in_memory:
      users:
        ryan: { password: bb87a29949f3a1ee0559f8a5735748, roles: 'ROLE_USER' }
        admin: { password: 74913f5cd5f61ec0bcfdb775414c2f, roles: 'ROLE_ADMIN' }

  encoders:
    Symfony\Component\Security\Core\User\User:
      algorithm:   sha1
      iterations: 1
      encode_as_base64: false

 

# app/config/security.yml
security:
  # ...

  encoders:
    Acme\UserBundle\Entity\User: sha512

Encoding the password on save (in controller):

$factory = $this->get('security.encoder_factory');
$user = new Acme\UserBundle\Entity\User();
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword('ryanpass', $user->getSalt());
$user->setPassword($password);

To get the authenticated User object use:

$user = $this->get('security.context')->getToken()->getUser();

Logging out:

# app/config/security.yml
security:
  firewalls:
    secured_area:
    # ...
    logout:
      path:   /logout
      target: /
  # ...

 

# app/config/routing.yml
logout:
  pattern:   /logout

That’s all! For more details and in depth documentation visit symfony.com.

2 thoughts on “A fast introduction to Symfony2

  1. If anybody reads my comment before reading the article – This article is one of the best cheat sheets on Symfony2 outta there, and you can print it and stick it on the wall. Thank you so much Dragos Holban!

  2. I wish I had found this page when I started learning symfony2… Had to find a lot of these things hidden behind fat layer of documentation or floating somewhere on internet. Great job with gathering all that stuff in one place.

Leave a Reply

Your email address will not be published. Required fields are marked *


− five = 2

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>