Symfony2 - The Unofficial "Best" Practices

65
Symfony2 The Unofficial BestPractices Gerry Vandermaesen

Transcript of Symfony2 - The Unofficial "Best" Practices

Page 1: Symfony2 - The Unofficial "Best" Practices

Symfony2

The Unofficial “Best” PracticesGerry Vandermaesen

Page 2: Symfony2 - The Unofficial "Best" Practices

AKA

How Gerry Does Symfony2

Page 3: Symfony2 - The Unofficial "Best" Practices

About Me

• Developer and trainer at King Foo

• SensioLabs Certified Symfony Developer

• We develop tailor-made PHP applications

• @gerryvdm / [email protected]

Page 4: Symfony2 - The Unofficial "Best" Practices

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

Page 5: Symfony2 - The Unofficial "Best" Practices

Keep your Business Logic Outside Bundles

Page 6: Symfony2 - The Unofficial "Best" Practices

• Your business logic hopefully outlives Symfony2

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

Page 7: Symfony2 - The Unofficial "Best" Practices

Keep Mapping and Validation Configuration Outside Entity Classes

= Do Not Use Annotations

Page 8: Symfony2 - The Unofficial "Best" Practices

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

Page 9: Symfony2 - The Unofficial "Best" Practices

Tip:Defining Constraints in

app/config/validation.yml

Page 10: Symfony2 - The Unofficial "Best" Practices

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

Page 11: Symfony2 - The Unofficial "Best" Practices

Combo-Tip: Defining Constraints in

app/config/validation/*.yml

Page 12: Symfony2 - The Unofficial "Best" Practices

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

Page 13: Symfony2 - The Unofficial "Best" Practices

The Front Controller

Page 14: Symfony2 - The Unofficial "Best" Practices

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

Page 15: Symfony2 - The Unofficial "Best" Practices

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

Page 16: Symfony2 - The Unofficial "Best" Practices

<VirtualHost *:80> # ... SetEnv SYMFONY_ENV dev SetEnv SYMFONY_DEBUG 1</VirtualHost>

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

Apache

Nginx

Page 17: Symfony2 - The Unofficial "Best" Practices

Taking It One Step Further

Page 18: Symfony2 - The Unofficial "Best" Practices

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

Page 19: Symfony2 - The Unofficial "Best" Practices

Use PSR-4 for the `src/` Directory

Page 20: Symfony2 - The Unofficial "Best" Practices

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

Page 21: Symfony2 - The Unofficial "Best" Practices

💫

Page 22: Symfony2 - The Unofficial "Best" Practices

Prevent Autoloading ofTests in Production

Page 23: Symfony2 - The Unofficial "Best" Practices

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

Page 24: Symfony2 - The Unofficial "Best" Practices

Add Requirements to your Routes

Page 25: Symfony2 - The Unofficial "Best" Practices

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

Page 26: Symfony2 - The Unofficial "Best" Practices

# 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

Page 27: Symfony2 - The Unofficial "Best" Practices

Regex 😕

Page 28: Symfony2 - The Unofficial "Best" Practices

Global “Requirements”

Page 29: Symfony2 - The Unofficial "Best" Practices

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

Page 30: Symfony2 - The Unofficial "Best" Practices

# 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

Page 31: Symfony2 - The Unofficial "Best" Practices

Bring Order in your Services Configuration

Page 32: Symfony2 - The Unofficial "Best" Practices

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

Page 33: Symfony2 - The Unofficial "Best" Practices

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

Page 34: Symfony2 - The Unofficial "Best" Practices

Try Defining Your Services as Private

Page 35: Symfony2 - The Unofficial "Best" Practices

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

Page 36: Symfony2 - The Unofficial "Best" Practices

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

Page 37: Symfony2 - The Unofficial "Best" Practices

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

Page 38: Symfony2 - The Unofficial "Best" Practices

Define your Repositories as Services

Page 39: Symfony2 - The Unofficial "Best" Practices

# 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]

Page 40: Symfony2 - The Unofficial "Best" Practices

Define your Controllers as Services

Page 41: Symfony2 - The Unofficial "Best" Practices

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

Page 42: Symfony2 - The Unofficial "Best" Practices

One Controller One Action

Page 43: Symfony2 - The Unofficial "Best" Practices

# 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]

Page 44: Symfony2 - The Unofficial "Best" Practices

<?phpnamespace KingFoo\Presentation\Controller\BlogPost;use Doctrine\Common\Persistence\ObjectManager;use Symfony\Component\HttpFoundation\Request;class CreateController{ public function __construct(ObjectManager $manager) { // ... } public function __invoke(Request $request) { // ... } }

Page 45: Symfony2 - The Unofficial "Best" Practices

Annotate your Controllers

Page 46: Symfony2 - The Unofficial "Best" Practices

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

Page 47: Symfony2 - The Unofficial "Best" Practices

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

Page 48: Symfony2 - The Unofficial "Best" Practices

Never Circumvent the Framework

Page 49: Symfony2 - The Unofficial "Best" Practices

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

Page 50: Symfony2 - The Unofficial "Best" Practices

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); } ); $cookie = new Cookie('lastGeneration', time()); $response->headers->setCookie($cookie); return $response; }}

Page 51: Symfony2 - The Unofficial "Best" Practices

Securing your Application

Page 52: Symfony2 - The Unofficial "Best" Practices

Avoid usage of `access_control` in

security.yml

access_control: - { path: ^/(nl|fr|en)/admin, roles: ROLE_ADMIN }

Page 53: Symfony2 - The Unofficial "Best" Practices

Regex 😕

Page 54: Symfony2 - The Unofficial "Best" Practices

class AdminController{ /** * @Route("/{_locale}/admin") * @Security("has_role('ROLE_ADMIN')") */ public function __invoke(Request $request) { // if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) { // throw new AccessDeniedHttpException(); // } // .. } }

Page 55: Symfony2 - The Unofficial "Best" Practices

Use Bcrypt

Page 56: Symfony2 - The Unofficial "Best" Practices

# security.ymlsecurity: encoders: KingFoo\Presentation\Security\User: algorithm: bcrypt cost: 13

Page 57: Symfony2 - The Unofficial "Best" Practices

# services.ymlservices: password_encoder: class: Symfony\Component\Security\...\BCryptPasswordEncoder arguments: [13]# security.ymlsecurity: encoders: KingFoo\Presentation\Security\User: id: password_encoder

Page 58: Symfony2 - The Unofficial "Best" Practices

Create Your Own Symfony Framework Edition

Page 59: Symfony2 - The Unofficial "Best" Practices

• Remove clutter

• Add/remove default dependencies

• Customize default configuration

Page 60: Symfony2 - The Unofficial "Best" Practices

• Fork symfony/symfony-standard

• Make modifications

• Modify composer.json package name

• Publish on GitHub and Packagist

Page 61: Symfony2 - The Unofficial "Best" Practices

{ "name": "kingfoo/symfony-project", "autoload": { "psr-4": { "": "src/" } }, "require": { "php": ">=5.5.0", "symfony/symfony": "~2.6", "doctrine/mongodb-odm": "~1.0@dev", "doctrine/mongodb-odm-bundle": "~3.0@dev", "symfony/monolog-bundle": "~2.4", "sensio/distribution-bundle": "~3.0,>=3.0.12", "sensio/framework-extra-bundle": "~3.0", "incenteev/composer-parameter-handler": "~2.0" }, "require-dev": { "phpunit/phpunit": "~4.4" } }

Page 62: Symfony2 - The Unofficial "Best" Practices

Miscellaneous / Flamebait

• Bundles are for extending the container and configuring (third-party) libraries that are reusable across projects in the container.

• Try doing without bundles for project-specific configuration.

• YAML is the preferred configuration file format for application-specific config, XML for configuration of bundles

Page 63: Symfony2 - The Unofficial "Best" Practices

Develop Your OwnBest Practices

• Do it as a team

• Write them down

• Be consistent

• Learn regex 😉

Page 64: Symfony2 - The Unofficial "Best" Practices

Next Symfony Training

• Getting Started with Symfony2

• 12-13th February 2015

• Aarschot

• Leave your name + email for a €50 reduction

Page 65: Symfony2 - The Unofficial "Best" Practices

Questions? 🙉