Symfony2

Planning

Some clarifications

The DIC

Contains a definition of all the inter-class/object dependencies in your project. Through this we can get rid of "new foo();" and "foo::getInstance()", they harm extensibility.

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:
    ctrl_home:
        class: Application\BlogBundle\Controller\BlogController
        arguments:
            - @service_container
            - @doctrine.orm.entity_manager
            - @router
            - @request
        shared: true

parameters:
    name: value
            

config_dev.yml imports and then overrides config.yml

MVC Basics

Main goal: Separation of concerns

Fat models, thin controllers

Views just display stuff

Models

Definition: *Bundle/Resources/config/doctrine/metadata/orm/>Name.Spaces.Class>.dcm.yml

Application\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 (once only!):

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

Views

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

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

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

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

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

Controllers

Base Definition: *Bundle/Controller/BlogController.php

namespace Application\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Application\BlogBundle\Entity\BlogPost;

class BlogController extends Controller
{
    protected $em;
    protected $routing;
    protected $request;

    public function __construct($container, $em, $router, $request)
    {
        $this->container = $container;
        $this->em = $em;
        $this->routing = $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.twig', $data);
}
            

Model code:

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

Routing

Declaring: /app/config/routing.yml

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

Generating:

{% route %}
{% route 'home' %}
{% route 'view_post' with ['postId': post.id] %}
            

Forms

Definition: *Bundle/Entity/BlogPost.php

public function getCreateForm($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.twig

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

{% block content %}
    <form action="{% route 'submitEvent' %}" method="POST">
        <ul>
            {% for field in form %}
                {% if field.isHidden() %}
                    {{ field.render()|safe }}
                {% else %}
                    <li>{{ field.renderErrors()|safe }}
                    <label for="{{ field.id }}">{{ field.key }}</label>
                    {{ field.render()|safe }}</li>
                {% endif %}
            {% endfor %}
            <li><input type="submit" name="submit" value="Post" /></li>
        </ul>
    </form>
{% endblock %}
            

Bundles

Bundle Definition: BlogBundle/BlogBundle.php

namespace Application\BlogBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class BlogBundle extends Bundle
{
}
            

Bundles

Extension Definition: BlogBundle/DependencyInjection/BlogExtension.php

namespace Application\BlogBundle\DependencyInjection;

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

class BlogExtension extends Extension
{
    protected $resources = array(
        'config' => 'blog.xml',
    );

    // 'config' in 'configLoad' is the second part of the config load statement
    public function configLoad($config, ContainerBuilder $container)
    {
        // 'blog' is the name of a service defined in config.xml here
        if (!$container->hasDefinition('blog')) {
            $loader = new XmlFileLoader($container, __DIR__.'/../Resources/config');
            $loader->load($this->resources['config']);
        }
        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">Bundle\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 Application\BlogBundle\BlogBundle(),
            

Enabling it: app/config/config.yml

blog.config: ~
            

CLI Command:

$ app/console init:bundle Application/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/Bundle/*/Tests</directory>
            <directory>../src/Bundle/*/*/Tests</directory>
            <directory>../src/Application/*/Tests</directory>
        </testsuite>
    </testsuites>

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

Testing

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

namespace Application\BlogBundle\Tests\Entity;

use Bundle\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.bat -c app/phpunit.xml.dist
            

Resources