Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

@meandmymonkey Andreas Hucks Software Architect & Trainer at SensioLabs

Slide 3

Slide 3 text

Best Practices

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Bad Practices


Slide 8

Slide 8 text

symfony Day 2009

Slide 9

Slide 9 text

Chapter One A Clean Project

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Care about your coding style • Again - follow PSR-0, PSR-1, PSR-2 • Use PHPCSFixer
 http://goo.gl/gQnc0N

Slide 12

Slide 12 text

.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

Slide 13

Slide 13 text

.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

Slide 14

Slide 14 text

.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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Committing parameters.yml is a
 Three Kitten Offense

Slide 17

Slide 17 text

Remove Acme\* (if present)

Slide 18

Slide 18 text

To Bundle or not to Bundle

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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


Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

What should go into a Bundle • Bundles should be self-contained • Sets of Features • Examples: API, Worker Commands,
 Admin Panel… • Configured in /app/config

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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: /

Slide 30

Slide 30 text

Chapter Two Controllers

Slide 31

Slide 31 text

• Put Controllers on a Diet • Decouple them • But don't go overboard (this is the 
 application layer)


Slide 32

Slide 32 text

Losing the Boilerplate

Slide 33

Slide 33 text

Inject the Request into Controllers public function createAction(Request $request)
 {
 // not: $request = $this->getRequest();
 
 $session = $request->getSession();
 
 // ..
 }

Slide 34

Slide 34 text

@Cache(...) /**
 * @Cache(smaxage=3600)
 */
 public function listAction()
 {
 // ..
 
 return $this->render(
 'SensioConferenceBundle:Default:show.html.twig',
 array(
 'conference' => $conference
 )
 );
 }

Slide 35

Slide 35 text

ParameterConverters public function showAction(Conference $conference)
 {
 return $this->render(
 'SensioConferenceBundle:Default:show.html.twig',
 array(
 'conference' => $conference
 )
 );
 }

Slide 36

Slide 36 text

@Template([name]) /**
 * @Template()
 */
 public function listAction()
 {
 // ..
 
 return array(
 'conferences' => $conferences
 );
 } compare to official best practices

Slide 37

Slide 37 text

Use handleRequest() public function createAction(Request $request)
 {
 // .. 
 
 $form->handleRequest($request);
 
 if ($form->isSubmitted() && $form->isValid())
 {
 // ... persist data, redirect
 }
 
 // ..
 }

Slide 38

Slide 38 text

Events for add-on actions if ($form->isSubmitted() && $form->isValid())
 {
 // ... persist data
 
 $this->get('event_dispatcher')->dispatch(
 ConferenceEvents::CREATED,
 new ConferenceEvent($conference)
 );
 
 // .. redirect
 }

Slide 39

Slide 39 text

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(
 // ...
 );
 }


Slide 40

Slide 40 text

this->get('conference_handler')->save(
 $conference
 );
 Service Layers – where sensible

Slide 41

Slide 41 text

Using the BaseController? Wrap calls to the Container to clean up /**
 * @return GeocoderInterface
 */
 private function getGeocoder()
 {
 return $this->get('geocoder');
 }

Slide 42

Slide 42 text

Chapter Three Dependency Injection

Slide 43

Slide 43 text

Injecting the Container class MyWebserviceConnector
 {
 public function __construct( ContainerInterface $container ) {
 // here be dragons
 }
 }

Slide 44

Slide 44 text

But... but Symfony is doing it!

Slide 45

Slide 45 text

...\TwigBundle\Extension\AssetsExtension class AssetsExtension extends \Twig_Extension
 {
 private $container; 
 public function __construct( ContainerInterface $container ) {
 $this->container = $container;
 }

Slide 46

Slide 46 text

So why not… Because Scopes! class MyMailer
 {
 public function __construct(Request $request)
 {
 // here be dragons
 }
 }

Slide 47

Slide 47 text

Solution: RequestStack (>= 2.4) class MyMailer
 {
 public function __construct(RequestStack $stack)
 {
 $this->stack = $stack;
 }
 
 public function doSomething()
 {
 $request = $this->stack->getCurrentRequest();
 }
 }

Slide 48

Slide 48 text

Alternative: Providing the Request (< 2.4) class RequestProvider
 {
 public function __construct( ContainerInterface $container ) {
 $this->container = $container;
 }
 
 public function getRequest()
 {
 $this->container->get('request');
 }
 }

Slide 49

Slide 49 text

Service Organization

Slide 50

Slide 50 text

Split up Service Definitions public function load( array $configs, ContainerBuilder $container ) {
 // … 
 $loader->load('storage.yml');
 $loader->load('imageprocessing.yml');
 }

Slide 51

Slide 51 text

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');
 }
 }

Slide 52

Slide 52 text

Semantic Configuration (for reusable Bundles) acme_conference:
 logging: ~
 thumbnails:
 width: 300
 height: 200

Slide 53

Slide 53 text

The Container as a Registry public function listAction()
 {
 $max = $this->container->getParameter( 'max_conferences' );
 $conferences = $someStorage->getConferences($max);
 
 // .. }

Slide 54

Slide 54 text

Instead: Proper Service Configuration services:
 my_storage:
 class: "Sensio\[…]\Storage\MyStorage"
 arguments: ["%max_conferences%"]

Slide 55

Slide 55 text

Binding to the Environment public function listAction()
 {
 $env = $this->container->getParameter( 'kernel.environment' );
 
 if ('dev' === $env) {
 // ...
 } // ...
 }

Slide 56

Slide 56 text

Instead: Use Your config files # /app/config/config.yml acme_conference:
 thumbnails:
 width: 300
 height: 200 # /app/config/config_dev.yml acme_conference:
 logging: ~


Slide 57

Slide 57 text

Miscellaneous • Use YAML for Service Definitions • Remember you can (and should) use 
 Environment Variables • Use %kernel.root_dir% as a reference


Slide 58

Slide 58 text

Intermezzo Random Tips out of Context

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Composer • Install globally • Use the --optimize-autoloader option
 for production

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

Security • Make sure there are no leaks in the
 security.yml access_control section! • Better: Check Authorization in
 Controller, use the @Security 
 annotation

Slide 63

Slide 63 text

Translation • Work with translation keys instead of
 full text to avoid breaking translations

Slide 64

Slide 64 text

Searching • Look for „Symfony2“
 (without the space)

Slide 65

Slide 65 text

PHPStorm • The Symfony Plugin is pretty stable, 
 use it! • Check out the PHP Annotations Plugin

Slide 66

Slide 66 text

Read the Documentation (and the Changelogs)


Slide 67

Slide 67 text

Chapter Four Forms

Slide 68

Slide 68 text

Forms in Controllers public function createAction(Request $request)
 {
 $form = $this->createFormBuilder()
 ->add('name')
 ->add('startDate', 'date')
 ->add('endDate', 'date')
 ->add('location', 'textarea')
 ->getForm()
 ;
 
 // ..

Slide 69

Slide 69 text

• Couples Form setup to Controller • No reusability


Slide 70

Slide 70 text

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')
 ;
 }

Slide 71

Slide 71 text

Always set the data_class public function setDefaultOptions( OptionsResolverInterface $resolver ) {
 $resolver->setDefaults(
 array(
 'data_class' => 'Acme\Conference'
 )
 );
 }

Slide 72

Slide 72 text

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');
 }
 }

Slide 73

Slide 73 text

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');
 }
 }
 );

Slide 74

Slide 74 text

Define Types as Services (for reusable Bundles) form_conference:
 class: "Sensio\[…]\Form\ConferenceType"
 tags:
 - { name: twig.extension, alias: conference }

Slide 75

Slide 75 text

public function createAction(Request $request)
 {
 $form = $this->createForm('conference');
 $form ->handleRequest();
 
 // ...
 Define Types as Services (for reusable Bundles)

Slide 76

Slide 76 text

Don‘t disable CSRF public function setDefaultOptions( OptionsResolverInterface $resolver ) {
 $resolver->setDefaults(
 array(
 // ...
 'csrf_protection' => false
 )
 );
 }

Slide 77

Slide 77 text

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