Jordi Boggiano | Liip AG

Symfony2 Introduction

Planning

Some Symfony2 wording

The DIC

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.

The DIC

Configuring it: /app/config/config*.yml

stuff.config:
    foo: bar

services:
    blog.controller:
        class: Project\BlogBundle\Controller\BlogController
        arguments:
            - @doctrine.orm.entity_manager
            - @router
            - @request
        shared: true

parameters:
    name: value
            

config_dev.yml will typically import and then override config.yml

Models

Definition:

*Bundle/Resources/config/doctrine/metadata/orm/Project.BlogBundle.Entity.BlogPost.dcm.yml

Project\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 // initial db creation
$ app/console doctrine:schema:update --dump-sql
$ app/console doctrine:schema:update --force
            

Views

Definition: *Bundle/Resources/views/Blog/viewPost.html.twig

{% extends "BlogBundle::layout.html.twig" %}

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

Layout template *Bundle/Resources/views/layout.html.twig

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

Controllers

Base Definition: *Bundle/Controller/BlogController.php

namespace Project\BlogBundle\Controller;

use Project\BlogBundle\Entity\BlogPost;

class BlogController
{
    protected $em;
    protected $router;
    protected $request;

    public function __construct($em, $router, $request)
    {
        $this->em = $em;
        $this->router = $router;
        $this->request = $request;
    }
}
            

Controllers

Actions: *Bundle/Controller/BlogController.php

public function viewPostAction($postId)
{
    $post = BlogPost::getById($this->em, $postId);
    $data = array(
        'post' => $post,
        'commonStuff' => $this->getCommonStuff(),
    );
    return $this->render('BlogBundle:Blog:index.html.twig', $data);
}
            

Model code *Bundle/Entity/BlogPost.php:

public static function getById($em, $id)
{
    return $em->getRepository(__CLASS__)
        ->findOneBy(array('id' => $id));
}
            

Routing

Declaring: /app/config/routing.yml

home:
    pattern:  /
    defaults: { _controller: home.controller:indexAction }
view_post:
    pattern:  /view/{postId}
    defaults: { _controller: home.controller:viewPostAction }
            

Generating:

{{ url('home') }}
{{ path('view_post', {'postId': post.id}) }}
            

Forms

Definition: *Bundle/Entity/BlogPost.php

public function getForm($validator)
{
    $form = new Form('blogpost', $this, $validator);
    $form->add(new TextField('title'));
    $form->add(new TextareaField('content'));
    return $form;
}
            

Validation:

/**
 * @var string
 * @validation:NotBlank(message="mandatory_field")
 */
protected $title;
            

Forms

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

{% extends "BlogBundle::layout.html.twig" %}

{% block content %}
    <form action="{% route 'submitPost' %}" method="POST">
        {{ form_errors(form) }}
        <ul>
            {% for field in form.visibleFields %}
                <li>
                    {{ form_label(field) }}
                    {{ form_field(field) }}
                </li>
            {% endfor %}
            <li><input type="submit" name="submit" value="Post" /></li>
        </ul>
        {{ form_hidden(form) }}
    </form>
{% endblock %}
            

Bundles

Bundle Definition: BlogBundle/BlogBundle.php

namespace Project\BlogBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class BlogBundle extends Bundle
{
    public function getNamespace()
    {
        return __NAMESPACE__;
    }

    public function getPath()
    {
        return strtr(__DIR__, '\\', '/');
    }
}
            

Bundles

Extension Definition: BlogBundle/DependencyInjection/BlogExtension.php

namespace Project\BlogBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class BlogExtension extends Extension
{
    // 'config' in 'configLoad' is the second part of the config load statement
    public function configLoad(array $configs, ContainerBuilder $container)
    {
        // TODO merge configs into one config array
        $config = end($configs);

        $loader = new XmlFileLoader($container, __DIR__.'/../Resources/config');
        $loader->load('blog.xml');

        foreach (array('api_key', 'api_secret') as $var) {
            if (isset($config[$var])) {
                $container->setParameter('blog.comments.'.$var, $config[$var]);
            }
        }
    }
}
                

Bundles

Extension Definition Part2: BlogBundle/DependencyInjection/BlogExtension.php

public function getXsdValidationBasePath()
{
    return __DIR__.'/../Resources/config/schema';
}

public function getNamespace()
{
    return 'http://seld.be/schema/dic/blog';
}

public function getAlias()
{
    // 'blog' is the first part of the config load statement
    return 'blog';
}
            

Bundles

Bundle XML Configuration: BlogBundle/Resources/config/blog.xml

<?xml version="1.0" ?>
<container xmlns="http://www.symfony-project.org/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="blog.comments.class">Project\BlogBundle\DisqusComments</parameter>
        <parameter key="blog.comments.api_key"></parameter>
        <parameter key="blog.comments.api_secret"></parameter>
    </parameters>

    <services>
        <service id="blog.comments" class="%blog.comments.class%">
            <argument type="service" id="some_service_id" />
            <argument>%blog.comments.api_key%</argument>
            <argument>%blog.comments.api_secret%</argument>
        </service>
    </services>
</container>
                

Bundles

Bundle XSD Configuration: BlogBundle/Resources/config/schema/blog-1.0.xsd

<?xml version="1.0" encoding="UTF-8" ?>

<xsd:schema xmlns="http://seld.be/schema/dic/blog"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://seld.be/schema/dic/blog"
    elementFormDefault="qualified">

    <xsd:element name="config">
        <xsd:complexType>
            <xsd:attribute name="api_key" type="xsd:string" />
            <xsd:attribute name="api_secret" type="xsd:string" />
        </xsd:complexType>
    </xsd:element>
</xsd:schema>
            

Bundles

Bundle Registration: app/AppKernel.php

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

Enabling it: app/config/config.yml

blog.config: ~
            

CLI Command:

$ app/console init:bundle Project/BlogBundle
            

Testing

PHPUnit Config: app/phpunit.xml.dist

<?xml version="1.0" encoding="UTF-8"?>

<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         colors="false"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"
         bootstrap="../src/autoload.php"
>
    <testsuites>
        <testsuite name="App/Bundles Test Suite">
            <directory>../src/*/*/Tests</directory>
            <directory>../src/*/*/*/Tests</directory>
        </testsuite>
    </testsuites>

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

Testing

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

namespace Project\BlogBundle\Tests\Entity;

use Project\BlogBundle\Entity\BlogPost;

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

Testing

Integration Testing: *Bundle/Tests/Controller/BlogController.php

namespace Application\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->assertTrue($count > 0);
    }
}
            

Running tests:

$ phpunit -c app/
            

Resources