UK Symfony Meetup
             November, 2012




                                           Symfony
                                           War Stories

                                                          Jakub Zalas
                                                         SensioLabsUK
http://www.rgbstock.com/bigphoto/mhY2DCY
Jakub Zalas a.k.a. Kuba


○ Remembers symfony 1.0
  (2007)
○ Open Source contributor
○ BDD advocate
○ Works at Sensio Labs UK
○ ZCE & Sensio Labs         ○ Twitter: @jakub_zalas
  Certified Symfony         ○ Blog: http://zalas.eu
  Developer                 ○ Github: @jakzal
Symfony War Stories
● Bundling the code
● Thinking outside of the bundle
● Being lazy
● Learning your stuff
● Keeping your playground clean
● Building the quality in
Bundling
                                           the code
http://www.rgbstock.com/bigphoto/mfXkYUq
VS
Whenever in doubt...
● Start simple and let the design emerge
● Extract bundles when you need to reuse it
  or you see it would improve clarity
● Avoid two-way dependencies between
  bundles
Thinking
                                            outside of
                                           the bundle




http://www.rgbstock.com/bigphoto/mfBYR2S
VS
To keep your design clean...
● Treat bundles as an integration layer
  between PHP libraries and the Symfony
  framework
● In your libraries, avoid dependencies on
  bundles
Being lazy




http://www.sxc.hu/photo/858871
There's a bundle for that!
Learning
                                  your stuff




http://www.sxc.hu/photo/1095604
Have you ever used?

● Event listeners and subscribers
● Form data transformers
● Twig extensions
● Profiler extensions
● DIC compiler passes
● more...
It's easy!




             let's see few examples...
Event listeners and subscribers (Doctrine)


 services:
    sensio.listener.search_indexer:
        class: SensioListenerSearchIndexer
        tags:
            -
                name: doctrine.event_listener
                event: postPersist
namespace SensioListener;

use DoctrineORMEventLifecycleEventArgs;
use SensioEntityProduct;

class SearchIndexer
{
    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $entityManager = $args->getEntityManager();

        if ($entity instanceof Product) { /* Index me */ }
    }
}
Lifecycle callbacks (Doctrine)

/**
  * @ORMEntity()
  * @ORMHasLifecycleCallbacks()
  */
class User
{
     /**
      * @ORMPrePersist
      */
     public function updateCreatedAt() { /* ... */ }
}
Event listeners and subscribers (Symfony)


services:
  sensio.action_listener:
    class: SensioMyBundleEventListenerMyListener
    tags:
      -
        name: kernel.event_listener
        event: kernel.controller
        method: onKernelController
namespace SensioMyBundleEventListener;

class MyListener
{
    public function onKernelController(
                             FilterControllerEvent $event)
    {
        $controller = $event->getController();
        $request = $event->getRequest();

        // ...
    }
}
Event listeners and subscribers (Forms)

$callback = function(FormEvent $event) {
    if (null === $event->getData()) {
        $formData = $event->getForm()->getData();
        $event->setData($formData);
    }
};

$builder->add('email', 'text');
$builder->get('email')
    ->addEventListener(
        FormEvents::PRE_BIND, $callback
    );
Data transformers (Forms)
class IssueToNumberTransformer
                 implements DataTransformerInterface
{
    public function transform($issue)
    {
        // return issue number
    }

    public function reverseTransform($number)
    {
        // return Issue instance
    }
}
$field = $builder->create('issue', 'text')
    ->addModelTransformer(new IssueToNumberTransformer());

$builder->add($field);
Twig extensions

<!-- Instead of: -->

<div class="price">
    ${{ 9800.333|number_format(2, '.', ',') }}
</div>

<!-- just do: -->

<div class="price">
    {{ 9800.333|price }}
</div>
class SensioExtension extends Twig_Extension
{
    public function getFilters()
    {
        return array(
            'price' => new Twig_Filter_Method(
                                     $this, 'priceFilter')
        );
    }

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

    public function priceFilter($number, /* ... */ ) {}
}
public function priceFilter(
    $number, $decimals = 0, $decPoint = '.', $sep = ',')
{
    $price = number_format(
        $number, $decimals, $decPoint, $sep
    );

    return '$'.$price;
}
services:
    sensio.twig.acme_extension:
        class: SensioTwigSensioExtension
        tags:
            - { name: twig.extension }
Web profiler
Debugging commands



./app/console container:debug
./app/console container:debug twig.loader

./app/console router:debug
./app/console router:debug homepage

./app/console router:match /logout
Debugging commands
http://www.rgbstock.com/bigphoto/mZTKnw6




Keeping your playground clean
Use the service container!
public function publishAction($date)
{
    $show = $this->findShow($date);
    $fileName = sprintf('show-%s.json', $date->format('Y-m-d'));

    while (file_exists($fileName)) {
        $fileName = $fileName.preg_replace('/.json$/', '-1.json');
    }

    $options = array( /* options here */ );
    $s3 = new AmazonS3($options);

    $objectOptions = array(
        'body' => json_encode($show), 'acl' => $options['objectAcl']);

    if (!$s3->create_object('my-bucket', $fileName, $objectOptions) {
        throw new Exception('Could not save file');
    }

    // ...
}
public function publishAction($date)
{
    $show = $this->findShow($date);

    $fileNameResolver = new FileNameResolver();
    $filesystem = $this->get('gafrette_filesystem');

    $publisher = new Publisher(
        $fileNameResolver, $filesystem
    );

    $publisher->publish($show, $date);
}
Use the service container!



public function publishAction($date)
{
    $show = $this->findShow($date);
    $publisher = $this->get('sensio.publisher');
    $publisher->publish($show, $date);
}
Be SOLID!
/**
  * @ORMEntity()
  */
class Product
{
     // ...

    public function createOrderReport($month)
    {
        // $this->em->getRepository() ...
    }

    public function setEntityManager($em)
    {
        $this->em = $em;
    }
}
class OrderReportGenerator
{
    private $em = null;

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

    public function generate(Product $product, $month)
    {
        // ...
    }
}
Building quality in




http://www.rgbstock.com/photo/nrcyovQ/perfect
Unit tests

class ReportTest extends PHPUnit_Framework_TestCase
{
    public function testThatItReturnsFilePath()
    {
        $report = new Report();

        $file = $report->create('January');

        $this->assertSame('/january.csv', $file);
    }
}
... or specs


namespace spec;

class Report extends PHPSpec2ObjectBehavior
{
    function it_returns_file_path()
    {
        $this->create('January')
             ->shouldReturn('/january.csv');
    }
}
Managing expectations
Feature: Viewing recent news on the homepage
  As a Marketing Manager
  I want Visitors to see recent news on the homepage
  In order to get them interested in the content

 Scenario: Viewing recent news
   Given there are 10 news items
     And there were 5 news items written since yesterday
    When I visit the homepage
    Then I should see 5 recent news items in the news section

 Scenario: There is not enough news
   Given there are 10 news items
     But there were 3 news items written since yesterday
    When I visit the homepage
    Then I should see 3 recent news items in the news section
Thank you!

Symfony War Stories

  • 1.
    UK Symfony Meetup November, 2012 Symfony War Stories Jakub Zalas SensioLabsUK http://www.rgbstock.com/bigphoto/mhY2DCY
  • 2.
    Jakub Zalas a.k.a.Kuba ○ Remembers symfony 1.0 (2007) ○ Open Source contributor ○ BDD advocate ○ Works at Sensio Labs UK ○ ZCE & Sensio Labs ○ Twitter: @jakub_zalas Certified Symfony ○ Blog: http://zalas.eu Developer ○ Github: @jakzal
  • 3.
    Symfony War Stories ●Bundling the code ● Thinking outside of the bundle ● Being lazy ● Learning your stuff ● Keeping your playground clean ● Building the quality in
  • 4.
    Bundling the code http://www.rgbstock.com/bigphoto/mfXkYUq
  • 5.
  • 6.
    Whenever in doubt... ●Start simple and let the design emerge ● Extract bundles when you need to reuse it or you see it would improve clarity ● Avoid two-way dependencies between bundles
  • 7.
    Thinking outside of the bundle http://www.rgbstock.com/bigphoto/mfBYR2S
  • 8.
  • 9.
    To keep yourdesign clean... ● Treat bundles as an integration layer between PHP libraries and the Symfony framework ● In your libraries, avoid dependencies on bundles
  • 10.
  • 11.
  • 12.
    Learning your stuff http://www.sxc.hu/photo/1095604
  • 13.
    Have you everused? ● Event listeners and subscribers ● Form data transformers ● Twig extensions ● Profiler extensions ● DIC compiler passes ● more...
  • 14.
    It's easy! let's see few examples...
  • 15.
    Event listeners andsubscribers (Doctrine) services: sensio.listener.search_indexer: class: SensioListenerSearchIndexer tags: - name: doctrine.event_listener event: postPersist
  • 16.
    namespace SensioListener; use DoctrineORMEventLifecycleEventArgs; useSensioEntityProduct; class SearchIndexer { public function postPersist(LifecycleEventArgs $args) { $entity = $args->getEntity(); $entityManager = $args->getEntityManager(); if ($entity instanceof Product) { /* Index me */ } } }
  • 17.
    Lifecycle callbacks (Doctrine) /** * @ORMEntity() * @ORMHasLifecycleCallbacks() */ class User { /** * @ORMPrePersist */ public function updateCreatedAt() { /* ... */ } }
  • 18.
    Event listeners andsubscribers (Symfony) services: sensio.action_listener: class: SensioMyBundleEventListenerMyListener tags: - name: kernel.event_listener event: kernel.controller method: onKernelController
  • 19.
    namespace SensioMyBundleEventListener; class MyListener { public function onKernelController( FilterControllerEvent $event) { $controller = $event->getController(); $request = $event->getRequest(); // ... } }
  • 20.
    Event listeners andsubscribers (Forms) $callback = function(FormEvent $event) { if (null === $event->getData()) { $formData = $event->getForm()->getData(); $event->setData($formData); } }; $builder->add('email', 'text'); $builder->get('email') ->addEventListener( FormEvents::PRE_BIND, $callback );
  • 21.
    Data transformers (Forms) classIssueToNumberTransformer implements DataTransformerInterface { public function transform($issue) { // return issue number } public function reverseTransform($number) { // return Issue instance } }
  • 22.
    $field = $builder->create('issue','text') ->addModelTransformer(new IssueToNumberTransformer()); $builder->add($field);
  • 23.
    Twig extensions <!-- Insteadof: --> <div class="price"> ${{ 9800.333|number_format(2, '.', ',') }} </div> <!-- just do: --> <div class="price"> {{ 9800.333|price }} </div>
  • 24.
    class SensioExtension extendsTwig_Extension { public function getFilters() { return array( 'price' => new Twig_Filter_Method( $this, 'priceFilter') ); } public function getName() { return 'sensio_extension'; } public function priceFilter($number, /* ... */ ) {} }
  • 25.
    public function priceFilter( $number, $decimals = 0, $decPoint = '.', $sep = ',') { $price = number_format( $number, $decimals, $decPoint, $sep ); return '$'.$price; }
  • 26.
    services: sensio.twig.acme_extension: class: SensioTwigSensioExtension tags: - { name: twig.extension }
  • 27.
  • 28.
    Debugging commands ./app/console container:debug ./app/consolecontainer:debug twig.loader ./app/console router:debug ./app/console router:debug homepage ./app/console router:match /logout
  • 29.
  • 30.
  • 31.
    Use the servicecontainer!
  • 32.
    public function publishAction($date) { $show = $this->findShow($date); $fileName = sprintf('show-%s.json', $date->format('Y-m-d')); while (file_exists($fileName)) { $fileName = $fileName.preg_replace('/.json$/', '-1.json'); } $options = array( /* options here */ ); $s3 = new AmazonS3($options); $objectOptions = array( 'body' => json_encode($show), 'acl' => $options['objectAcl']); if (!$s3->create_object('my-bucket', $fileName, $objectOptions) { throw new Exception('Could not save file'); } // ... }
  • 33.
    public function publishAction($date) { $show = $this->findShow($date); $fileNameResolver = new FileNameResolver(); $filesystem = $this->get('gafrette_filesystem'); $publisher = new Publisher( $fileNameResolver, $filesystem ); $publisher->publish($show, $date); }
  • 34.
    Use the servicecontainer! public function publishAction($date) { $show = $this->findShow($date); $publisher = $this->get('sensio.publisher'); $publisher->publish($show, $date); }
  • 35.
  • 36.
    /** *@ORMEntity() */ class Product { // ... public function createOrderReport($month) { // $this->em->getRepository() ... } public function setEntityManager($em) { $this->em = $em; } }
  • 37.
    class OrderReportGenerator { private $em = null; public function __construct(EntityManager $em) { $this->em = $em; } public function generate(Product $product, $month) { // ... } }
  • 38.
  • 39.
    Unit tests class ReportTestextends PHPUnit_Framework_TestCase { public function testThatItReturnsFilePath() { $report = new Report(); $file = $report->create('January'); $this->assertSame('/january.csv', $file); } }
  • 40.
    ... or specs namespacespec; class Report extends PHPSpec2ObjectBehavior { function it_returns_file_path() { $this->create('January') ->shouldReturn('/january.csv'); } }
  • 41.
    Managing expectations Feature: Viewingrecent news on the homepage As a Marketing Manager I want Visitors to see recent news on the homepage In order to get them interested in the content Scenario: Viewing recent news Given there are 10 news items And there were 5 news items written since yesterday When I visit the homepage Then I should see 5 recent news items in the news section Scenario: There is not enough news Given there are 10 news items But there were 3 news items written since yesterday When I visit the homepage Then I should see 3 recent news items in the news section
  • 42.