Symfony tips and tricks

Software

javier-eguiluz
  • 1. SYMFONYTIPS & TRICKSSymfonyConMadrid - 27 November 2014Javier Eguiluz
  • 2. About meevangelist and trainer at
  • 3. My Symfony work
  • 4. Why Symfony Tips and Tricks?I don’t work asa developer But I read andreview code everysingle dayI also read everysingle blog postpublished aboutSymfony These are the tips andtricks discoveredduring this year.
  • 5. Thanks to my awesome co-workers forsharing their knowledge!Grégoire Nicolas HugoTugdual SarahJulienLoïc Jérémy RomainJosephFX
  • 6. Agenda Assets Performance LoggingLegacy Testing ConfigParallel Doctrine ValueObjects
  • 7. Assetmanagement
  • 8. Named assets
  • 9. Typical asset linking{% javascripts'@AppBundle/Resources/public/js/jquery.js''@AppBundle/Resources/public/js/jquery.ui.js''@AppBundle/Resources/public/js/bootstrap.js''@AppBundle/Resources/public/js/*' %}<script src="{{ asset_url }}"></script>{% endjavascripts %}
  • 10. Defining named assets# app/config/config.ymlassetic:assets:# ...bootstrap_js:inputs:- '@AppBundle/Resources/public/js/jquery.js'- '@AppBundle/Resources/public/js/jquery.ui.js'- '@AppBundle/Resources/public/js/bootstrap.js'
  • 11. Using named assets{% javascripts'@bootstrap_js''@AppBundle/Resources/public/js/*' %}<script src="{{ asset_url }}"></script>{% endjavascripts %}
  • 12. Assetpackages
  • 13. asset( ) adds a leading slash<img src="{{ asset('images/logo.png') }}" />
  • 14. asset( ) adds a leading slash<img src="{{ asset('images/logo.png') }}" /><img src="/images/logo.png" />
  • 15. Easiest way to define a base URL# app/config/config.ymlframework:templating:assets_base_urls:http: ['http://static.example.com/images']ssl: ['https://static.example.com/images']
  • 16. Easiest way to define a base URL# app/config/config.ymlframework:templating:assets_base_urls:http: ['http://static.example.com/images']ssl: ['https://static.example.com/images']if you configure severalbase URLs, Symfony willselect one each time tobalance asset load
  • 17. Protocol-relative base URL# app/config/config.ymlframework:templating:assets_base_urls: '//static.example.com/images'
  • 18. AWshast eif wt ep waanct ktoa mgodeifsythe base URL just for a fewselected assets?
  • 19. What if we want to modifythe base URL just for a fewselected assets?Asset packages
  • 20. Asset packagesGroup assetslogically.Define config foreach package.
  • 21. Defining asset packages# app/config/config.ymlframework:templating:packages:avatars:base_urls: '//static.example.com/avatars/'
  • 22. Asset packages in practice<img src="{{ asset('...', 'avatars') }}" />
  • 23. Asset packages in practice<img src="{{ asset('...', 'avatars') }}" /><img src="//static.example.com/avatars/logo.png" />
  • 24. Packages can define any asset option# app/config/config.ymlframework:templating:packages:avatars:version: '20141127'base_urls: '//static.example.com/avatars/'
  • 25. mini-tip
  • 26. Using the Symfony console$ php app/console --env=prodassetic:dump
  • 27. Using the Symfony console$ php app/console --env=prodassetic:dumpWRONG RIGHT
  • 28. Using the Symfony console$ ./app/console --env=prodassetic:dumpWRONG RIGHT
  • 29. Using the Symfony console$ sf --env=prod assetic:dumpalias sf = php app/consoleWRONG RIGHT
  • 30. Using good Symfony console shortcuts$ dev ...$ prod ...alias dev = php app/console --env=devalias prod = php app/console --env=prod
  • 31. PROS$ prod a:d$ dev r:m /$ dev cont:dNEWCOMERS$ php app/console --env=prod assetic:dump$ php app/console router:match /$ php app/console container:debug
  • 32. Performance
  • 33. Optimizeautoloading
  • 34. Unoptimized auto-loading
  • 35. Optimized auto-loading$ composer dump-autoload --optimize
  • 36. Don't load test classes in production{"autoload": {"psr-4": { "MyLibrary": "src/" }},"autoload-dev": {"psr-4": { "MyLibraryTests": "tests/" }}}
  • 37. Add classes tocompile
  • 38. Three important Symfony files• app/bootstrap.php.cacheInternal Symfony classes.• app/cache/<env>/appDevDebugProjectContainer.phpCompiled container (services and parameters).• app/cache/<env>/classes.phpSymfony bundles classes.
  • 39. Adding your own classes for compilingnamespace AppBundleDependencyInjection;!use SymfonyComponentConfigFileLocator;use SymfonyComponentDependencyInjectionContainerBuilder;use SymfonyComponentHttpKernelDependencyInjectionExtension;!class AppExtension extends Extension{public function load(array $configs, ContainerBuilder $container){// ...!$this->addClassesToCompile(['AppBundleFullNamespaceClass']);}}
  • 40. Adding your own classes for compilingnamespace AppBundleDependencyInjection;!use SymfonyComponentConfigFileLocator;use SymfonyComponentDependencyInjectionContainerBuilder;use SymfonyComponentHttpKernelDependencyInjectionExtension;!class AppExtension extends Extension{public function load(array $configs, ContainerBuilder $container){// ...!$this->addClassesToCompile(['AppBundleFullNamespaceClass']);}}WARNINGIt doesn't work ifthe classes defineannotations
  • 41. mini-tip
  • 42. How can you enable theprofiler on production forsome specific users?
  • 43. The easy way: restrict by path# app/config/config.ymlframework:# ...profiler:matcher:path: ^/admin/
  • 44. The right way: restrict by user (1/2)# app/config/config.ymlframework:# ...profiler:matcher:service: app.profiler_matcher!services:app.profiler_matcher:class: AppBundleProfilerMatcherarguments: ["@security.context"]
  • 45. The right way: restrict by user (2/2)namespace AppBundleProfiler;!use SymfonyComponentSecurityCoreSecurityContext;use SymfonyComponentHttpFoundationRequest;use SymfonyComponentHttpFoundationRequestMatcherInterface;!class Matcher implements RequestMatcherInterface{protected $securityContext;!public function __construct(SecurityContext $securityContext){$this->securityContext = $securityContext;}!public function matches(Request $request){return $this->securityContext->isGranted('ROLE_ADMIN');}}
  • 46. Better logs withMonolog
  • 47. De-clutteryour logs
  • 48. Symfony logs a lot of information[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundleControllerDefaultController::indexAction", "_route": "homepage") [] [][2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous Token [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerDebugHandlersListener::configure". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleFrameworkBundleEventListenerSessionListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerFragmentListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleAsseticBundleEventListenerRequestListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyBundleFrameworkBundleDataCollectorRouterDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyComponentHttpKernelDataCollectorRequestDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerControllerListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerParamConverterListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerSecurityListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerTemplateListener::onKernelController". [] [][2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slug AS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail AS authorEmail5,p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ? ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"] [][2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpFirewallContextListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpRememberMeResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyBundleWebProfilerBundleEventListenerWebDebugToolbarListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerStreamedResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyBundleSwiftmailerBundleEventListenerEmailSenderListener::onTerminate". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelTerminate". [] []
  • 49. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundleControllerDefaultController::indexAction", "_route": "homepage") [] [][2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous Token [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerDebugHandlersListener::configure". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleFrameworkBundleEventListenerSessionListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerFragmentListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleAsseticBundleEventListenerRequestListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyBundleFrameworkBundleDataCollectorRouterDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyComponentHttpKernelDataCollectorRequestDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerControllerListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerParamConverterListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerSecurityListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerTemplateListener::onKernelController". [] [][2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slug AS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail AS authorEmail5,p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ? ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"] [][2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpFirewallContextListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpRememberMeResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyBundleWebProfilerBundleEventListenerWebDebugToolbarListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerStreamedResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyBundleSwiftmailerBundleEventListenerEmailSenderListener::onTerminate". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelTerminate". [] []
  • 50. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundleControllerDefaultController::indexAction", "_route": "homepage") [] []2014-[[2014-2014-11-24 12:24:22] security.11-INFO: Populated 24 SecurityContext 12:24:with an anonymous Token [] []11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener 22] "SymfonyComponentevent.HttpKernelEventListenerDEBUG: DebugHandlersListener::Notifiedconfigure". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleFrameworkBundleEventListenerSessionListener::onKernelRequest". [] [][event 2014-11-24 12:24:22] event."DEBUG: kernel.Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerFragmentListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentto HttpKernellistener EventListenerRouterListener::onKernelRequest". "Symfony[] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelRequest". [] [][2014-Bundle11-24 12:24:22] event.DEBUG: FrameworkBundleNotified event "kernel.request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleAsseticBundleEventListenerRequestListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyBundleFrameworkBundleEventListenerDataCollectorRouterDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyComponentHttpKernelDataCollectorRequestDataCollector::onKernelController". [] [][2014-SessionListener::11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener onKernelRequest". "SensioBundleFrameworkExtraBundleEventListenerControllerListener::[] onKernelController". [2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerParamConverterListener::onKernelController". [][] [][] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerSecurityListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerTemplateListener::onKernelController". [] [][2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slug AS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail AS authorEmail5,p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ? ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"] [][2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpFirewallContextListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpRememberMeResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyBundleWebProfilerBundleEventListenerWebDebugToolbarListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerStreamedResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyBundleSwiftmailerBundleEventListenerEmailSenderListener::onTerminate". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelTerminate". [] []
  • 51. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: ...[2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...
  • 52. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: ...[2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...log channel
  • 53. Hide event logs (if you don't need them)# app/config/dev.ymlmonolog:handlers:main:type: streampath: "%kernel.logs_dir%/%kernel.environment%.log"level: debugchannels: "!event"
  • 54. De-cluttered log files[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller":"AppBundleControllerDefaultController::indexAction", "_route": "homepage")![2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous Token![2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slugAS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail ASauthorEmail5, p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ?ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"]![2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session
  • 55. Better logemails
  • 56. Email logs related to 5xx errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: criticalhandler: bufferedbuffered:type: bufferhandler: swiftswift:type: swift_mailerfrom_email: error@example.comto_email: error@example.comsubject: An Error Occurred!level: debug
  • 57. Email logs related to 5xx errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: criticalhandler: bufferedbuffered:type: bufferhandler: swiftswift:type: swift_mailerfrom_email: error@example.comto_email: error@example.comsubject: An Error Occurred!level: debug
  • 58. Email logs related to 4xx errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: errorhandler: bufferedbuffered:# ...swift:# ...
  • 59. Don't email 404 errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: errorexcluded_404:- ^/handler: bufferedbuffered:# ...swift:# ...
  • 60. Organize yourlogs
  • 61. Use different log files for each channel# app/config/config_prod.ymlmonolog:handlers:main:type: streampath: "%kernel.logs_dir%/%kernel.environment%.log"level: debugchannels: ["!event"]security:type: streampath: "%kernel.logs_dir%/security-%kernel.environment%.log"level: debugchannels: "security"
  • 62. Format yourlog messages
  • 63. Don't do this$logger->info(sprintf("Order number %s processed for %s user",$number, $user));
  • 64. Do this instead$logger->info("Order number {num} processed for {name} user",['num' => $number, 'name' => $user]);
  • 65. Do this instead$logger->info("Order number {num} processed for {name} user",['num' => $number, 'name' => $user]);WARNINGIt doesn't workunless the PSRlog processor isenabled.
  • 66. Enable the PsrLogMessageProcessor# app/config/config_prod.ymlservices:monolog_processor:class: MonologProcessorPsrLogMessageProcessortags:- { name: monolog.processor }
  • 67. Monolog includes several processors# app/config/config_prod.ymlservices:monolog_processor:class: MonologProcessorIntrospectionProcessortags:- { name: monolog.processor }
  • 68. Create your own processor$logger->info("Order processed successfully.", $order);
  • 69. Create your own processor$logger->info("Order processed successfully.", $order);[2014-11-24 15:15:58] app.INFO: Order processedsuccessfully. { "order": { "number": "023924382982","user":"John Smith", "total":"34.99","status":"PAID" }}
  • 70. Custom monolog processornamespace AppBundleLoggerOrderProcessor;!use AppBundleEntityOrder;!class OrderProcessor{public function __invoke(array $record){if (isset($record['context']['order']) && $record['context']['order'] instanceof Order) {$order = $record['context']['order'];$record['context']['order'] = ['number' => $order->getNumber(),'user' => $order->get...,'total' => $order->get...,'status' => $order->get...,];}!return $record;}# app/config/config_prod.ymlservices:monolog_processor:class: AppBundleLoggerOrderProcessortags:- { name: monolog.processor }
  • 71. mini-tip
  • 72. Medium-sized andLarge ApplicationsMedium-sized andSmall ApplicationsUsing your own mailer is OK.
  • 73. Not all emails should be treated equallySign UpsLost passwordNotificationsNewslettersSpam
  • 74. Instant and delayed mailers# app/config/config.ymlswiftmailer:default_mailer: delayedmailers:instant:# ...spool: ~delayed:# ...spool:type: filepath: "%kernel.cache_dir%/swiftmailer/spool"
  • 75. Using prioritized mailers// high-priority emails$container->get('swiftmailer.mailer.instant')->...!// regular emails$container->get('swiftmailer.mailer')->...$container->get('swiftmailer.mailer.delayed')->...
  • 76. Legacyapplications
  • 77. Pokemon™strategy(catch’em all)
  • 78. Controller that captures legacy URLsclass LegacyController extends Controller{/*** @Route("/{filename}.php", name="_legacy")*/public function legacyAction($filename){$legacyPath = $this->container->getParameter('legacy.path');!ob_start();chdir($legacyAppPath);require_once $legacyAppPath.$filename.'.php';$content = ob_get_clean();!return new Response($content);}}Source: http://cvuorinen.net/slides/symfony-legacy-integration/
  • 79. Embbeddinglegacy apps
  • 80. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy App
  • 81. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequest
  • 82. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.request
  • 83. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpException
  • 84. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exception
  • 85. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exception
  • 86. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exception
  • 87. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exceptionResponse
  • 88. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exceptionResponseResponse
  • 89. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestResponseNotFoundHttpExceptionkernel.exceptionResponseResponse
  • 90. LegacyKernel to process requestsclass LegacyKernel implements HttpKernelInterface{public function handle(Request $request, ...){ob_start();!$legacyDir = dirname($this->legacyAppPath);chdir($legacyDir);require_once $this->legacyAppPath;!$content = ob_get_clean();!return new Response($content);}}Source: http://cvuorinen.net/slides/symfony-legacy-integration/
  • 91. Listener to treat 404 as legacy requestsclass LegacyKernelListener implements EventSubscriberInterface{public function onKernelException($event){$exception = $event->getException();!if ($exception instanceof NotFoundHttpException) {$request = $event->getRequest();$response = $this->legacyKernel->handle($request);!$response->headers->set('X-Status-Code', 200);!$event->setResponse($response);}}} Source: http://cvuorinen.net/slides/symfony-legacy-integration/
  • 92. mini-tip
  • 93. Click-to-file in Symfony ExceptionsTextmate
  • 94. PHPStorm is now supported natively# app/config/config.ymlframework:ide: "phpstorm://open?file=%%f&line=%%l"
  • 95. Testing tips
  • 96. Prophecy
  • 97. Prophecy is a highlyopinionated yet verypowerful and flexible PHPobject mocking framework.
  • 98. PHPUnit has built-inProphecy mocks supportsince 4.5 version.
  • 99. A complete Prophecy mockclass SubjectTest extends PHPUnit_Framework_TestCase{public function testObserversAreUpdated(){$subject = new Subject('My subject');!$observer = $this->prophesize('Observer');$observer->update('something')->shouldBeCalled();!$subject->attach($observer->reveal());!$subject->doSomething();}}
  • 100. Prophecy vs traditional mocks (1/2)!!$observer = $this->prophesize('Observer');!!!$observer = $this->getMockBuilder('Observer')->setMethods(array('update'))->getMock();ProphecyPHPUnit
  • 101. Prophecy vs traditional mocks (2/2)!Prophecy!$observer->update('something')->shouldBeCalled();!!!$observer->expects($this->once())->method('update')->with($this->equalTo('something'));PHPUnit
  • 102. Faster smoketesting
  • 103. Smoke testing is preliminarytesting to reveal simplefailures severe enough toreject a prospectivesoftware release.
  • 104. Unnecessarily slow testsnamespace AppBundleTestsController;!use SymfonyBundleFrameworkBundleTestWebTestCase;!class DefaultControllerTest extends WebTestCase{/** @dataProvider provideUrls */public function testPageIsSuccessful($url){$client = self::createClient();$client->request('GET', $url);$this->assertTrue($client->getResponse()->isSuccessful());}!public function provideUrls(){// ...}}
  • 105. Functional tests without WebTestCaseclass SmokeTest extends PHPUnit_Framework_TestCase{private $app;!protected function setUp(){$this->app = new AppKernel('test', false);$this->app->boot();}!/** @dataProvider provideUrls */public function testUrl($url){$request = new Request::create($url, 'GET');$response = $this->app->handle($request);!$this->assertTrue($response->isSuccessful());}} Source: http://gnugat.github.io/2014/11/15/sf2-quick-functional-tests.html
  • 106. mini-tip
  • 107. Impersonating users# app/config/security.ymlsecurity:firewalls:main:# ...switch_user: truehttp://example.com/path?_switch_user=fabien
  • 108. WARNINGImpersonating is verydangerous and it canlead to severe errors.
  • 109. Visual hints when impersonating users<body class="{% if app.user and is_granted('ROLE_PREVIOUS_ADMIN') %}impersonating{% endif %}">!.impersonating {background: rgba(204, 0, 0, 0.15);}
  • 110. Be careful when impersonating users
  • 111. Configuration
  • 112. Yaml casting
  • 113. Implicit YAML castingkey:value1: 5value2: 2.7value3: 2014-11-27
  • 114. Implicit YAML castingkey:value1: 5value2: 2.7value3: 2014-11-27array(1) {["key"]=> array(3) {["value1"] => int(5)["value2"] => float(2.7)["value3"] => int(1417042800)}}
  • 115. Explicit YAML castingkey:value1: !str 5value2: ! 2.7value3: !str 2014-11-27
  • 116. Explicit YAML castingkey:value1: !str 5value2: ! 2.7value3: !str 2014-11-27array(1) {["key"]=> array(3) {["value1"] => string(1) "5"["value2"] => int(2)["value3"] => string(10) "2014-11-27"}}
  • 117. Explicit YAML castingkey:value1: !str 5value2: ! 2.7value3: !str 2014-11-27array(1) {["key"]=> array(3) {["value1"] => string(1) "5"["value2"] => int(2)["value3"] => string(10) "2014-11-27"}}! = int !str = string!!php/object: = serialized object
  • 118. CustomExpressions
  • 119. Expression Language in action/*** @Route("/post/{id}")* @Cache(smaxage="15", lastModified="post.getUpdatedAt()")*/public function showAction(Post $post) { ... }!!/*** @AssertExpression("this.getFoo() == 'fo'", message="Not good!")*/class Obj { ... }
  • 120. Custom md5( ) functionuse SymfonyComponentExpressionLanguageExpressionFunction;use SymfonyComponentExpressionLanguageExpressionFunctionProviderInterface;!class AppExpressionLanguageProvider implements ExpressionFunctionProviderInterface{return array(new ExpressionFunction('md5',function ($str) {return sprintf('(is_string(%1$s) ? md5(%1$s) : %1$s)', $str);},function ($arguments, $str) {return is_string($str) ? md5($str) : $str;}););}
  • 121. Custom md5( ) functionuse SymfonyComponentExpressionLanguageExpressionFunction;use SymfonyComponentExpressionLanguageExpressionFunctionProviderInterface;!class AppExpressionLanguageProvider implements ExpressionFunctionProviderInterface{return array(new ExpressionFunction('md5',function ($str) {return sprintf('(is_string(%1$s) ? md5(%1$s) : %1$s)', $str);},function ($arguments, $str) {return is_string($str) ? md5($str) : $str;}););}compiled expression
  • 122. Custom md5( ) functionuse SymfonyComponentExpressionLanguageExpressionFunction;use SymfonyComponentExpressionLanguageExpressionFunctionProviderInterface;!class AppExpressionLanguageProvider implements ExpressionFunctionProviderInterface{return array(new ExpressionFunction('md5',function ($str) {return sprintf('(is_string(%1$s) ? md5(%1$s) : %1$s)', $str);},function ($arguments, $str) {return is_string($str) ? md5($str) : $str;}););}compiled expressionexecuted expression
  • 123. Using custom functions in expressionsnamespace AppBundleController;!use SymfonyBundleFrameworkBundleControllerController;use SensioBundleFrameworkExtraBundleConfigurationSecurity;!class BlogController extends Controller{/*** @Security("md5(user.email) == post.author_id")* ...*/public function editAction(...){// ...}}# app/config/services.ymlservices:app.expressions:class: AppBundleExpressionLanguageProvidertags:- { name: security.expression_language_provider }
  • 124. Expression Language service tagsservices:app.expressions:class: ...tags:- { name: security.expression_language_provider }!!services:app.expressions:class: ...tags:- { name: routing.expression_language_provider }
  • 125. mini-tip
  • 126. Have you ever used the{% do %} Twig tag?
  • 127. Using the {% do %} Twig tag{% do form.field.setRendered %}!{{ form_start(form) }}{{ form_errors(form) }}!{{ form_row(...) }}{{ form_end(form) }}Source: http://stackoverflow.com/q/10570002
  • 128. Using the {% do %} Twig tag{% do form.field.setRendered %}!{{ form_start(form) }}{{ form_errors(form) }}!{{ form_row(...) }}{{ form_end(form) }} Makes form_rest( )and form_end( ) notdisplay the given field. Source: http://stackoverflow.com/q/10570002
  • 129. Paralleleverything
  • 130. How many cores/threadshas your CPU?
  • 131. How many of them are usedwhile developing theapplication?
  • 132. Parallel tests
  • 133. « TDD test suites should runin 10 seconds or less »Source: http://blog.ploeh.dk/2012/05/24/TDDtestsuitesshouldrunin10secondsorless/
  • 134. Parallel test execution with Fastest# Install$ composer require --dev 'liuggio/fastest' 'dev-master'!# Execute tests in 4 parallel threads$ find src/ -name "*Test.php" | ./vendor/bin/fastest"phpunit -c app {};"
  • 135. Database isolation for parallel testsgetenv('ENV_TEST_CHANNEL');getenv('ENV_TEST_CHANNEL_READABLE');getenv('ENV_TEST_CHANNELS_NUMBER');getenv('ENV_TEST_ARGUMENT');getenv('ENV_TEST_INC_NUMBER');getenv('ENV_TEST_IS_FIRST_ON_CHANNEL');
  • 136. Database isolation for parallel testsgetenv('ENV_TEST_CHANNEL');getenv('ENV_TEST_CHANNEL_READABLE');getenv('ENV_TEST_CHANNELS_NUMBER');getenv('ENV_TEST_ARGUMENT');getenv('ENV_TEST_INC_NUMBER');getenv('ENV_TEST_IS_FIRST_ON_CHANNEL');30minutes7minutes
  • 137. Alternatives to Fastest• Paratest (Official PHPUnit plugin)github.com/brianium/paratest• Parallel PHPUnitgithub.com/socialpoint-labs/parallel-phpunit• Docker + PHPUnitmarmelab.com/blog/2013/11/04/how-to-use-docker-to-run-phpunit-tests-in-parallel.html
  • 138. Parallel assets
  • 139. Dumping assets … slowly$ php app/console --env=prodassetic:dump
  • 140. Parallel asset dumping$ php app/console --env=prodassetic:dump--forks=4
  • 141. Parallel asset dumping$ php app/console --env=prod$ prod a:d --forks=4assetic:dump--forks=4
  • 142. Parallel asset dumping$ php app/console --env=prod$ prod a:d --forks=4assetic:dump--forks=4# it requires to install the Spork library$ composer require kriswallsmith/spork
  • 143. Parallel HTTPrequests
  • 144. Parallel HTTP requests with Guzzleuse GuzzleHttpPool;use GuzzleHttpClient;!$client = new Client();!$requests = [$client->createRequest('GET', 'http://example.org'),$client->createRequest('DELETE', 'http://example.org/delete'),$client->createRequest('PUT', 'http://example.org/put', ['body' => 'test'])];!(new Pool($client, $requests))->wait();
  • 145. Rarely usedDoctrine features
  • 146. Ordering withexpressions
  • 147. This does not workSELECT tFROM Talks tORDER BY t.comments + t.likes_count
  • 148. This does workSELECT t,(t.comments + t.likes_count) AS HIDDEN scoreFROM Talks tORDER BY score!
  • 149. Partial queries
  • 150. Selecting specific propertiespublic function findPost(){return $this->createQueryBuilder('p')->select('p.id, p.title')->where('p.id = :id')->setParameter('id', 1)->getQuery()->getResult();}
  • 151. The executed SQL querydoctrine.DEBUG:!SELECT p0_.id AS id_0,p0_.title AS title_1FROM Post p0_WHERE p0_.id = ?
  • 152. The resulting "object"array(1) {[0]=> array(2) {["id"] => int(1)["title"] => string(7) "Post #1"}}
  • 153. Using Doctrine partialspublic function findPost(){return $this->createQueryBuilder('p')->select('partial p.{id, title}')->where('p.id = :id')->setParameter('id', 1)->getQuery()->getResult();}
  • 154. The executed SQL querydoctrine.DEBUG:!SELECT p0_.id AS id_0,p0_.title AS title_1FROM Post p0_WHERE p0_.id = ?
  • 155. The resulting objectarray(1) {[0]=>object(stdClass)#676 (9) {["__CLASS__"] => string(21) "AppBundleEntityPost"["id"] => int(1)["title"] => string(7) "Post #1"["slug"] => NULL["summary"] => NULL["content"] => NULL["authorEmail"] => NULL["publishedAt"] => NULL["comments"] => string(8) "Array(5)"}}
  • 156. The resulting objectarray(1) {[0]=>object(stdClass)#676 (9) {["__CLASS__"] => string(21) "AppBundleEntityPost"["id"] => int(1)["title"] => string(7) "Post #1"["slug"] => NULL["summary"] => NULL["content"] => NULL["authorEmail"] => NULL["publishedAt"] => NULL["comments"] => string(8) "Array(5)"}}
  • 157. WARNINGDoctrine Partials makeyour code very fragile.Use them with caution.
  • 158. Legitimate use cases for Partials• Specific (and limited) parts of theapplication where performance is the toppriority.• Never when developing the application(wrong premature optimization).
  • 159. schema_filteroption
  • 160. Configuring schema_filter option# app/config/config.ymldoctrine:dbal:default_connection: default# ...schema_filter: ~^(?!legacy_)~
  • 161. Filtering tables managed by other apps# app/config/config.ymldoctrine:dbal:default_connection: default# ...schema_filter: ~^(?!(django|auth)_)~Source: http://javaguirre.net/2013/10/16/symfony-migrations-and-django-admin/
  • 162. Using schema_filter to migrate gradually# app/config/config.ymldoctrine:dbal:default_connection: defaultconnections:default:dbname: "%database_name%"# ...legacy:# ...schema_filter: ~^(?!(prefix)_)~Source: http://espend.de/artikel/doctrine-und-symfony2-orm-entities-aus-datenbank-erzeugen.html
  • 163. DoctrineFilters
  • 164. How do you deal withglobal SQL clauses?
  • 165. Examples of global SQL clausesCMS applicationsDisplay only publishedcontents.WHERE status = :published
  • 166. Examples of global SQL clausesCMS applicationsDisplay only publishedNever display (soft)contents.deleted items.WHERE status = :published WHERE status = :not_deleted
  • 167. Examples of global SQL clausesCMS applicationsDisplay only publishedNever display (soft)contents.deleted items.WHERE status = :published WHERE status = :not_deletedCommerce applicationsDisplay only invoices/orders for the given user. WHERE user_id = :user_id
  • 168. Doctrine filters allow toadd SQL to the conditionalclauses of queries,regardless the place wherethe SQL is generated.
  • 169. Creating a global Locale filternamespace AppBundleFilterLocaleFilter;!use DoctrineORMMappingClassMetaData;use DoctrineORMQueryFilterSQLFilter;!class LocaleFilter extends SQLFilter{public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias){return sprintf('%s.%s = %s',$targetTableAlias, 'locale', $this->getParameter('locale'));}}WHERE locale = 'es'
  • 170. Enabling the filter globally# app/config/config.ymldoctrine:orm:filters:locale_filter:class: AppBundleFilterLocaleFilterenabled: true
  • 171. Using the filter and setting its values$filter = $this->em->getFilters()->enable('locale_filter');!$filter->setParameter('locale', 'es');
  • 172. What's the point of usingglobal filters if you have toset up values manually foreach query?
  • 173. Setting filter values automatically (1/2)services:app.locale_filter:class: AppBundleFilterLocaleFilterarguments: [@session]tags:- {name: kernel.event_listener,event: kernel.request,method: onKernelRequest}Source: http://stackoverflow.com/a/15792119
  • 174. Setting filter values automatically (2/2)public function __construct(SessionInterface $session){$this->session = $session;}!// ...!public function onKernelRequest(){$this->em->getConfiguration()->addFilter('locale_filter', 'AppBundleFilterLocaleFilter');!$filter = $this->em->getFilters()->enable('locale_filter');$filter->setParameter('locale', $this->session->get('user_locale'));}Source: http://stackoverflow.com/a/15792119
  • 175. Constraining the scope of the filtersclass LocaleFilter extends SQLFilter{public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias){if (!$targetEntity->reflClass->implementsInterface('LocaleAware')) {return '';}!// ...}}
  • 176. Constraining the scope of the filtersclass LocaleFilter extends SQLFilter{public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias){if (!$targetEntity->reflClass->implementsInterface('LocaleAware')) {return '';}!// ...}}/** @ORMEntity */class Content implements LocalAware{// ...}
  • 177. Combining filters with annotationsnamespace AppBundleEntity;!use AppBundleAnnotationUserAware;/*** @UserAware(userFieldName="user_id")*/class Order { ... }
  • 178. Combining filters with annotationsnamespace AppBundleEntity;WHERE user_id = :id!use AppBundleAnnotationUserAware;/*** @UserAware(userFieldName="user_id")*/class Order { ... }
  • 179. Combining filters with annotationsnamespace AppBundleEntity;WHERE user_id = :id!use AppBundleAnnotationUserAware;/*** @UserAware(userFieldName="user_id")*/class Order { ... }Learn how to do this at:http://blog.michaelperrin.fr/2014/07/25/doctrine-filters/
  • 180. Value Objects
  • 181. Value Objectsin theory
  • 182. A value object is a smallobject that represents asimple entity whose equalityis not based on identity.
  • 183. Two value objects are equalwhen they have the samevalue, not necessarily beingthe same object.
  • 184. Identity is irrelevant for value objectsobject(AppBundleEntityMoney)#25 (3) {["id"] => int(25)["amount"] => int(10)["currency"] => string(3) "EUR"}!object(AppBundleEntityMoney)#267 (3) {["id"] => int(267)["amount"] => int(10)["currency"] => string(3) "EUR"}10 EUR10 EUR
  • 185. Value Objectsin Doctrine
  • 186. A typical Doctrine entitynamespace AppBundleEntity;!/** @Entity */class Customer{/** @Id @GeneratedValue @Column(type="integer") */protected $id;!/** @Column(type="string") */protected $name;!/** @Column(type="string", length=120) **/protected $email_address;!protected $address_line_1;protected $address_line_2;protected $city;protected $state;protected $postalCode;protected $country;}
  • 187. A typical Doctrine entitynamespace AppBundleEntity;!/** @Entity */class Customer{/** @Id @GeneratedValue @Column(type="integer") */protected $id;!/** @Column(type="string") */protected $name;!/** @Column(type="string", length=120) **/protected $email_address;!protected $address_line_1;protected $address_line_2;protected $city;protected $state;protected $postalCode;protected $country;}AddressValue Object
  • 188. Defining a Value Object in Doctrinenamespace AppBundleValueObject;!use DoctrineORMMapping as ORM;!/** @ORMEmbeddable */class Address{/** @ORMColumn(type = "string") */ /** @ORMColumn(type = "string") */protected $line_1; protected $line_2;!/** @ORMColumn(type = "string") */ /** @ORMColumn(type = "string") */protected $city; protected $state;!/** @ORMColumn(type = "string") */ /** @ORMColumn(type = "string") */protected $postalCode; protected $country;}
  • 189. Using a Value Object in Doctrinenamespace AppBundleEntity;!use DoctrineORMMapping as ORM;!/** @ORMEntity */class Customer{/** @ORMId @ORMGeneratedValue @ORMColumn(type="integer") */protected $id;!/** @ORMColumn(type="string") */protected $name = '';!/** @ORMColumn(type="string", length=120) **/protected $email_address = '';!/** @ORMEmbedded(class="AppBundleValueObjectAddress") */protected $address;}
  • 190. Value Objects at SQL levelCREATE TABLE Post (id INTEGER NOT NULL,title VARCHAR(255) NOT NULL,slug VARCHAR(255) NOT NULL,summary VARCHAR(255) NOT NULL,content CLOB NOT NULL,authorEmail VARCHAR(255) NOT NULL,publishedAt DATETIME NOT NULL,address_line_1 VARCHAR(255) NOT NULL,address_line_2 VARCHAR(255) NOT NULL,address_city VARCHAR(255) NOT NULL,address_state VARCHAR(255) NOT NULL,address_postalCode VARCHAR(255) NOT NULL,address_country VARCHAR(255) NOT NULL,PRIMARY KEY(id))
  • 191. Value Objects at query levelSELECT cFROM Customer cWHERE c.address.city = :city
  • 192. Embeddables are classeswhich are not entitiesthemself, but are embeddedin entities and can also bequeried in DQL.
  • 193. Agenda Assets Performance LoggingLegacy Testing ConfigParallel Doctrine ValueObjects
  • 194. Thank you.
  • 195. Questions? http://joind.in/talk/view/12944
  • 196. Contact info!!javier.eguiluz@sensiolabs.com
  • 197. SymfonyConMadrid - 27 November 2014
    Please download to view
  • 197
    All materials on our website are shared by users. If you have any questions about copyright issues, please report us to resolve them. We are always happy to assist you.
    Description
    A quick overview of tips, tricks and code snippets for developers using Symfony and all its ecosystem, from Monolog to Doctrine. Learn how to become more productive and discover some rarely used options and features.
    Text
    • 1. SYMFONYTIPS & TRICKSSymfonyConMadrid - 27 November 2014Javier Eguiluz
  • 2. About meevangelist and trainer at
  • 3. My Symfony work
  • 4. Why Symfony Tips and Tricks?I don’t work asa developer But I read andreview code everysingle dayI also read everysingle blog postpublished aboutSymfony These are the tips andtricks discoveredduring this year.
  • 5. Thanks to my awesome co-workers forsharing their knowledge!Grégoire Nicolas HugoTugdual SarahJulienLoïc Jérémy RomainJosephFX
  • 6. Agenda Assets Performance LoggingLegacy Testing ConfigParallel Doctrine ValueObjects
  • 7. Assetmanagement
  • 8. Named assets
  • 9. Typical asset linking{% javascripts'@AppBundle/Resources/public/js/jquery.js''@AppBundle/Resources/public/js/jquery.ui.js''@AppBundle/Resources/public/js/bootstrap.js''@AppBundle/Resources/public/js/*' %}<script src="{{ asset_url }}"></script>{% endjavascripts %}
  • 10. Defining named assets# app/config/config.ymlassetic:assets:# ...bootstrap_js:inputs:- '@AppBundle/Resources/public/js/jquery.js'- '@AppBundle/Resources/public/js/jquery.ui.js'- '@AppBundle/Resources/public/js/bootstrap.js'
  • 11. Using named assets{% javascripts'@bootstrap_js''@AppBundle/Resources/public/js/*' %}<script src="{{ asset_url }}"></script>{% endjavascripts %}
  • 12. Assetpackages
  • 13. asset( ) adds a leading slash<img src="{{ asset('images/logo.png') }}" />
  • 14. asset( ) adds a leading slash<img src="{{ asset('images/logo.png') }}" /><img src="/images/logo.png" />
  • 15. Easiest way to define a base URL# app/config/config.ymlframework:templating:assets_base_urls:http: ['http://static.example.com/images']ssl: ['https://static.example.com/images']
  • 16. Easiest way to define a base URL# app/config/config.ymlframework:templating:assets_base_urls:http: ['http://static.example.com/images']ssl: ['https://static.example.com/images']if you configure severalbase URLs, Symfony willselect one each time tobalance asset load
  • 17. Protocol-relative base URL# app/config/config.ymlframework:templating:assets_base_urls: '//static.example.com/images'
  • 18. AWshast eif wt ep waanct ktoa mgodeifsythe base URL just for a fewselected assets?
  • 19. What if we want to modifythe base URL just for a fewselected assets?Asset packages
  • 20. Asset packagesGroup assetslogically.Define config foreach package.
  • 21. Defining asset packages# app/config/config.ymlframework:templating:packages:avatars:base_urls: '//static.example.com/avatars/'
  • 22. Asset packages in practice<img src="{{ asset('...', 'avatars') }}" />
  • 23. Asset packages in practice<img src="{{ asset('...', 'avatars') }}" /><img src="//static.example.com/avatars/logo.png" />
  • 24. Packages can define any asset option# app/config/config.ymlframework:templating:packages:avatars:version: '20141127'base_urls: '//static.example.com/avatars/'
  • 25. mini-tip
  • 26. Using the Symfony console$ php app/console --env=prodassetic:dump
  • 27. Using the Symfony console$ php app/console --env=prodassetic:dumpWRONG RIGHT
  • 28. Using the Symfony console$ ./app/console --env=prodassetic:dumpWRONG RIGHT
  • 29. Using the Symfony console$ sf --env=prod assetic:dumpalias sf = php app/consoleWRONG RIGHT
  • 30. Using good Symfony console shortcuts$ dev ...$ prod ...alias dev = php app/console --env=devalias prod = php app/console --env=prod
  • 31. PROS$ prod a:d$ dev r:m /$ dev cont:dNEWCOMERS$ php app/console --env=prod assetic:dump$ php app/console router:match /$ php app/console container:debug
  • 32. Performance
  • 33. Optimizeautoloading
  • 34. Unoptimized auto-loading
  • 35. Optimized auto-loading$ composer dump-autoload --optimize
  • 36. Don't load test classes in production{"autoload": {"psr-4": { "MyLibrary": "src/" }},"autoload-dev": {"psr-4": { "MyLibraryTests": "tests/" }}}
  • 37. Add classes tocompile
  • 38. Three important Symfony files• app/bootstrap.php.cacheInternal Symfony classes.• app/cache/<env>/appDevDebugProjectContainer.phpCompiled container (services and parameters).• app/cache/<env>/classes.phpSymfony bundles classes.
  • 39. Adding your own classes for compilingnamespace AppBundleDependencyInjection;!use SymfonyComponentConfigFileLocator;use SymfonyComponentDependencyInjectionContainerBuilder;use SymfonyComponentHttpKernelDependencyInjectionExtension;!class AppExtension extends Extension{public function load(array $configs, ContainerBuilder $container){// ...!$this->addClassesToCompile(['AppBundleFullNamespaceClass']);}}
  • 40. Adding your own classes for compilingnamespace AppBundleDependencyInjection;!use SymfonyComponentConfigFileLocator;use SymfonyComponentDependencyInjectionContainerBuilder;use SymfonyComponentHttpKernelDependencyInjectionExtension;!class AppExtension extends Extension{public function load(array $configs, ContainerBuilder $container){// ...!$this->addClassesToCompile(['AppBundleFullNamespaceClass']);}}WARNINGIt doesn't work ifthe classes defineannotations
  • 41. mini-tip
  • 42. How can you enable theprofiler on production forsome specific users?
  • 43. The easy way: restrict by path# app/config/config.ymlframework:# ...profiler:matcher:path: ^/admin/
  • 44. The right way: restrict by user (1/2)# app/config/config.ymlframework:# ...profiler:matcher:service: app.profiler_matcher!services:app.profiler_matcher:class: AppBundleProfilerMatcherarguments: ["@security.context"]
  • 45. The right way: restrict by user (2/2)namespace AppBundleProfiler;!use SymfonyComponentSecurityCoreSecurityContext;use SymfonyComponentHttpFoundationRequest;use SymfonyComponentHttpFoundationRequestMatcherInterface;!class Matcher implements RequestMatcherInterface{protected $securityContext;!public function __construct(SecurityContext $securityContext){$this->securityContext = $securityContext;}!public function matches(Request $request){return $this->securityContext->isGranted('ROLE_ADMIN');}}
  • 46. Better logs withMonolog
  • 47. De-clutteryour logs
  • 48. Symfony logs a lot of information[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundleControllerDefaultController::indexAction", "_route": "homepage") [] [][2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous Token [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerDebugHandlersListener::configure". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleFrameworkBundleEventListenerSessionListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerFragmentListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleAsseticBundleEventListenerRequestListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyBundleFrameworkBundleDataCollectorRouterDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyComponentHttpKernelDataCollectorRequestDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerControllerListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerParamConverterListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerSecurityListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerTemplateListener::onKernelController". [] [][2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slug AS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail AS authorEmail5,p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ? ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"] [][2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpFirewallContextListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpRememberMeResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyBundleWebProfilerBundleEventListenerWebDebugToolbarListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerStreamedResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyBundleSwiftmailerBundleEventListenerEmailSenderListener::onTerminate". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelTerminate". [] []
  • 49. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundleControllerDefaultController::indexAction", "_route": "homepage") [] [][2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous Token [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerDebugHandlersListener::configure". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleFrameworkBundleEventListenerSessionListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerFragmentListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleAsseticBundleEventListenerRequestListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyBundleFrameworkBundleDataCollectorRouterDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyComponentHttpKernelDataCollectorRequestDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerControllerListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerParamConverterListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerSecurityListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerTemplateListener::onKernelController". [] [][2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slug AS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail AS authorEmail5,p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ? ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"] [][2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpFirewallContextListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpRememberMeResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyBundleWebProfilerBundleEventListenerWebDebugToolbarListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerStreamedResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyBundleSwiftmailerBundleEventListenerEmailSenderListener::onTerminate". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelTerminate". [] []
  • 50. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundleControllerDefaultController::indexAction", "_route": "homepage") [] []2014-[[2014-2014-11-24 12:24:22] security.11-INFO: Populated 24 SecurityContext 12:24:with an anonymous Token [] []11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener 22] "SymfonyComponentevent.HttpKernelEventListenerDEBUG: DebugHandlersListener::Notifiedconfigure". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleFrameworkBundleEventListenerSessionListener::onKernelRequest". [] [][event 2014-11-24 12:24:22] event."DEBUG: kernel.Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerFragmentListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentto HttpKernellistener EventListenerRouterListener::onKernelRequest". "Symfony[] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelRequest". [] [][2014-Bundle11-24 12:24:22] event.DEBUG: FrameworkBundleNotified event "kernel.request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener "SymfonyBundleAsseticBundleEventListenerRequestListener::onKernelRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyBundleFrameworkBundleEventListenerDataCollectorRouterDataCollector::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SymfonyComponentHttpKernelDataCollectorRequestDataCollector::onKernelController". [] [][2014-SessionListener::11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener onKernelRequest". "SensioBundleFrameworkExtraBundleEventListenerControllerListener::[] onKernelController". [2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerParamConverterListener::onKernelController". [][] [][] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerSecurityListener::onKernelController". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.controller" to listener "SensioBundleFrameworkExtraBundleEventListenerTemplateListener::onKernelController". [] [][2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slug AS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail AS authorEmail5,p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ? ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"] [][2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpFirewallContextListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentSecurityHttpRememberMeResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SensioBundleFrameworkExtraBundleEventListenerHttpCacheListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyBundleWebProfilerBundleEventListenerWebDebugToolbarListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.response" to listener "SymfonyComponentHttpKernelEventListenerStreamedResponseListener::onKernelResponse". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerLocaleListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentHttpKernelEventListenerRouterListener::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.finish_request" to listener "SymfonyComponentSecurityHttpFirewall::onKernelFinishRequest". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyBundleSwiftmailerBundleEventListenerEmailSenderListener::onTerminate". [] [][2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.terminate" to listener "SymfonyComponentHttpKernelEventListenerProfilerListener::onKernelTerminate". [] []
  • 51. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: ...[2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...
  • 52. Is this useful for your application?[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: ...[2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...[2014-11-24 12:24:22] event.DEBUG: Notified event "kernel.request" to listener ...log channel
  • 53. Hide event logs (if you don't need them)# app/config/dev.ymlmonolog:handlers:main:type: streampath: "%kernel.logs_dir%/%kernel.environment%.log"level: debugchannels: "!event"
  • 54. De-cluttered log files[2014-11-24 12:24:22] request.INFO: Matched route "homepage" (parameters: "_controller":"AppBundleControllerDefaultController::indexAction", "_route": "homepage")![2014-11-24 12:24:22] security.INFO: Populated SecurityContext with an anonymous Token![2014-11-24 12:24:22] doctrine.DEBUG: SELECT p0_.id AS id0, p0_.title AS title1, p0_.slugAS slug2, p0_.summary AS summary3, p0_.content AS content4, p0_.authorEmail ASauthorEmail5, p0_.publishedAt AS publishedAt6 FROM Post p0_ WHERE p0_.publishedAt <= ?ORDER BY p0_.publishedAt DESC LIMIT 10 ["2014-11-24 12:24:22"]![2014-11-24 12:24:22] security.DEBUG: Write SecurityContext in the session
  • 55. Better logemails
  • 56. Email logs related to 5xx errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: criticalhandler: bufferedbuffered:type: bufferhandler: swiftswift:type: swift_mailerfrom_email: error@example.comto_email: error@example.comsubject: An Error Occurred!level: debug
  • 57. Email logs related to 5xx errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: criticalhandler: bufferedbuffered:type: bufferhandler: swiftswift:type: swift_mailerfrom_email: error@example.comto_email: error@example.comsubject: An Error Occurred!level: debug
  • 58. Email logs related to 4xx errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: errorhandler: bufferedbuffered:# ...swift:# ...
  • 59. Don't email 404 errors# app/config/config_prod.ymlmonolog:handlers:mail:type: fingers_crossedaction_level: errorexcluded_404:- ^/handler: bufferedbuffered:# ...swift:# ...
  • 60. Organize yourlogs
  • 61. Use different log files for each channel# app/config/config_prod.ymlmonolog:handlers:main:type: streampath: "%kernel.logs_dir%/%kernel.environment%.log"level: debugchannels: ["!event"]security:type: streampath: "%kernel.logs_dir%/security-%kernel.environment%.log"level: debugchannels: "security"
  • 62. Format yourlog messages
  • 63. Don't do this$logger->info(sprintf("Order number %s processed for %s user",$number, $user));
  • 64. Do this instead$logger->info("Order number {num} processed for {name} user",['num' => $number, 'name' => $user]);
  • 65. Do this instead$logger->info("Order number {num} processed for {name} user",['num' => $number, 'name' => $user]);WARNINGIt doesn't workunless the PSRlog processor isenabled.
  • 66. Enable the PsrLogMessageProcessor# app/config/config_prod.ymlservices:monolog_processor:class: MonologProcessorPsrLogMessageProcessortags:- { name: monolog.processor }
  • 67. Monolog includes several processors# app/config/config_prod.ymlservices:monolog_processor:class: MonologProcessorIntrospectionProcessortags:- { name: monolog.processor }
  • 68. Create your own processor$logger->info("Order processed successfully.", $order);
  • 69. Create your own processor$logger->info("Order processed successfully.", $order);[2014-11-24 15:15:58] app.INFO: Order processedsuccessfully. { "order": { "number": "023924382982","user":"John Smith", "total":"34.99","status":"PAID" }}
  • 70. Custom monolog processornamespace AppBundleLoggerOrderProcessor;!use AppBundleEntityOrder;!class OrderProcessor{public function __invoke(array $record){if (isset($record['context']['order']) && $record['context']['order'] instanceof Order) {$order = $record['context']['order'];$record['context']['order'] = ['number' => $order->getNumber(),'user' => $order->get...,'total' => $order->get...,'status' => $order->get...,];}!return $record;}# app/config/config_prod.ymlservices:monolog_processor:class: AppBundleLoggerOrderProcessortags:- { name: monolog.processor }
  • 71. mini-tip
  • 72. Medium-sized andLarge ApplicationsMedium-sized andSmall ApplicationsUsing your own mailer is OK.
  • 73. Not all emails should be treated equallySign UpsLost passwordNotificationsNewslettersSpam
  • 74. Instant and delayed mailers# app/config/config.ymlswiftmailer:default_mailer: delayedmailers:instant:# ...spool: ~delayed:# ...spool:type: filepath: "%kernel.cache_dir%/swiftmailer/spool"
  • 75. Using prioritized mailers// high-priority emails$container->get('swiftmailer.mailer.instant')->...!// regular emails$container->get('swiftmailer.mailer')->...$container->get('swiftmailer.mailer.delayed')->...
  • 76. Legacyapplications
  • 77. Pokemon™strategy(catch’em all)
  • 78. Controller that captures legacy URLsclass LegacyController extends Controller{/*** @Route("/{filename}.php", name="_legacy")*/public function legacyAction($filename){$legacyPath = $this->container->getParameter('legacy.path');!ob_start();chdir($legacyAppPath);require_once $legacyAppPath.$filename.'.php';$content = ob_get_clean();!return new Response($content);}}Source: http://cvuorinen.net/slides/symfony-legacy-integration/
  • 79. Embbeddinglegacy apps
  • 80. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy App
  • 81. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequest
  • 82. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.request
  • 83. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpException
  • 84. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exception
  • 85. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exception
  • 86. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exception
  • 87. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exceptionResponse
  • 88. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestNotFoundHttpExceptionkernel.exceptionResponseResponse
  • 89. AppKernelRouterListenerLegacyKernel ListenerLegacyKernel Legacy AppRequestkernel.requestResponseNotFoundHttpExceptionkernel.exceptionResponseResponse
  • 90. LegacyKernel to process requestsclass LegacyKernel implements HttpKernelInterface{public function handle(Request $request, ...){ob_start();!$legacyDir = dirname($this->legacyAppPath);chdir($legacyDir);require_once $this->legacyAppPath;!$content = ob_get_clean();!return new Response($content);}}Source: http://cvuorinen.net/slides/symfony-legacy-integration/
  • 91. Listener to treat 404 as legacy requestsclass LegacyKernelListener implements EventSubscriberInterface{public function onKernelException($event){$exception = $event->getException();!if ($exception instanceof NotFoundHttpException) {$request = $event->getRequest();$response = $this->legacyKernel->handle($request);!$response->headers->set('X-Status-Code', 200);!$event->setResponse($response);}}} Source: http://cvuorinen.net/slides/symfony-legacy-integration/
  • 92. mini-tip
  • 93. Click-to-file in Symfony ExceptionsTextmate
  • 94. PHPStorm is now supported natively# app/config/config.ymlframework:ide: "phpstorm://open?file=%%f&line=%%l"
  • 95. Testing tips
  • 96. Prophecy
  • 97. Prophecy is a highlyopinionated yet verypowerful and flexible PHPobject mocking framework.
  • 98. PHPUnit has built-inProphecy mocks supportsince 4.5 version.
  • 99. A complete Prophecy mockclass SubjectTest extends PHPUnit_Framework_TestCase{public function testObserversAreUpdated(){$subject = new Subject('My subject');!$observer = $this->prophesize('Observer');$observer->update('something')->shouldBeCalled();!$subject->attach($observer->reveal());!$subject->doSomething();}}
  • 100. Prophecy vs traditional mocks (1/2)!!$observer = $this->prophesize('Observer');!!!$observer = $this->getMockBuilder('Observer')->setMethods(array('update'))->getMock();ProphecyPHPUnit
  • 101. Prophecy vs traditional mocks (2/2)!Prophecy!$observer->update('something')->shouldBeCalled();!!!$observer->expects($this->once())->method('update')->with($this->equalTo('something'));PHPUnit
  • 102. Faster smoketesting
  • 103. Smoke testing is preliminarytesting to reveal simplefailures severe enough toreject a prospectivesoftware release.
  • 104. Unnecessarily slow testsnamespace AppBundleTestsController;!use SymfonyBundleFrameworkBundleTestWebTestCase;!class DefaultControllerTest extends WebTestCase{/** @dataProvider provideUrls */public function testPageIsSuccessful($url){$client = self::createClient();$client->request('GET', $url);$this->assertTrue($client->getResponse()->isSuccessful());}!public function provideUrls(){// ...}}
  • 105. Functional tests without WebTestCaseclass SmokeTest extends PHPUnit_Framework_TestCase{private $app;!protected function setUp(){$this->app = new AppKernel('test', false);$this->app->boot();}!/** @dataProvider provideUrls */public function testUrl($url){$request = new Request::create($url, 'GET');$response = $this->app->handle($request);!$this->assertTrue($response->isSuccessful());}} Source: http://gnugat.github.io/2014/11/15/sf2-quick-functional-tests.html
  • 106. mini-tip
  • 107. Impersonating users# app/config/security.ymlsecurity:firewalls:main:# ...switch_user: truehttp://example.com/path?_switch_user=fabien
  • 108. WARNINGImpersonating is verydangerous and it canlead to severe errors.
  • 109. Visual hints when impersonating users<body class="{% if app.user and is_granted('ROLE_PREVIOUS_ADMIN') %}impersonating{% endif %}">!.impersonating {background: rgba(204, 0, 0, 0.15);}
  • 110. Be careful when impersonating users
  • 111. Configuration
  • 112. Yaml casting
  • 113. Implicit YAML castingkey:value1: 5value2: 2.7value3: 2014-11-27
  • 114. Implicit YAML castingkey:value1: 5value2: 2.7value3: 2014-11-27array(1) {["key"]=> array(3) {["value1"] => int(5)["value2"] => float(2.7)["value3"] => int(1417042800)}}
  • 115. Explicit YAML castingkey:value1: !str 5value2: ! 2.7value3: !str 2014-11-27
  • 116. Explicit YAML castingkey:value1: !str 5value2: ! 2.7value3: !str 2014-11-27array(1) {["key"]=> array(3) {["value1"] => string(1) "5"["value2"] => int(2)["value3"] => string(10) "2014-11-27"}}
  • 117. Explicit YAML castingkey:value1: !str 5value2: ! 2.7value3: !str 2014-11-27array(1) {["key"]=> array(3) {["value1"] => string(1) "5"["value2"] => int(2)["value3"] => string(10) "2014-11-27"}}! = int !str = string!!php/object: = serialized object
  • 118. CustomExpressions
  • 119. Expression Language in action/*** @Route("/post/{id}")* @Cache(smaxage="15", lastModified="post.getUpdatedAt()")*/public function showAction(Post $post) { ... }!!/*** @AssertExpression("this.getFoo() == 'fo'", message="Not good!")*/class Obj { ... }
  • 120. Custom md5( ) functionuse SymfonyComponentExpressionLanguageExpressionFunction;use SymfonyComponentExpressionLanguageExpressionFunctionProviderInterface;!class AppExpressionLanguageProvider implements ExpressionFunctionProviderInterface{return array(new ExpressionFunction('md5',function ($str) {return sprintf('(is_string(%1$s) ? md5(%1$s) : %1$s)', $str);},function ($arguments, $str) {return is_string($str) ? md5($str) : $str;}););}
  • 121. Custom md5( ) functionuse SymfonyComponentExpressionLanguageExpressionFunction;use SymfonyComponentExpressionLanguageExpressionFunctionProviderInterface;!class AppExpressionLanguageProvider implements ExpressionFunctionProviderInterface{return array(new ExpressionFunction('md5',function ($str) {return sprintf('(is_string(%1$s) ? md5(%1$s) : %1$s)', $str);},function ($arguments, $str) {return is_string($str) ? md5($str) : $str;}););}compiled expression
  • 122. Custom md5( ) functionuse SymfonyComponentExpressionLanguageExpressionFunction;use SymfonyComponentExpressionLanguageExpressionFunctionProviderInterface;!class AppExpressionLanguageProvider implements ExpressionFunctionProviderInterface{return array(new ExpressionFunction('md5',function ($str) {return sprintf('(is_string(%1$s) ? md5(%1$s) : %1$s)', $str);},function ($arguments, $str) {return is_string($str) ? md5($str) : $str;}););}compiled expressionexecuted expression
  • 123. Using custom functions in expressionsnamespace AppBundleController;!use SymfonyBundleFrameworkBundleControllerController;use SensioBundleFrameworkExtraBundleConfigurationSecurity;!class BlogController extends Controller{/*** @Security("md5(user.email) == post.author_id")* ...*/public function editAction(...){// ...}}# app/config/services.ymlservices:app.expressions:class: AppBundleExpressionLanguageProvidertags:- { name: security.expression_language_provider }
  • 124. Expression Language service tagsservices:app.expressions:class: ...tags:- { name: security.expression_language_provider }!!services:app.expressions:class: ...tags:- { name: routing.expression_language_provider }
  • 125. mini-tip
  • 126. Have you ever used the{% do %} Twig tag?
  • 127. Using the {% do %} Twig tag{% do form.field.setRendered %}!{{ form_start(form) }}{{ form_errors(form) }}!{{ form_row(...) }}{{ form_end(form) }}Source: http://stackoverflow.com/q/10570002
  • 128. Using the {% do %} Twig tag{% do form.field.setRendered %}!{{ form_start(form) }}{{ form_errors(form) }}!{{ form_row(...) }}{{ form_end(form) }} Makes form_rest( )and form_end( ) notdisplay the given field. Source: http://stackoverflow.com/q/10570002
  • 129. Paralleleverything
  • 130. How many cores/threadshas your CPU?
  • 131. How many of them are usedwhile developing theapplication?
  • 132. Parallel tests
  • 133. « TDD test suites should runin 10 seconds or less »Source: http://blog.ploeh.dk/2012/05/24/TDDtestsuitesshouldrunin10secondsorless/
  • 134. Parallel test execution with Fastest# Install$ composer require --dev 'liuggio/fastest' 'dev-master'!# Execute tests in 4 parallel threads$ find src/ -name "*Test.php" | ./vendor/bin/fastest"phpunit -c app {};"
  • 135. Database isolation for parallel testsgetenv('ENV_TEST_CHANNEL');getenv('ENV_TEST_CHANNEL_READABLE');getenv('ENV_TEST_CHANNELS_NUMBER');getenv('ENV_TEST_ARGUMENT');getenv('ENV_TEST_INC_NUMBER');getenv('ENV_TEST_IS_FIRST_ON_CHANNEL');
  • 136. Database isolation for parallel testsgetenv('ENV_TEST_CHANNEL');getenv('ENV_TEST_CHANNEL_READABLE');getenv('ENV_TEST_CHANNELS_NUMBER');getenv('ENV_TEST_ARGUMENT');getenv('ENV_TEST_INC_NUMBER');getenv('ENV_TEST_IS_FIRST_ON_CHANNEL');30minutes7minutes
  • 137. Alternatives to Fastest• Paratest (Official PHPUnit plugin)github.com/brianium/paratest• Parallel PHPUnitgithub.com/socialpoint-labs/parallel-phpunit• Docker + PHPUnitmarmelab.com/blog/2013/11/04/how-to-use-docker-to-run-phpunit-tests-in-parallel.html
  • 138. Parallel assets
  • 139. Dumping assets … slowly$ php app/console --env=prodassetic:dump
  • 140. Parallel asset dumping$ php app/console --env=prodassetic:dump--forks=4
  • 141. Parallel asset dumping$ php app/console --env=prod$ prod a:d --forks=4assetic:dump--forks=4
  • 142. Parallel asset dumping$ php app/console --env=prod$ prod a:d --forks=4assetic:dump--forks=4# it requires to install the Spork library$ composer require kriswallsmith/spork
  • 143. Parallel HTTPrequests
  • 144. Parallel HTTP requests with Guzzleuse GuzzleHttpPool;use GuzzleHttpClient;!$client = new Client();!$requests = [$client->createRequest('GET', 'http://example.org'),$client->createRequest('DELETE', 'http://example.org/delete'),$client->createRequest('PUT', 'http://example.org/put', ['body' => 'test'])];!(new Pool($client, $requests))->wait();
  • 145. Rarely usedDoctrine features
  • 146. Ordering withexpressions
  • 147. This does not workSELECT tFROM Talks tORDER BY t.comments + t.likes_count
  • 148. This does workSELECT t,(t.comments + t.likes_count) AS HIDDEN scoreFROM Talks tORDER BY score!
  • 149. Partial queries
  • 150. Selecting specific propertiespublic function findPost(){return $this->createQueryBuilder('p')->select('p.id, p.title')->where('p.id = :id')->setParameter('id', 1)->getQuery()->getResult();}
  • 151. The executed SQL querydoctrine.DEBUG:!SELECT p0_.id AS id_0,p0_.title AS title_1FROM Post p0_WHERE p0_.id = ?
  • 152. The resulting "object"array(1) {[0]=> array(2) {["id"] => int(1)["title"] => string(7) "Post #1"}}
  • 153. Using Doctrine partialspublic function findPost(){return $this->createQueryBuilder('p')->select('partial p.{id, title}')->where('p.id = :id')->setParameter('id', 1)->getQuery()->getResult();}
  • 154. The executed SQL querydoctrine.DEBUG:!SELECT p0_.id AS id_0,p0_.title AS title_1FROM Post p0_WHERE p0_.id = ?
  • 155. The resulting objectarray(1) {[0]=>object(stdClass)#676 (9) {["__CLASS__"] => string(21) "AppBundleEntityPost"["id"] => int(1)["title"] => string(7) "Post #1"["slug"] => NULL["summary"] => NULL["content"] => NULL["authorEmail"] => NULL["publishedAt"] => NULL["comments"] => string(8) "Array(5)"}}
  • 156. The resulting objectarray(1) {[0]=>object(stdClass)#676 (9) {["__CLASS__"] => string(21) "AppBundleEntityPost"["id"] => int(1)["title"] => string(7) "Post #1"["slug"] => NULL["summary"] => NULL["content"] => NULL["authorEmail"] => NULL["publishedAt"] => NULL["comments"] => string(8) "Array(5)"}}
  • 157. WARNINGDoctrine Partials makeyour code very fragile.Use them with caution.
  • 158. Legitimate use cases for Partials• Specific (and limited) parts of theapplication where performance is the toppriority.• Never when developing the application(wrong premature optimization).
  • 159. schema_filteroption
  • 160. Configuring schema_filter option# app/config/config.ymldoctrine:dbal:default_connection: default# ...schema_filter: ~^(?!legacy_)~
  • 161. Filtering tables managed by other apps# app/config/config.ymldoctrine:dbal:default_connection: default# ...schema_filter: ~^(?!(django|auth)_)~Source: http://javaguirre.net/2013/10/16/symfony-migrations-and-django-admin/
  • 162. Using schema_filter to migrate gradually# app/config/config.ymldoctrine:dbal:default_connection: defaultconnections:default:dbname: "%database_name%"# ...legacy:# ...schema_filter: ~^(?!(prefix)_)~Source: http://espend.de/artikel/doctrine-und-symfony2-orm-entities-aus-datenbank-erzeugen.html
  • 163. DoctrineFilters
  • 164. How do you deal withglobal SQL clauses?
  • 165. Examples of global SQL clausesCMS applicationsDisplay only publishedcontents.WHERE status = :published
  • 166. Examples of global SQL clausesCMS applicationsDisplay only publishedNever display (soft)contents.deleted items.WHERE status = :published WHERE status = :not_deleted
  • 167. Examples of global SQL clausesCMS applicationsDisplay only publishedNever display (soft)contents.deleted items.WHERE status = :published WHERE status = :not_deletedCommerce applicationsDisplay only invoices/orders for the given user. WHERE user_id = :user_id
  • 168. Doctrine filters allow toadd SQL to the conditionalclauses of queries,regardless the place wherethe SQL is generated.
  • 169. Creating a global Locale filternamespace AppBundleFilterLocaleFilter;!use DoctrineORMMappingClassMetaData;use DoctrineORMQueryFilterSQLFilter;!class LocaleFilter extends SQLFilter{public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias){return sprintf('%s.%s = %s',$targetTableAlias, 'locale', $this->getParameter('locale'));}}WHERE locale = 'es'
  • 170. Enabling the filter globally# app/config/config.ymldoctrine:orm:filters:locale_filter:class: AppBundleFilterLocaleFilterenabled: true
  • 171. Using the filter and setting its values$filter = $this->em->getFilters()->enable('locale_filter');!$filter->setParameter('locale', 'es');
  • 172. What's the point of usingglobal filters if you have toset up values manually foreach query?
  • 173. Setting filter values automatically (1/2)services:app.locale_filter:class: AppBundleFilterLocaleFilterarguments: [@session]tags:- {name: kernel.event_listener,event: kernel.request,method: onKernelRequest}Source: http://stackoverflow.com/a/15792119
  • 174. Setting filter values automatically (2/2)public function __construct(SessionInterface $session){$this->session = $session;}!// ...!public function onKernelRequest(){$this->em->getConfiguration()->addFilter('locale_filter', 'AppBundleFilterLocaleFilter');!$filter = $this->em->getFilters()->enable('locale_filter');$filter->setParameter('locale', $this->session->get('user_locale'));}Source: http://stackoverflow.com/a/15792119
  • 175. Constraining the scope of the filtersclass LocaleFilter extends SQLFilter{public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias){if (!$targetEntity->reflClass->implementsInterface('LocaleAware')) {return '';}!// ...}}
  • 176. Constraining the scope of the filtersclass LocaleFilter extends SQLFilter{public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias){if (!$targetEntity->reflClass->implementsInterface('LocaleAware')) {return '';}!// ...}}/** @ORMEntity */class Content implements LocalAware{// ...}
  • 177. Combining filters with annotationsnamespace AppBundleEntity;!use AppBundleAnnotationUserAware;/*** @UserAware(userFieldName="user_id")*/class Order { ... }
  • 178. Combining filters with annotationsnamespace AppBundleEntity;WHERE user_id = :id!use AppBundleAnnotationUserAware;/*** @UserAware(userFieldName="user_id")*/class Order { ... }
  • 179. Combining filters with annotationsnamespace AppBundleEntity;WHERE user_id = :id!use AppBundleAnnotationUserAware;/*** @UserAware(userFieldName="user_id")*/class Order { ... }Learn how to do this at:http://blog.michaelperrin.fr/2014/07/25/doctrine-filters/
  • 180. Value Objects
  • 181. Value Objectsin theory
  • 182. A value object is a smallobject that represents asimple entity whose equalityis not based on identity.
  • 183. Two value objects are equalwhen they have the samevalue, not necessarily beingthe same object.
  • 184. Identity is irrelevant for value objectsobject(AppBundleEntityMoney)#25 (3) {["id"] => int(25)["amount"] => int(10)["currency"] => string(3) "EUR"}!object(AppBundleEntityMoney)#267 (3) {["id"] => int(267)["amount"] => int(10)["currency"] => string(3) "EUR"}10 EUR10 EUR
  • 185. Value Objectsin Doctrine
  • 186. A typical Doctrine entitynamespace AppBundleEntity;!/** @Entity */class Customer{/** @Id @GeneratedValue @Column(type="integer") */protected $id;!/** @Column(type="string") */protected $name;!/** @Column(type="string", length=120) **/protected $email_address;!protected $address_line_1;protected $address_line_2;protected $city;protected $state;protected $postalCode;protected $country;}
  • 187. A typical Doctrine entitynamespace AppBundleEntity;!/** @Entity */class Customer{/** @Id @GeneratedValue @Column(type="integer") */protected $id;!/** @Column(type="string") */protected $name;!/** @Column(type="string", length=120) **/protected $email_address;!protected $address_line_1;protected $address_line_2;protected $city;protected $state;protected $postalCode;protected $country;}AddressValue Object
  • 188. Defining a Value Object in Doctrinenamespace AppBundleValueObject;!use DoctrineORMMapping as ORM;!/** @ORMEmbeddable */class Address{/** @ORMColumn(type = "string") */ /** @ORMColumn(type = "string") */protected $line_1; protected $line_2;!/** @ORMColumn(type = "string") */ /** @ORMColumn(type = "string") */protected $city; protected $state;!/** @ORMColumn(type = "string") */ /** @ORMColumn(type = "string") */protected $postalCode; protected $country;}
  • 189. Using a Value Object in Doctrinenamespace AppBundleEntity;!use DoctrineORMMapping as ORM;!/** @ORMEntity */class Customer{/** @ORMId @ORMGeneratedValue @ORMColumn(type="integer") */protected $id;!/** @ORMColumn(type="string") */protected $name = '';!/** @ORMColumn(type="string", length=120) **/protected $email_address = '';!/** @ORMEmbedded(class="AppBundleValueObjectAddress") */protected $address;}
  • 190. Value Objects at SQL levelCREATE TABLE Post (id INTEGER NOT NULL,title VARCHAR(255) NOT NULL,slug VARCHAR(255) NOT NULL,summary VARCHAR(255) NOT NULL,content CLOB NOT NULL,authorEmail VARCHAR(255) NOT NULL,publishedAt DATETIME NOT NULL,address_line_1 VARCHAR(255) NOT NULL,address_line_2 VARCHAR(255) NOT NULL,address_city VARCHAR(255) NOT NULL,address_state VARCHAR(255) NOT NULL,address_postalCode VARCHAR(255) NOT NULL,address_country VARCHAR(255) NOT NULL,PRIMARY KEY(id))
  • 191. Value Objects at query levelSELECT cFROM Customer cWHERE c.address.city = :city
  • 192. Embeddables are classeswhich are not entitiesthemself, but are embeddedin entities and can also bequeried in DQL.
  • 193. Agenda Assets Performance LoggingLegacy Testing ConfigParallel Doctrine ValueObjects
  • 194. Thank you.
  • 195. Questions? http://joind.in/talk/view/12944
  • 196. Contact info!!javier.eguiluz@sensiolabs.com
  • 197. SymfonyConMadrid - 27 November 2014
  • Comments
    Top