Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. Naming Things in Symphony • Follow PSR-0, PSR-1, PSR-2 •

    Find a common scheme for your team • Be explicit • Be consistent
  2. Care about your coding style • Again - follow PSR-0,

    PSR-1, PSR-2 • Use PHPCSFixer
 http://goo.gl/gQnc0N
  3. .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
  4. .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
  5. .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
  6. .gitignore /web/bundles/   /app/bootstrap.php.cache   /app/cache/*   /app/config/parameters.yml   /app/logs/*

      /build/   /vendor/   /bin/   /composer.phar   /data/uploads   .idea   this.
  7. FAIL a.k.a. „because I can“ • MyCompleteAppBundle (ok for small

    projects) • MyAppNeedingGlobalResourcesBundle • MyBundleInsideAnotherBundleBundle

  8. FAIL a.k.a. „because I can“ • MyCompleteAppBundle (ok for small

    projects) • MyAppNeedingGlobalResourcesBundle • MyBundleInsideAnotherBundleBundle
 [sic]
  9. What should go into a Bundle • Bundles should be

    self-contained • Sets of Features • Examples: API, Worker Commands,
 Admin Panel… • Configured in /app/config
  10. Your config options • YAML • XML • Annotations •

    INI • PHP Routing, Bundle Config, Parameters Services Routing, Validators, ORM/ODM
  11. 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: /
  12. • Put Controllers on a Diet • Decouple them •

    But don't go overboard (this is the 
 application layer)

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


    // not: $request = $this->getRequest();
 
 $session = $request->getSession();
 
 // ..
 }
  14. @Cache(...) /**
 * @Cache(smaxage=3600)
 */
 public function listAction()
 {
 //

    ..
 
 return $this->render(
 'SensioConferenceBundle:Default:show.html.twig',
 array(
 'conference' => $conference
 )
 );
 }
  15. @Template([name]) /**
 * @Template()
 */
 public function listAction()
 {
 //

    ..
 
 return array(
 'conferences' => $conferences
 );
 } compare to official best practices
  16. Use handleRequest() public function createAction(Request $request)
 {
 // .. 


    
 $form->handleRequest($request);
 
 if ($form->isSubmitted() && $form->isValid())
 {
 // ... persist data, redirect
 }
 
 // ..
 }
  17. Events for add-on actions if ($form->isSubmitted() && $form->isValid())
 {
 //

    ... persist data
 
 $this->get('event_dispatcher')->dispatch(
 ConferenceEvents::CREATED,
 new ConferenceEvent($conference)
 );
 
 // .. redirect
 }
  18. 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(
 // ...
 );
 }

  19. Using the BaseController? Wrap calls to the Container to clean

    up /**
 * @return GeocoderInterface
 */
 private function getGeocoder()
 {
 return $this->get('geocoder');
 }
  20. ...\TwigBundle\Extension\AssetsExtension class AssetsExtension extends \Twig_Extension
 {
 private $container; 
 public

    function __construct( ContainerInterface $container ) {
 $this->container = $container;
 }
  21. So why not… Because Scopes! class MyMailer
 {
 public function

    __construct(Request $request)
 {
 // here be dragons
 }
 }
  22. Solution: RequestStack (>= 2.4) class MyMailer
 {
 public function __construct(RequestStack

    $stack)
 {
 $this->stack = $stack;
 }
 
 public function doSomething()
 {
 $request = $this->stack->getCurrentRequest();
 }
 }
  23. Alternative: Providing the Request (< 2.4) class RequestProvider
 {
 public

    function __construct( ContainerInterface $container ) {
 $this->container = $container;
 }
 
 public function getRequest()
 {
 $this->container->get('request');
 }
 }
  24. Split up Service Definitions public function load( array $configs, ContainerBuilder

    $container ) {
 // … 
 $loader->load('storage.yml');
 $loader->load('imageprocessing.yml');
 }
  25. 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');
 }
 }
  26. The Container as a Registry public function listAction()
 {
 $max

    = $this->container->getParameter( 'max_conferences' );
 $conferences = $someStorage->getConferences($max);
 
 // .. }
  27. Binding to the Environment public function listAction()
 {
 $env =

    $this->container->getParameter( 'kernel.environment' );
 
 if ('dev' === $env) {
 // ...
 } // ...
 }
  28. Instead: Use Your config files # /app/config/config.yml acme_conference:
 thumbnails:
 width:

    300
 height: 200 # /app/config/config_dev.yml acme_conference:
 logging: ~

  29. Miscellaneous • Use YAML for Service Definitions • Remember you

    can (and should) use 
 Environment Variables • Use %kernel.root_dir% as a reference

  30. 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
  31. Security • Make sure there are no leaks in the


    security.yml access_control section! • Better: Check Authorization in
 Controller, use the @Security 
 annotation
  32. PHPStorm • The Symfony Plugin is pretty stable, 
 use

    it! • Check out the PHP Annotations Plugin
  33. Forms in Controllers public function createAction(Request $request)
 {
 $form =

    $this->createFormBuilder()
 ->add('name')
 ->add('startDate', 'date')
 ->add('endDate', 'date')
 ->add('location', 'textarea')
 ->getForm()
 ;
 
 // ..
  34. 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')
 ;
 }
  35. Always set the data_class public function setDefaultOptions( OptionsResolverInterface $resolver )

    {
 $resolver->setDefaults(
 array(
 'data_class' => 'Acme\Conference'
 )
 );
 }
  36. 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');
 }
 }
  37. 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');
 }
 }
 );
  38. Don‘t disable CSRF public function setDefaultOptions( OptionsResolverInterface $resolver ) {


    $resolver->setDefaults(
 array(
 // ...
 'csrf_protection' => false
 )
 );
 }