$30 off During Our Annual Pro Sale. View Details »

Best Practices in Symfony2

Best Practices in Symfony2

Going beyond the Official Best Practices in Symfony2

Andreas Hucks

November 12, 2014
Tweet

More Decks by Andreas Hucks

Other Decks in Programming

Transcript

  1. Best Practices in Symfony2
    php[world] 2014
    Washington D.C.
    Nov. 12th
    Andreas Hucks

    View Slide

  2. @meandmymonkey
    Andreas Hucks
    Software Architect & Trainer at
    SensioLabs

    View Slide

  3. Best Practices

    View Slide

  4. symfony.com/doc/current/best_practices/index.html

    View Slide

  5. View Slide

  6. View Slide

  7. Bad
    Practices


    View Slide

  8. symfony Day 2009

    View Slide

  9. Chapter One
    A Clean Project

    View Slide

  10. Naming Things in Symphony
    • Follow PSR-0, PSR-1, PSR-2
    • Find a common scheme for your team
    • Be explicit
    • Be consistent

    View Slide

  11. Care about your coding style
    • Again - follow PSR-0, PSR-1, PSR-2
    • Use PHPCSFixer

    http://goo.gl/gQnc0N

    View Slide

  12. .gitignore
    /web/bundles/  
    /app/bootstrap.php.cache  
    /app/cache/*  
    /app/config/parameters.yml  
    /app/logs/*  
    /build/  
    /vendor/  
    /bin/  
    /composer.phar  
    /data/uploads  
    .idea  
    add your own

    View Slide

  13. .gitignore
    /web/bundles/  
    /app/bootstrap.php.cache  
    /app/cache/*  
    /app/config/parameters.yml  
    /app/logs/*  
    /build/  
    /vendor/  
    /bin/  
    /composer.phar  
    /data/uploads  
    .idea  
    should be in your
    global .gitignore

    View Slide

  14. .gitignore
    /web/bundles/  
    /app/bootstrap.php.cache  
    /app/cache/*  
    /app/config/parameters.yml  
    /app/logs/*  
    /build/  
    /vendor/  
    /bin/  
    /composer.phar  
    /data/uploads  
    .idea  
    this too

    View Slide

  15. .gitignore
    /web/bundles/  
    /app/bootstrap.php.cache  
    /app/cache/*  
    /app/config/parameters.yml  
    /app/logs/*  
    /build/  
    /vendor/  
    /bin/  
    /composer.phar  
    /data/uploads  
    .idea  
    this.

    View Slide

  16. Committing parameters.yml is a

    Three Kitten Offense

    View Slide

  17. Remove Acme\*
    (if present)

    View Slide

  18. To Bundle or not to Bundle

    View Slide

  19. If you DO need separate Bundles...
    Vendor\AwesomeBundle
    vs.
    Vendor\Bundle\AwesomeBundle

    View Slide

  20. If you need separate Bundles...
    Vendor\AwesomeBundle
    vs.
    Vendor\Bundle\AwesomeBundle

    View Slide

  21. FAIL a.k.a. „because I can“
    • MyCompleteAppBundle (ok for small projects)
    • MyAppNeedingGlobalResourcesBundle
    • MyBundleInsideAnotherBundleBundle


    View Slide

  22. FAIL a.k.a. „because I can“
    • MyCompleteAppBundle (ok for small projects)
    • MyAppNeedingGlobalResourcesBundle
    • MyBundleInsideAnotherBundleBundle

    [sic]

    View Slide

  23. What should go into a Bundle
    • Bundles should be self-contained
    • Sets of Features
    • Examples: API, Worker Commands,

    Admin Panel…
    • Configured in /app/config

    View Slide

  24. Your config options
    • YAML
    • XML
    • Annotations
    • INI
    • PHP

    View Slide

  25. Your config options
    • YAML
    • XML
    • Annotations
    • INI
    • PHP
    meh.

    View Slide

  26. Your config options
    • YAML
    • XML
    • Annotations
    • INI
    • PHP
    special use cases

    View Slide

  27. Your config options
    • YAML
    • XML
    • Annotations
    • INI
    • PHP
    mostly good

    View Slide

  28. Your config options
    • YAML
    • XML
    • Annotations
    • INI
    • PHP
    Routing, Bundle Config,
    Parameters
    Services
    Routing, Validators,
    ORM/ODM

    View Slide

  29. If not using Annotations…
    Nest your routing files
    # /app/config/routing.yml

    acme_conference:

    resource: @AcmeConferenceBundle/[…]/routing.yml

    prefix: /conference



    # /src/Acme/Bundle/ConferenceBundle/[…]/routing.yml

    acme_conference_admin:

    resource: routing_admin.yml

    prefix: /


    acme_conference_frontend:

    resource: routing_frontend.yml

    prefix: /

    View Slide

  30. Chapter Two
    Controllers

    View Slide

  31. • Put Controllers on a Diet
    • Decouple them
    • But don't go overboard (this is the 

    application layer)


    View Slide

  32. Losing the Boilerplate

    View Slide

  33. Inject the Request into Controllers
    public function createAction(Request $request)

    {

    // not: $request = $this->getRequest();


    $session = $request->getSession();


    // ..

    }

    View Slide

  34. @Cache(...)
    /**

    * @Cache(smaxage=3600)

    */

    public function listAction()

    {

    // ..


    return $this->render(

    'SensioConferenceBundle:Default:show.html.twig',

    array(

    'conference' => $conference

    )

    );

    }

    View Slide

  35. ParameterConverters
    public function showAction(Conference $conference)

    {

    return $this->render(

    'SensioConferenceBundle:Default:show.html.twig',

    array(

    'conference' => $conference

    )

    );

    }

    View Slide

  36. @Template([name])
    /**

    * @Template()

    */

    public function listAction()

    {

    // ..


    return array(

    'conferences' => $conferences

    );

    }
    compare to
    official best
    practices

    View Slide

  37. Use handleRequest()
    public function createAction(Request $request)

    {

    // .. 


    $form->handleRequest($request);


    if ($form->isSubmitted() && $form->isValid())

    {

    // ... persist data, redirect

    }


    // ..

    }

    View Slide

  38. Events for add-on actions
    if ($form->isSubmitted() && $form->isValid())

    {

    // ... persist data


    $this->get('event_dispatcher')->dispatch(

    ConferenceEvents::CREATED,

    new ConferenceEvent($conference)

    );


    // .. redirect

    }

    View Slide

  39. Service Layers – where sensible
    class ConferenceHandler

    {

    public function __construct(

    ObjectManager $manager,

    EventDispatcherInterface $dispatcher

    ) {

    $this->manager = $manager;

    $this->dispatcher = $dispatcher;

    }


    public function save(Conference $conference)

    {

    $this->manager->persist($conference);

    $this->manager->flush();


    $this->dispatcher->dispatch(

    // ...

    );

    }


    View Slide

  40. this->get('conference_handler')->save(

    $conference

    );

    Service Layers – where sensible

    View Slide

  41. Using the BaseController?
    Wrap calls to the Container to clean up
    /**

    * @return GeocoderInterface

    */

    private function getGeocoder()

    {

    return $this->get('geocoder');

    }

    View Slide

  42. Chapter Three
    Dependency Injection

    View Slide

  43. Injecting the Container
    class MyWebserviceConnector

    {

    public function __construct(
    ContainerInterface $container
    ) {

    // here be dragons

    }

    }

    View Slide

  44. But... but Symfony is doing it!

    View Slide

  45. ...\TwigBundle\Extension\AssetsExtension
    class AssetsExtension extends \Twig_Extension

    {

    private $container;

    public function __construct(
    ContainerInterface $container
    ) {

    $this->container = $container;

    }

    View Slide

  46. So why not… Because Scopes!
    class MyMailer

    {

    public function __construct(Request $request)

    {

    // here be dragons

    }

    }

    View Slide

  47. Solution: RequestStack (>= 2.4)
    class MyMailer

    {

    public function __construct(RequestStack $stack)

    {

    $this->stack = $stack;

    }


    public function doSomething()

    {

    $request = $this->stack->getCurrentRequest();

    }

    }

    View Slide

  48. Alternative: Providing the Request (< 2.4)
    class RequestProvider

    {

    public function __construct(
    ContainerInterface $container
    ) {

    $this->container = $container;

    }


    public function getRequest()

    {

    $this->container->get('request');

    }

    }

    View Slide

  49. Service Organization

    View Slide

  50. Split up Service Definitions
    public function load(
    array $configs,
    ContainerBuilder $container
    ) {

    // …

    $loader->load('storage.yml');

    $loader->load('imageprocessing.yml');

    }

    View Slide

  51. Dynamic Loading of Service Definitions
    public function load(
    array $configs,
    ContainerBuilder $container
    ) {

    // …


    $loader->load('storage.yml');

    $loader->load('imageprocessing.yml');


    if (isset($config['logging'])) {

    $loader->load('logging.yml');

    }

    }

    View Slide

  52. Semantic Configuration
    (for reusable Bundles)
    acme_conference:

    logging: ~

    thumbnails:

    width: 300

    height: 200

    View Slide

  53. The Container as a Registry
    public function listAction()

    {

    $max = $this->container->getParameter(
    'max_conferences'
    );

    $conferences = $someStorage->getConferences($max);


    // ..
    }

    View Slide

  54. Instead: Proper Service Configuration
    services:

    my_storage:

    class: "Sensio\[…]\Storage\MyStorage"

    arguments: ["%max_conferences%"]

    View Slide

  55. Binding to the Environment
    public function listAction()

    {

    $env = $this->container->getParameter(
    'kernel.environment'
    );


    if ('dev' === $env) {

    // ...

    }
    // ...

    }

    View Slide

  56. Instead: Use Your config files
    # /app/config/config.yml
    acme_conference:

    thumbnails:

    width: 300

    height: 200
    # /app/config/config_dev.yml
    acme_conference:

    logging: ~


    View Slide

  57. Miscellaneous
    • Use YAML for Service Definitions
    • Remember you can (and should) use 

    Environment Variables
    • Use %kernel.root_dir% as a reference


    View Slide

  58. Intermezzo
    Random Tips out of Context

    View Slide

  59. PHP
    • Please drop 5.3 (it‘s also faster)
    • Use an Opcode Cache

    View Slide

  60. Composer
    • Install globally
    • Use the --optimize-autoloader option

    for production

    View Slide

  61. Doctrine
    • Activate Metadata Cache
    • Activate Query Cache
    • Pro Level only: Use factory-service to 

    register Repos & Managers as Services
    • Do NOT inject the EntityManager into 

    your entities

    View Slide

  62. Security
    • Make sure there are no leaks in the

    security.yml access_control section!
    • Better: Check Authorization in

    Controller, use the @Security 

    annotation

    View Slide

  63. Translation
    • Work with translation keys instead of

    full text to avoid breaking translations

    View Slide

  64. Searching
    • Look for „Symfony2“

    (without the space)

    View Slide

  65. PHPStorm
    • The Symfony Plugin is pretty stable, 

    use it!
    • Check out the PHP Annotations Plugin

    View Slide

  66. Read the Documentation
    (and the Changelogs)


    View Slide

  67. Chapter Four
    Forms

    View Slide

  68. Forms in Controllers
    public function createAction(Request $request)

    {

    $form = $this->createFormBuilder()

    ->add('name')

    ->add('startDate', 'date')

    ->add('endDate', 'date')

    ->add('location', 'textarea')

    ->getForm()

    ;


    // ..

    View Slide

  69. • Couples Form setup to Controller
    • No reusability


    View Slide

  70. Better: Use Type Classes
    class ConferenceType extends AbstractType

    {

    public function buildForm(
    FormBuilderInterface $builder,
    array $options
    ) {

    $builder

    ->add('name')

    ->add('startDate', 'date')

    ->add('endDate', 'date')

    ->add('location', 'textarea')

    ;

    }

    View Slide

  71. Always set the data_class
    public function setDefaultOptions(
    OptionsResolverInterface $resolver
    ) {

    $resolver->setDefaults(

    array(

    'data_class' => 'Acme\Conference'

    )

    );

    }

    View Slide

  72. Using Data in Constructors
    class ConferenceType extends AbstractType

    {

    public function __construct(Venue $venue)

    {

    $this->venue = $venue;

    }


    public function buildForm(
    FormBuilderInterface $builder,
    array $options
    ) {

    // ...


    if ($this->venue->hasWifi()) {

    $builder->add('ssid');

    }

    }

    View Slide

  73. Use Form Events for Related Data
    public function buildForm(FormBuilderInterface
    $builder, array $options)

    {

    // ...


    $builder->addEventListener(

    FormEvents::PRE_SET_DATA,

    function (FormEvent $e) {

    if ($e->getData()->hasWifi()) {

    $e->getForm()->add('ssid');

    }

    }

    );

    View Slide

  74. Define Types as Services (for reusable
    Bundles)
    form_conference:

    class: "Sensio\[…]\Form\ConferenceType"

    tags:

    - { name: twig.extension, alias: conference }

    View Slide

  75. public function createAction(Request $request)

    {

    $form = $this->createForm('conference');

    $form ->handleRequest();


    // ...

    Define Types as Services (for reusable
    Bundles)

    View Slide

  76. Don‘t disable CSRF
    public function setDefaultOptions(
    OptionsResolverInterface $resolver
    ) {

    $resolver->setDefaults(

    array(

    // ...

    'csrf_protection' => false

    )

    );

    }

    View Slide

  77. Thanks! Questions?
    Please give feedback:
    https://joind.in/11884

    View Slide