Symfony2 Introduction


Jordi Boggiano   @seldaek

http://nelm.io/

About Me

Belgian living in Z├╝rich

Weby stuff for 10 years
http://seld.be

Symfony2 core dev and other OSS contributions
http://github.com/Seldaek

Recently started Nelmio
http://nelm.io
We do Symfony2 & Frontend Performance consulting

Agenda

Symfony2 is ..

PHP 5.3 / Namespaces

namespace Acme\Session\Storage;

use Foo\Bar\Baz;

class TestStorage extends Baz
{
    // [...]
}
            

PHP 5.3 / Namespaces

Directory Structure

app/                # App Config
  - cache/
  - config/
  - logs/
  - Resources/
  - AppKernel.php
  - autoload.php
  - console
bin/vendors
src/                # App Bundles & Source
  - Acme/
  - Foo/
vendor/             # Third Party Libraries
  - bundles/        # Third Party Bundles
  - symfony/
  - twig/
  - ...
web/
    app.php
    app_dev.php
            

Symfony Environments

Controllers

Base Definition: *Bundle/Controller/BlogController.php

namespace Acme\BlogBundle\Controller;

use Acme\BlogBundle\Entity\BlogPost;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class BlogController extends Controller
{
    // [...]
}
            

Controllers

Actions return Responses*Bundle/Controller/BlogController.php

// [...]

public function indexAction()
{
    $content = 'Hello world';
    $response = new Response($content, 200);
    $response->headers->set('X-Hello', 'World');
    return $response;
}
            

Controllers

Actions: Reading GET/POST params*Bundle/Controller/BlogController.php

// [...]

public function indexAction()
{
    $request = $this->getRequest();
    $request->server->get('foo'); // $_SERVER
    $request->request->get('foo', 'bar'); // $_POST
    $request->query->get('foo'); // $_GET
    $request->cookies->get('foo'); // $_COOKIE

    // [...]
}
            

Controllers

Actions: Accessing the session*Bundle/Controller/BlogController.php

// [...]

public function indexAction()
{
    $request = $this->getRequest();
    $session = $request->getSession();
    $session->set('foo', 'bar');
    $session->get('foo');
    $session->setFlash('foo', 'bar');

    // [...]
}
            

Controllers

Actions: Redirects*Bundle/Controller/BlogController.php

// [...]

public function indexAction()
{
    return new RedirectResponse($this->generateUrl('home'));
}
            

Controllers

Actions: Error pages*Bundle/Controller/BlogController.php

// [...]

public function indexAction()
{
    throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException('Lala not found'); // 404
    throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException('Go away'); // 403
}
            

Controllers

Actions: Rendering Templates*Bundle/Controller/BlogController.php

// [...]

public function viewPostAction($postId)
{
    $repo = $this->getDoctrine()
        ->getRepository('AcmeBlogBundle:BlogPost');
    $post = $repo->findOneBy(array('id' => $postId));
    $data = array(
        'post' => $post,
        'commonStuff' => $this->getCommonStuff(),
    );
    return $this->render(
        'AcmeBlogBundle:Blog:index.html.twig',
        $data
    );
}
            

Routing

Declaring: /app/config/routing.yml

<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://www.symfony-project.org/sc [...]">
    <route id="home" pattern="/">
        <default key="_controller">
            AcmeBlogBundle:Blog:index
        </default>
    </route>
    <route id="view_post" pattern="/view/{postId}">
        <default key="_controller">
            AcmeBlogBundle:Blog:viewPost
        </default>
    </route>
</routes>
            
home:
    pattern:  /
    defaults:
        _controller: AcmeBlogBundle:Blog:index
view_post:
    pattern:  /view/{postId}
    defaults:
        _controller: AcmeBlogBundle:Blog:viewPost
            

Bundles

Bundles

Bundle Definition: *Bundle/AcmeBlogBundle.php

namespace Acme\BlogBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeBlogBundle extends Bundle
{
    // only if you want to extend
    public function getParent()
    {
        return 'FooBarBundle';
    }
}
            

Dependency Injection

class MovieFilter
{
    public function __construct()
    {
        $this->lister = new MovieLister('movies.csv');
    }

    public function getMovies($filter)
    {
        foreach ($this->lister as $movie) {
            if (false !== strpos($movie, $filter)) {
                $movies[] = $movie;
            }
        }
        return $movies;
    }
}
            

Dependency Injection

class MovieFilter
{
    public function __construct($file)
    {
        $this->lister = new MovieLister($file);
    }

    public function getMovies($filter)
    {
        foreach ($this->lister as $movie) {
            if (false !== strpos($movie, $filter)) {
                $movies[] = $movie;
            }
        }
        return $movies;
    }
}
            

Dependency Injection

class MovieFilter
{
    public function __construct(ListerInterface $lister)
    {
        $this->lister = $lister;
    }

    public function getMovies($filter)
    {
        foreach ($this->lister as $movie) {
            if (false !== strpos($movie, $filter)) {
                $movies[] = $movie;
            }
        }
        return $movies;
    }
}
            

Dependency Injection Container

Contains a definition of all the inter-class & inter-object dependencies in your project.

Goals:

It adds some development overhead when creating bundles, but it'll save you a bunch of hairs when writing application code.

Dependency Injection Container

Configuring it: src/*/*Bundle/Resources/config/services.xml

<?xml version="1.0"?>
<container xmlns="http://symfony.com/schema [...]">
    <parameters>
        <parameter key="session.class">Symfony\Component\HttpFoundation\Session</parameter>
    </parameters>
    <services>
        <service id="session" class="%session.class%">
            <argument type="service" id="session.storage" />
            <argument>%session.default_locale%</argument>
        </service>
    </services>
</container>
            
parameters:
    session.class: Symfony\Component\HttpFoundation\Session
services:
    session:
        class: %session.class%
        arguments:
            - @session.storage
            - %session.default_locale%
            

Bundles

Extension Definition: *Bundle/DependencyInjection/BlogExtension.php

namespace Acme\BlogBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

class AcmeBlogExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $locator = new FileLocator(__DIR__.'/../Resources/config');
        $loader = new Loader\XmlFileLoader($container, $locator);
        $loader->load('services.xml');

        foreach ($config as $option => $value) {
            $container->setParameter('acme_blog.'.$option, $value);
        }
    }
}
                

Bundles

Bundle Registration: app/AppKernel.php

// add this to the registerBundles() array of bundles
new Acme\BlogBundle\BlogBundle(),
            

Bundles

Extension Configuration: app/config/config*.yml

acme_blog:
    foo_option: bar
    api_key: bigsecret
            

Models

Model code *Bundle/Entity/BlogPost.php:

namespace Acme\BlogBundle\Entity;

class BlogPost
{
    private $id
    private $title;
    private $content;
    private $createdAt;

    public function getTitle()
    {
        return $title;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    // [...]
}
            

Models

Definition:

*Bundle/Resources/config/doctrine/mapping.orm.yml

Acme\BlogBundle\Entity\BlogPost:
    type: entity
    table: blog_post
    fields:
        id:
            type: integer
            id: true
            generator:
                strategy: IDENTITY
        title:
            type: string
            length: 255
        content:
            type: text
        createdAt:
            type: datetime
            

Generating:

$ app/console doctrine:generate:entities
$ app/console doctrine:schema:create
$ app/console doctrine:schema:update --dump-sql
$ app/console doctrine:schema:update --force
            

Views

Twig Templates

Why not PHP?

<ul>
    {% for user in users %}
        <li>{{ user.username }}</li>
    {% else %}
        <li>No users found</li>
    {% endfor %}

    {# comments #}
    {{ varOutput }}
</ul>
            

Views

Template Inheritance: *Bundle/Resources/views/Blog/viewPost.html.twig

{% extends "::base.html.twig" %}

{% block content %}
    <h1>{{ post.title|upper }}</h1>
    {{ post.content|raw }}
{% endblock %}
            

Layout template app/Resources/views/base.html.twig

<html>
    <head></head>
    <body>
    header, menu, whatever
    {% block content %}
    {% endblock %}
    footer
    </body>
</html>
            

Views

Generating routes:

{{ url('home') }} {# => http://example.org/ #}
{{ path('view_post', {'postId': post.id}) }} {# => /view/2 #}
            

Views

Controlling whitespace:

{% if foo -%}
    <a href=".... looong line .....">
        {{- someText -}}
    </a>
{%- endif %}
            
Outputs:
<a href=".... looong line .....">Text</a>
            

Views

More...

{% include 'AcmeBlogBundle:Post:index.html.twig'
    with { 'foo': 'bar' } %}

{% render 'AcmeBlogBundle:Post:index' %}

{% render 'AcmeBlogBundle:Post:index'
    { 'standalone': true } %}

{% render 'AcmeBlogBundle:Post:index'
    with { 'limit': 2 }, { 'standalone': true } %}
            

Forms

Definition: *Bundle/Form/Type/PostType.php

namespace Acme\BlogBundle\Form\Type;

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

class PostType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('title');
        $builder->add('content', 'textarea');
    }

    // [...]
}
            

Forms

Definition: *Bundle/Form/Type/PostType.php

// [...]

public function getDefaultOptions(array $options)
{
    return array(
        'data_class' => 'Acme\BlogBundle\Entity\BlogPost',
    );
}

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

Forms

Validation:

use Symfony\Component\Validator\Constraints as Assert;

/**
 * @var string
 * @Assert\NotBlank()
 * @Assert\MinLength(limit="5", message="This is too short!")
 */
protected $title;
            

Forms

Rendering: *Bundle/Resources/views/Default/form.html.twig

{% extends "::base.html.twig" %}

{% block content %}
    <form action="{{ path('submitPost') }}" method="POST">
        <p>
            {{ form_label(form.title) }}
            {{ form_errors(form.title) }}
            {{ form_widget(form.title) }}
        </p>
        {{ form_row(form.content) }}
        <input id="submit" type="submit" value="Submit" />
        {{ form_rest(form) }}
    </form>
{% endblock %}
            

Forms

Leveraging: *Bundle/Controller/BlogController.php

// [...]
public function submitPackageAction()
{
    $post = new BlogPost;
    $form = $this->createForm(new PostType, $post);
    $em = $this->getDoctrine()->getEntityManager();
    $request = $this->get('request');
    if ('POST' === $request->getMethod()) {
        $form->bindRequest($request);
        if ($form->isValid()) {
            $post->setAuthor($this->getUser());
            $em->persist($post);
            $em->flush();

            $request->getSession()->setFlash('success',
                'Blog post added');
            return new RedirectResponse(
                $this->generateUrl('home'));
        }
    }
    return array('form' => $form->createView());
}
            

Testing

PHPUnit Config: app/phpunit.xml.dist

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    backupGlobals               = "false"
    backupStaticAttributes      = "false"
    colors                      = "true"
    convertErrorsToExceptions   = "true"
    convertNoticesToExceptions  = "true"
    convertWarningsToExceptions = "true"
    processIsolation            = "false"
    stopOnFailure               = "false"
    syntaxCheck                 = "false"
    bootstrap                   = "bootstrap.php.cache" >

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>../src/*/*Bundle/Tests</directory>
            <directory>../src/*/Bundle/*Bundle/Tests</directory>
        </testsuite>
    </testsuites>

    <!--
    <php>
        <server name="KERNEL_DIR" value="/path/to/your/app/" />
    </php>
    -->

    <filter>
        <whitelist>
            <directory>../src</directory>
            <exclude>
                <directory>../src/*/*Bundle/Resources</directory>
                <directory>../src/*/*Bundle/Tests</directory>
                <directory>../src/*/Bundle/*Bundle/Resources</directory>
                <directory>../src/*/Bundle/*Bundle/Tests</directory>
            </exclude>
        </whitelist>
    </filter>
</phpunit>
                

Testing

Unit Testing: *Bundle/Tests/Entity/BlogPostTest.php

namespace Acme\BlogBundle\Tests\Entity;

use Acme\BlogBundle\Entity\BlogPost;

class BlogPostTest extends \PHPUnit_Framework_TestCase
{
    public function testStuff()
    {
        $this->assertTrue(true);
    }
}
            

Testing

Functional Testing: *Bundle/Tests/Controller/BlogControllerTest.php

namespace Acme\BlogBundle\Tests\Entity;

use Bundle\BlogBundle\Controller\BlogController;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class TranslatorTest extends WebTestCase
{
    public function testSpecifications()
    {
        $client = $this->createClient();
        $crawler = $client->request('GET', '/view/3');
        $count = $crawler
            ->filter('html:contains("Blah blah")')
            ->count();
        $this->assertEquals(1, $count);
    }
}
            

Running tests:

$ phpunit -c app/
            

And more...

Resources

Thank you.

Questions?

Slides up at slides.seld.be

jordi@nelm.io