Symfony2 - The Unofficial "Best" Practices
-
Upload
gerry-vandermaesen -
Category
Technology
-
view
1.057 -
download
2
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 Configuration Outside 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);
<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
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]
<?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) { // ... } }
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); } ); $cookie = new Cookie('lastGeneration', time()); $response->headers->setCookie($cookie); return $response; }}
Securing your Application
Avoid usage of `access_control` in
security.yml
access_control: - { path: ^/(nl|fr|en)/admin, roles: ROLE_ADMIN }
Regex 😕
class AdminController{ /** * @Route("/{_locale}/admin") * @Security("has_role('ROLE_ADMIN')") */ public function __invoke(Request $request) { // if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) { // throw new AccessDeniedHttpException(); // } // .. } }
Use Bcrypt
# security.ymlsecurity: encoders: KingFoo\Presentation\Security\User: algorithm: bcrypt cost: 13
# services.ymlservices: password_encoder: class: Symfony\Component\Security\...\BCryptPasswordEncoder arguments: [13]# security.ymlsecurity: encoders: KingFoo\Presentation\Security\User: id: password_encoder
Create Your Own Symfony Framework Edition
• Remove clutter
• Add/remove default dependencies
• Customize default configuration
• Fork symfony/symfony-standard
• Make modifications
• Modify composer.json package name
• Publish on GitHub and Packagist
{ "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" } }
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
Develop Your OwnBest Practices
• Do it as a team
• Write them down
• Be consistent
• Learn regex 😉
Next Symfony Training
• Getting Started with Symfony2
• 12-13th February 2015
• Aarschot
• Leave your name + email for a €50 reduction
Questions? 🙉