Share
Embed
Best Practices in Symfony2
Going beyond the Official Best Practices in Symfony2
Andreas Hucks
November 12, 2014
Transcript
77 slides
-
Naming Things in Symphony • Follow PSR-0, PSR-1, PSR-2 •
Find a common scheme for your team • Be explicit • Be consistent -
Care about your coding style • Again - follow PSR-0,
PSR-1, PSR-2 • Use PHPCSFixer http://goo.gl/gQnc0N -
.gitignore /web/bundles/ /app/bootstrap.php.cache /app/cache/* /app/config/parameters.yml /app/logs/*
/build/ /vendor/ /bin/ /composer.phar /data/uploads .idea add your own -
.gitignore /web/bundles/ /app/bootstrap.php.cache /app/cache/* /app/config/parameters.yml /app/logs/*
/build/ /vendor/ /bin/ /composer.phar /data/uploads .idea should be in your global .gitignore -
.gitignore /web/bundles/ /app/bootstrap.php.cache /app/cache/* /app/config/parameters.yml /app/logs/*
/build/ /vendor/ /bin/ /composer.phar /data/uploads .idea this too -
.gitignore /web/bundles/ /app/bootstrap.php.cache /app/cache/* /app/config/parameters.yml /app/logs/*
/build/ /vendor/ /bin/ /composer.phar /data/uploads .idea this. -
FAIL a.k.a. „because I can“ • MyCompleteAppBundle (ok for small
projects) • MyAppNeedingGlobalResourcesBundle • MyBundleInsideAnotherBundleBundle -
FAIL a.k.a. „because I can“ • MyCompleteAppBundle (ok for small
projects) • MyAppNeedingGlobalResourcesBundle • MyBundleInsideAnotherBundleBundle [sic] -
What should go into a Bundle • Bundles should be
self-contained • Sets of Features • Examples: API, Worker Commands, Admin Panel… • Configured in /app/config -
Your config options • YAML • XML • Annotations •
INI • PHP meh. -
Your config options • YAML • XML • Annotations •
INI • PHP special use cases -
Your config options • YAML • XML • Annotations •
INI • PHP mostly good -
Your config options • YAML • XML • Annotations •
INI • PHP Routing, Bundle Config, Parameters Services Routing, Validators, ORM/ODM -
If not using Annotations… Nest your routing files # /app/config/routing.yml
acme_conference: resource: @AcmeConferenceBundle/[…]/routing.yml prefix: /conference # /src/Acme/Bundle/ConferenceBundle/[…]/routing.yml acme_conference_admin: resource: routing_admin.yml prefix: / acme_conference_frontend: resource: routing_frontend.yml prefix: / -
• Put Controllers on a Diet • Decouple them •
But don't go overboard (this is the application layer) -
Inject the Request into Controllers public function createAction(Request $request) {
// not: $request = $this->getRequest(); $session = $request->getSession(); // .. } -
@Cache(...) /** * @Cache(smaxage=3600) */ public function listAction() { //
.. return $this->render( 'SensioConferenceBundle:Default:show.html.twig', array( 'conference' => $conference ) ); } -
@Template([name]) /** * @Template() */ public function listAction() { //
.. return array( 'conferences' => $conferences ); } compare to official best practices -
Use handleRequest() public function createAction(Request $request) { // ..
$form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // ... persist data, redirect } // .. } -
Events for add-on actions if ($form->isSubmitted() && $form->isValid()) { //
... persist data $this->get('event_dispatcher')->dispatch( ConferenceEvents::CREATED, new ConferenceEvent($conference) ); // .. redirect } -
Service Layers – where sensible class ConferenceHandler { public function
__construct( ObjectManager $manager, EventDispatcherInterface $dispatcher ) { $this->manager = $manager; $this->dispatcher = $dispatcher; } public function save(Conference $conference) { $this->manager->persist($conference); $this->manager->flush(); $this->dispatcher->dispatch( // ... ); } -
Using the BaseController? Wrap calls to the Container to clean
up /** * @return GeocoderInterface */ private function getGeocoder() { return $this->get('geocoder'); } -
Injecting the Container class MyWebserviceConnector { public function __construct( ContainerInterface
$container ) { // here be dragons } } -
...\TwigBundle\Extension\AssetsExtension class AssetsExtension extends \Twig_Extension { private $container; public
function __construct( ContainerInterface $container ) { $this->container = $container; } -
So why not… Because Scopes! class MyMailer { public function
__construct(Request $request) { // here be dragons } } -
Solution: RequestStack (>= 2.4) class MyMailer { public function __construct(RequestStack
$stack) { $this->stack = $stack; } public function doSomething() { $request = $this->stack->getCurrentRequest(); } } -
Alternative: Providing the Request (< 2.4) class RequestProvider { public
function __construct( ContainerInterface $container ) { $this->container = $container; } public function getRequest() { $this->container->get('request'); } } -
Split up Service Definitions public function load( array $configs, ContainerBuilder
$container ) { // … $loader->load('storage.yml'); $loader->load('imageprocessing.yml'); } -
Dynamic Loading of Service Definitions public function load( array $configs,
ContainerBuilder $container ) { // … $loader->load('storage.yml'); $loader->load('imageprocessing.yml'); if (isset($config['logging'])) { $loader->load('logging.yml'); } } -
The Container as a Registry public function listAction() { $max
= $this->container->getParameter( 'max_conferences' ); $conferences = $someStorage->getConferences($max); // .. } -
Binding to the Environment public function listAction() { $env =
$this->container->getParameter( 'kernel.environment' ); if ('dev' === $env) { // ... } // ... } -
Instead: Use Your config files # /app/config/config.yml acme_conference: thumbnails: width:
300 height: 200 # /app/config/config_dev.yml acme_conference: logging: ~ -
Miscellaneous • Use YAML for Service Definitions • Remember you
can (and should) use Environment Variables • Use %kernel.root_dir% as a reference -
PHP • Please drop 5.3 (it‘s also faster) • Use
an Opcode Cache -
Doctrine • Activate Metadata Cache • Activate Query Cache •
Pro Level only: Use factory-service to register Repos & Managers as Services • Do NOT inject the EntityManager into your entities -
Security • Make sure there are no leaks in the
security.yml access_control section! • Better: Check Authorization in Controller, use the @Security annotation -
Translation • Work with translation keys instead of full text
to avoid breaking translations -
PHPStorm • The Symfony Plugin is pretty stable, use
it! • Check out the PHP Annotations Plugin -
Forms in Controllers public function createAction(Request $request) { $form =
$this->createFormBuilder() ->add('name') ->add('startDate', 'date') ->add('endDate', 'date') ->add('location', 'textarea') ->getForm() ; // .. -
Better: Use Type Classes class ConferenceType extends AbstractType { public
function buildForm( FormBuilderInterface $builder, array $options ) { $builder ->add('name') ->add('startDate', 'date') ->add('endDate', 'date') ->add('location', 'textarea') ; } -
Always set the data_class public function setDefaultOptions( OptionsResolverInterface $resolver )
{ $resolver->setDefaults( array( 'data_class' => 'Acme\Conference' ) ); } -
Using Data in Constructors class ConferenceType extends AbstractType { public
function __construct(Venue $venue) { $this->venue = $venue; } public function buildForm( FormBuilderInterface $builder, array $options ) { // ... if ($this->venue->hasWifi()) { $builder->add('ssid'); } } -
Use Form Events for Related Data public function buildForm(FormBuilderInterface $builder,
array $options) { // ... $builder->addEventListener( FormEvents::PRE_SET_DATA, function (FormEvent $e) { if ($e->getData()->hasWifi()) { $e->getForm()->add('ssid'); } } ); -
Define Types as Services (for reusable Bundles) form_conference: class: "Sensio\[…]\Form\ConferenceType"
tags: - { name: twig.extension, alias: conference } -
public function createAction(Request $request) { $form = $this->createForm('conference'); $form ->handleRequest();
// ... Define Types as Services (for reusable Bundles) -
Don‘t disable CSRF public function setDefaultOptions( OptionsResolverInterface $resolver ) {
$resolver->setDefaults( array( // ... 'csrf_protection' => false ) ); }