Symfony2 - The Unofficial "Best" Practices

Click here to load reader

  • date post

    16-Jul-2015
  • Category

    Technology

  • view

    1.049
  • download

    2

Embed Size (px)

Transcript of Symfony2 - The Unofficial "Best" Practices

  • Symfony2

    The Unofficial Best PracticesGerry Vandermaesen

  • AKA

    How Gerry Does Symfony2

  • About Me

    Developer and trainer at King Foo

    SensioLabs Certified Symfony Developer

    We develop tailor-made PHP applications

    @gerryvdm / [email protected]

  • The Official Best Practices

    Excellent guidelines for beginning Symfony developers

    Use a single bundle for application specific code

    Use the app/Resources/views/ directory for all application specific templates

    Use YAML for application configuration

  • Keep your Business Logic Outside Bundles

  • Your business logic hopefully outlives Symfony2

    Keep your model agnostic / decoupled from the framework (and ORM)

  • Keep Mapping and Validation ConfigurationOutside Entity Classes

    = Do Not Use Annotations

  • # config.yml doctrine: orm: auto_mapping: false mappings: model: type: yml prefix: KingFoo\Presentation\Model dir: %kernel.root_dir%/config/doctrine alias: Model

  • Tip:Defining Constraints in

    app/config/validation.yml

  • use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;use Symfony\Component\DependencyInjection\ContainerBuilder;class RegisterValidationMappingsPass implements CompilerPassInterface{ public function process(ContainerBuilder $container) { if ($container->hasDefinition('validator.builder')) { $file = $container->getParameter('kernel.root_dir') . '/config/validation.yml'; $container->getDefinition('validator.builder') ->addMethodCall('addYamlMappings', [$file]); } }}

  • Combo-Tip: Defining Constraints in

    app/config/validation/*.yml

  • use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\Finder\Finder;class RegisterValidationMappingsPass implements CompilerPassInterface{ public function process(ContainerBuilder $container) { if ($container->hasDefinition('validator.builder')) { $dir = $container->getParameter('kernel.root_dir') . '/config/validation'; $finder = new Finder(); $finder->files()->name('*.yml')->in($dir); $files = []; foreach ($finder as $file) { $files[] = $file->getRealPath(); } if (count($files)) { $container->getDefinition('validator.builder') ->addMethodCall('addYamlMappings', [$files]); } } }}

  • The Front Controller

  • $kernel = new AppKernel('prod', false);

  • use Symfony\Component\Debug\Debug;

    $env = getenv('SYMFONY_ENV') ?: 'prod'; $debug = getenv('SYMFONY_DEBUG') === '1' && $env !== 'prod';

    if ($debug) { Debug::enable();}

    $kernel = new AppKernel($env, $debug);

  • # ... SetEnv SYMFONY_ENV dev SetEnv SYMFONY_DEBUG 1

    server { location /app.php { # ... fastcgi_param SYMFONY_ENV dev; fastcgi_param SYMFONY_DEBUG 1; } }

    Apache

    Nginx

  • Taking It One Step Further

  • server { # ... location / { try_files $uri @app; } location @app { fastcgi_pass unix:/var/run/php5-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME /path/to/project/app/app.php; fastcgi_param SCRIPT_NAME /path/to/project/app/app.php; fastcgi_param SYMFONY_ENV dev; fastcgi_param SYMFONY_DEBUG 1; } }

  • Use PSR-4 for the `src/` Directory

  • { "autoload": { "psr-4": { "KingFoo\\Presentation\\": "src/" } }}

  • Prevent Autoloading ofTests in Production

  • { "autoload": { "psr-4": { "KingFoo\\Presentation\\": "src/" } }, "autoload-dev": { "psr-4": { KingFoo\\Presentation\\Tests\\: "tests/" } }}

  • Add Requirements to your Routes

  • # routing.ymlhomepage: path: /{_locale}ticket_index: path: /{_locale}/ticketstickets_detail: path: /{_locale}/tickets/{id}tickets_create: path: /{_locale}/tickets/createlogin: path: /login

  • # routing.ymlhomepage: path: /{_locale} requirements: { _locale: "(nl|fr|en)" } ticket_index: path: /{_locale}/tickets requirements: { _locale: "(nl|fr|en)" } tickets_detail: path: /{_locale}/tickets/{id} requirements: { _locale: "(nl|fr|en)", id: "[1-9][0-9]*" } tickets_create: path: /{_locale}/tickets/create requirements: { _locale: "(nl|fr|en)" } login: path: /login

  • Regex

  • Global Requirements

  • use Symfony\Component\HttpKernel\Event\GetResponseEvent;use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;class CheckLocaleListener{ private $availableLocales; public function __construct(array $availableLocales) { $this->availableLocales = $availableLocales; } public function checkLocale(GetResponseEvent $event) { $request = $event->getRequest(); if ($locale = $request->attributes->get('_locale')) { if (!in_array($locale, $this->availableLocales)) { throw new NotFoundHttpException(); } } }}

  • # services.ymlparameters: available_locales: [nl, fr, en]services: check_locale_listener: class: CheckLocaleListener arguments: [%available_locales%] tags: - name: kernel.event_listener event: kernel.request method: checkLocale priority: 24

  • Bring Order in your Services Configuration

  • # services.yml# 500 lines of service declarations...

  • # services.ymlimports: - { resource: services/controllers.yml } - { resource: services/event_listeners.yml } - { resource: services/repositories.yml }

  • Try Defining Your Services as Private

  • # services.ymlservices: my_fantastic_service: class: KingFoo\Presentation\FantasticService public: false

  • class DependentService extends ContainerAware{ public function doSomething() { $this->container->get('my_fantastic_service') ->doSomethingElse(); }}

  • class DependentService{ public function __construct(FantasticService $service) { $this->service = $service; } public function doSomething() { $this->service->doSomethingElse(); }}

  • Define your Repositories as Services

  • # repositories.ymlservices: abstract_repository: abstract: true factory_service: doctrine.orm.entity_manager factory_method: getRepository public: false blog_post_repository: parent: abstract_repository class: KingFoo\Presentation\Repository\BlogPostRepository arguments: [Model:BlogPost] comment_repository: parent: abstract_repository class: KingFoo\Presentation\Repository\CommentRepository arguments: [Model:Comment]

  • Define your Controllers as Services

  • # controllers.ymlservices: blog_post_controller: class: KingFoo\Presentation\Controller\BlogPostController arguments: - @blog_post_repository - @doctrine.orm.entity_manager

  • One Controller One Action

  • # controllers.ymlservices: blog_post_list_controller: class: KingFoo\Presentation\Controller\BlogPost\ListController arguments: [@blog_post_repository] blog_post_create_controller: class: KingFoo\Presentation\Controller\BlogPost\CreateController arguments: [@doctrine.orm.entity_manager]

  • Annotate your Controllers

  • public function __invoke(Request $request) { if (!$this->authorizationChecker->isGranted('ROLE_EDITOR')) { throw new AccessDeniedHttpException(); } $response = new Response(); $response->setMaxAge(3600); return $this->templating->renderResponse( 'blog_post/create.html.twig', array( // view parameters ), $response ); }

  • use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;/** * @Route(service="blog_post_create_controller") */class CreateController{ /** * @Route("/blog/create", name="foo") * @Cache(maxage=3600) * @Security("has_role('ROLE_EDITOR')") * @Template("blog_post/create.html.twig") */ public function __invoke(Request $request) { return array( // view parameters ); }}

  • Never Circumvent the Framework

  • class BadController{ public function __invoke() { $template = $_GET['template']; setcookie('lastGeneration', time()); $this->archaicPdfLib->output($template); exit; }}

  • use Symfony\Component\HttpFoundation\Cookie;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\StreamedResponse;class BetterController{ public function __invoke(Request $request) { $template = $request->query->get('template'); $response = new StreamedResponse( function() use ($template) { $this->archaicPdfLib->output($template); } );