Symfony Documentation

The Documentation Changelog

This documentation is always changing: All new features need new documentation and bugs/typos get fixed. This article holds all important changes of the documentation.

小技巧

Do you also want to participate in the Symfony Documentation? Take a look at the “Contributing to the Documentation” article.

January, 2015

New Documentation

  • b32accb minor #4935 Fix typos (ifdattic)
  • ad74169 #4628 Varnish cookbook session cookie handling (dbu)
  • 3bb7b61 #4645 Remove note that’s no longer the case (thewilkybarkid)
  • 3293286 #4801 [Cookbook][cache][varnish] be more precise about version differences (dbu)
  • 528e8e1 #4740 Use AppBundle whenever it’s possible (javiereguiluz)
  • 9742b92 #4761 [Cookbook][Security] don’t output message from AuthenticationException (xabbuh)
  • a23e7d2 #4643 How to override vendor directory location (gajdaw)
  • 99aca45 #4749 [2.3][Book][Security] Add isPasswordValid doc as in 2.6 (xelaris)
  • d9935a3 #4141 Notes about caching pages with a CSRF Form (ricardclau)
  • 207f2f0 #4711 [Reference] Add default_locale config description (xelaris)
  • 1b0fe77 #4708 Change Apache php-fpm proxy configuration (TeLiXj)
  • 127ebc1 #4650 Documented the characters that provoke a YAML escaping string (javiereguiluz)
  • 0c0b708 #4454 More concrete explanation of validation groups (peterrehm)
  • 144e5af #4611 Adding a guide about upgrading (weaverryan)
  • 01df3e7 #4626 clean up cache invalidation information on the cache chapter (dbu)
  • 5f7ef85 #4651 Documented the security:check command (javiereguiluz)

Fixed Documentation

  • ea51aeb #4926 Finish #4505: Fixed composer create-project command (windows) (Epskampie)
  • b32accb minor #4935 Fix typos (ifdattic)
  • 7e84533 #4886 [Best Pracitices] restore example in the “Service: No Class Parameter” section (u-voelkel)
  • a6b7d72 #4861 Ifdattic’s fixes (ifdattic)
  • b9359a2 #4905 Update routing.rst (IlhamiD)
  • 9fee9ee #4746 Revert #4651 for 2.3 branch (xelaris)
  • 5940d52 #4735 [BestPractices] remove @Security annotation for Symfony 2.3 (xabbuh)
  • ffe3425 #4765 [Book][Forms] avoid the request service where possible (xabbuh)
  • d8e8d75 #4756 [Components][Config] don’t show deprecated usage of Yaml::parse() (xabbuh)
  • 310f4ae #4639 Update by_reference.rst.inc (docteurklein)

Minor Documentation Changes

  • 2cff942 #4878 [Book][Security] Remove out-dated anchor (xelaris)
  • a97646f #4882 Remove horizontal scrollbar (ifdattic)
  • c24c787 #4931 Remove horizontal scrollbar (ifdattic)
  • 83696b8 #4934 Fixes for 2.3 branch (ifdattic)
  • 99d225b #4943 Fixes for 2.3 branch (ifdattic)
  • 137ba72 #4945 Fixes for 2.3 branch (ifdattic)
  • b32accb #4935 Fix typos (ifdattic)
  • 0fa9cbd #4937 Keeping documentation consistent (thecatontheflat)
  • 3921d70 #4918 Quick proofread of the email cookbook (weaverryan)
  • 418a73b #4922 Fix typo: missing space (ifdattic)
  • 20d80c3 #4916 Fixes for 2.3 branch (ifdattic)
  • d7acccf #4914 Fix typo, remove horizontal scrollbar (ifdattic)
  • fc776ab #4894 Align methods in YAML example (ifdattic)
  • bd279f6 #4908 Set twig service as private (ifdattic)
  • 37fd035 #4899 Fix typo: looks => look (ifdattic)
  • fbaeecd #4898 added Kévin Dunglas as a merger for the Serializer component (fabpot)
  • 7c66a8b #4893 Move annotations example to front (ifdattic)
  • 2b7e5ee #4891 fixed typo (acme -> app) (adiebler)
  • 00981de #4890 Fixed typo (beni0888)
  • dc87147 #4876 Remove horizontal scrollbar (ifdattic)
  • f5f3c1b #4865 Removed literals for bundle names (WouterJ)
  • 9a6d7b9 #4831 Update override.rst (ifdattic)
  • f9c2d69 #4803 [Book][Translation] Added tip for routing params (xelaris)
  • 3774a37 #4881 Remove ‘acme’ (ifdattic)
  • 6a15077 #4874 Remove trailing whitespace (WouterJ)
  • 80bef5a #4873 [BestPractices] fix typo (xabbuh)
  • 6cffa4e #4866 Remove horizontal scrollbar (ifdattic)
  • bcf1508 #4785 [Book][Security] add back old anchors (xabbuh)
  • cf3d38a #4731 [Book][Testing] bump required PHPUnit version (xabbuh)
  • 4f47dec #4837 Monolog Cookbook Typo Fix: “allows to” should be “allows you to” (mattjanssen)
  • c454fd2 #4857 Add custom link labels where Cookbook articles titles looked wrong (javiereguiluz)
  • 17989fd #4860 [Components][HttpKernel] replace API link for SwiftmailerBundle (xabbuh)
  • e347ec8 #4819 Removed a leftover comma in security config sample (javiereguiluz)
  • 11b9d23 #4772 Tweaks to the new form csrf caching entry (weaverryan)
  • f9c1389 #4845 Update security.rst (meelijane)
  • 9680ec0 #4844 Update routing.rst (bglamer)
  • c243d00 #4843 Fixed typo (beni0888)
  • 13ffb83 #4835 Fixed broken link (SofHad)
  • d2a67ac #4826 Fixed 404 page (SofHad)
  • f34fc2d #4825 Fixed the 404 not found error (SofHad)
  • 91a89b7 #4821 Fixed typo (SofHad)
  • f7179df #4818 [Routing] Removed deprecated usage (WouterJ)
  • 892586b #4808 Email message instantiation changed to a more ‘symfonysh’ way. (alebo)
  • e913808 #4802 [Cookbook][Routing] Fixed typo (xelaris)
  • 236c26f #4796 Update service_container.rst (ifdattic)
  • f85c44c #4795 Remove horizontal scrollbar (ifdattic)
  • 45189bb #4792 [BestPractices] add filename to codeblock (xelaris)
  • fccea1d #4791 Fix heading level in form_login_setup.rst (xelaris)
  • 74c3a35 #4788 Controller is a callable (timglabisch)
  • 28571fc #4780 Add missing semicolon (NightFox7)
  • dc5d8f8 #4760 Update routing.rst (ifdattic)
  • 4e880c1 #4755 fix typo (xabbuh)
  • 463c30b #4751 [BestPractices] fix alignment of YAML values (xelaris)
  • 1972757 #4775 Corrected validation information on inheritance (peterrehm)
  • f4f8621 #4762 [Cookbook][Configuration] update text to use SetHandler (not ProxyPassMatch) (xabbuh)
  • 43543bb #4748 Re-reading private service section (weaverryan)
  • e447e70 #4743 [Book][Security] Fix typo and remove redundant sentence (xelaris)
  • 9819113 #4702 Clarify tip for creating a new AppBundle (xelaris)
  • 8f2fe87 #4683 [Reference] update the configuration reference (xabbuh)
  • e889813 #4677 Add exception to console exception log (adrienbrault)
  • 9958c41 #4656 Tried to clarify private services (WouterJ)
  • 1d5966c #4703 Fix representation (ifdattic)
  • aa9d982 #4697 Set twig service as private (ifdattic)
  • ece2c81 #4722 Improve readability (ifdattic)
  • dcc9516 #4725 Remove horizontal scrollbar (ifdattic)
  • 25dd825 #4730 Fix typo: as => is (ifdattic)
  • 760a441 #4734 [BestPractices] add missing comma (xabbuh)
  • 8c1afb9 #4738 [Contributing][Code] update year in license (xabbuh)
  • 4ad72d0 #4741 use the doc role for internal links (jms85, xabbuh)

December, 2014

New Documentation

  • 00a13d6 #4606 Completely re-reading the security book (weaverryan)
  • bd65c3c #4673 [Reference] add validation config reference section (xabbuh)
  • 55a32cf #4173 use a global Composer installation (xabbuh)
  • c5e409b #4526 Deploy Symfony application on Platform.sh. (GuGuss)
  • c837ea1 #4665 Documented the console environment variables (javiereguiluz)
  • f4a7196 #4627 Rewrite the varnish cookbook article (dbu)
  • 92a186d #4654 Rewritten from scratch the chapter about installing Symfony (javiereguiluz)
  • 90ef4ec #4580 Updated installation instructions to use the new Symfony Installer (javiereguiluz)
  • f591e6e #4532 GetResponse*Events stop after a response was set (Lumbendil)
  • 71495e8 #4528 Update web_server_configuration.rst (thePanz)
  • 9b330ef #4507 Comply with best practices, Round 2 (WouterJ)
  • 39a36bc #4405 Finish 3744 (mickaelandrieu, xabbuh)
  • db35c42 #4591 Instructions for setting SYMFONY_ENV on Heroku (dzuelke)
  • 8bba316 #4457 [RFC] Clarification on formatting for bangs (!) (bryanagee)

Fixed Documentation

  • 153565e #4707 [Cookbook] Fix XML example for RTE (dunglas)
  • cad4d3f #4582 Completed the needed context to successfully test commands with Helpers (peterrehm)
  • a137918 #4641 Add missing autoload include in basic console application example (senkal)
  • 0de8286 #4513 [Contributing] update contribution guide for 2.7/3.0 (xabbuh)
  • 7ea4b10 #4646 Update the_controller.rst (teggen)
  • baf61a0 #4623 [OptionsResolver] Fix Namespace link (xavren)
  • 8246693 #4613 Change refering block name from content to body (martin-cerny)
  • 1750b9b #4599 [Contributing] fix feature freeze dates (xabbuh)
  • 8e2e988 #4603 Replace form_enctype(form) with form_start(form). (xelaris)
  • 7acf27c #4552 required PHPUnit version in the docs should be updated to 4.2 (or later)... (jzawadzki)
  • df60ba7 #4548 Remove ExpressionLanguage reference for 2.3 version (dangarzon)
  • 727c92a #4594 Missing attribute ‘original’ (Marcelsj)
  • 97a9c43 #4533 Add command to make symfony.phar executable. (xelaris)

Minor Documentation Changes

  • 8bd694f #4709 [Reference] fix wording (xabbuh)
  • 1bd9ed4 #4721 [Cookbook][Composer] fix note directive (xabbuh)
  • 5055ef4 #4715 Improve readability (ifdattic)
  • d3d6d22 #4716 Fix typo: con => on (ifdattic)
  • afe8684 #4720 Link fixed (kuldipem)
  • 4b442a0 #4695 Misc changes (ifdattic)
  • 0db36ea #4706 Fix typo: than in Twig => than Twig templates (ifdattic)
  • 94b833e #4679 General grammar and style fixes in the book (frne)
  • 3f3464f #4689 Update form_customization.rst (rodrigorigotti)
  • 8d32393 #4691 replace “or” with ”,” (timglabisch)
  • 9b4d747 #4670 Change PHPUnit link to avoid redirect to homepage (xelaris)
  • 8ccffb0 #4669 Harmonize PHPUnit version to 4.2 or above (xelaris)
  • 84bf5e5 #4667 Remove redundant “default” connection (xelaris)
  • ceca63f #4653 update ordered list syntax (xabbuh)
  • 459875b #4550 Ref #3903 - Normalize methods listings (ternel)
  • 87365fa #4648 Update forms.rst (keefekwan)
  • 70f2ae8 #4640 [Book] link to the API documentation (xabbuh)
  • 95fc487 #4608 Removing some installation instructions (weaverryan)
  • 96455e6 #4539 Normalization of method listings (pedronofuentes)
  • bd44e6b #4664 Spelling mistake tens to tons (albabar)
  • 48cc9cd #4647 Update controllers.rst (keefekwan)
  • 2efed8c #4660 Fix indentation of YAML example (xelaris)
  • b55ec30 #4659 Fixed some code indentation (javiereguiluz)
  • 18af18b #4652 replace Symfony2 with Symfony (xabbuh)
  • a70c489 #4649 Linked the PDO/DBAL Session article from the Doctrine category (javiereguiluz)
  • f672a66 #4625 Added ‘-ing’ title ending to unify titles look (kix)
  • 9600950 #4617 [Filesystem] filesystem headlines match method names (xabbuh)
  • 8b006bb #4607 [Best Practices] readd mistakenly removed label (xabbuh)
  • 7dcce1b #4585 When explaining how to install dependencies for running unit tests, (carlosbuenosvinos)
  • 33ca697 #4561 Use the new build env on Travis (joshk)
  • 107610e #4531 [symfony] [Hackday] Fixed typos (pborreli)
  • 3b1611d #4519 remove service class parameters (xabbuh)
  • 3bd17af #4518 [Components][DependencyInjection] backport service factory improvements (xabbuh)
  • d203e5a #4495 [Best Practices][Business Logic] link to a bundle’s current (not master) docs (xabbuh)
  • 0a9c146 #4422 Fix typos in code (ifdattic)
  • 4f0051d #4574 fixed little typo (adridev)

November, 2014

New Documentation

  • 135aae6 #4433 Completely re-reading the controller chapter (weaverryan)
  • 422e0f1 #4465 Modifying the best practice to use form_start() instead of <form (weaverryan, WouterJ)
  • 0a21446 #4463 [BestPractices] Proposing that we make the service names just a little bit longer (weaverryan)
  • 1d88a1b #4443 Added the release dates for the upcoming Symfony 3 versions (javiereguiluz)
  • f2ab245 #4374 [WCM] Revamped the Quick Start tutorial (javiereguiluz)
  • 2c190ed #4427 Update most important book articles to follow the best practices (WouterJ)
  • 12a09ab #4377 Added interlinking and fixed install template for reusable bundles (WouterJ)
  • 8259d71 #4425 Updating component usage to use composer require (weaverryan)
  • 0e80aba #4369 [reference][configuration][security]Added key_length for pbkdf2 encoder (Guillaume-Rossignol)
  • 5165419 #4295 [Security] Hidden front controller for Nginx (phansys)

Fixed Documentation

  • 9d599a0 minor #4544 #4273 - fix doctrine version in How to Provide Model Classes for several Doctrine Implementations cookbook (ternel)
  • 6aabece #4273 - fix doctrine version in How to Provide Model Classes for several Doctrine Implementations cookbook
  • 4f66d48 #4506 SetDescription required on Product entities (yearofthegus)
  • 85bf906 #4444 fix elseif statement (MightyBranch)
  • ad14e78 #4494 Updated the Symfony Installer installation instructions (javiereguiluz)
  • 33bf462 #4407 [Components][Console] array options need array default values (xabbuh)
  • 2ab2e1f #4342 Reworded a misleading Doctrine explanation (javiereguiluz)

Minor Documentation Changes

  • 05f5dba #4536 Add Ryan Weaver as 10th core team member (ifdattic)
  • 7b1ff2a #4554 Changed url to PHP-CS-FIXER repository (jzawadzki)
  • 9d599a0 #4544 bug #4273 - fix doctrine version in How to Provide Model Classes for several Doctrine Implementations cookbook (ternel)
  • 7b3500c #4542 Update conventions.rst (csuarez)
  • 5aaba1e #4529 Best Practices: Update link title to match cookbook article title (dangarzon)
  • ab8e7f5 #4530 Book: Update link title to match cookbook article title (dangarzon)
  • bf61658 #4523 Add missing semicolons to PropertyAccess examples (loonytoons)
  • 5db8386 #4462 [Reference] Fixed lots of things using the review bot (WouterJ)
  • dbfaac1 #4459 Fix up the final sentence to be a bit cleaner. (micheal)
  • 3761e50 #4514 [Contributing][Documentation] typo fix (xabbuh)
  • 21afb4c #4445 Removed unnecessary use statement (Alex Salguero)
  • 3969fd6 #4432 [Reference][Twig] tweaks to the Twig reference (xabbuh)
  • 188dd1f #4400 Continues #4307 (SamanShafigh, WouterJ)
  • c008733 #4399 Explain form() and form_widget() in form customization (oopsFrogs, WouterJ)
  • 2139754 #4253 Adder and remover sidenote (kshishkin)
  • b81eb4d #4488 Terrible mistake! Comma instead of semicolon... (nuvolapl)
  • 0ee3ae7 #4481 [Cookbook][Cache] add syntax highlighting for Varnish code blocks (xabbuh)
  • 0577559 #4418 use the C lexer for Varnish config examples (xabbuh)
  • 97d8f61 #4403 Improved naming (WouterJ)
  • 6298595 #4453 Fixed make file (WouterJ)
  • 0c7dd72 #4475 Fixed typos (pborreli)
  • b847b2d #4480 Fix spelling (nurikabe)
  • 0d91cc5 #4461 Update doctrine.rst (guiguiboy)
  • 81fc1c6 #4448 [Book][HTTP Cache] moved inlined URL to the bottom of the file (xabbuh)
  • 6995b07 #4435 consistent table headlines (xabbuh)
  • 0380d34 #4447 [Book] tweaks to #4427 (xabbuh)
  • eb0d8ac #4441 Updated first code-block``::`` bash (Nitaco)
  • 41bc061 #4106 removed references to documentation from external sources (fabpot, WouterJ)
  • c9a8dff #4352 [Best Practices] update best practices index (xabbuh)
  • 8a93c95 #4437 Correct link to scopes page (mayeco)
  • 91eb652 #4438 Fix typo: Objected => Object (ifdattic)
  • 5d6d0c2 #4436 remove semicolons in PHP templates (xabbuh)
  • 97c4b2e #4434 remove unused label (xabbuh)
  • 4be6786 #4326 [Components][Form] Grammar improvement (fabschurt)
  • a27238e #4313 Improved and fixed twig reference (WouterJ)
  • 1ce9dc5 #4398 A few small improvements to the EventDispatcher Component docs (GeertDD)
  • 42abc66 #4421 [Best Practices] removed unused links in business-logic (77web)
  • 61c0bc5 #4419 [DependencyInjection] Add missing space in code (michaelperrin)

October, 2014

New Documentation

  • d7ef1c7 #4348 Updated information about handling validation of embedded forms to Valid... (peterrehm)
  • 691b13d #4340 [Cookbook][Web Server] add sidebar for the built-in server in VMs (xabbuh)
  • d79c48d #4280 [Cookbook][Cache] Added config example for Varnish 4.0 (thierrymarianne)
  • 5849f7f #4168 [Components][Form] describe how to access form errors (xabbuh)
  • c10e9c1 #4371 Added a code example for emailing on 4xx and 5xx errors without 404’s (weaverryan)
  • 0c57939 #4327 First import of the “Official Best Practices” book (javiereguiluz)
  • 8dc90ef #4224 [Components][HttpKernel] outline implications of the kernel.terminate event (xabbuh)
  • d3b5ba2 #4085 [Component][Forms] add missing features introduced in 2.3 (xabbuh)
  • f433e64 #4099 Composer installation verbosity tip (dannykopping)
  • 925a162 #4290 Updating library/bundle install docs to use “require” (weaverryan)
  • 44f570b #4294 Improve cookbook entry for error pages in 2.3~ (mpdude)
  • 3b6c2b9 #4269 [Cookbook][External Parameters] Enhance content (bicpi)
  • 62bafad #4246 [Reference] add description for the ```validation_groups``` option (xabbuh)
  • c2342a7 #4241 [Form] Added information about float choice lists (peterrehm)

Fixed Documentation

  • 68a2c7b #4381 Updated Valid constraint reference (inso)
  • db01e57 #4362 Missing apostrophe in source example. (astery)
  • d49d51f #4350 Removed extra parenthesis (sivolobov)
  • e6d7d8f #4315 Update choice.rst (odolbeau)
  • 1b15d57 #4300 [Components][PropertyAccess] Fix PropertyAccessorBuilder usage (Thierry Geindre)
  • 061324f #4297 [Cookbook][Doctrine] Fix typo in XML configuration for custom SQL functions (jdecool)
  • f81b7ad #4292 Fixed broken external link to DemoController Test (danielsan)
  • 9591a04 #4284 change misleading language identifier (Kristof Van Cauwenbergh, kristofvc)

Minor Documentation Changes

  • a4f7d51 #4396 Corrected latin abbreviation (GeertDD)
  • ebf2927 #4387 Inline condition removed for easier reading (acidjames)
  • aa70028 #4375 Removed the redundant usage of layer. (micheal)
  • f3dd676 #4394 update Sphinx extension submodule reference (xabbuh)
  • 9e03f2d #4388 Minor spelling fix (GeertDD)
  • 4dfd607 #4356 Remove incoherence between Doctrine and Propel introduction paragraphs (arnaugm)
  • 1d71332 #4344 [Templating] Added a sentence that explains what a Template Helper is (iltar)
  • 9a76309 #4384 fix typo (kokoon)
  • 3e8aa59 #4376 Cleaned up javascript code (flip111)
  • 06e7c5f #4364 changed submit button label (OskarStark)
  • d1810ca #4357 fix Twig-extensions links (mhor)
  • e2e2915 #4359 Added missing closing parenthesis to example. (mattjanssen)
  • f1bb8bb #4358 Fixed link to documentation standards (sivolobov)
  • 65c891d #4355 Missing space (ErikSaunier)
  • 7359cb4 #4196 Clarified the bundle base template bit. (Veltar)
  • 6ceb8cb #4345 Correct capitalization for the Content-Type header (GeertDD)
  • 3e4c92a #4104 Use ${APACHE_LOG_DIR} instead of /var/log/apache2 (xamgreen)
  • 3da0776 #4338 ESI Variable Details Continuation (Farkie, weaverryan)
  • 7f461d2 #4325 [Components][Form] Correct a typo (fabschurt)
  • d162329 #4276 [Components][HttpFoundation] Make a small grammatical adjustment (fabschurt)
  • 69bfac1 #4322 [Components][DependencyInjection] Correct a typo: replace “then” by “the” (fabschurt)
  • 8073239 #4318 [Cookbook][Bundles] Correct a typo: remove unnecessary “the” word (fabschurt)
  • 34e22d6 #4317 Remove horizontal scrollbar and change event name to follow conventions (ifdattic)
  • 090afab #4287 support Varnish in configuration blocks (xabbuh)
  • 1603463 #4306 Improve readability (ifdattic)
  • 31d7905 #4302 View documentation had a reference to the wrong twig template (milan)
  • ef11ef4 #4250 Clarifying Bundle Best Practices is for reusable bundles (weaverryan)
  • 430eabf #4298 Book HTTP Fundamentals routing example fixed with routing.xml file (peterkokot)
  • 7ab6df9 #4237 Finished #3886 (ahsio, WouterJ)
  • 990b453 #4245 [Contributing] tweaks to the contribution chapter (xabbuh)

September, 2014

New Documentation

  • eac0e51 #4195 Added a note about the total deprecation of YUI (javiereguiluz)
  • e44c791 #4047 Documented info method (WouterJ)
  • d5d46ec #4017 Clarify that route defaults don’t need a placeholder (iamdto)
  • 1d56da4 #4239 Remove redundant references to trusting HttpCache (thewilkybarkid)
  • c306b68 #4249 provide node path on configuration (desarrolla2)
  • 9b4b36f #4236 Javiereguiluz bundle install instructions (WouterJ)
  • a578de9 #4223 Revamped the documentation about “Contributing Docs” (javiereguiluz)
  • de60dbe #4182 Added note about exporting SYMFONY_ENV (jpb0104)
  • a8dc2bf #4166 Translation custom loaders (raulfraile)

Fixed Documentation

  • 5500e0b #4267 Fix error in bundle installation standard example (WouterJ)
  • 082755d #4240 [Components][EventDispatcher] fix ContainerAwareEventDispatcher definition (xabbuh)
  • 2319d6a #4213 Handle “constraints” option in form unit testing (sarcher)
  • c567707 #4222 [Components][DependencyInjection] do not reference services in parameters (xabbuh)

Minor Documentation Changes

  • df16779 #4226 add note about parameters in imports (xabbuh)
  • c332063 #4278 Missing word in DependencyInjection => Types of Injection (fabschurt)
  • 3a4e226 #4263 Fixed typo (zebba)
  • 187c255 #4259 Added feature freeze dates for Symfony versions (javiereguiluz)
  • efc1436 #4247 [Reference] link translation DIC tags to components section (xabbuh)
  • 17addb1 #4238 Finished #3924 (WouterJ)
  • 19a0c35 #4252 Removed unnecessary comma (allejo)
  • 9fd91d6 #4219 Cache needs be cleared (burki94)
  • 025f02e #4220 Added a note about the side effects of enabling both PHP and Twig (javiereguiluz)
  • 46fcb67 #4218 Caution that roles should start with ROLE_ (jrjohnson)
  • 78eea60 #4077 Removed outdated translations from the official list (WouterJ)
  • 2cf9e47 #4171 Fixed version for composer install (zomberg)
  • 5c62b36 #4216 Update Collection.rst (azarzag)
  • 8591b87 #4215 Fixed code highlighting (WouterJ)
  • f276e34 #4205 replace “Symfony2” with “Symfony” (xabbuh)
  • 6db13ac #4208 Added a note about the lacking features of Yaml Component (javiereguiluz)
  • f8c6201 #4200 Moved ‘contributing’ images to their own directory (javiereguiluz)
  • b4650fa #4199 fix name of the Yaml component (xabbuh)
  • 9d89bb0 #4190 add link to form testing chapter in test section (xabbuh)

August, 2014

New Documentation

  • bccb080 #4140 [Cookbook][Logging] document multiple recipients in XML configs (xabbuh)
  • 7a6e3d1 #4150 Added the schema_filter option to the reference (peterrehm)
  • be90d8a #4142 [Cookbook][Configuration] tweaks for the web server configuration chapter (xabbuh)
  • 041105c #3883 Removed redundant POST request exclusion info (ryancastle)
  • 4f9fef6 #4000 [Cookbook] add cookbook article for the server:run command (xabbuh)
  • 4ea4dfe #3915 [Cookbook][Configuration] documentation of Apache + PHP-FPM (xabbuh)
  • 4d5adaa #4125 Added link to JSFiddle example (WouterJ)
  • 75bda4b #4124 Rebased #3965 (WouterJ)
  • fdb8a32 #3950 [Components][EventDispatcher] describe the usage of the RegisterListenersPass (xabbuh)
  • 7e09383 #3940 Updated docs for Monolog “swift” handler in cookbook. (phansys)
  • 8adfe98 #3894 Rewrote Extension & Configuration docs (WouterJ)
  • cafea43 #3888 Updated the example used to explain page creation (javiereguiluz)
  • df0cf68 #3885 [RFR] Added “How to Organize Configuration Files” cookbook (javiereguiluz)
  • 41116da #4081 [Components][ClassLoader] documentation for the ClassMapGenerator class (xabbuh)
  • 35a0f66 #4102 Adding a new entry about reverse proxies in the framework (weaverryan)
  • 95c2066 #4096 labels in submit buttons + new screenshot (ricardclau)

Fixed Documentation

  • 4882b99 #4164 Fixed minor typos. (ahsio)
  • eaaa35a #4145 Fix documentation for group_sequence_provider (giosh94mhz)
  • 2c93aa5 #4147 [Cookbook][Logging] add missing Monolog handler type in XML config (xabbuh)
  • 53b2c2b #4139 cleaned up the code example (gondo)
  • b5c9f2a #4138 fixed wrongly linked dependency (gondo)
  • b486b22 #4131 Replaced old way of specifying http method by the new one (Baptouuuu)
  • 93481d7 #4120 Fix use mistakes (mbutkereit)
  • c0a0120 #4119 Fix class name in ConsoleTerminateListener example (alOneh)
  • d699255 #4083 [Reference] field dependent empty_data option description (xabbuh)
  • 3ffc20f #4103 [Cookbook][Forms] fix PHP template file name (xabbuh)
  • 234fa36 #4095 Fix php template (piotrantosik)
  • 01fb9f2 #4093 See #4091 (dannykopping)
  • 7d39b03 #4079 Fixed typo in filesystem component (kohkimakimoto)
  • f0bde03 #4075 Fixed typo in the yml validation (timothymctim)

Minor Documentation Changes

  • e9d317a #4160 [Reference] consistent & complete config examples (xabbuh)
  • 3e68ee7 #4152 Adding ‘attr’ option to the Textarea options list (ronanguilloux)
  • c4eb628 #4130 A set of small typos (Baptouuuu)
  • 236d8e0 #4137 fixed directive syntax (WouterJ)
  • 6e90520 #4135 [#3940] Adding php example for an array of emails (weaverryan)
  • b37ee61 #4132 Use proper way to reference a doc page for legacy sessions (Baptouuuu)
  • 189a123 #4129 [Components] consistent & complete config examples (xabbuh)
  • 46f3108 #4126 Rebased #3848 (WouterJ)
  • 84e6e7f #4114 [Book] consistent and complete config examples (xabbuh)
  • 03fcab1 #4112 [Contributing][Documentation] add order of translation formats (xabbuh)
  • 650120a #4002 added Github teams for the core team (fabpot)
  • 10792c3 #3959 [book][cache][tip] added cache annotations. (aitboudad)
  • ebaed21 #3944 Update dbal.rst (bpiepiora)
  • 16e346a #3890 [Components][HttpFoundation] use a placeholder for the constructor arguments (xabbuh)
  • 7bb4f34 #4115 [Documentation] [Minor] Changes foobar.net in example.com (magnetik)
  • 12d0b82 #4113 tweaks to the new reverse proxy/load balancer chapter (xabbuh)
  • 4cce133 #4057 Update introduction.rst (carltondickson)
  • 26141d6 #4080 [Reference] order form type options alphabetically (xabbuh)
  • 7806aa7 #4117 Added a note about the automatic handling of the memory spool in the CLI (stof)
  • 5959b6c #4101 [Contributing] extended Symfony 2.4 maintenance (xabbuh)
  • e2056ad #4072 [Contributing][Code] add note on Symfony SE forks for bug reports (xabbuh)
  • 665c091 #4087 Typo (tvlooy)
  • f95bbf3 #4023 [Cookbook][Security] usage of a non-default entity manager in an entity user provider (xabbuh)
  • 27b1003 #4074 Fixed (again) a typo: Toolbet –> Toolbelt (javiereguiluz)
  • c97418f #4073 Reworded bundle requirement (WouterJ)
  • e5d5eb8 #4066 Update inherit_data_option.rst (Oylex)
  • 9c08572 #4064 Fixed typo on tag service (saro0h)

July, 2014

New Documentation

  • 1b4c1c8 #4045 Added a new “Deploying to Heroku Cloud” cookbook article (javiereguiluz)
  • f943eee #4009 Remove “Controllers extends ContainerAware” best practice (tgalopin)
  • eae9ad0 #3875 Added a note about customizing a form with more than one template (javiereguiluz)
  • d6787b7 #3989 adde stof as a merger (fabpot)
  • 4a9e49e #3946 DQL custom functions on doctrine reference page (healdropper)

Fixed Documentation

  • 1b695b5 #4063 fix parent form types (xabbuh)
  • 7901005 #4048 $this->request replaced by $request (danielsan)
  • f6123f1 #4031 Update form_events.rst (redstar504)
  • eb813a5 #3979 removed invalid processors option (ricoli)

Minor Documentation Changes

  • a4bdb97 #4070 Added a note about permissions in the Quick Tour (javiereguiluz)
  • b3f15b2 #4059 eraseCredentials method typo (danielsan)
  • 44091b1 #4053 Update doctrine.rst (sr972)
  • b06ad60 #4052 [Security] [Custom Provider] Use properties on WebserviceUser (entering)
  • a834a7e #4042 [Cookbook] apply headline guidelines to the cookbook articles (xabbuh)
  • f25faf3 #4046 Fixed a syntax error (javiereguiluz)
  • 3c660d1 #4044 Added editorconfig (WouterJ)
  • ae3ec04 #4041 [Cookbook][Deployment] link to the deployment index (xabbuh)
  • 2e4fc7f #4030 enclose YAML strings containing % with quotes (xabbuh)
  • 9520d92 #4038 Update rendered tag (kirill-oficerov)
  • f5c2602 #4036 Update page_creation.rst (redstar504)
  • c2eda93 #4034 Update internals.rst (redstar504)
  • a5ad0df #4035 Update version in Rework your Patch section (yguedidi)
  • d8b037a #4019 Update twig_reference.rst (redstar504)
  • 579a873 #4015 Fixed bad indenting (the list was treated as a blockquote) (javiereguiluz)
  • 4669620 #4004 use GitHub instead of Github (xabbuh)
  • a3fe74f #3993 [Console] Fix Console component getHelperSet()->get() to getHelper() (eko)
  • a41af7e #3880 document the mysterious abc part of the header (greg0ire)
  • 90773b0 #3990 Move the section about collect: false to the cookbook entry (weaverryan)
  • 2ae8281 #3864 plug rules for static methods (cordoval)
  • d882cc0 #3988 fix typos. (yositani2002)
  • b67a059 #3986 Rebased #3982 - Some fixes (WouterJ)
  • 801c756 #3977 [WCM] removed call to deprecated getRequest() method (Baptouuuu)
  • 4c1d4ae #3968 Proofreading the new Azure deployment article (weaverryan)

June, 2014

New Documentation

  • 5540e0b #3963 [cookbook] [deployment] added cookbook showing how to deploy to the Microsoft Azure Website Cloud (hhamon)
  • 6cba0f1 #3936 Varnish only takes into account max-age (gonzalovilaseca)
  • 3c95af5 #3928 Reorder page from simple to advanced (rebased) (clemens-tolboom)
  • 350b805 #3916 [Component][EventDispatcher] documentation for the TraceableEventDispatcher (xabbuh)
  • 1702133 #3913 [Cookbook][Security] Added doc for x509 pre authenticated listener (zefrog)
  • 32b9058 #3909 Update the CssSelector component documentation (stof)
  • 23b51c8 #3901 Bootstraped the standards for “Files and Directories” (javiereguiluz)
  • 8931c36 #3889 Fixed the section about getting services from a command (javiereguiluz)
  • 9fddab6 #3877 Added a note about configuring several paths under the same namespace (javiereguiluz)

Fixed Documentation

  • aeffd12 #3961 Fixing php coding (mvhirsch)
  • d8329dc #3943 Fixing simple quotes in double quotes (ptitlazy)
  • 0626f2b #3897 Collection constraint (hhamon)
  • 3387cb2 #3871 Fix missing Front Controller (parthasarathigk)
  • 8257be9 #3891 Fixed wrong method call. (cmfcmf)

Minor Documentation Changes

  • 75ee6b4 #3969 [cookbook] [deployment] removed marketing introduction in Azure Deployme... (hhamon)
  • 02aeade #3967 fix typo. (yositani2002)
  • 208b0dc #3951 fix origin of AcmeDemoBundle (hice3000)
  • fba083e #3957 [Cookbook][Bundles] fix typos in the prepend extension chapter (xabbuh)
  • c444b5d #3948 update the Sphinx extensions to raise warnings when backslashes are not ... (xabbuh)
  • 8fef7b7 #3938 [Contributing][Documentation] don’t render the list inside a blockquote (xabbuh)
  • 222a014 #3933 render directory inside a code block (xabbuh)
  • 7937864 #3927 [Cookbook][Security] Explicit ‘your_user_provider’ configuration parameter (zefrog)
  • 26d00d0 #3925 Fixed the indentation of two code blocks (javiereguiluz)
  • 351b2cf #3922 update fabpot Sphinx extensions version (xabbuh)
  • 35cbffc #3920 [Components][Form] remove blank line to render the versionadded directive properly (xabbuh)
  • 36337e7 #3906 Blockquote introductions (xabbuh)
  • 5e0e119 #3899 [RFR] Misc. fixes mostly related to formatting issues (javiereguiluz)
  • 349cbeb #3900 Fixed the formatting of the table headers (javiereguiluz)
  • 1dc8b4a #3898 clarifying the need of a factory for auth-provider (leberknecht)
  • 0c20141 #3896 Fixing comment typo for Doctrine findBy and findOneBy code example (beenanner)
  • b00573c #3870 Fix wrong indentation for lists (WouterJ)

May, 2014

New Documentation

  • af8c20f #3818 [Form customization] added block_name example. (aitboudad)
  • c788325 #3841 [Cookbook][Logging] register processor per handler and per channel (xabbuh)
  • 979533a #3839 document how to test actions (greg0ire)
  • d8aaac3 #3835 Updated framework.ide configuration (WouterJ)
  • f665e14 #3704 [Form] Added documentation for Form Events (csarrazi)
  • 14b9f14 #3777 added docs for the core team (fabpot)

Fixed Documentation

  • 0649c21 #3869 Add a missing argument to the PdoSessionHandler (jakzal)
  • 259a2b7 #3866 [Book][Security]fixed Login when there is no session. (aitboudad)
  • 9b7584f #3863 Error in XML (tvlooy)
  • 0cb9c3b #3827 Update ‘How to Create and store a Symfony2 Project in Git’ (nicwortel)
  • 4ed9a08 #3830 Generate an APC prefix based on __FILE__ (trsteel88)
  • 9a65412 #3840 Update dialoghelper.rst (jdecoster)
  • 1853fea #3716 Fix issue #3712 (umpirsky)
  • 80d70a4 #3779 [Book][Security] constants are defined in the SecurityContextInterface (xabbuh)

Minor Documentation Changes

  • 302fa82 #3872 Update hostname_pattern.rst (sofany)
  • 50672f7 #3867 fixed missing info about FosUserBundle. (aitboudad)
  • b32ec15 #3856 Update voters_data_permission.rst (MarcomTeam)
  • bffe163 #3859 Add filter cssrewrite (DOEO)
  • f617ff8 #3764 Update testing.rst (NAYZO)
  • 3792fee #3858 Clarified Password Encoders example (WouterJ)
  • 663d68c #3857 Added little bit information about the route name (WouterJ)
  • 4211bff #3852 Fixed link and typo in type_guesser.rst (rpg600)
  • 78ae7ec #3845 added link to /cookbook/security/force_https. (aitboudad)
  • 6c69362 #3846 [Routing][Loader] added JMSI18nRoutingBundle (aitboudad)
  • 136864b #3844 [Components] Fixed some typos. (ahsio)
  • b0710bc #3842 Update dialoghelper.rst (bijsterdee)
  • 9f1a354 #3804 [Components][DependencyInjection] add note about a use case that requires to compile the container (xabbuh)
  • d92c522 #3769 Updated references to new Session() (scottwarren)
  • 7288a33 #3789 [Reference][Forms] Improvements to the form type (xabbuh)
  • 72fae25 #3790 [Reference][Forms] move versionadded directives for form options directly below the option’s headline (xabbuh)
  • b4d4ac3 #3838 fix filename typo in cookbook/form/unit_testing.rst (hice3000)
  • 0b06287 #3836 remove unnecessary rewrite from nginx conf (Burgov)
  • e58e39f #3832 fix the wording in versionadded directives (for the 2.3 branch) (xabbuh)
  • 09d6ca1 #3829 [Components] consistent headlines (xabbuh)
  • 54e0882 #3828 [Contributing] consistent headlines (xabbuh)
  • b1336d7 #3823 Added empty line after if statements (zomberg)
  • 79b9fdc #3822 Update voters_data_permission.rst (mimol91)
  • 69cb7b8 #3821 Update custom_authentication_provider.rst (leberknecht)
  • 9f602c4 #3820 Update page_creation.rst (adreeun)
  • 52518c0 #3819 Update csrf_in_login_form.rst (micheal)
  • 1adfd9b #3802 Add a note about which types can be used in Symfony (fabpot)
  • fa27ded #3801 [Cookbook][Form] Fixed Typo & missing word. (ahsio)
  • 127beed #3770 Update factories.rst (AlaaAttya)
  • 822d985 #3817 Update translation.rst (richardpi)
  • 241d923 #3813 [Reference][Forms]fix time field count. (yositani2002)
  • bc96f55 #3812 [Cookbook][Configuration] Fixed broken link. (ahsio)
  • 5867327 #3809 Fixed typo (WouterJ)

April, 2014

New Documentation

  • 322972e #3803 [Book][Validation] configuration examples for the GroupSequenceProvider (xabbuh)
  • d4ca16a #3743 Improve examples in parent services (WouterJ)
  • d611e77 #3701 [Serializer] add documentation for serializer callbacks (cordoval)
  • 80c645c #3719 Fixed event listeners priority (tony-co)

Fixed Documentation

  • f801e2e #3805 Add missing autocomplete argument in askAndValidate method (ifdattic)
  • a81d367 #3786 replaceArguments should be setArguments (RobinvdVleuten)
  • 33b64e1 #3788 Fix link for StopwatchEvent class (rpg600)
  • 529d4ce #3761 buildViewBottomUp has been renamed to finishView (Nyholm)
  • d743139 #3768 the Locale component does not have elements tagged with @api (xabbuh)
  • 2b8e44d #3747 Fix Image constraint class and validator link (weaverryan)
  • fa362ca #3741 correct RuntimeException reference (shieldo)
  • d92545e #3734 [book] [testing] fixed the path of the phpunit.xml file (javiereguiluz)

Minor Documentation Changes

  • 1094a13 #3807 Added some exceptions to the method order in CS (stof)
  • 55442b5 #3800 Fixed another blockquote rendering issue (WouterJ)
  • 969fd71 #3785 ensure that destination directories don’t exist before creating them (xabbuh)
  • 79322ff #3799 Fix list to not render in a block quote (WouterJ)
  • 1a6f730 #3793 language tweak for the tip introduced in #3743 (xabbuh)
  • dda9e88 #3778 Adding information on internal reverse proxy (tcz)
  • d36bbd9 #3765 [WIP] make headlines consistent with our standards (xabbuh)
  • daa81a0 #3766 [Book] add note about services and the service container in the form cha... (xabbuh)
  • 4529858 #3767 [Book] link to the bc promise in the stable API description (xabbuh)
  • a5471b3 #3775 Fixed variable naming (peterrehm)
  • 703c2a6 #3772 [Cookbook][Sessions] some language improvements (xabbuh)
  • 3d30b56 #3773 modify Symfony CMF configuration values in the build process so that the... (xabbuh)
  • cfd6d7c #3758 [Book][Routing] Fixed typo on PHP version of a route definition (saro0h)
  • 6bd134c #3754 ignore more files and directories which are created when building the documentation (xabbuh)
  • 54d6a9e #3736 [book] Misc. routing fixes (javiereguiluz)
  • f149dcf #3739 [book] [forms] misc. fixes and tweaks (javiereguiluz)
  • ce582ec #3735 [book] [controller] fixed the code of a session sample code (javiereguiluz)
  • 499ba5c #3733 [book] [validation] fixed typos (javiereguiluz)
  • 4d0ff8f #3732 Update routing.rst. Explain using url() v. path(). (ackerman)
  • 44c6273 #3727 Added a note about inlined private services (javiereguiluz)

March, 2014

New Documentation

  • 3b640aa #3644 made some small addition about our BC promise and semantic versioning (fabpot)
  • 2d1ecd9 #3525 Update file_uploads.rst (juanmf)
  • b1e8f56 #3368 The host parameter has to be in defaults, not requirements (MarieMinasyan)
  • 00a462a minor #3658 Fix PSR coding standards error (ifdattic)
  • acf255d #3328 [WIP] Travis integration (WouterJ)
  • 3e7028d #3659 [Internals] Complete notification description for kernel.terminate (bicpi)
  • db3cde7 #3124 Add note about the property attribute (Property Accessor) (raziel057)
  • 5965ec8 #3420 [Cookbook][Configuration] add configuration cookbook handlig parameters in Configurator class (cordoval)
  • a1050eb #3411 [Cookbook][Dynamic Form Modification] Add AJAX sample (bicpi)
  • 6951460 #3601 Added documentation for missing ctype extension (slavafomin)
  • 2657ee7 #3597 Document how to create a custom type guesser (WouterJ)
  • 5ad1599 #3577 Development of custom error pages is impractical if you need to set kernel.debug=false (mpdude)
  • 3f4b319 #3610 [HttpFoundation] Add doc for Request::getContent() method (bicpi)
  • 56bc266 #3589 Finishing the Templating component docs (WouterJ)
  • d881181 #3588 Documented all form variables (WouterJ)
  • e96e12d #3234 [Cookbook] New cookbok: How to use the Cloud to send Emails (bicpi)
  • d5d64ce #3436 [Reference][Form Types] Add missing docs for “action” and “method” option (bicpi)
  • 3df34af #3490 Tweaking Doctrine book chapter (WouterJ)
  • b9608a7 #3594 New Data Voter Article (continuation) (weaverryan)

Fixed Documentation

  • 06c56c1 #3709 [Components][Security] Fix #3708 (bicpi)
  • aadc61d #3707 make method supportsClass() in custom voter compatible with the interface’s documentation (xabbuh)
  • 65150f9 #3637 Update render_without_controller.rst (94noni)
  • 9fcccc7 #3634 Fix goal of “framework.profiler.only_exceptions“ option which profile on each exceptions on controller (not only 500) (stephpy)
  • 9dd8d96 #3689 Fix cache warmer description (WouterJ)
  • 6221f35 #3671 miss extends keyword in define BlogController class (ghanbari)
  • 4ce7a15 #3543 Fix the definition of customizing form’s global errors. (mtrojanowski)
  • 5d4a3a4 #3343 [Testing] Fix phpunit test dir paths (bicpi)
  • badaae7 #3622 [Components][Routing] Fix addPrefix() sample code (bicpi)
  • de0a5e1 #3665 [Cookbook][Test] fix sample code (inalgnu)
  • 4ef746a #3614 [Internals] Fix Profiler:find() arguments (bicpi)
  • 0c41762 #3600 [Security][Authentication] Fix instructions for creating password encoders (bicpi)
  • 0ab1f24 #3593 Clarified Default and ClassName groups (WouterJ)
  • 178984b #3648 [Routing] Remove outdated tip about sticky locale (bicpi)

Minor Documentation Changes

  • abca098 #3726 Minor tweaks after merging #3644 by @stof and @xabbuh (weaverryan)
  • d16be31 #3725 Minor tweaks related to #3368 (weaverryan)
  • aa9bb25 #3636 Update security.rst (nomack84)
  • 9f26da8 #3720 [#3539] A backport of a sentence - the parts that apply to 2.3 (weaverryan)
  • 5a3ba1b #3715 change variable name to a better fitting one (xabbuh)
  • e7580c0 #3713 Updated versionadded directives to use “introduced” (WouterJ)
  • e15afe0 #3711 Simplified the Travis configuration (stof)
  • 5035837 #3706 Add support for nginx (guiditoito)
  • 00a462a #3658 Fix PSR coding standards error (ifdattic)
  • 868de1e #3698 Dynamic form modification cookbook: Fix inclusion of code (michaelperrin)
  • 41b2eb8 #3693 Tweak to Absolute URL generation (weaverryan)
  • bd473db #3563 Add another tip to setup permissions (tony-co)
  • 67129b1 #3611 [Reference][Forms] add an introductory table containing all options of the basic form type (xabbuh)
  • fd8f7ae #3694 fix the referenced documents names (xabbuh)
  • d617011 #3657 Fix typos, remove trailing whitespace. (ifdattic)
  • 1b4f6a6 #3656 Minimize horizontal scrolling, add missing characters, remove trailing whitespace. (ifdattic)
  • 7c0c5d1 #3653 Http cache validation rewording (weaverryan)
  • 0fb2c5f #3651 [Reference][Forms] remove the label_attr option which is not available in the button type (xabbuh)
  • 69ac21b #3642 Fixed some typos and formatting issues (javiereguiluz)
  • 93c35d0 #3641 Added some examples to the “services as parameters” section (javiereguiluz)
  • 12a6676 #3640 [minor] fixed one typo and one formatting issue (javiereguiluz)
  • 9967b0c #3638 [#3116] Fixing wrong table name - singular is used elsewhere (weaverryan)
  • 4fbf1cd #3635 [QuickTour] close opened literals (xabbuh)
  • 2192c32 #3650 Fixing some build errors (xabbuh)
  • fa3f531 #3677 [Reference][Forms] Remove variables section from tables (xabbuh)
  • 1f384bc #3631 Added documentation for message option of the True constraint (naitsirch)
  • f6a41b9 #3630 Minor tweaks to form action/method (weaverryan)
  • ae755e0 #3628 Added anchor for permissions (WouterJ)
  • 6380113 #3667 Update index.rst (NAYZO)
  • 97ef2f7 #3566 Changes ACL permission setting hints (MicheleOnGit)
  • 9f7d742 #3654 [Cookbook][Security] Fix VoterInterface signature (bicpi)
  • e34204e #3605 Fixed a plural issue (benjaminpaap)
  • e7d5a45 #3599 [CHANGELOG] fix reference to contributing docs (xabbuh)
  • 3582bf1 #3598 add changelog to hidden toctree (xabbuh)
  • 58b7f96 #3596 [HTTP Cache] Validation model: Fix header name (bicpi)
  • 6d1378e #3592 Added a tip about hardcoding URLs in functional tests (javiereguiluz)
  • 04cf9f8 #3595 Collection of fixes and improvements (bicpi)
  • 2ed0943 #3645 Adjusted the BC rules to be consistent (stof)
  • 664a0be #3633 Added missing PHP syntax coloration (DerekRoth)
  • 1714a31 #3585 Use consistent method chaining in BlogBundle sample application (ockcyp)
  • cb61f4f #3581 Add missing hyphen in HTTP Fundamentals page (ockcyp)

February, 2014

New Documentation

  • 9dcf467 #3613 Javiereguiluz revamped quick tour (weaverryan)
  • 89c6f1d #3439 [Review] Added detailed Backwards Compatibility Promise text (webmozart)
  • 0029408 #3558 Created Documentation CHANGELOG (WouterJ)
  • f6dd678 #3548 Update forms.rst (atmosf3ar)
  • 527c8b6 #3496 Added a section about using named assets (vmattila)

Fixed Documentation

  • 5c367b4 #3517 Fixed OptionsResolver component docs (WouterJ)
  • adcbb5d #3615 Fixes to cookbook/doctrine/registration_form.rst (Crushnaut)
  • a21fb26 #3559 Remove reference to copying parameters.yml from Git cookbook (pwaring)
  • de71a51 #3551 [Cookbook][Dynamic Form Modification] Fix sample code (rybakit)
  • 143db2f #3550 Update introduction.rst (taavit)
  • 384538b #3549 Fixed createPropertyAccessorBuilder usage (antonbabenko)
  • d275302 #3541 Update generic_event.rst (Lumbendil)
  • 819949c #3537 Add missing variable assignment (colinodell)
  • d7e8262 #3535 fix form type name. (yositani2002)
  • 821af3b #3493 Type fix in remove.rst (weaverryan)
  • 003230f #3530 Update form_customization.rst (dczech)
  • 696313c #3513 [Component-DI] Fixed typo (saro0h)
  • 27dcebd #3509 Fix typo: side.bar.twig => sidebar.twig (ifdattic)
  • e385d28 #3503 file extension correction xfliff to xliff (nixilla)
  • 7fe0de3 #3475 Fixed doc for framework.session.cookie_lifetime refrence. (tyomo4ka)
  • 8155e4c #3473 Update proxy_examples.rst (AZielinski)

Minor Documentation Changes

  • 0928249 #3568 Update checkbox_compound.rst.inc (joshuaadickerson)
  • 38def3b #3567 Update checkbox_compound.rst.inc (joshuaadickerson)
  • 15d8ab8 #3553 Minimize horizontal scrolling in code blocks to improve readability (ifdattic)
  • 5120863 #3547 Update acl.rst (iqfoundry)
  • d974c77 #3556 Fix PSR error (ifdattic)
  • f4bb017 #3555 Wrap variables in {} for safer interpolation (ifdattic)
  • 5f02bca #3552 Fix typos (ifdattic)
  • 6e32c47 #3546 Fix README: contributions should be based off 2.3 or higher (colinodell)
  • ffa8f76 #3545 Example of getting entity managers directly from the container (colinodell)
  • 6a2a55b #3579 Fix build errors (xabbuh)
  • 73adf8b #3528 Clarify service parameters usages (WouterJ)
  • 9ba4fa7 #3527 Changes to components domcrawler (ifdattic)
  • 8973c81 #3526 Changes for Console component (ifdattic)
  • 6848bed #3538 Rebasing #3518 (weaverryan)
  • c838df8 #3511 [Component-DI] Removed useless else statement in code example (saro0h)
  • 1af6742 #3510 add empty line (lazyants)
  • 1131247 #3508 Add ‘in XML’ for additional clarity (ifdattic)
  • a650b93 #3506 Nykopol overriden options (weaverryan)
  • ab10035 #3505 replace Akamaï with Akamai (xabbuh)
  • 7f56c20 #3501 [Security] Fix markup (tyx)
  • 80a90ba #3500 Minimize horizontal scrolling in code blocks (improve readability) (ifdattic)
  • e5bc4ea #3498 Remove second empty data (xabbuh)
  • d084d87 #3485 [Cookbook][Assetic] Fix “javascripts” tag name typo (bicpi)
  • 3250aba #3481 Fix code block (minimise horizontal scrolling), typo in yaml (ifdattic)

January, 2014

New Documentation

No changes

Fixed Documentation

  • e385d28 #3503 file extension correction xfliff to xliff (nixilla)
  • 7fe0de3 #3475 Fixed doc for framework.session.cookie_lifetime refrence. (tyomo4ka)
  • 8155e4c #3473 Update proxy_examples.rst (AZielinski)
  • c205bc6 #3468 enclose YAML string with double quotes to fix syntax highlighting (xabbuh)
  • 89963cc #3463 Fix typos in cookbook/testing/database (ifdattic)
  • e0a52ec #3460 remove confusing outdated note on interactive rebasing (xabbuh)
  • 6831b13 #3455 [Contributing][Code] fix indentation so that the text is rendered properly (xabbuh)
  • ea5816f #3433 [WIP][Reference][Form Types] Update “radio” form type (bicpi)
  • 42c80d1 #3448 Overridden tweak (weaverryan)
  • d9d7c58 #3444 Fix issue #3442 (ifdattic)
  • 9e2e64b #3427 Removed code references to Symfony Standard Distribution (danielcsgomes)
  • 26b8146 #3415 [#3334] the data_class option was not introduced in 2.4 (xabbuh)
  • 0b2a491 #3414 add missing code-block directive (xabbuh)
  • 4988118 #3432 [Reference][Form Types] Add “max_length” option in form type (nykopol)
  • 26a7b1b #3423 [Session Configuration] add clarifying notes on session save handler proxies (cordoval)

Minor Documentation Changes

  • 1131247 #3508 Add ‘in XML’ for additional clarity (ifdattic)
  • a650b93 #3506 Nykopol overriden options (weaverryan)
  • ab10035 #3505 replace Akamaï with Akamai (xabbuh)
  • 7f56c20 #3501 [Security] Fix markup (tyx)
  • 80a90ba #3500 Minimize horizontal scrolling in code blocks (improve readability) (ifdattic)
  • e5bc4ea #3498 Remove second empty data (xabbuh)
  • d084d87 #3485 [Cookbook][Assetic] Fix “javascripts” tag name typo (bicpi)
  • 3250aba #3481 Fix code block (minimise horizontal scrolling), typo in yaml (ifdattic)
  • f285d93 #3451 some language tweaks (AE, third-person perspective) (xabbuh)
  • 2b7e0f6 #3497 Fix highlighting (WouterJ)
  • a535ae0 #3471 Fixed ```versionadded``` inconsistencies in Symfony 2.3 (danielcsgomes)
  • f077a8e #3465 change wording in versionadded example to be consistent with what we use... (xabbuh)
  • f9f7548 #3462 Replace ... with etc (ifdattic)
  • 65efcc4 #3445 [Reference][Form Types] Add missing (but existing) options to “form” type (bicpi)
  • 1d1b91d #3431 [Config] add cautionary note on ini file loader limitation (cordoval)
  • f2eaf9b #3419 doctrine file upload example uses dir – caution added (cordoval)
  • 72b53ad #3404 [#3276] Trying to further clarify the session storage directory details (weaverryan)
  • 67b7bbd #3413 [Cookbook][Bundles] improve explanation of code block for bundle removal (cordoval)
  • 7c5a914 #3369 Indicate that Group Sequence Providers can use YAML (karptonite)
  • 1e0311e #3416 add empty_data option where required option is used (xabbuh)
  • 2be3f52 #3422 [Cookbook][Custom Authentication Provider] add a note of warning for when forbidding anonymous users (cordoval)

Quick Tour

Get started fast with the Symfony Quick Tour:

The Quick Tour

The Big Picture

Start using Symfony in 10 minutes! This chapter will walk you through the most important concepts behind Symfony and explain how you can get started quickly by showing you a simple project in action.

If you’ve used a web framework before, you should feel right at home with Symfony. If not, welcome to a whole new way of developing web applications.

The only technical requisite to follow this tutorial is to have PHP 5.4 or higher installed on your computer. If you use a packaged PHP solution such as WAMP, XAMP or MAMP, check out that they are using PHP 5.4 or a more recent version. You can also execute the following command in your terminal or command console to display the installed PHP version:

$ php --version
Installing Symfony

In the past, Symfony had to be installed manually for each new project. Now you can use the Symfony Installer, which has to be installed the very first time you use Symfony on a computer.

On Linux and Mac OS X systems, execute the following console commands:

$ curl -LsS http://symfony.com/installer > symfony.phar
$ sudo mv symfony.phar /usr/local/bin/symfony
$ chmod a+x /usr/local/bin/symfony

注解

If your system doesn’t have cURL installed, execute the following commands instead:

$ php -r "readfile('http://symfony.com/installer');" > symfony.phar
$ sudo mv symfony.phar /usr/local/bin/symfony
$ chmod a+x /usr/local/bin/symfony

After installing the Symfony installer, you’ll have to open a new console window to be able to execute the new symfony command:

$ symfony

On Windows systems, execute the following console command:

c:\> php -r "readfile('http://symfony.com/installer');" > symfony.phar

This command downloads a file called symfony.phar which contains the Symfony installer. Save or move that file to the directory where you create the Symfony projects and then, execute the Symfony installer right away with this command:

c:\> php symfony.phar
Creating Your First Symfony Project

Once the Symfony Installer is set up, use the new command to create new Symfony projects. Let’s create a new project called myproject:

# Linux and Mac OS X
$ symfony new myproject

# Windows
c:\> php symfony.phar new myproject

This command downloads the latest Symfony stable version and creates an empty project in the myproject/ directory so you can start developing your application right away.

Running Symfony

This tutorial leverages the internal web server provided by PHP to run Symfony applications. Therefore, running a Symfony application is a matter of browsing the project directory and executing this command:

$ cd myproject/
$ php app/console server:run

Open your browser and access the http://localhost:8000 URL to see the Welcome page of Symfony:

Symfony Welcome Page

Congratulations! Your first Symfony project is up and running!

注解

Instead of the welcome page, you may see a blank page or an error page. This is caused by a directory permission misconfiguration. There are several possible solutions depending on your operating system. All of them are explained in the Setting up Permissions section of the official book.

When you are finished working on your Symfony application, you can stop the server with the server:stop command:

$ php app/console server:stop

小技巧

If you prefer a traditional web server such as Apache or Nginx, read the Configuring a Web Server article.

Understanding the Fundamentals

One of the main goals of a framework is to keep your code organized and to allow your application to evolve easily over time by avoiding the mixing of database calls, HTML tags and other PHP code in the same script. To achieve this goal with Symfony, you’ll first need to learn a few fundamental concepts.

When developing a Symfony application, your responsibility as a developer is to write the code that maps the user’s request (e.g. http://localhost:8000/) to the resource associated with it (the Welcome to Symfony! HTML page).

The code to execute is defined in actions and controllers. The mapping between user’s requests and that code is defined via the routing configuration. And the contents displayed in the browser are usually rendered using templates.

When you browsed http://localhost:8000/ earlier, Symfony executed the controller defined in the src/AppBundle/Controller/DefaultController.php file and rendered the app/Resources/views/default/index.html.twig template. In the following sections you’ll learn in detail the inner workings of Symfony controllers, routes and templates.

Actions and Controllers

Open the src/AppBundle/Controller/DefaultController.php file and you’ll see the following code (for now, don’t look at the @Route configuration because that will be explained in the next section):

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return $this->render('default/index.html.twig');
    }
}

In Symfony applications, controllers are usually PHP classes whose names are suffixed with the Controller word. In this example, the controller is called Default and the PHP class is called DefaultController.

The methods defined in a controller are called actions, they are usually associated with one URL of the application and their names are suffixed with Action. In this example, the Default controller has only one action called index and defined in the indexAction method.

Actions are usually very short - around 10-15 lines of code - because they just call other parts of the application to get or generate the needed information and then they render a template to show the results to the user.

In this example, the index action is practically empty because it doesn’t need to call any other method. The action just renders a template with the Welcome to Symfony! content.

Routing

Symfony routes each request to the action that handles it by matching the requested URL against the paths configured by the application. Open again the src/AppBundle/Controller/DefaultController.php file and take a look at the three lines of code above the indexAction method:

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return $this->render('default/index.html.twig');
    }
}

These three lines define the routing configuration via the @Route() annotation. A PHP annotation is a convenient way to configure a method without having to write regular PHP code. Beware that annotation blocks start with /**, whereas regular PHP comments start with /*.

The first value of @Route() defines the URL that will trigger the execution of the action. As you don’t have to add the host of your application to the URL (e.g. `http://example.com), these URLs are always relative and they are usually called paths. In this case, the / path refers to the application homepage. The second value of @Route() (e.g. name="homepage") is optional and sets the name of this route. For now this name is not needed, but later it’ll be useful for linking pages.

Considering all this, the @Route("/", name="homepage") annotation creates a new route called homepage which makes Symfony execute the index action of the Default controller when the user browses the / path of the application.

小技巧

In addition to PHP annotations, routes can be configured in YAML, XML or PHP files, as explained in the Routing chapter of the Symfony book. This flexibility is one of the main features of Symfony, a framework that never imposes a particular configuration format on you.

Templates

The only content of the index action is this PHP instruction:

return $this->render('default/index.html.twig');

The $this->render() method is a convenient shortcut to render a template. Symfony provides some useful shortcuts to any controller extending from the Controller class.

By default, application templates are stored in the app/Resources/views/ directory. Therefore, the default/index.html.twig template corresponds to the app/Resources/views/default/index.html.twig. Open that file and you’ll see the following code:

{# app/Resources/views/default/index.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Welcome to Symfony!</h1>
{% endblock %}

This template is created with Twig, a new template engine created for modern PHP applications. The second part of this tutorial will introduce how templates work in Symfony.

Working with Environments

Now that you have a better understanding of how Symfony works, take a closer look at the bottom of any Symfony rendered page. You should notice a small bar with the Symfony logo. This is the “Web Debug Toolbar”, and it is a Symfony developer’s best friend!

_images/web_debug_toolbar.png

But what you see initially is only the tip of the iceberg; click on any of the bar sections to open the profiler and get much more detailed information about the request, the query parameters, security details, and database queries:

_images/profiler.png

This tool provides so much internal information about your application that you may be worried about your visitors accessing sensible information. Symfony is aware of this issue and for that reason, it won’t display this bar when your application is running in the production server.

How does Symfony know whether your application is running locally or on a production server? Keep reading to discover the concept of execution environments.

What is an Environment?

An Environment represents a group of configurations that’s used to run your application. Symfony defines two environments by default: dev (suited for when developing the application locally) and prod (optimized for when executing the application on production).

When you visit the http://localhost:8000 URL in your browser, you’re executing your Symfony application in the dev environment. To visit your application in the prod environment, visit the http://localhost:8000/app.php URL instead. If you prefer to always show the dev environment in the URL, you can visit http://localhost:8000/app_dev.php URL.

The main difference between environments is that dev is optimized to provide lots of information to the developer, which means worse application performance. Meanwhile, prod is optimized to get the best performance, which means that debug information is disabled, as well as the Web Debug Toolbar.

The other difference between environments is the configuration options used to execute the application. When you access the dev environment, Symfony loads the app/config/config_dev.yml configuration file. When you access the prod environment, Symfony loads app/config/config_prod.yml file.

Typically, the environments share a large amount of configuration options. For that reason, you put your common configuration in config.yml and override the specific configuration file for each environment where necessary:

# app/config/config_dev.yml
imports:
    - { resource: config.yml }

web_profiler:
    toolbar: true
    intercept_redirects: false

In this example, the config_dev.yml configuration file imports the common config.yml file and then overrides any existing web debug toolbar configuration with its own options.

For more details on environments, see “Environments & Front Controllers” article.

Final Thoughts

Congratulations! You’ve had your first taste of Symfony code. That wasn’t so hard, was it? There’s a lot more to explore, but you should already see how Symfony makes it really easy to implement web sites better and faster. If you are eager to learn more about Symfony, dive into the next section: “The View”.

The View

After reading the first part of this tutorial, you have decided that Symfony was worth another 10 minutes. In this second part, you will learn more about Twig, the fast, flexible, and secure template engine for PHP applications. Twig makes your templates more readable and concise; it also makes them more friendly for web designers.

Getting familiar with Twig

The official Twig documentation is the best resource to learn everything about this template engine. This section just gives you a quick overview of its main concepts.

A Twig template is a text file that can generate any type of content (HTML, CSS, JavaScript, XML, CSV, LaTeX, etc.) Twig elements are separated from the rest of the template contents using any of these delimiters:

{{ ... }}
Prints the content of a variable or the result of evaluating an expression;
{% ... %}
Controls the logic of the template; it is used for example to execute for loops and if statements.
{# ... #}
Allows including comments inside templates. Contrary to HTML comments, they aren’t included in the rendered template.

Below is a minimal template that illustrates a few basics, using two variables page_title and navigation, which would be passed into the template:

<!DOCTYPE html>
<html>
    <head>
        <title>{{ page_title }}</title>
    </head>
    <body>
        <h1>{{ page_title }}</h1>

        <ul id="navigation">
            {% for item in navigation %}
                <li><a href="{{ item.url }}">{{ item.label }}</a></li>
            {% endfor %}
        </ul>
    </body>
</html>

To render a template in Symfony, use the render method from within a controller. If the template needs variables to generate its contents, pass them as an array using the second optional argument:

$this->render('default/index.html.twig', array(
    'variable_name' => 'variable_value',
));

Variables passed to a template can be strings, arrays or even objects. Twig abstracts the difference between them and lets you access “attributes” of a variable with the dot (.) notation. The following code listing shows how to display the content of a variable passed by the controller depending on its type:

{# 1. Simple variables #}
{# $this->render('template.html.twig', array('name' => 'Fabien') ) #}
{{ name }}

{# 2. Arrays #}
{# $this->render('template.html.twig', array('user' => array('name' => 'Fabien')) ) #}
{{ user.name }}

{# alternative syntax for arrays #}
{{ user['name'] }}

{# 3. Objects #}
{# $this->render('template.html.twig', array('user' => new User('Fabien')) ) #}
{{ user.name }}
{{ user.getName }}

{# alternative syntax for objects #}
{{ user.name() }}
{{ user.getName() }}
Decorating Templates

More often than not, templates in a project share common elements, like the well-known header and footer. Twig solves this problem elegantly with a concept called “template inheritance”. This feature allows you to build a base template that contains all the common elements of your site and defines “blocks” of contents that child templates can override.

The index.html.twig template uses the extends tag to indicate that it inherits from the base.html.twig template:

{# app/Resources/views/default/index.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Welcome to Symfony!</h1>
{% endblock %}

Open the app/Resources/views/base.html.twig file that corresponds to the base.html.twig template and you’ll find the following Twig code:

{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
        <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

The {% block %} tags tell the template engine that a child template may override those portions of the template. In this example, the index.html.twig template overrides the body block, but not the title block, which will display the default content defined in the base.html.twig template.

Using Tags, Filters, and Functions

One of the best features of Twig is its extensibility via tags, filters, and functions. Take a look at the following sample template that uses filters extensively to modify the information before displaying it to the user:

<h1>{{ article.title|capitalize }}</h1>

<p>{{ article.content|striptags|slice(0, 255) }} ...</p>

<p>Tags: {{ article.tags|sort|join(", ") }}</p>

<p>Activate your account before {{ 'next Monday'|date('M j, Y') }}</p>

Don’t forget to check out the official Twig documentation to learn everything about filters, functions and tags.

Including other Templates

The best way to share a snippet of code between several templates is to create a new template fragment that can then be included from other templates.

Imagine that we want to display ads on some pages of our application. First, create a banner.html.twig template:

{# app/Resources/views/ads/banner.html.twig #}
<div id="ad-banner">
    ...
</div>

To display this ad on any page, include the banner.html.twig template using the include() function:

{# app/Resources/views/default/index.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Welcome to Symfony!</h1>

    {{ include('ads/banner.html.twig') }}
{% endblock %}
Embedding other Controllers

And what if you want to embed the result of another controller in a template? That’s very useful when working with Ajax, or when the embedded template needs some variable not available in the main template.

Suppose you’ve created a topArticlesAction controller method to display the most popular articles of your website. If you want to “render” the result of that method (usually some HTML content) inside the index template, use the render() function:

{# app/Resources/views/index.html.twig #}
{{ render(controller('AppBundle:Default:topArticles')) }}

Here, the render() and controller() functions use the special AppBundle:Default:topArticles syntax to refer to the topArticlesAction action of the Default controller (the AppBundle part will be explained later):

// src/AppBundle/Controller/DefaultController.php

class DefaultController extends Controller
{
    public function topArticlesAction()
    {
        // look for the most popular articles in the database
        $articles = ...;

        return $this->render('default/top_articles.html.twig', array(
            'articles' => $articles,
        ));
    }

    // ...
}
Including Assets: Images, JavaScripts and Stylesheets

What would the Internet be without images, JavaScripts, and stylesheets? Symfony provides the asset function to deal with them easily:

<link href="{{ asset('css/blog.css') }}" rel="stylesheet" type="text/css" />

<img src="{{ asset('images/logo.png') }}" />

The asset() function looks for the web assets inside the web/ directory. If you store them in another directory, read this article to learn how to manage web assets.

Using the asset function, your application is more portable. The reason is that you can move the application root directory anywhere under your web root directory without changing anything in your template’s code.

Final Thoughts

Twig is simple yet powerful. Thanks to layouts, blocks, templates and action inclusions, it is very easy to organize your templates in a logical and extensible way.

You have only been working with Symfony for about 20 minutes, but you can already do pretty amazing stuff with it. That’s the power of Symfony. Learning the basics is easy, and you will soon learn that this simplicity is hidden under a very flexible architecture.

But I’m getting ahead of myself. First, you need to learn more about the controller and that’s exactly the topic of the next part of this tutorial. Ready for another 10 minutes with Symfony?

The Controller

Still here after the first two parts? You are already becoming a Symfony fan! Without further ado, discover what controllers can do for you.

Returning Raw Responses

Symfony defines itself as a Request-Response framework. When the user makes a request to your application, Symfony creates a Request object to encapsulate all the information related to that request. Similarly, the result of executing any action of any controller is the creation of a Response object which Symfony uses to generate the HTML content returned to the user.

So far, all the actions shown in this tutorial used the $this->render() shortcut to return a rendered response as result. In case you need it, you can also create a raw Response object to return any text content:

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return new Response('Welcome to Symfony!');
    }
}
Route Parameters

Most of the time, the URLs of applications include variable parts on them. If you are creating for example a blog application, the URL to display the articles should include their title or some other unique identifier to let the application know the exact article to display.

In Symfony applications, the variable parts of the routes are enclosed in curly braces (e.g. /blog/read/{article_title}/). Each variable part is assigned a unique name that can be used later in the controller to retrieve each value.

Let’s create a new action with route variables to show this feature in action. Open the src/AppBundle/Controller/DefaultController.php file and add a new method called helloAction with the following content:

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller
{
    // ...

    /**
     * @Route("/hello/{name}", name="hello")
     */
    public function helloAction($name)
    {
        return $this->render('default/hello.html.twig', array(
            'name' => $name
        ));
    }
}

Open your browser and access the http://localhost:8000/hello/fabien URL to see the result of executing this new action. Instead of the action result, you’ll see an error page. As you probably guessed, the cause of this error is that we’re trying to render a template (default/hello.html.twig) that doesn’t exist yet.

Create the new app/Resources/views/default/hello.html.twig template with the following content:

{# app/Resources/views/default/hello.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Hi {{ name }}! Welcome to Symfony!</h1>
{% endblock %}

Browse again the http://localhost:8000/hello/fabien URL and you’ll see this new template rendered with the information passed by the controller. If you change the last part of the URL (e.g. http://localhost:8000/hello/thomas) and reload your browser, the page will display a different message. And if you remove the last part of the URL (e.g. http://localhost:8000/hello), Symfony will display an error because the route expects a name and you haven’t provided it.

Using Formats

Nowadays, a web application should be able to deliver more than just HTML pages. From XML for RSS feeds or Web Services, to JSON for Ajax requests, there are plenty of different formats to choose from. Supporting those formats in Symfony is straightforward thanks to a special variable called _format which stores the format requested by the user.

Tweak the hello route by adding a new _format variable with html as its default value:

// src/AppBundle/Controller/DefaultController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

// ...

/**
 * @Route("/hello/{name}.{_format}", defaults={"_format"="html"}, name="hello")
 */
public function helloAction($name, $_format)
{
    return $this->render('default/hello.'.$_format.'.twig', array(
        'name' => $name
    ));
}

Obviously, when you support several request formats, you have to provide a template for each of the supported formats. In this case, you should create a new hello.xml.twig template:

<!-- app/Resources/views/default/hello.xml.twig -->
<hello>
    <name>{{ name }}</name>
</hello>

Now, when you browse to http://localhost:8000/hello/fabien, you’ll see the regular HTML page because html is the default format. When visiting http://localhost:8000/hello/fabien.html you’ll get again the HTML page, this time because you explicitly asked for the html format. Lastly, if you visit http://localhost:8000/hello/fabien.xml you’ll see the new XML template rendered in your browser.

That’s all there is to it. For standard formats, Symfony will also automatically choose the best Content-Type header for the response. To restrict the formats supported by a given action, use the requirements option of the @Route() annotation:

// src/AppBundle/Controller/DefaultController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

// ...

/**
 * @Route("/hello/{name}.{_format}",
 *     defaults = {"_format"="html"},
 *     requirements = { "_format" = "html|xml|json" },
 *     name = "hello"
 * )
 */
public function helloAction($name, $_format)
{
    return $this->render('default/hello.'.$_format.'.twig', array(
        'name' => $name
    ));
}

The hello action will now match URLs like /hello/fabien.xml or /hello/fabien.json, but it will show a 404 error if you try to get URLs like /hello/fabien.js, because the value of the _format variable doesn’t meet its requirements.

Redirecting and Forwarding

If you want to redirect the user to another page, use the redirectToRoute() method:

// src/AppBundle/Controller/DefaultController.php
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return $this->redirectToRoute('hello', array('name' => 'Fabien'));
    }
}

The redirectToRoute() method takes as arguments the route name and an optional array of parameters and redirects the user to the URL generated with those arguments.

You can also internally forward the action to another action of the same or different controller using the forward() method:

// src/AppBundle/Controller/DefaultController.php
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return $this->forward('AppBundle:Blog:index', array(
            'name'  => $name
        );
    }
}
Displaying Error Pages

Errors will inevitably happen during the execution of every web application. In the case of 404 errors, Symfony includes a handy shortcut that you can use in your controllers:

// src/AppBundle/Controller/DefaultController.php
// ...

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        // ...
        throw $this->createNotFoundException();
    }
}

For 500 errors, just throw a regular PHP exception inside the controller and Symfony will transform it into a proper 500 error page:

// src/AppBundle/Controller/DefaultController.php
// ...

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        // ...
        throw new \Exception('Something went horribly wrong!');
    }
}
Getting Information from the Request

Sometimes your controllers need to access the information related to the user request, such as their preferred language, IP address or the URL query parameters. To get access to this information, add a new argument of type Request to the action. The name of this new argument doesn’t matter, but it must be preceded by the Request type in order to work (don’t forget to add the new use statement that imports this Request class):

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        // is it an Ajax request?
        $isAjax = $request->isXmlHttpRequest();

        // what's the preferred language of the user?
        $language = $request->getPreferredLanguage(array('en', 'fr'));

        // get the value of a $_GET parameter
        $pageName = $request->query->get('page');

        // get the value of a $_POST parameter
        $pageName = $request->request->get('page');
    }
}

In a template, you can also access the Request object via the special app.request variable automatically provided by Symfony:

{{ app.request.query.get('page') }}

{{ app.request.request.get('page') }}
Persisting Data in the Session

Even if the HTTP protocol is stateless, Symfony provides a nice session object that represents the client (be it a real person using a browser, a bot, or a web service). Between two requests, Symfony stores the attributes in a cookie by using native PHP sessions.

Storing and retrieving information from the session can be easily achieved from any controller:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $session = $request->getSession();

    // store an attribute for reuse during a later user request
    $session->set('foo', 'bar');

    // get the value of a session attribute
    $foo = $session->get('foo');

    // use a default value if the attribute doesn't exist
    $foo = $session->get('foo', 'default_value');
}

You can also store “flash messages” that will auto-delete after the next request. They are useful when you need to set a success message before redirecting the user to another page (which will then show the message):

public function indexAction(Request $request)
{
    // ...

    // store a message for the very next request
    $this->addFlash('notice', 'Congratulations, your action succeeded!');
}

And you can display the flash message in the template like this:

<div>
    {{ app.session.flashbag.get('notice') }}
</div>
Final Thoughts

That’s all there is to it, and I’m not even sure you’ll have spent the full 10 minutes. You were briefly introduced to bundles in the first part, and all the features you’ve learned about so far are part of the core framework bundle. But thanks to bundles, everything in Symfony can be extended or replaced. That’s the topic of the next part of this tutorial.

The Architecture

You are my hero! Who would have thought that you would still be here after the first three parts? Your efforts will be well rewarded soon. The first three parts didn’t look too deeply at the architecture of the framework. Because it makes Symfony stand apart from the framework crowd, let’s dive into the architecture now.

Understanding the Directory Structure

The directory structure of a Symfony application is rather flexible, but the recommended structure is as follows:

app/
The application configuration, templates and translations.
src/
The project’s PHP code.
vendor/
The third-party dependencies.
web/
The web root directory.
The web/ Directory

The web root directory is the home of all public and static files like images, stylesheets, and JavaScript files. It is also where each front controller lives, such as the production controller shown here:

// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();

The controller first bootstraps the application using a kernel class (AppKernel in this case). Then, it creates the Request object using the PHP’s global variables and passes it to the kernel. The last step is to send the response contents returned by the kernel back to the user.

The app/ Directory

The AppKernel class is the main entry point of the application configuration and as such, it is stored in the app/ directory.

This class must implement two methods:

registerBundles()
Must return an array of all bundles needed to run the application, as explained in the next section.
registerContainerConfiguration()
Loads the application configuration (more on this later).

Autoloading is handled automatically via Composer, which means that you can use any PHP class without doing anything at all! All dependencies are stored under the vendor/ directory, but this is just a convention. You can store them wherever you want, globally on your server or locally in your projects.

Understanding the Bundle System

This section introduces one of the greatest and most powerful features of Symfony, the bundle system.

A bundle is kind of like a plugin in other software. So why is it called a bundle and not a plugin? This is because everything is a bundle in Symfony, from the core framework features to the code you write for your application.

All the code you write for your application is organized in bundles. In Symfony speak, a bundle is a structured set of files (PHP files, stylesheets, JavaScripts, images, ...) that implements a single feature (a blog, a forum, ...) and which can be easily shared with other developers.

Bundles are first-class citizens in Symfony. This gives you the flexibility to use pre-built features packaged in third-party bundles or to distribute your own bundles. It makes it easy to pick and choose which features to enable in your application and optimize them the way you want. And at the end of the day, your application code is just as important as the core framework itself.

Symfony already includes an AppBundle that you may use to start developing your application. Then, if you need to split the application into reusable components, you can create your own bundles.

Registering a Bundle

An application is made up of bundles as defined in the registerBundles() method of the AppKernel class. Each bundle is a directory that contains a single Bundle class that describes it:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Symfony\Bundle\SecurityBundle\SecurityBundle(),
        new Symfony\Bundle\TwigBundle\TwigBundle(),
        new Symfony\Bundle\MonologBundle\MonologBundle(),
        new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
        new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
        new Symfony\Bundle\AsseticBundle\AsseticBundle(),
        new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
        new AppBundle\AppBundle();
    );

    if (in_array($this->getEnvironment(), array('dev', 'test'))) {
        $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
        $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
    }

    return $bundles;
}

In addition to the AppBundle that was already talked about, notice that the kernel also enables other bundles that are part of Symfony, such as FrameworkBundle, DoctrineBundle, SwiftmailerBundle and AsseticBundle.

Configuring a Bundle

Each bundle can be customized via configuration files written in YAML, XML, or PHP. Have a look at this sample of the default Symfony configuration:

# app/config/config.yml
imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services.yml }

framework:
    #esi:             ~
    #translator:      { fallback: "%locale%" }
    secret:          "%secret%"
    router:
        resource: "%kernel.root_dir%/config/routing.yml"
        strict_requirements: "%kernel.debug%"
    form:            true
    csrf_protection: true
    validation:      { enable_annotations: true }
    templating:      { engines: ['twig'] }
    default_locale:  "%locale%"
    trusted_proxies: ~
    session:         ~

# Twig Configuration
twig:
    debug:            "%kernel.debug%"
    strict_variables: "%kernel.debug%"

# Swift Mailer Configuration
swiftmailer:
    transport: "%mailer_transport%"
    host:      "%mailer_host%"
    username:  "%mailer_user%"
    password:  "%mailer_password%"
    spool:     { type: memory }

# ...

Each first level entry like framework, twig and swiftmailer defines the configuration for a specific bundle. For example, framework configures the FrameworkBundle while swiftmailer configures the SwiftmailerBundle.

Each environment can override the default configuration by providing a specific configuration file. For example, the dev environment loads the config_dev.yml file, which loads the main configuration (i.e. config.yml) and then modifies it to add some debugging tools:

# app/config/config_dev.yml
imports:
    - { resource: config.yml }

framework:
    router:   { resource: "%kernel.root_dir%/config/routing_dev.yml" }
    profiler: { only_exceptions: false }

web_profiler:
    toolbar: true
    intercept_redirects: false

# ...
Extending a Bundle

In addition to being a nice way to organize and configure your code, a bundle can extend another bundle. Bundle inheritance allows you to override any existing bundle in order to customize its controllers, templates, or any of its files.

Logical File Names

When you want to reference a file from a bundle, use this notation: @BUNDLE_NAME/path/to/file; Symfony will resolve @BUNDLE_NAME to the real path to the bundle. For instance, the logical path @AppBundle/Controller/DefaultController.php would be converted to src/AppBundle/Controller/DefaultController.php, because Symfony knows the location of the AppBundle.

Logical Controller Names

For controllers, you need to reference actions using the format BUNDLE_NAME:CONTROLLER_NAME:ACTION_NAME. For instance, AppBundle:Default:index maps to the indexAction method from the AppBundle\Controller\DefaultController class.

Extending Bundles

If you follow these conventions, then you can use bundle inheritance to override files, controllers or templates. For example, you can create a bundle - NewBundle - and specify that it overrides AppBundle. When Symfony loads the AppBundle:Default:index controller, it will first look for the DefaultController class in NewBundle and, if it doesn’t exist, then look inside AppBundle. This means that one bundle can override almost any part of another bundle!

Do you understand now why Symfony is so flexible? Share your bundles between applications, store them locally or globally, your choice.

Using Vendors

Odds are that your application will depend on third-party libraries. Those should be stored in the vendor/ directory. You should never touch anything in this directory, because it is exclusively managed by Composer. This directory already contains the Symfony libraries, the SwiftMailer library, the Doctrine ORM, the Twig templating system and some other third party libraries and bundles.

Understanding the Cache and Logs

Symfony applications can contain several configuration files defined in several formats (YAML, XML, PHP, etc.) Instead of parsing and combining all those files for each request, Symfony uses its own cache system. In fact, the application configuration is only parsed for the very first request and then compiled down to plain PHP code stored in the app/cache/ directory.

In the development environment, Symfony is smart enough to update the cache when you change a file. But in the production environment, to speed things up, it is your responsibility to clear the cache when you update your code or change its configuration. Execute this command to clear the cache in the prod environment:

$ php app/console cache:clear --env=prod

When developing a web application, things can go wrong in many ways. The log files in the app/logs/ directory tell you everything about the requests and help you fix the problem quickly.

Using the Command Line Interface

Each application comes with a command line interface tool (app/console) that helps you maintain your application. It provides commands that boost your productivity by automating tedious and repetitive tasks.

Run it without any arguments to learn more about its capabilities:

$ php app/console

The --help option helps you discover the usage of a command:

$ php app/console router:debug --help
Final Thoughts

Call me crazy, but after reading this part, you should be comfortable with moving things around and making Symfony work for you. Everything in Symfony is designed to get out of your way. So, feel free to rename and move directories around as you see fit.

And that’s all for the quick tour. From testing to sending emails, you still need to learn a lot to become a Symfony master. Ready to dig into these topics now? Look no further - go to the official The Book and pick any topic you want.

Book

Dive into Symfony with the topical guides:

The Book

Symfony and HTTP Fundamentals

Congratulations! By learning about Symfony, you’re well on your way towards being a more productive, well-rounded and popular web developer (actually, you’re on your own for the last part). Symfony is built to get back to basics: to develop tools that let you develop faster and build more robust applications, while staying out of your way. Symfony is built on the best ideas from many technologies: the tools and concepts you’re about to learn represent the efforts of thousands of people, over many years. In other words, you’re not just learning “Symfony”, you’re learning the fundamentals of the web, development best practices and how to use many amazing new PHP libraries, inside or independently of Symfony. So, get ready.

True to the Symfony philosophy, this chapter begins by explaining the fundamental concept common to web development: HTTP. Regardless of your background or preferred programming language, this chapter is a must-read for everyone.

HTTP is Simple

HTTP (Hypertext Transfer Protocol to the geeks) is a text language that allows two machines to communicate with each other. That’s it! For example, when checking for the latest xkcd comic, the following (approximate) conversation takes place:

_images/http-xkcd.png

And while the actual language used is a bit more formal, it’s still dead-simple. HTTP is the term used to describe this simple text-based language. No matter how you develop on the web, the goal of your server is always to understand simple text requests, and return simple text responses.

Symfony is built from the ground up around that reality. Whether you realize it or not, HTTP is something you use every day. With Symfony, you’ll learn how to master it.

Step1: The Client Sends a Request

Every conversation on the web starts with a request. The request is a text message created by a client (e.g. a browser, a smartphone app, etc) in a special format known as HTTP. The client sends that request to a server, and then waits for the response.

Take a look at the first part of the interaction (the request) between a browser and the xkcd web server:

_images/http-xkcd-request.png

In HTTP-speak, this HTTP request would actually look something like this:

GET / HTTP/1.1
Host: xkcd.com
Accept: text/html
User-Agent: Mozilla/5.0 (Macintosh)

This simple message communicates everything necessary about exactly which resource the client is requesting. The first line of an HTTP request is the most important and contains two things: the URI and the HTTP method.

The URI (e.g. /, /contact, etc) is the unique address or location that identifies the resource the client wants. The HTTP method (e.g. GET) defines what you want to do with the resource. The HTTP methods are the verbs of the request and define the few common ways that you can act upon the resource:

GET Retrieve the resource from the server
POST Create a resource on the server
PUT Update the resource on the server
DELETE Delete the resource from the server

With this in mind, you can imagine what an HTTP request might look like to delete a specific blog entry, for example:

DELETE /blog/15 HTTP/1.1

注解

There are actually nine HTTP methods defined by the HTTP specification, but many of them are not widely used or supported. In reality, many modern browsers don’t even support the PUT and DELETE methods.

In addition to the first line, an HTTP request invariably contains other lines of information called request headers. The headers can supply a wide range of information such as the requested Host, the response formats the client accepts (Accept) and the application the client is using to make the request (User-Agent). Many other headers exist and can be found on Wikipedia’s List of HTTP header fields article.

Step 2: The Server Returns a Response

Once a server has received the request, it knows exactly which resource the client needs (via the URI) and what the client wants to do with that resource (via the method). For example, in the case of a GET request, the server prepares the resource and returns it in an HTTP response. Consider the response from the xkcd web server:

_images/http-xkcd.png

Translated into HTTP, the response sent back to the browser will look something like this:

HTTP/1.1 200 OK
Date: Sat, 02 Apr 2011 21:05:05 GMT
Server: lighttpd/1.4.19
Content-Type: text/html

<html>
  <!-- ... HTML for the xkcd comic -->
</html>

The HTTP response contains the requested resource (the HTML content in this case), as well as other information about the response. The first line is especially important and contains the HTTP response status code (200 in this case). The status code communicates the overall outcome of the request back to the client. Was the request successful? Was there an error? Different status codes exist that indicate success, an error, or that the client needs to do something (e.g. redirect to another page). A full list can be found on Wikipedia’s List of HTTP status codes article.

Like the request, an HTTP response contains additional pieces of information known as HTTP headers. For example, one important HTTP response header is Content-Type. The body of the same resource could be returned in multiple different formats like HTML, XML, or JSON and the Content-Type header uses Internet Media Types like text/html to tell the client which format is being returned. A list of common media types can be found on Wikipedia’s List of common media types article.

Many other headers exist, some of which are very powerful. For example, certain headers can be used to create a powerful caching system.

Requests, Responses and Web Development

This request-response conversation is the fundamental process that drives all communication on the web. And as important and powerful as this process is, it’s inescapably simple.

The most important fact is this: regardless of the language you use, the type of application you build (web, mobile, JSON API) or the development philosophy you follow, the end goal of an application is always to understand each request and create and return the appropriate response.

Symfony is architected to match this reality.

小技巧

To learn more about the HTTP specification, read the original HTTP 1.1 RFC or the HTTP Bis, which is an active effort to clarify the original specification. A great tool to check both the request and response headers while browsing is the Live HTTP Headers extension for Firefox.

Requests and Responses in PHP

So how do you interact with the “request” and create a “response” when using PHP? In reality, PHP abstracts you a bit from the whole process:

$uri = $_SERVER['REQUEST_URI'];
$foo = $_GET['foo'];

header('Content-Type: text/html');
echo 'The URI requested is: '.$uri;
echo 'The value of the "foo" parameter is: '.$foo;

As strange as it sounds, this small application is in fact taking information from the HTTP request and using it to create an HTTP response. Instead of parsing the raw HTTP request message, PHP prepares superglobal variables such as $_SERVER and $_GET that contain all the information from the request. Similarly, instead of returning the HTTP-formatted text response, you can use the header() function to create response headers and simply print out the actual content that will be the content portion of the response message. PHP will create a true HTTP response and return it to the client:

HTTP/1.1 200 OK
Date: Sat, 03 Apr 2011 02:14:33 GMT
Server: Apache/2.2.17 (Unix)
Content-Type: text/html

The URI requested is: /testing?foo=symfony
The value of the "foo" parameter is: symfony
Requests and Responses in Symfony

Symfony provides an alternative to the raw PHP approach via two classes that allow you to interact with the HTTP request and response in an easier way. The Request class is a simple object-oriented representation of the HTTP request message. With it, you have all the request information at your fingertips:

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();

// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();

// retrieve GET and POST variables respectively
$request->query->get('foo');
$request->request->get('bar', 'default value if bar does not exist');

// retrieve SERVER variables
$request->server->get('HTTP_HOST');

// retrieves an instance of UploadedFile identified by foo
$request->files->get('foo');

// retrieve a COOKIE value
$request->cookies->get('PHPSESSID');

// retrieve an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content_type');

$request->getMethod();    // GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // an array of languages the client accepts

As a bonus, the Request class does a lot of work in the background that you’ll never need to worry about. For example, the isSecure() method checks the three different values in PHP that can indicate whether or not the user is connecting via a secured connection (i.e. HTTPS).

Symfony also provides a Response class: a simple PHP representation of an HTTP response message. This allows your application to use an object-oriented interface to construct the response that needs to be returned to the client:

use Symfony\Component\HttpFoundation\Response;

$response = new Response();

$response->setContent('<html><body><h1>Hello world!</h1></body></html>');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');

// prints the HTTP headers followed by the content
$response->send();

If Symfony offered nothing else, you would already have a toolkit for easily accessing request information and an object-oriented interface for creating the response. Even as you learn the many powerful features in Symfony, keep in mind that the goal of your application is always to interpret a request and create the appropriate response based on your application logic.

小技巧

The Request and Response classes are part of a standalone component included with Symfony called HttpFoundation. This component can be used entirely independently of Symfony and also provides classes for handling sessions and file uploads.

The Journey from the Request to the Response

Like HTTP itself, the Request and Response objects are pretty simple. The hard part of building an application is writing what comes in between. In other words, the real work comes in writing the code that interprets the request information and creates the response.

Your application probably does many things, like sending emails, handling form submissions, saving things to a database, rendering HTML pages and protecting content with security. How can you manage all of this and still keep your code organized and maintainable?

Symfony was created to solve these problems so that you don’t have to.

The Front Controller

Traditionally, applications were built so that each “page” of a site was its own physical file:

index.php
contact.php
blog.php

There are several problems with this approach, including the inflexibility of the URLs (what if you wanted to change blog.php to news.php without breaking all of your links?) and the fact that each file must manually include some set of core files so that security, database connections and the “look” of the site can remain consistent.

A much better solution is to use a front controller: a single PHP file that handles every request coming into your application. For example:

/index.php executes index.php
/index.php/contact executes index.php
/index.php/blog executes index.php

小技巧

Using Apache’s mod_rewrite (or equivalent with other web servers), the URLs can easily be cleaned up to be just /, /contact and /blog.

Now, every request is handled exactly the same way. Instead of individual URLs executing different PHP files, the front controller is always executed, and the routing of different URLs to different parts of your application is done internally. This solves both problems with the original approach. Almost all modern web apps do this - including apps like WordPress.

Stay Organized

Inside your front controller, you have to figure out which code should be executed and what the content to return should be. To figure this out, you’ll need to check the incoming URI and execute different parts of your code depending on that value. This can get ugly quickly:

// index.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$path = $request->getPathInfo(); // the URI path being requested

if (in_array($path, array('', '/'))) {
    $response = new Response('Welcome to the homepage.');
} elseif ('/contact' === $path) {
    $response = new Response('Contact us');
} else {
    $response = new Response('Page not found.', 404);
}
$response->send();

Solving this problem can be difficult. Fortunately it’s exactly what Symfony is designed to do.

The Symfony Application Flow

When you let Symfony handle each request, life is much easier. Symfony follows the same simple pattern for every request:

Symfony request flow

Incoming requests are interpreted by the routing and passed to controller functions that return Response objects.

Each “page” of your site is defined in a routing configuration file that maps different URLs to different PHP functions. The job of each PHP function, called a controller, is to use information from the request - along with many other tools Symfony makes available - to create and return a Response object. In other words, the controller is where your code goes: it’s where you interpret the request and create a response.

It’s that easy! To review:

  • Each request executes a front controller file;
  • The routing system determines which PHP function should be executed based on information from the request and routing configuration you’ve created;
  • The correct PHP function is executed, where your code creates and returns the appropriate Response object.
A Symfony Request in Action

Without diving into too much detail, here is this process in action. Suppose you want to add a /contact page to your Symfony application. First, start by adding an entry for /contact to your routing configuration file:

  • YAML
    # app/config/routing.yml
    contact:
        path:     /contact
        defaults: { _controller: AppBundle:Main:contact }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="contact" path="/contact">
            <default key="_controller">AppBundle:Main:contact</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\Route;
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->add('contact', new Route('/contact', array(
        '_controller' => 'AppBundle:Main:contact',
    )));
    
    return $collection;
    

When someone visits the /contact page, this route is matched, and the specified controller is executed. As you’ll learn in the routing chapter, the AcmeDemoBundle:Main:contact string is a short syntax that points to a specific PHP method contactAction inside a class called MainController:

// src/AppBundle/Controller/MainController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class MainController
{
    public function contactAction()
    {
        return new Response('<h1>Contact us!</h1>');
    }
}

In this very simple example, the controller simply creates a Response object with the HTML <h1>Contact us!</h1>. In the controller chapter, you’ll learn how a controller can render templates, allowing your “presentation” code (i.e. anything that actually writes out HTML) to live in a separate template file. This frees up the controller to worry only about the hard stuff: interacting with the database, handling submitted data, or sending email messages.

Symfony: Build your App, not your Tools

You now know that the goal of any app is to interpret each incoming request and create an appropriate response. As an application grows, it becomes more difficult to keep your code organized and maintainable. Invariably, the same complex tasks keep coming up over and over again: persisting things to the database, rendering and reusing templates, handling form submissions, sending emails, validating user input and handling security.

The good news is that none of these problems is unique. Symfony provides a framework full of tools that allow you to build your application, not your tools. With Symfony, nothing is imposed on you: you’re free to use the full Symfony framework, or just one piece of Symfony all by itself.

Standalone Tools: The Symfony Components

So what is Symfony? First, Symfony is a collection of over twenty independent libraries that can be used inside any PHP project. These libraries, called the Symfony Components, contain something useful for almost any situation, regardless of how your project is developed. To name a few:

HttpFoundation
Contains the Request and Response classes, as well as other classes for handling sessions and file uploads.
Routing
Powerful and fast routing system that allows you to map a specific URI (e.g. /contact) to some information about how that request should be handled (e.g. execute the contactAction() method).
Form
A full-featured and flexible framework for creating forms and handling form submissions.
Validator
A system for creating rules about data and then validating whether or not user-submitted data follows those rules.
Templating
A toolkit for rendering templates, handling template inheritance (i.e. a template is decorated with a layout) and performing other common template tasks.
Security
A powerful library for handling all types of security inside an application.
Translation
A framework for translating strings in your application.

Each one of these components is decoupled and can be used in any PHP project, regardless of whether or not you use the Symfony framework. Every part is made to be used if needed and replaced when necessary.

The Full Solution: The Symfony Framework

So then, what is the Symfony Framework? The Symfony Framework is a PHP library that accomplishes two distinct tasks:

  1. Provides a selection of components (i.e. the Symfony Components) and third-party libraries (e.g. Swift Mailer for sending emails);
  2. Provides sensible configuration and a “glue” library that ties all of these pieces together.

The goal of the framework is to integrate many independent tools in order to provide a consistent experience for the developer. Even the framework itself is a Symfony bundle (i.e. a plugin) that can be configured or replaced entirely.

Symfony provides a powerful set of tools for rapidly developing web applications without imposing on your application. Normal users can quickly start development by using a Symfony distribution, which provides a project skeleton with sensible defaults. For more advanced users, the sky is the limit.

使用 Symfony 与不使用框架的对比

为什么用 Symfony 开发比打开一个文件直接写 PHP 代码更好?

如果你没有接触过 PHP 框架,也不清楚什么是 MVC,或者对 Symfony 好处的传言感到好奇,那么这一章就是写给你的。在这里我们不会 告诉 你 Symfony 可以让你的开发更快速、更好,而是会带你你亲身见证这一切。

在本章中,我们将带你用纯 PHP 写一个简单的应用程序,然后将其重构,使之更有条理。你将会穿越时空,了解为什么网站开发在过去几年中会发生如此翻天覆地的变化。

最后带你体会为什么 Symfony 可以让你摆脱掉一切繁琐,从而真正掌控你的代码。

先用纯 PHP 写一个简单的博客程序

首先,让我们不使用框架写一个博客程序。创建一个来显示数据库里保存的文章的页面。纯 PHP 的话非常简单,但看起来并不舒服:

<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);

$result = mysql_query('SELECT id, title FROM post', $link);
?>

<!DOCTYPE html>
<html>
    <head>
        <title>文章列表</title>
    </head>
    <body>
        <h1>文章列表</h1>
        <ul>
            <?php while ($row = mysql_fetch_assoc($result)): ?>
            <li>
                <a href="/show.php?id=<?php echo $row['id'] ?>">
                    <?php echo $row['title'] ?>
                </a>
            </li>
            <?php endwhile ?>
        </ul>
    </body>
</html>

<?php
mysql_close($link);
?>

这样写起来并不费事,运行起来也不慢,但有没有想过随着你程序规模的增大,你该如何维护它。这里列出了几个你可能遇到的问题:

  • 没有出错检查:如果数据库连接失败怎么办?
  • 代码结构性差:随着代码的增多,文件将越来越多 最后导致你没法继续维护。如果你要处理表单,对应的代码放在 哪儿?如何验证用户提交上来的数据?发邮件的代码写在 哪儿呢?
  • 代码重复利用率低:因为所有的代码都写在一个文件里, 也就没法在这个博客的别的“页面”里重复使用任何一段代码了。

注解

另外一个没有指出的问题是,上面的代码只能用来连接 MySQL 数据库。虽然有些超出本章的范围,但还是很想让你知道,Symfony 完整集成了 Doctrine, 一个提供抽象数据库操作和表字段映射的库。

抽离表现层

现在可以立即将包含了 HTML 代码的“表现层”代码单独保存为一个文件,让表现层与主“逻辑”文件分离:

<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);

$result = mysql_query('SELECT id, title FROM post', $link);

$posts = array();
while ($row = mysql_fetch_assoc($result)) {
    $posts[] = $row;
}

mysql_close($link);

// 导入 HTML 表现层文件
require 'templates/list.php';

现在 HTML 代码都保存在一个独立的文件(templates/list.php)中,这个文件在 HTML 代码中嵌入了模板风格的 PHP 代码:

<!DOCTYPE html>
<html>
    <head>
        <title>文章列表</title>
    </head>
    <body>
        <h1>文章列表</h1>
        <ul>
            <?php foreach ($posts as $post): ?>
            <li>
                <a href="/read?id=<?php echo $post['id'] ?>">
                    <?php echo $post['title'] ?>
                </a>
            </li>
            <?php endforeach ?>
        </ul>
    </body>
</html>

根据惯例,上面的包含所有程序逻辑的文件 index.php 被称为“Controller(控制器)”。所谓 controller 是无论你使用的是语言还是框架都会经常听到的一个术语。简单来讲,它是一块 你写的 处理用户输入并准备响应的代码。

在上面的例子里,控制器从数据库里读出数据,然后导入一个模板文件来展现这些数据。通过分离控制器的代码,你将可以轻松地修改模板文件,比如以另外的格式来扩展博客文章的渲染方式(如创建一个对应 JSON 格式的 list.json.php 模板)。

分离应用程序逻辑(域)

到目前为止,我们的程序只有一个页面。但是,如果第二个页面需要使用相同的连接数据库的代码或者要用相同的博客文章的数组呢?让我们再次重构代码,将核心的行为和数据访问功能从原来的程序代码中分离出来放入一个叫做 model.php 的新文件中:

<?php
// model.php
function open_database_connection()
{
    $link = mysql_connect('localhost', 'myuser', 'mypassword');
    mysql_select_db('blog_db', $link);

    return $link;
}

function close_database_connection($link)
{
    mysql_close($link);
}

function get_all_posts()
{
    $link = open_database_connection();

    $result = mysql_query('SELECT id, title FROM post', $link);
    $posts = array();
    while ($row = mysql_fetch_assoc($result)) {
        $posts[] = $row;
    }
    close_database_connection($link);

    return $posts;
}

小技巧

使用 model.php 来命名刚才的新文件是因为程序逻辑和数据访问 一般被叫做“Model(模型)”层。在一个代码组织良好的 程序中,大多数“业务逻辑”的代码 都在模型层中(而不是控制器中)。不像 这个例子里的模型层只关注 访问数据库这一小部分。

现在的控制器( index.php )就很简单了:

<?php
require_once 'model.php';

$posts = get_all_posts();

require 'templates/list.php';

现在控制器的唯一任务就是从模型层中得到数据,然后调用一个模板来渲染这些数据。这就是一个最简单的 MVC 模式。

抽离布局

现在已经把程序重构成三个有着明显不同优势的部分,并且能在不同的页面中重复使用几乎所有的东西。

在代码中唯一 不能 被重用的就只有布局了,因此让我们创建一个新的 layout.php 文件来解决这个问题。

<!-- templates/layout.php -->
<!DOCTYPE html>
<html>
    <head>
        <title><?php echo $title ?></title>
    </head>
    <body>
        <?php echo $content ?>
    </body>
</html>

现在模板文件(templates/list.php)可以简单地从基础布局中“扩展”出来。

<?php $title = '文章列表' ?>

<?php ob_start() ?>
    <h1>文章列表</h1>
    <ul>
        <?php foreach ($posts as $post): ?>
        <li>
            <a href="/read?id=<?php echo $post['id'] ?>">
                <?php echo $post['title'] ?>
            </a>
        </li>
        <?php endforeach ?>
    </ul>
<?php $content = ob_get_clean() ?>

<?php include 'layout.php' ?>

现在你已经知道了重复使用布局的方法。但不幸的是按照现在的思路,你不得不在模板中使用很多丑陋的PHP函数(诸如 ob_start()ob_get_clean())。在 Symfony 中,可以使用模板组件来让这一切变得更整洁、更方便。你马上就会看到我们如何使用它。

添加一个显示博文的页面

我们已经重构了博客的“列表”页,使它的代码具有了更好的组织性和可重复使用性。为了检验这一点,让我们添加一个显示博文的页面,来显示被通过 id 参数标记了的单篇博文。

首先在 model.php 文件中新增一个函数,用来通过给定的 id 检索单篇博文:

// model.php
function get_post_by_id($id)
{
    $link = open_database_connection();

    $id = intval($id);
    $query = 'SELECT date, title, body FROM post WHERE id = '.$id;
    $result = mysql_query($query);
    $row = mysql_fetch_assoc($result);

    close_database_connection($link);

    return $row;
}

接下来创建一个新的叫做 show.php 文件,作为新页面的控制器:

<?php
require_once 'model.php';

$post = get_post_by_id($_GET['id']);

require 'templates/show.php';

最后创建新的模板文件 templates/show.php ,来渲染单篇博文:

<?php $title = $post['title'] ?>

<?php ob_start() ?>
    <h1><?php echo $post['title'] ?></h1>

    <div class="date"><?php echo $post['date'] ?></div>
    <div class="body">
        <?php echo $post['body'] ?>
    </div>
<?php $content = ob_get_clean() ?>

<?php include 'layout.php' ?>

现在创建第二页已经非常容易了,也没有写重复的代码。然而这一页还有一堆的问题。选择一个框架吧,把这些问题交给它来解决。例如,缺失或无效的 id 参数会导致页面崩溃。如果能够触发 404 页面将会更好,但做到这一点并不容易。更糟的是,如果你忘记了用 intval() 函数对 id 参数进行清理的话,你将会让整个数据库陷入 SQL 注入攻击的危险之中。

另一个问题就是每一个单独的控制器都必须包含 model.php 文件。如果每个控制器都突然需要包含一个别的文件或者执行其它全局任务(如安全管理)呢?按照目前的情况,这些代码必须添加到每个控制器文件中。如果你忘了包含某个文件,希望这不会给我们带来不安全的因素…

用一个“前端控制器”来解救

现在,使用 front controller: 来解救我们的程序吧,它是一个单独的 PHP 文件,我们可以通过它来处理 所有 的请求。有了前端控制器,程序的 URI 略有变化,但开始变得更灵活了:

没有前端控制器
/index.php          => 博客的列表页(index.php 被运行)
/show.php           => 博客的博文展示页(show.php 被运行)

使用 index.php 作为前端控制器
/index.php          => 博客的列表页(index.php 被运行)
/index.php/show          => 博客的博文展示页(index.php 被运行)

小技巧

如果使用了 Apache 网页服务器的 rewrite 规则 (或别的网页服务器的相同功能),URI 中的 index.php 部分就可以省略掉了。这样的话,博客的 博文展示页的 URI 结果就可以简单地用 /show 来表示。

当使用前端控制器时,单个 PHP 文件(在这里是 index.php )将渲染 所有的 请求,对于博文展示页来说, /index.php/show 最终实际执行的是 index.php ,它现在负责用完整的 URI 来进行内部路由请求。。如你所见,前端控制器是个非常强大的工具。

制作前端控制器

我们就要对程序进行 重大 改动了。一旦单个文件接管了所有的请求,你就可以集中精力处理诸如安全、加载配置、路由等等这类事情了。在这个例子里, index.php 要足够智能,以便根据请求的 URL 区分并渲染博客列表页和博文展示页:

<?php
// index.php

// 加载并初始化任何全局库
require_once 'model.php';
require_once 'controllers.php';

// 在内部路由用户的请求
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ('/index.php' == $uri) {
    list_action();
} elseif ('/index.php/show' == $uri && isset($_GET['id'])) {
    show_action($_GET['id']);
} else {
    header('Status: 404 Not Found');
    echo '<html><body><h1>页面未找到!</h1></body></html>';
}

为了更好地组织代码,将两个控制器(之前分别在 index.phpshow.php``里)写成两个 PHP 函数,并放到新的 ``controllers.php 文件里:

function list_action()
{
    $posts = get_all_posts();
    require 'templates/list.php';
}

function show_action($id)
{
    $post = get_post_by_id($id);
    require 'templates/show.php';
}

作为前端控制器, index.php 扮演了一个新的角色:加载核心库并且路由所有的请求,以便使两个控制器之一( list_action()show_action() 函数)被调用。实际上,前端控制器看来去也变得很像 Symfony 中处理请求和路由请求的机制了。

小技巧

前端控制器另一个优点就是可以提供更灵活的 URL 。注意, 博客显示页的URL只需在一个位置修改一下, 就可以从 /show 变成 /read 。而在此之前,你需要将整个文件 重命名。在 Symfony 中,URL 将更加灵活。

现在,我们的程序已经从单个的文件发展为拥有良好架构并允许代码重新使用的程序了。你应该觉得高兴,但别感到满意。例如,“路由”系统是多变的,列表页( /index.php )也要可以通过 / 来访问(如果添加了 Apache 重写规则的话)。而且,大量的时间花费在“架构”(如路由、控制器和模板等)上,而非花在真正的博客的开发上。你还需要在处理提交上来的表单、验证用户的输入、记录运行日志和安全上花费更多的时间。为什么你要重新发明这些轮子呢?

接触一下 Symfony

Symfony 来支援我们啦!在用 Symfony 之前,你要先下载它。你可以用 Composer ,它会给你下载正确的版本并安装相关依赖,而且还提供了一个自动加载器。自动加载器是一个可以让你在没有明确声明包含所用的 PHP 类文件时,就可以使用这个类的一个工具。

在网站的根目录创建 composer.json 文件并写入以下内容:

{
    "require": {
        "symfony/symfony": "2.3.*"
    },
    "autoload": {
        "files": ["model.php","controllers.php"]
    }
}

下一步, download Composer 并运行以下命令来把 Symfony 下载到 vendor/ 目录下:

$ composer install

Composer 在下载依赖的时候会同时生成 vendor/autoload.php 文件,这个文件会自动装载 Symfony 的所有的文件到 composer.json 描述的自动装载的文件中。

Symfony 哲学的核心是:程序的主要任务就是解释每个请求并返回对应的响应。因此,Symfony 提供了 RequestResponse , class. 这两个类是原始的 HTTP 中处理请求和返回响应的面向对象的表述。使用它们来改善我们的博客:

<?php
// index.php
require_once 'vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$uri = $request->getPathInfo();
if ('/' == $uri) {
    $response = list_action();
} elseif ('/show' == $uri && $request->query->has('id')) {
    $response = show_action($request->query->get('id'));
} else {
    $html = '<html><body><h1>页面未找到!</h1></body></html>';
    $response = new Response($html, 404);
}

// 输出响应头并发回响应
$response->send();

现在控制器可以通过返回一个 Response 对象来返回响应。为了更加方便,你可以加入一个新的 render_template() 函数,该函数的行为很像 Symfony 的模板引擎:

// controllers.php
use Symfony\Component\HttpFoundation\Response;

function list_action()
{
    $posts = get_all_posts();
    $html = render_template('templates/list.php', array('posts' => $posts));

    return new Response($html);
}

function show_action($id)
{
    $post = get_post_by_id($id);
    $html = render_template('templates/show.php', array('post' => $post));

    return new Response($html);
}

// 模板渲染帮手函数
function render_template($path, array $args)
{
    extract($args);
    ob_start();
    require $path;
    $html = ob_get_clean();

    return $html;
}

通过运用 Symfony 的一小部分,我们的程序变得更加灵活可靠。Request 类提供了一个访问 HTTP 请求信息的可靠方式。具体来说, getPathInfo() 方法返回一个被清理过的的 URI(比如它会返回 /show ,而不会是 /index.php/show)。因此即使用户在地址栏里写的是 /index.php/show,应用程序也会智能地将请求路由到 show_action()

在构造 HTTP 响应时, Response 对象十分灵活,它允许通过一个面向对象的接口写入响应头和内容。虽然在我们的这个博客程序中响应是很简单的,但你将体会到当程序增长时这种灵活性将带来的好处。

Symfony 程序示例

我们的程序走到现在花了 很长 的时间,相信你已经体会到即使这么简单的程序也包含了大量的代码。一路走来,我们制作了简单的路由系统,并且还写了一个使用 ob_start()ob_get_clean() 渲染模板的方法。如果在你下一次从零开始搭建“框架”的时候,你至少可以使用 Symfony 中的独立 RoutingTemplating 组件,因为它们已经帮你解决了很多问题。

为了不用重新发明轮子,你可以让 Symfony 接管一些部分,下面是我们的程序基于 Symfony 的写法:

// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function listAction()
    {
        $posts = $this->get('doctrine')
            ->getManager()
            ->createQuery('SELECT p FROM AcmeBlogBundle:Post p')
            ->execute();

        return $this->render('Blog/list.html.php', array('posts' => $posts));
    }

    public function showAction($id)
    {
        $post = $this->get('doctrine')
            ->getManager()
            ->getRepository('AppBundle:Post')
            ->find($id);

        if (!$post) {
            // 抛出 404 错误
            throw $this->createNotFoundException();
        }

        return $this->render('Blog/show.html.php', array('post' => $post));
    }
}

这两个控制器仍然很轻量,它们都使用 Doctrine ORM 库 从数据库中检索对象,并使用模板组件渲染模板,最后返回 Response 对象。模板文件现在超级简单:

<!-- app/Resources/views/Blog/list.html.php -->
<?php $view->extend('layout.html.php') ?>

<?php $view['slots']->set('title', 'List of Posts') ?>

<h1>文章列表</h1>
<ul>
    <?php foreach ($posts as $post): ?>
    <li>
        <a href="<?php echo $view['router']->generate(
            'blog_show',
            array('id' => $post->getId())
        ) ?>">
            <?php echo $post->getTitle() ?>
        </a>
    </li>
    <?php endforeach ?>
</ul>

布局文件几乎没变:

<!-- app/Resources/views/layout.html.php -->
<!DOCTYPE html>
<html>
    <head>
        <title><?php echo $view['slots']->output(
            'title',
            'Default title'
        ) ?></title>
    </head>
    <body>
        <?php echo $view['slots']->output('_content') ?>
    </body>
</html>

注解

在这里我们将博文展示页面模板留做练习,实现它相对于实现 博文列表模板来说几乎微不足道。

在 Symfony 引擎(我们称其为 Kernel)启动时,它需要根据一张地图来判断请求信息需要被路由到哪个控制器。所谓的路由表则是一张我们也能读懂的“地图”:

# app/config/routing.yml
blog_list:
    path:     /blog
    defaults: { _controller: AppBundle:Blog:list }

blog_show:
    path:     /blog/show/{id}
    defaults: { _controller: AppBundle:Blog:show }

现在 Symfony 就开始处理所有的简单任务了。前端控制器极其简单,它被创建之后你就无须再去接触它了。(如果你使用 Symfony 的发行版,你都无须去创建它):

// web/app.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();

前端控制器的唯一工作就是初始化 Symfony 引擎(内核)并把一个需要处理的 Request 对象传入内核。Symfony 内核再根据路由表来确定调用哪个控制器。和之前一样,控制器方法负责返回最终的 Response 对象。对它来说就真的没有别的可做的了。

至于 Symfony 如何处理请求,请参阅 请求处理流程图

进入 Symfony 的世界

在接下来的章节中,我们将学到更多关于 Symfony 的各部分的工作原理,以及推荐的项目组织形式。现在,看看我们的博客程序从纯 PHP 迁移到 Symfony 后有什么优势:

  • 现在我们的应用程序代码 很整洁,组织很好 (虽然 Symfony 并不强制你做到这一点)。这提高了我们代码的 重用率 并且 可以让新加入项目的开发者很快进入角色;
  • 所写的代码100%是为了 你的 程序,你 不再需要 开发和维护低级的程序了,比如 自动载入路由、 或渲染 控制器
  • Symfony 可以让你 使用开源工具 如 Doctrine 、 模板、安全、表单、验证组建(只是 几个例子);
  • 感谢路由组件让我们的程序拥有 十分灵活的URL
  • Symfony 以 HTTP 为中心的架构可以让你使用强大的工具, 例如使用 Symfony 的内建 HTTP 缓存 或更为强大的 Varnish 来实现 HTTP 缓存。这将在稍后的 缓存 一章中进行讲解 。

最值得高兴的是,通过使用 Symfony,你现在可以获得一整套 Symfony 社区开发的高品质开源工具。想获得 Symfony 社区工具请移步 KnpBundles.com

更好的模板

Symfony 标配的模板引擎叫 Twig,如果你选择使用它,它将让你可以更快地书写更有可读性的模板。这意味着我们的博客程序可以用更少的代码来写。比如,列表模板用 Twig 写的话是下面的样子:

{# app/Resources/views/Blog/list.html.twig #}
{% extends "layout.html.twig" %}

{% block title %}文章列表{% endblock %}

{% block body %}
    <h1>文章列表</h1>
    <ul>
        {% for post in posts %}
        <li>
            <a href="{{ path('blog_show', {'id': post.id}) }}">
                {{ post.title }}
            </a>
        </li>
        {% endfor %}
    </ul>
{% endblock %}

同样的, layout.html.twig 也不难写:

{# app/Resources/views/layout.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <title>{% block title %}默认标题{% endblock %}</title>
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>

Symfony 很好地支持 Twig。虽然 Symfony 永远支持 PHP 风格模板,但我们将继续讨论 Twig 的更多优势。更多信息请参阅 模板章节

Installing and Configuring Symfony

The goal of this chapter is to get you up and running with a working application built on top of Symfony. In order to simplify the process of creating new applications, Symfony provides an installer that must be installed before creating the first application.

Installing the Symfony Installer

Using the Symfony Installer is the only recommended way to create new Symfony applications. This installer is a PHP application that has to be installed only once and then it can create any number of Symfony applications.

注解

The installer requires PHP 5.4 or higher. If you still use the legacy PHP 5.3 version, you cannot use the Symfony Installer. Read the Creating Symfony Applications without the Installer section to learn how to proceed.

Depending on your operating system, the installer must be installed in different ways.

Linux and Mac OS X Systems

Open your command console and execute the following three commands:

$ curl -LsS http://symfony.com/installer > symfony.phar
$ sudo mv symfony.phar /usr/local/bin/symfony
$ chmod a+x /usr/local/bin/symfony

This will create a global symfony command in your system that will be used to create new Symfony applications.

Windows Systems

Open your command console and execute the following command:

c:\> php -r "readfile('http://symfony.com/installer');" > symfony.phar

Then, move the downloaded symfony.phar file to your projects directory and execute it as follows:

c:\> move symfony.phar c:\projects
c:\projects\> php symfony.phar
Creating the Symfony Application

Once the Symfony Installer is ready, create your first Symfony application with the new command:

# Linux, Mac OS X
$ symfony new my_project_name

# Windows
c:\> cd projects/
c:\projects\> php symfony.phar new my_project_name

This command creates a new directory called my_project_name that contains a fresh new project based on the most recent stable Symfony version available. In addition, the installer checks if your system meets the technical requirements to execute Symfony applications. If not, you’ll see the list of changes needed to meet those requirements.

小技巧

For security reasons, all Symfony versions are digitally signed before distributing them. If you want to verify the integrity of any Symfony version, follow the steps explained in this post.

Basing your Project on a Specific Symfony Version

If your project needs to be based on a specific Symfony version, pass the version number as the second argument of the new command:

# Linux, Mac OS X
$ symfony new my_project_name 2.3.23

# Windows
c:\projects\> php symfony.phar new my_project_name 2.3.23

Read the Symfony Release process to better understand why there are several Symfony versions and which one to use for your projects.

Creating Symfony Applications without the Installer

If you still use PHP 5.3, or if you can’t execute the installer for any reason, you can create Symfony applications using the alternative installation method based on Composer.

Composer is the dependency manager used by modern PHP applications and it can also be used to create new applications based on the Symfony framework. If you don’t have installed it globally, start by reading the next section.

Installing Composer Globally

Start with installing Composer globally.

Creating a Symfony Application with Composer

Once Composer is installed on your computer, execute the create-project command to create a new Symfony application based on its latest stable version:

$ composer create-project symfony/framework-standard-edition my_project_name

If you need to base your application on a specific Symfony version, provide that version as the second argument of the create-project command:

$ composer create-project symfony/framework-standard-edition my_project_name "2.3.*"

小技巧

If your Internet connection is slow, you may think that Composer is not doing anything. If that’s your case, add the -vvv flag to the previous command to display a detailed output of everything that Composer is doing.

Running the Symfony Application

Symfony leverages the internal web server provided by PHP to run applications while developing them. Therefore, running a Symfony application is a matter of browsing the project directory and executing this command:

$ cd my_project_name/
$ php app/console server:run

Then, open your browser and access the http://localhost:8000 URL to see the Welcome page of Symfony:

Symfony Welcome Page

Instead of the Welcome Page, you may see a blank page or an error page. This is caused by a directory permission misconfiguration. There are several possible solutions depending on your operating system. All of them are explained in the Setting up Permissions section.

注解

PHP’s internal web server is available in PHP 5.4 or higher versions. If you still use the legacy PHP 5.3 version, you’ll have to configure a virtual host in your web server.

The server:run command is only suitable while developing the application. In order to run Symfony applications on production servers, you’ll have to configure your Apache or Nginx web server as explained in Configuring a Web Server.

When you are finished working on your Symfony application, you can stop the server with the server:stop command:

$ php app/console server:stop
Checking Symfony Application Configuration and Setup

Symfony applications come with a visual server configuration tester to show if your environment is ready to use Symfony. Access the following URL to check your configuration:

http://localhost:8000/config.php

If there are any issues, correct them now before moving on.

Updating Symfony Applications

At this point, you’ve created a fully-functional Symfony application in which you’ll start to develop your own project. A Symfony application depends on a number of external libraries. These are downloaded into the vendor/ directory and they are managed exclusively by Composer.

Updating those third-party libraries frequently is a good practice to prevent bugs and security vulnerabilities. Execute the update Composer command to update them all at once:

$ cd my_project_name/
$ composer update

Depending on the complexity of your project, this update process can take up to several minutes to complete.

Installing a Symfony Distribution

Symfony project packages “distributions”, which are fully-functional applications that include the Symfony core libraries, a selection of useful bundles, a sensible directory structure and some default configuration. In fact, when you created a Symfony application in the previous sections, you actually downloaded the default distribution provided by Symfony, which is called Symfony Standard Edition.

The Symfony Standard Edition is by far the most popular distribution and it’s also the best choice for developers starting with Symfony. However, the Symfony Community has published other popular distributions that you may use in your applications:

  • The Symfony CMF Standard Edition is the best distribution to get started with the Symfony CMF project, which is a project that makes it easier for developers to add CMS functionality to applications built with the Symfony framework.
  • The Symfony REST Edition shows how to build an application that provides a RESTful API using the FOSRestBundle and several other related bundles.
Using Source Control

If you’re using a version control system like Git, you can safely commit all your project’s code. The reason is that Symfony applications already contain a .gitignore file specially prepared for Symfony.

For specific instructions on how best to set up your project to be stored in Git, see How to Create and Store a Symfony Project in Git.

Checking out a versioned Symfony Application

When using Composer to manage application’s dependencies, it’s recommended to ignore the entire vendor/ directory before committing its code to the repository. This means that when checking out a Symfony application from a Git repository, there will be no vendor/ directory and the application won’t work out-of-the-box.

In order to make it work, check out the Symfony application and then execute the install Composer command to download and install all the dependencies required by the application:

$ cd my_project_name/
$ composer install

How does Composer know which specific dependencies to install? Because when a Symfony application is committed to a repository, the composer.json and composer.lock files are also committed. These files tell Composer which dependencies (and which specific versions) to install for the application.

Beginning Development

Now that you have a fully-functional Symfony application, you can begin development! Your distribution may contain some sample code - check the README.md file included with the distribution (open it as a text file) to learn about what sample code was included with your distribution.

If you’re new to Symfony, check out “Creating Pages in Symfony”, where you’ll learn how to create pages, change configuration, and do everything else you’ll need in your new application.

Be sure to also check out the Cookbook, which contains a wide variety of articles about solving specific problems with Symfony.

注解

If you want to remove the sample code from your distribution, take a look at this cookbook article: “How to Remove the AcmeDemoBundle

Creating Pages in Symfony

Creating a new page in Symfony is a simple two-step process:

  • Create a route: A route defines the URL (e.g. /about) to your page and specifies a controller (which is a PHP function) that Symfony should execute when the URL of an incoming request matches the route path;
  • Create a controller: A controller is a PHP function that takes the incoming request and transforms it into the Symfony Response object that’s returned to the user.

This simple approach is beautiful because it matches the way that the Web works. Every interaction on the Web is initiated by an HTTP request. The job of your application is simply to interpret the request and return the appropriate HTTP response.

Symfony follows this philosophy and provides you with tools and conventions to keep your application organized as it grows in users and complexity.

Environments & Front Controllers

Every Symfony application runs within an environment. An environment is a specific set of configuration and loaded bundles, represented by a string. The same application can be run with different configurations by running the application in different environments. Symfony comes with three environments defined — dev, test and prod — but you can create your own as well.

Environments are useful by allowing a single application to have a dev environment built for debugging and a production environment optimized for speed. You might also load specific bundles based on the selected environment. For example, Symfony comes with the WebProfilerBundle (described below), enabled only in the dev and test environments.

Symfony comes with two web-accessible front controllers: app_dev.php provides the dev environment, and app.php provides the prod environment. All web accesses to Symfony normally go through one of these front controllers. (The test environment is normally only used when running unit tests, and so doesn’t have a dedicated front controller. The console tool also provides a front controller that can be used with any environment.)

When the front controller initializes the kernel, it provides two parameters: the environment, and also whether the kernel should run in debug mode. To make your application respond faster, Symfony maintains a cache under the app/cache/ directory. When debug mode is enabled (such as app_dev.php does by default), this cache is flushed automatically whenever you make changes to any code or configuration. When running in debug mode, Symfony runs slower, but your changes are reflected without having to manually clear the cache.

The “Random Number” Page

In this chapter, you’ll develop an application that can generate random numbers. When you’re finished, the user will be able to get a random number between 1 and the upper limit set by the URL:

http://localhost/app_dev.php/random/100

Actually, you’ll be able to replace 100 with any other number to generate numbers up to that upper limit. To create the page, follow the simple two-step process.

注解

The tutorial assumes that you’ve already downloaded Symfony and configured your webserver. The above URL assumes that localhost points to the web directory of your new Symfony project. For detailed information on this process, see the documentation on the web server you are using. Here are some relevant documentation pages for the web server you might be using:

Before you begin: Create the Bundle

Before you begin, you’ll need to create a bundle. In Symfony, a bundle is like a plugin, except that all the code in your application will live inside a bundle.

A bundle is nothing more than a directory that houses everything related to a specific feature, including PHP classes, configuration, and even stylesheets and JavaScript files (see The Bundle System).

Depending on the way you installed Symfony, you may already have a bundle called AcmeDemoBundle. Browse the src/ directory of your project and check if there is a DemoBundle/ directory inside an Acme/ directory. If those directories already exist, skip the rest of this section and go directly to create the route.

To create a bundle called AcmeDemoBundle (a play bundle that you’ll build in this chapter), run the following command and follow the on-screen instructions (use all the default options):

$ php app/console generate:bundle --namespace=Acme/DemoBundle --format=yml

Behind the scenes, a directory is created for the bundle at src/Acme/DemoBundle. A line is also automatically added to the app/AppKernel.php file so that the bundle is registered with the kernel:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        // ...
        new Acme\DemoBundle\AcmeDemoBundle(),
    );
    // ...

    return $bundles;
}

Now that you have a bundle setup, you can begin building your application inside the bundle.

Step 1: Create the Route

By default, the routing configuration file in a Symfony application is located at app/config/routing.yml. Like all configuration in Symfony, you can also choose to use XML or PHP out of the box to configure routes.

If you look at the main routing file, you’ll see that Symfony already added an entry when you generated the AcmeDemoBundle:

  • YAML
    # app/config/routing.yml
    acme_website:
        resource: "@AcmeDemoBundle/Resources/config/routing.yml"
        prefix:   /
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <import
            resource="@AcmeDemoBundle/Resources/config/routing.xml"
            prefix="/" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $acmeDemo = $loader->import('@AcmeDemoBundle/Resources/config/routing.php');
    $acmeDemo->addPrefix('/');
    
    $collection = new RouteCollection();
    $collection->addCollection($acmeDemo);
    
    return $collection;
    

This entry is pretty basic: it tells Symfony to load routing configuration from the Resources/config/routing.yml (routing.xml or routing.php in the XML and PHP code example respectively) file that lives inside the AcmeDemoBundle. This means that you place routing configuration directly in app/config/routing.yml or organize your routes throughout your application, and import them from here.

注解

You are not limited to load routing configurations that are of the same format. For example, you could also load a YAML file in an XML configuration and vice versa.

Now that the routing.yml file from the bundle is being imported, add the new route that defines the URL of the page that you’re about to create:

  • YAML
    # src/Acme/DemoBundle/Resources/config/routing.yml
    random:
        path:     /random/{limit}
        defaults: { _controller: AcmeDemoBundle:Random:index }
    
  • XML
    <!-- src/Acme/DemoBundle/Resources/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="random" path="/random/{limit}">
            <default key="_controller">AcmeDemoBundle:Random:index</default>
        </route>
    </routes>
    
  • PHP
    // src/Acme/DemoBundle/Resources/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('random', new Route('/random/{limit}', array(
        '_controller' => 'AcmeDemoBundle:Random:index',
    )));
    
    return $collection;
    

The routing consists of two basic pieces: the path, which is the URL that this route will match, and a defaults array, which specifies the controller that should be executed. The placeholder syntax in the path ({limit}) is a wildcard. It means that /random/10, /random/327 or any other similar URL will match this route. The {limit} placeholder parameter will also be passed to the controller so that you can use its value to generate the proper random number.

注解

The routing system has many more great features for creating flexible and powerful URL structures in your application. For more details, see the chapter all about Routing.

Step 2: Create the Controller

When a URL such as /random/10 is handled by the application, the random route is matched and the AcmeDemoBundle:Random:index controller is executed by the framework. The second step of the page-creation process is to create that controller.

The controller - AcmeDemoBundle:Random:index is the logical name of the controller, and it maps to the indexAction method of a PHP class called Acme\DemoBundle\Controller\RandomController. Start by creating this file inside your AcmeDemoBundle:

// src/Acme/DemoBundle/Controller/RandomController.php
namespace Acme\DemoBundle\Controller;

class RandomController
{
}

In reality, the controller is nothing more than a PHP method that you create and Symfony executes. This is where your code uses information from the request to build and prepare the resource being requested. Except in some advanced cases, the end product of a controller is always the same: a Symfony Response object.

Create the indexAction method that Symfony will execute when the random route is matched:

// src/Acme/DemoBundle/Controller/RandomController.php
namespace Acme\DemoBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class RandomController
{
    public function indexAction($limit)
    {
        return new Response(
            '<html><body>Number: '.rand(1, $limit).'</body></html>'
        );
    }
}

The controller is simple: it creates a new Response object, whose first argument is the content that should be used in the response (a small HTML page in this example).

Congratulations! After creating only a route and a controller, you already have a fully-functional page! If you’ve setup everything correctly, your application should generate a random number for you:

http://localhost/app_dev.php/random/10

小技巧

You can also view your app in the “prod” environment by visiting:

http://localhost/app.php/random/10

If you get an error, it’s likely because you need to clear your cache by running:

$ php app/console cache:clear --env=prod --no-debug

An optional, but common, third step in the process is to create a template.

注解

Controllers are the main entry point for your code and a key ingredient when creating pages. Much more information can be found in the Controller Chapter.

Optional Step 3: Create the Template

Templates allow you to move all the presentation code (e.g. HTML) into a separate file and reuse different portions of the page layout. Instead of writing the HTML inside the controller, render a template instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Acme/DemoBundle/Controller/RandomController.php
namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class RandomController extends Controller
{
    public function indexAction($limit)
    {
        $number = rand(1, $limit);

        return $this->render(
            'AcmeDemoBundle:Random:index.html.twig',
            array('number' => $number)
        );

        // render a PHP template instead
        // return $this->render(
        //     'AcmeDemoBundle:Random:index.html.php',
        //     array('number' => $number)
        // );
    }
}

注解

In order to use the render() method, your controller must extend the Controller class, which adds shortcuts for tasks that are common inside controllers. This is done in the above example by adding the use statement on line 4 and then extending Controller on line 6.

The render() method creates a Response object filled with the content of the given, rendered template. Like any other controller, you will ultimately return that Response object.

Notice that there are two different examples for rendering the template. By default, Symfony supports two different templating languages: classic PHP templates and the succinct but powerful Twig templates. Don’t be alarmed - you’re free to choose either or even both in the same project.

The controller renders the AcmeDemoBundle:Random:index.html.twig template, which uses the following naming convention:

BundleName:ControllerName:TemplateName

This is the logical name of the template, which is mapped to a physical location using the following convention.

/path/to/BundleName/Resources/views/ControllerName/TemplateName

In this case, AcmeDemoBundle is the bundle name, Random is the controller, and index.html.twig the template:

  • Twig
    1
    2
    3
    4
    5
    6
     {# src/Acme/DemoBundle/Resources/views/Random/index.html.twig #}
     {% extends '::base.html.twig' %}
    
     {% block body %}
         Number: {{ number }}
     {% endblock %}
    
  • PHP
    <!-- src/Acme/DemoBundle/Resources/views/Random/index.html.php -->
    <?php $view->extend('::base.html.php') ?>
    
    Number: <?php echo $view->escape($number) ?>
    

Step through the Twig template line-by-line:

  • line 2: The extends token defines a parent template. The template explicitly defines a layout file inside of which it will be placed.
  • line 4: The block token says that everything inside should be placed inside a block called body. As you’ll see, it’s the responsibility of the parent template (base.html.twig) to ultimately render the block called body.

The parent template, ::base.html.twig, is missing both the BundleName and ControllerName portions of its name (hence the double colon (::) at the beginning). This means that the template lives outside of the bundle and in the app directory:

  • Twig
    {# app/Resources/views/base.html.twig #}
    <!DOCTYPE html>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <title>{% block title %}Welcome!{% endblock %}</title>
            {% block stylesheets %}{% endblock %}
            <link rel="shortcut icon" href="{{ asset('favicon.ico') }}" />
        </head>
        <body>
            {% block body %}{% endblock %}
            {% block javascripts %}{% endblock %}
        </body>
    </html>
    
  • PHP
    <!-- app/Resources/views/base.html.php -->
    <!DOCTYPE html>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <title><?php $view['slots']->output('title', 'Welcome!') ?></title>
            <?php $view['slots']->output('stylesheets') ?>
            <link rel="shortcut icon"
                href="<?php echo $view['assets']->getUrl('favicon.ico') ?>" />
        </head>
        <body>
            <?php $view['slots']->output('_content') ?>
            <?php $view['slots']->output('javascripts') ?>
        </body>
    </html>
    

The base template file defines the HTML layout and renders the body block that you defined in the index.html.twig template. It also renders a title block, which you could choose to define in the index.html.twig template. Since you did not define the title block in the child template, it defaults to “Welcome!”.

Templates are a powerful way to render and organize the content for your page. A template can render anything, from HTML markup, to CSS code, or anything else that the controller may need to return.

In the lifecycle of handling a request, the templating engine is simply an optional tool. Recall that the goal of each controller is to return a Response object. Templates are a powerful, but optional, tool for creating the content for that Response object.

The Directory Structure

After just a few short sections, you already understand the philosophy behind creating and rendering pages in Symfony. You’ve also already begun to see how Symfony projects are structured and organized. By the end of this section, you’ll know where to find and put different types of files and why.

Though entirely flexible, by default, each Symfony application has the same basic and recommended directory structure:

app/
This directory contains the application configuration.
src/
All the project PHP code is stored under this directory.
vendor/
Any vendor libraries are placed here by convention.
web/
This is the web root directory and contains any publicly accessible files.

参见

You can easily override the default directory structure. See How to Override Symfony’s default Directory Structure for more information.

The Web Directory

The web root directory is the home of all public and static files including images, stylesheets, and JavaScript files. It is also where each front controller lives:

// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();

The front controller file (app.php in this example) is the actual PHP file that’s executed when using a Symfony application and its job is to use a Kernel class, AppKernel, to bootstrap the application.

小技巧

Having a front controller means different and more flexible URLs than are used in a typical flat PHP application. When using a front controller, URLs are formatted in the following way:

http://localhost/app.php/random/10

The front controller, app.php, is executed and the “internal:” URL /random/10 is routed internally using the routing configuration. By using Apache mod_rewrite rules, you can force the app.php file to be executed without needing to specify it in the URL:

http://localhost/random/10

Though front controllers are essential in handling every request, you’ll rarely need to modify or even think about them. They’ll be mentioned again briefly in the Environments section.

The Application (app) Directory

As you saw in the front controller, the AppKernel class is the main entry point of the application and is responsible for all configuration. As such, it is stored in the app/ directory.

This class must implement two methods that define everything that Symfony needs to know about your application. You don’t even need to worry about these methods when starting - Symfony fills them in for you with sensible defaults.

registerBundles()
Returns an array of all bundles needed to run the application (see The Bundle System).
registerContainerConfiguration()
Loads the main application configuration resource file (see the Application Configuration section).

In day-to-day development, you’ll mostly use the app/ directory to modify configuration and routing files in the app/config/ directory (see Application Configuration). It also contains the application cache directory (app/cache), a log directory (app/logs) and a directory for application-level resource files, such as templates (app/Resources). You’ll learn more about each of these directories in later chapters.

The Source (src) Directory

Put simply, the src/ directory contains all the actual code (PHP code, templates, configuration files, stylesheets, etc) that drives your application. When developing, the vast majority of your work will be done inside one or more bundles that you create in this directory.

But what exactly is a bundle?

The Bundle System

A bundle is similar to a plugin in other software, but even better. The key difference is that everything is a bundle in Symfony, including both the core framework functionality and the code written for your application. Bundles are first-class citizens in Symfony. This gives you the flexibility to use pre-built features packaged in third-party bundles or to distribute your own bundles. It makes it easy to pick and choose which features to enable in your application and to optimize them the way you want.

注解

While you’ll learn the basics here, an entire cookbook entry is devoted to the organization and best practices of bundles.

A bundle is simply a structured set of files within a directory that implement a single feature. You might create a BlogBundle, a ForumBundle or a bundle for user management (many of these exist already as open source bundles). Each directory contains everything related to that feature, including PHP files, templates, stylesheets, JavaScripts, tests and anything else. Every aspect of a feature exists in a bundle and every feature lives in a bundle.

An application is made up of bundles as defined in the registerBundles() method of the AppKernel class:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Symfony\Bundle\SecurityBundle\SecurityBundle(),
        new Symfony\Bundle\TwigBundle\TwigBundle(),
        new Symfony\Bundle\MonologBundle\MonologBundle(),
        new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
        new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
        new Symfony\Bundle\AsseticBundle\AsseticBundle(),
        new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
    );

    if (in_array($this->getEnvironment(), array('dev', 'test'))) {
        $bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
        $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
        $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
    }

    return $bundles;
}

With the registerBundles() method, you have total control over which bundles are used by your application (including the core Symfony bundles).

小技巧

A bundle can live anywhere as long as it can be autoloaded (via the autoloader configured at app/autoload.php).

Creating a Bundle

The Symfony Standard Edition comes with a handy task that creates a fully-functional bundle for you. Of course, creating a bundle by hand is pretty easy as well.

To show you how simple the bundle system is, create a new bundle called AcmeTestBundle and enable it.

小技巧

The Acme portion is just a dummy name that should be replaced by some “vendor” name that represents you or your organization (e.g. ABCTestBundle for some company named ABC).

Start by creating a src/Acme/TestBundle/ directory and adding a new file called AcmeTestBundle.php:

// src/Acme/TestBundle/AcmeTestBundle.php
namespace Acme\TestBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeTestBundle extends Bundle
{
}

小技巧

The name AcmeTestBundle follows the standard Bundle naming conventions. You could also choose to shorten the name of the bundle to simply TestBundle by naming this class TestBundle (and naming the file TestBundle.php).

This empty class is the only piece you need to create the new bundle. Though commonly empty, this class is powerful and can be used to customize the behavior of the bundle.

Now that you’ve created the bundle, enable it via the AppKernel class:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        // ...
        // register your bundle
        new Acme\TestBundle\AcmeTestBundle(),
    );
    // ...

    return $bundles;
}

And while it doesn’t do anything yet, AcmeTestBundle is now ready to be used.

And as easy as this is, Symfony also provides a command-line interface for generating a basic bundle skeleton:

$ php app/console generate:bundle --namespace=Acme/TestBundle

The bundle skeleton generates with a basic controller, template and routing resource that can be customized. You’ll learn more about Symfony’s command-line tools later.

小技巧

Whenever creating a new bundle or using a third-party bundle, always make sure the bundle has been enabled in registerBundles(). When using the generate:bundle command, this is done for you.

Bundle Directory Structure

The directory structure of a bundle is simple and flexible. By default, the bundle system follows a set of conventions that help to keep code consistent between all Symfony bundles. Take a look at AcmeDemoBundle, as it contains some of the most common elements of a bundle:

Controller/
Contains the controllers of the bundle (e.g. RandomController.php).
DependencyInjection/
Holds certain dependency injection extension classes, which may import service configuration, register compiler passes or more (this directory is not necessary).
Resources/config/
Houses configuration, including routing configuration (e.g. routing.yml).
Resources/views/
Holds templates organized by controller name (e.g. Hello/index.html.twig).
Resources/public/
Contains web assets (images, stylesheets, etc) and is copied or symbolically linked into the project web/ directory via the assets:install console command.
Tests/
Holds all tests for the bundle.

A bundle can be as small or large as the feature it implements. It contains only the files you need and nothing else.

As you move through the book, you’ll learn how to persist objects to a database, create and validate forms, create translations for your application, write tests and much more. Each of these has their own place and role within the bundle.

Application Configuration

An application consists of a collection of bundles representing all the features and capabilities of your application. Each bundle can be customized via configuration files written in YAML, XML or PHP. By default, the main configuration file lives in the app/config/ directory and is called either config.yml, config.xml or config.php depending on which format you prefer:

  • YAML
    # app/config/config.yml
    imports:
        - { resource: parameters.yml }
        - { resource: security.yml }
    
    framework:
        secret:          "%secret%"
        router:          { resource: "%kernel.root_dir%/config/routing.yml" }
        # ...
    
    # Twig Configuration
    twig:
        debug:            "%kernel.debug%"
        strict_variables: "%kernel.debug%"
    
    # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xmlns:twig="http://symfony.com/schema/dic/twig"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd
            http://symfony.com/schema/dic/twig
            http://symfony.com/schema/dic/twig/twig-1.0.xsd">
    
        <imports>
            <import resource="parameters.yml" />
            <import resource="security.yml" />
        </imports>
    
        <framework:config secret="%secret%">
            <framework:router resource="%kernel.root_dir%/config/routing.xml" />
            <!-- ... -->
        </framework:config>
    
        <!-- Twig Configuration -->
        <twig:config debug="%kernel.debug%" strict-variables="%kernel.debug%" />
    
        <!-- ... -->
    </container>
    
  • PHP
    // app/config/config.php
    $this->import('parameters.yml');
    $this->import('security.yml');
    
    $container->loadFromExtension('framework', array(
        'secret' => '%secret%',
        'router' => array(
            'resource' => '%kernel.root_dir%/config/routing.php',
        ),
        // ...
    ));
    
    // Twig Configuration
    $container->loadFromExtension('twig', array(
        'debug'            => '%kernel.debug%',
        'strict_variables' => '%kernel.debug%',
    ));
    
    // ...
    

注解

You’ll learn exactly how to load each file/format in the next section Environments.

Each top-level entry like framework or twig defines the configuration for a particular bundle. For example, the framework key defines the configuration for the core Symfony FrameworkBundle and includes configuration for the routing, templating, and other core systems.

For now, don’t worry about the specific configuration options in each section. The configuration file ships with sensible defaults. As you read more and explore each part of Symfony, you’ll learn about the specific configuration options of each feature.

Default Configuration Dump

You can dump the default configuration for a bundle in YAML to the console using the config:dump-reference command. Here is an example of dumping the default FrameworkBundle configuration:

$ app/console config:dump-reference FrameworkBundle

The extension alias (configuration key) can also be used:

$ app/console config:dump-reference framework

注解

See the cookbook article: How to Load Service Configuration inside a Bundle for information on adding configuration for your own bundle.

Environments

An application can run in various environments. The different environments share the same PHP code (apart from the front controller), but use different configuration. For instance, a dev environment will log warnings and errors, while a prod environment will only log errors. Some files are rebuilt on each request in the dev environment (for the developer’s convenience), but cached in the prod environment. All environments live together on the same machine and execute the same application.

A Symfony project generally begins with three environments (dev, test and prod), though creating new environments is easy. You can view your application in different environments simply by changing the front controller in your browser. To see the application in the dev environment, access the application via the development front controller:

http://localhost/app_dev.php/random/10

If you’d like to see how your application will behave in the production environment, call the prod front controller instead:

http://localhost/app.php/random/10

Since the prod environment is optimized for speed; the configuration, routing and Twig templates are compiled into flat PHP classes and cached. When viewing changes in the prod environment, you’ll need to clear these cached files and allow them to rebuild:

$ php app/console cache:clear --env=prod --no-debug

注解

If you open the web/app.php file, you’ll find that it’s configured explicitly to use the prod environment:

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

You can create a new front controller for a new environment by copying this file and changing prod to some other value.

注解

The test environment is used when running automated tests and cannot be accessed directly through the browser. See the testing chapter for more details.

Environment Configuration

The AppKernel class is responsible for actually loading the configuration file of your choice:

// app/AppKernel.php
public function registerContainerConfiguration(LoaderInterface $loader)
{
    $loader->load(
        __DIR__.'/config/config_'.$this->getEnvironment().'.yml'
    );
}

You already know that the .yml extension can be changed to .xml or .php if you prefer to use either XML or PHP to write your configuration. Notice also that each environment loads its own configuration file. Consider the configuration file for the dev environment.

  • YAML
    # app/config/config_dev.yml
    imports:
        - { resource: config.yml }
    
    framework:
        router:   { resource: "%kernel.root_dir%/config/routing_dev.yml" }
        profiler: { only_exceptions: false }
    
    # ...
    
  • XML
    <!-- app/config/config_dev.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <imports>
            <import resource="config.xml" />
        </imports>
    
        <framework:config>
            <framework:router resource="%kernel.root_dir%/config/routing_dev.xml" />
            <framework:profiler only-exceptions="false" />
        </framework:config>
    
        <!-- ... -->
    </container>
    
  • PHP
    // app/config/config_dev.php
    $loader->import('config.php');
    
    $container->loadFromExtension('framework', array(
        'router' => array(
            'resource' => '%kernel.root_dir%/config/routing_dev.php',
        ),
        'profiler' => array('only-exceptions' => false),
    ));
    
    // ...
    

The imports key is similar to a PHP include statement and guarantees that the main configuration file (config.yml) is loaded first. The rest of the file tweaks the default configuration for increased logging and other settings conducive to a development environment.

Both the prod and test environments follow the same model: each environment imports the base configuration file and then modifies its configuration values to fit the needs of the specific environment. This is just a convention, but one that allows you to reuse most of your configuration and customize just pieces of it between environments.

Summary

Congratulations! You’ve now seen every fundamental aspect of Symfony and have hopefully discovered how easy and flexible it can be. And while there are a lot of features still to come, be sure to keep the following basic points in mind:

  • Creating a page is a three-step process involving a route, a controller and (optionally) a template;
  • Each project contains just a few main directories: web/ (web assets and the front controllers), app/ (configuration), src/ (your bundles), and vendor/ (third-party code) (there’s also a bin/ directory that’s used to help updated vendor libraries);
  • Each feature in Symfony (including the Symfony framework core) is organized into a bundle, which is a structured set of files for that feature;
  • The configuration for each bundle lives in the Resources/config directory of the bundle and can be specified in YAML, XML or PHP;
  • The global application configuration lives in the app/config directory;
  • Each environment is accessible via a different front controller (e.g. app.php and app_dev.php) and loads a different configuration file.

From here, each chapter will introduce you to more and more powerful tools and advanced concepts. The more you know about Symfony, the more you’ll appreciate the flexibility of its architecture and the power it gives you to rapidly develop applications.

控制器(Controller)

控制器是一段可以被调用的 PHP 代码,它从 HTTP 请求中获取信息,并且相应地构造和返回一个 HTTP 响应(作为 Symfony 的 Response 对象)。 响应可以是一个 HTML 页面,可以是一个 XML 文件,或是一个序列化了的 JSON 数组,或是一张图片,也可以是一个重定向甚至一个 404 错误,它可以是你能想到的一切!控制器将包含 你的程序 所需的一切渲染页面内容的逻辑。

通过看这个 Symfony 控制器来了解这一切是多么的简单吧!这是一个渲染著名的 Hello world! 页面的控制器:

use Symfony\Component\HttpFoundation\Response;

public function helloAction()
{
    return new Response('Hello world!');
}

控制器的目标永远都是明确的:创建并返回一个 Response 对象。在这个过程中,控制器可能会从请求(Request)中读取一些信息,载入几个数据库资源,发送一封电子邮件,或者在用户会话(Session)中写入一些东西。但不论在哪一种情况下,控制器最终都要返回将要发回客户端的 Response 对象。

这里面没有魔法,也没有需要你担心的其他需求~这儿有一些简单的例子:

  • 控制器 A 要创建一个 Response 对象来展现 网站主页的内容。
  • 控制器 B 从用户请求中读取 slug 参数来从数据库中加载 博客的条目然后创建一个 Response 对象来把博客的内容显示 出来。如果数据库中没有 slug ,控制器就创建一个 包含 404 状态码的 Response 对象并把它发送回客户端。
  • 控制器 C 来处理一个联系表格的表单子任务。它从 用户请求中读取信息,存储在 数据库中并给你发送一封包含联系信息的电子邮件。最后,它创建 一个 Response 对象来把用户的浏览器重定向到 表单的“谢谢您”页面。
请求(Requests)、控制器(Controller)、响应(Response)生命周期

每一个 Symfony 项目处理的请求都经过这个简单的生命周期。框架将接管所有重复的活动,这意味着你只需要把你自己的特有的代码写入控制器的函数即可:

  1. 每个请求都被交给一个单一的前端控制器(Front Controller)处理并引导整个程序(如 app.phpapp_dev.php);
  2. Router(路由器) 从请求中读取信息(比如 URI),并 寻找一条符合这个信息的路由,然后读取路由信息中的 _controller 参数;
  3. 被命中的路由信息中给出的控制器将被执行,控制器中的代码将 创建并返回一个 Response 对象;
  4. Response 对象中的 HTTP 头和内容将被送回 客户端。

创建一个页面简单到只需要创建一个控制器(第三步中用到),再添加一条路由将 URL 映射到控制器上(第二步中用到)。

注解

虽然名字很像,但“前端控制器”和本章讨论的“控制器” 不是同一个东西。前端控制器 在你网站目录下的一个短小的 PHP 文件, 所有的请求都被指向它。典型的程序会有一个生产环境 前端控制器(比如 app.php)和一个开发环境前端控制器。 (比如 app_dev.php)。你基本不需要编辑、浏览或者担心 你的程序中前端控制器的代码。

一个简单的控制器

虽然刚才说到控制器可以是任何一段可以被调用的 PHP 代码(比如一个函数、对象中的方法或者一个 Closure(闭包)),但控制器一般都是控制器类中的一个方法。控制器也被叫做 Action(动作)

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

小技巧

注意这里的 控制器 是在 控制器类*(``HelloController``)的 ``indexAction`` 方法。 别被 *控制器类 这个名字搞糊涂了,这只是一种将几个 控制器(方法)组合在一起的简便方法而已。一般情况下,控制器类 里面会有一些控制器(比如 updateActiondeleteAction 等等)。

这个控制器相当明了:

  • 第4行: Symfony 使用 PHP 5.3 的命名空间这一很方便的功能来 命名整个控制器类。use 关键字将导入 控制器必须返回的 Response 类。
  • 第6行:类名是在你给控制器类起的名字 (比如 Hello)后面加上 Controller 这个单词构成的。这个规范 保持控制器的一致性并且将允许你只使用 类名的第一个部分 (在这里是 Hello)来配置路由。
  • 第8行:控制器类中的每一个动作都要以 Action 结尾 这样你就可以在配置路由时只写动作本身的名字(这里是 index )了。 在下一节你将创建一条路由将 URL 映射到这个动作上。 你也将学到如何把路由中的占位符(这里是``{name}``)变成 动作的方法的参数(这里是``$name``)。
  • 第10行:控制器创建并返回一个 Response 对象。
将 URL 映射到控制器上

这个新控制器返回一个简单的 HTML 页面。要想真正地访问这个页面,你需要创建一条将指定 URL 路径映射到对应控制器的路由:

  • Annotations
    // src/AppBundle/Controller/HelloController.php
    namespace AppBundle\Controller;
    
    use Symfony\Component\HttpFoundation\Response;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    class HelloController
    {
        /**
         * @Route("/hello/{name}", name="hello")
         */
        public function indexAction($name)
        {
            return new Response('<html><body>Hello '.$name.'!</body></html>');
        }
    }
    
  • YAML
    # app/config/routing.yml
    hello:
        path:      /hello/{name}
        # 使用这种特定的表达式来指向控制器 - 参阅下面的注解
        defaults:  { _controller: AppBundle:Hello:index }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="hello" path="/hello/{name}">
            <!-- 使用这种特定的表达式来指向控制器 - 参阅下面的注解 -->
            <default key="_controller">AppBundle:Hello:index</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\Route;
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->add('hello', new Route('/hello/{name}', array(
        // 使用这种特定的表达式来指向控制器 - 参阅下面的注解
        '_controller' => 'AppBundle:Hello:index',
    )));
    
    return $collection;
    

好了,现在如果你访问 /hello/ryan (比如在你使用:doc:built-in web server </cookbook/web_server/built_in> 链接就是 http://localhost:8000/app_dev.php/hello/ryan)时, Symfony 就会执行 HelloController::indexAction() 控制器并将 ryan 传入作为``$name`` 变量的值。创建“页面”的意思只是简单地创建一个控制器的方法和对应的路由。

简单吧?

参见

你可以从 Routing chapter 更详细地学习路由系统。

作为控制器参数的路由占位符

你已经知道了路由指向了 AppBundle 中的 HelloController::indexAction() 方法。更有趣的东西是传入那个方法的参数:

// src/AppBundle/Controller/HelloController.php
// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/hello/{name}", name="hello")
 */
public function indexAction($name)
{
    // ...
}

控制器有一个与被命中的路由信息中的 {name} 占位符对应的参数 $name``(如果你访问 ``/hello/ryan 就是 ryan)。当你的控制器被执行时,Symfony 会将控制器的参数与路由占位符一一对应。所以 {name} 的值将被传递给 $name

看一下这个更有趣的例子吧:

  • Annotations
    // src/AppBundle/Controller/HelloController.php
    // ...
    
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    class HelloController
    {
        /**
         * @Route("/hello/{firstName}/{lastName}", name="hello")
         */
        public function indexAction($firstName, $lastName)
        {
            // ...
        }
    }
    
  • YAML
    # app/config/routing.yml
    hello:
        path:      /hello/{firstName}/{lastName}
        defaults:  { _controller: AppBundle:Hello:index }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="hello" path="/hello/{firstName}/{lastName}">
            <default key="_controller">AppBundle:Hello:index</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\Route;
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->add('hello', new Route('/hello/{firstName}/{lastName}', array(
        '_controller' => 'AppBundle:Hello:index',
    )));
    
    return $collection;
    

现在,控制器可以有两个参数了:

public function indexAction($firstName, $lastName)
{
    // ...
}

将路由占位符映射到控制器参数是简单且灵活的。在开发时请记住以下几条准则。.

  • 控制器参数与顺序无关

    Symfony 使用路由占位符的 名字 和控制器参数的 名字 来进行映射。控制器参数可以被完全 重新排序而且仍然可以完美运行:

    public function indexAction($lastName, $firstName)
    {
        // ...
    }
    
  • 控制器需要的所有参数都必须有一个路由占位符与之对应

    下面的代码将抛出一个 RuntimeException(运行时异常) 因为在路由中 foo 这个占位符没有被定义:

    public function indexAction($firstName, $lastName, $foo)
    {
        // ...
    }
    

    但是将 foo 这个参数设为可选参数是可行的。下面这个 例子就不会抛出异常:

    public function indexAction($firstName, $lastName, $foo = 'bar')
    {
        // ...
    }
    
  • 并不是所有的路由占位符都需要有一个控制器参数与之对应

    如果假设 lastName 在你的控制器中并不是那么重要, 你可以完全忽略掉它:

    public function indexAction($firstName)
    {
        // ...
    }
    

小技巧

每一个路由也都有一个特殊的 _route 占位符,它等同于 被命中的路由的名字(比如在这里是 hello)。虽然并不经常 用到,,它同样可以被用于一个控制器参数。你也可以 你也可以将其他来自你的路由的变量传入控制器。参阅 How to Pass Extra Information from a Route to a Controller.

Request 作为控制器参数

假设你需要读取一个查询参数,抓取一个请求头,或者访问一个被上传上来的文件。所有的这些信息都被存储到了 Symfony 的 Request(请求) 对象中。如果想在你的控制器中使用它,只需要将它添加为参数并 使用Request 类对其进行类型约束(Type-Hint)

use Symfony\Component\HttpFoundation\Request;

public function indexAction($firstName, $lastName, Request $request)
{
    $page = $request->query->get('page', 1);

    // ...
}

参见

想学习关于从请求中获取信息的更多?参阅 Access Request Information.

控制器基类

为了更加方便,Symfony 提供了一个 Controller 基类。如果你将其继承,你就可以访问很多的帮手方法,也可以通过容器来访问你的服务(参阅 访问其他服务)。

use 的声明放在 Controller 类的上面,然后修改一下 HelloController 去继承基类:

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
    // ...
}

这并不会实际地修改你控制器工作的任何部分:它只是可以让你访问基类提供的帮手方法。这只是一些使用 Symfony 核心功能的快捷方法,这些核心功能无论你是否使用 Controller 基类都可用。查看正在运作的核心功能的方法就是看看 Controller class

参见

如果你很好奇控制器在 继承 这个基类时如何工作,请参阅 Controllers as Services。 这是可选的,但可以让你更精确地控制注入到你控制器中的 类或者依赖。

重定向

如果你想将用户重定向到另一个页面,请使用 redirect() 方法

public function indexAction()
{
    return $this->redirect($this->generateUrl('homepage'));
}

上面的 generateUrl() 方法只是一个生成给定路由的 URL 的帮手方法。获取更多信息,请参阅 Routing 章节。

在默认情况下, redirect() 方法生成的是 302(暂时)重定向。要想生成 301(永久)重定向,请修改第二个参数

public function indexAction()
{
    return $this->redirect($this->generateUrl('homepage'), 301);
}

小技巧

上面提到的 redirect() 方法只是一个创建专门重定向用户的 Response 类的快捷方式。它等价于:

use Symfony\Component\HttpFoundation\RedirectResponse;

return new RedirectResponse($this->generateUrl('homepage'));
渲染模板

如果你要使用 HTML,你就一定要渲染模板。一个叫做 render() 的方法会渲染一个模板 并且 为你把内容放入 Response 类中:

// 渲染 app/Resources/views/Hello/index.html.twig
return $this->render('Hello/index.html.twig', array('name' => $name));

你也可以将模板文件放入更深的子文件夹中。但还是要避免创建不必要的更深的结构:

// 渲染 app/Resources/views/Hello/Greetings/index.html.twig
return $this->render('Hello/Greetings/index.html.twig', array('name' => $name));

Templating 一章详细讲解了 Symfony 模板引擎。

访问其他服务

Symfony 打包了很多有用的类,它们被称为服务。这些服务被用来渲染模板、发送邮件、查询数据库,也可以用来做一些你想让它们“做”的工作。当你安装新的包时,它可能会引入 更多的 服务。

当你继承了控制器基类时,你就可以通过 get() 方法来访问任何的 Symfony 服务。这里有一些你可能会用到的基本服务:

$templating = $this->get('templating');

$router = $this->get('router');

$mailer = $this->get('mailer');

那么别的服务在哪儿呢?你可以用 container:debug 这个控制台命令列出所有的服务:

$ php app/console container:debug

更多信息,请参阅 Service Container 一章。

管理错误和 404 页面

当没有找到一些东西事,你应该用好 HTTP 协议并返回一个 404 响应。为了达到目的,你可以抛出一个特殊的异常。如果你继承了控制器基类,按照下面的来做:

public function indexAction()
{
    // 从数据库中检索目标
    $product = ...;
    if (!$product) {
        throw $this->createNotFoundException('产品不存在');
    }

    return $this->render(...);
}

上面用到的 createNotFoundException() 方法只是一个创建特殊的 NotFoundHttpException 对象(一个创建 HTTP 404 响应的 Symfony 类)的快捷方式。

当然,你可以自由的从你的控制器中抛出任何 Exception(异常) 类——Symfony 将会自动的生成 HTTP 500(内部服务器错误)响应。

throw new \Exception('出错了!');

任何情况下,最终用户看到的都是错误页面,开发者看到的都是完整的调试信息 (例如当你使用 app_dev.php 时——参阅 Environments & Front Controllers)。

你一定想自定义终端用户看到的错误页面。为达到目的,请参阅技巧书中的 “How to Customize Error Pages” 这一技巧。

管理会话

Symfony 提供一个很好用的会话类,你可以用它在请求间存储用户(可以是一个使用浏览器的真实的人类,或是一个蜘蛛机器人,或是一个网络服务)的信息。默认情况下,Symfony 使用 PHP 原生的会话管理工具将这些信息储存在 Cookie 中。

不管在哪个控制器中,向会话写入信息和从会话中读取信息都可以轻易实现:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $session = $request->getSession();

    // 存储一个在处理用户之后的请求时会用到的属性
    $session->set('foo', 'bar');

    // 获取在别的会话中别的控制器设置的属性
    $foobar = $session->get('foobar');

    // 在属性不存在时使用一个默认值
    $filters = $session->get('filters', array());
}

这些属性将持续到用户其余的请求中。

闪电消息

你也可以向用户会话存储一条只在紧接着的下一个请求中可用的短消息。这在处理表格时很有用:你想将用户重定向并在 下一个 页面中显示一条特定的消息。这种消息被称为“闪电”消息。

设想你正在处理一个提交上来的表格:

use Symfony\Component\HttpFoundation\Request;

public function updateAction(Request $request)
{
    $form = $this->createForm(...);

    $form->handleRequest($request);

    if ($form->isValid()) {
        // 做一些处理

        $request->getSession()->getFlashBag()->add(
            'notice',
            '更改已保存!'
        );

        return $this->redirect($this->generateUrl(...));
    }

    return $this->render(...);
}

处理完请求后,控制器在会话中设置了一个叫做 notice 的闪电消息并重定向。名字(上面的例子里是``notice``)并没有特殊的意义,只是个你起的名字,方便你在下一步中使用它。

在下一个页面中的模板里(更聪明的方法是写入主模板框架),下面的代码将渲染 notice 这个消息。

  • Twig
    {% for flashMessage in app.session.flashbag.get('notice') %}
        <div class="flash-notice">
            {{ flashMessage }}
        </div>
    {% endfor %}
    
  • PHP
    <?php foreach ($view['session']->getFlash('notice') as $message): ?>
        <div class="flash-notice">
            <?php echo "<div class='flash-error'>$message</div>" ?>
        </div>
    <?php endforeach ?>
    

闪电消息被专门设计为只能在紧接着的请求中使用(它们像闪电一样转瞬即逝)。像刚才这样在重定向时传递消息就可以用到闪电消息。

Response(响应)对象

对控制器的要求只有一个:返回一个 Response 对象。Symfony 中的 Response 类是对 HTTP 响应的抽象:响应头和内容被填入基于文本的消息中发回客户端:

use Symfony\Component\HttpFoundation\Response;

// 创建一个有 200 状态码(默认)的简单响应
$response = new Response('Hello '.$name, 200);

// 创建一个有 200 状态码(默认)的 JSON 响应
$response = new Response(json_encode(array('name' => $name)));
$response->headers->set('Content-Type', 'application/json');

上面的 headers 属性是一个 HeaderBag 类,它有一些很棒的读写响应头的方法。响应头的名字是标准化了的,所以用 Content-Type 等价于 content-type 或者 content_type

也有一些可以简单快速地创建其他类型的响应的类。

参见

别担心!在足见的文档里还有很多关于响应对象的 信息。参阅 Response

请求(Request)对象

除了来自路由占位符的值,控制器还可以访问 Request(请求) 对象。如果一个变量被使用 Request 进行类型约束,框架就会将 请求 对象注入控制器中:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $request->isXmlHttpRequest(); // 是一个Ajax请求吗?

    $request->getPreferredLanguage(array('en', 'fr'));

    $request->query->get('page'); // 获取一个 $_GET 的参数

    $request->request->get('page'); // 获取一个 $_POST 的参数
}

就像 响应 对象一样,请求头被存储在 HeaderBag(请求头包) 对象中,访问起来很容易。

参见

别担心!在足见的文档里还有很多关于请求对象的 信息。参阅 Request

创建静态页面

你也可以创建一个不需要控制器的静态页面(只需要路由和模板)。

参阅 How to Render a Template without a custom Controller

重定向到另一个控制器

虽然不是很常用,但你还是可以使用 forward() 这一方法来在内部重定向到别的控制器。这样做并不会重定向用户的浏览器,而会建立一个内部子请求并调用对应的控制器。刚才提到的 forward() 方法会返回一个来自 那个(重定向到的) 控制器的 Response 对象:

public function indexAction($name)
{
    $response = $this->forward('AppBundle:Something:fancy', array(
        'name'  => $name,
        'color' => 'green',
    ));

    // ... 做一些别的更改或者直接返回它

    return $response;
}

请注意 forward() 方法使用一种特殊的控制器定位表达式(参阅 Controller Naming Pattern)。在这个例子中,目标控制器是 AppBundle 中的 SomethingController::fancyAction() 控制器。作为方法的参数的数组将会被作为控制器参数传入目标控制器。在将控制器嵌入模板时也会用到这一方法(参阅 Embedding Controllers)。目标控制器可以像下面这样工作:

public function fancyAction($name, $color)
{
    // ... 创建并返回一个 Response 对象
}

就像在给路由创建控制器时那样, fancyAction 的参数的顺序并不影响运行。Symfony 会将数组的键名(比如 name)与控制器方法的参数名(比如 $name)对应起来。如果你更改了参数的顺序,Symfony 还是会将正确的值传递给各个变量。

结语

不论在什么时候,当你创建一个页面时,你最终都需要写一些包括这个页面的逻辑的代码。在 Symfony 里,这被称为控制器,并且它是一个可以为了返回最终会被返回给用户的 Response 对象而做任何事的 PHP 函数。

简单起见,你可以选择继承 Controller 基类,它包含了很多控制器要做的基本的事情的快捷方式。比如,因为你不想在控制器里写 HTML 代码,你就可以用 render() 方法来从模板中渲染内容并返回。

在别的章节中,你将学到控制器如何将对象持久化到数据库中或从数据库中获取对象、在子任务中处理、处理缓存还有更多更多。

Routing

Beautiful URLs are an absolute must for any serious web application. This means leaving behind ugly URLs like index.php?article_id=57 in favor of something like /read/intro-to-symfony.

Having flexibility is even more important. What if you need to change the URL of a page from /blog to /news? How many links should you need to hunt down and update to make the change? If you’re using Symfony’s router, the change is simple.

The Symfony router lets you define creative URLs that you map to different areas of your application. By the end of this chapter, you’ll be able to:

  • Create complex routes that map to controllers
  • Generate URLs inside templates and controllers
  • Load routing resources from bundles (or anywhere else)
  • Debug your routes
Routing in Action

A route is a map from a URL path to a controller. For example, suppose you want to match any URL like /blog/my-post or /blog/all-about-symfony and send it to a controller that can look up and render that blog entry. The route is simple:

  • Annotations
    // src/AppBundle/Controller/BlogController.php
    namespace AppBundle\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    class BlogController extends Controller
    {
        /**
         * @Route("/blog/{slug}")
         */
        public function showAction($slug)
        {
            // ...
        }
    }
    
  • YAML
    # app/config/routing.yml
    blog_show:
        path:      /blog/{slug}
        defaults:  { _controller: AppBundle:Blog:show }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog_show" path="/blog/{slug}">
            <default key="_controller">AppBundle:Blog:show</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog_show', new Route('/blog/{slug}', array(
        '_controller' => 'AppBundle:Blog:show',
    )));
    
    return $collection;
    

2.2 新版功能: The path option was introduced in Symfony 2.2, pattern is used in older versions.

The path defined by the blog_show route acts like /blog/* where the wildcard is given the name slug. For the URL /blog/my-blog-post, the slug variable gets a value of my-blog-post, which is available for you to use in your controller (keep reading). The blog_show is the internal name of the route, which doesn’t have any meaning yet and just needs to be unique. Later, you’ll use it to generate URLs.

If you don’t want to use annotations, because you don’t like them or because you don’t want to depend on the SensioFrameworkExtraBundle, you can also use Yaml, XML or PHP. In these formats, the _controller parameter is a special key that tells Symfony which controller should be executed when a URL matches this route. The _controller string is called the logical name. It follows a pattern that points to a specific PHP class and method, in this case the AppBundle\Controller\BlogController::showAction method.

Congratulations! You’ve just created your first route and connected it to a controller. Now, when you visit /blog/my-post, the showAction controller will be executed and the $slug variable will be equal to my-post.

This is the goal of the Symfony router: to map the URL of a request to a controller. Along the way, you’ll learn all sorts of tricks that make mapping even the most complex URLs easy.

Routing: Under the Hood

When a request is made to your application, it contains an address to the exact “resource” that the client is requesting. This address is called the URL, (or URI), and could be /contact, /blog/read-me, or anything else. Take the following HTTP request for example:

GET /blog/my-blog-post

The goal of the Symfony routing system is to parse this URL and determine which controller should be executed. The whole process looks like this:

  1. The request is handled by the Symfony front controller (e.g. app.php);
  2. The Symfony core (i.e. Kernel) asks the router to inspect the request;
  3. The router matches the incoming URL to a specific route and returns information about the route, including the controller that should be executed;
  4. The Symfony Kernel executes the controller, which ultimately returns a Response object.
Symfony request flow

The routing layer is a tool that translates the incoming URL into a specific controller to execute.

Creating Routes

Symfony loads all the routes for your application from a single routing configuration file. The file is usually app/config/routing.yml, but can be configured to be anything (including an XML or PHP file) via the application configuration file:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        router: { resource: "%kernel.root_dir%/config/routing.yml" }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <!-- ... -->
            <framework:router resource="%kernel.root_dir%/config/routing.xml" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'router' => array(
            'resource' => '%kernel.root_dir%/config/routing.php',
        ),
    ));
    

小技巧

Even though all routes are loaded from a single file, it’s common practice to include additional routing resources. To do so, just point out in the main routing configuration file which external files should be included. See the Including External Routing Resources section for more information.

Basic Route Configuration

Defining a route is easy, and a typical application will have lots of routes. A basic route consists of just two parts: the path to match and a defaults array:

  • Annotations
    // src/AppBundle/Controller/MainController.php
    
    // ...
    class MainController extends Controller
    {
        /**
         * @Route("/")
         */
        public function homepageAction()
        {
            // ...
        }
    }
    
  • YAML
    # app/config/routing.yml
    _welcome:
        path:      /
        defaults:  { _controller: AppBundle:Main:homepage }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="_welcome" path="/">
            <default key="_controller">AppBundle:Main:homepage</default>
        </route>
    
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('_welcome', new Route('/', array(
        '_controller' => 'AppBundle:Main:homepage',
    )));
    
    return $collection;
    

This route matches the homepage (/) and maps it to the AppBundle:Main:homepage controller. The _controller string is translated by Symfony into an actual PHP function and executed. That process will be explained shortly in the Controller Naming Pattern section.

Routing with Placeholders

Of course the routing system supports much more interesting routes. Many routes will contain one or more named “wildcard” placeholders:

  • Annotations
    // src/AppBundle/Controller/BlogController.php
    
    // ...
    class BlogController extends Controller
    {
        /**
         * @Route("/blog/{slug}")
         */
        public function showAction($slug)
        {
            // ...
        }
    }
    
  • YAML
    # app/config/routing.yml
    blog_show:
        path:      /blog/{slug}
        defaults:  { _controller: AppBundle:Blog:show }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog_show" path="/blog/{slug}">
            <default key="_controller">AppBundle:Blog:show</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog_show', new Route('/blog/{slug}', array(
        '_controller' => 'AppBundle:Blog:show',
    )));
    
    return $collection;
    

The path will match anything that looks like /blog/*. Even better, the value matching the {slug} placeholder will be available inside your controller. In other words, if the URL is /blog/hello-world, a $slug variable, with a value of hello-world, will be available in the controller. This can be used, for example, to load the blog post matching that string.

The path will not, however, match simply /blog. That’s because, by default, all placeholders are required. This can be changed by adding a placeholder value to the defaults array.

Required and Optional Placeholders

To make things more exciting, add a new route that displays a list of all the available blog posts for this imaginary blog application:

  • Annotations
    // src/AppBundle/Controller/BlogController.php
    
    // ...
    class BlogController extends Controller
    {
        // ...
    
        /**
         * @Route("/blog")
         */
        public function indexAction()
        {
            // ...
        }
    }
    
  • YAML
    # app/config/routing.yml
    blog:
        path:      /blog
        defaults:  { _controller: AppBundle:Blog:index }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog" path="/blog">
            <default key="_controller">AppBundle:Blog:index</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog', new Route('/blog', array(
        '_controller' => 'AppBundle:Blog:index',
    )));
    
    return $collection;
    

So far, this route is as simple as possible - it contains no placeholders and will only match the exact URL /blog. But what if you need this route to support pagination, where /blog/2 displays the second page of blog entries? Update the route to have a new {page} placeholder:

  • Annotations
    // src/AppBundle/Controller/BlogController.php
    
    // ...
    
    /**
     * @Route("/blog/{page}")
     */
    public function indexAction($page)
    {
        // ...
    }
    
  • YAML
    # app/config/routing.yml
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AppBundle:Blog:index }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog" path="/blog/{page}">
            <default key="_controller">AppBundle:Blog:index</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog', new Route('/blog/{page}', array(
        '_controller' => 'AppBundle:Blog:index',
    )));
    
    return $collection;
    

Like the {slug} placeholder before, the value matching {page} will be available inside your controller. Its value can be used to determine which set of blog posts to display for the given page.

But hold on! Since placeholders are required by default, this route will no longer match on simply /blog. Instead, to see page 1 of the blog, you’d need to use the URL /blog/1! Since that’s no way for a rich web app to behave, modify the route to make the {page} parameter optional. This is done by including it in the defaults collection:

  • Annotations
    // src/AppBundle/Controller/BlogController.php
    
    // ...
    
    /**
     * @Route("/blog/{page}", defaults={"page" = 1})
     */
    public function indexAction($page)
    {
        // ...
    }
    
  • YAML
    # app/config/routing.yml
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AppBundle:Blog:index, page: 1 }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog" path="/blog/{page}">
            <default key="_controller">AppBundle:Blog:index</default>
            <default key="page">1</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog', new Route('/blog/{page}', array(
        '_controller' => 'AppBundle:Blog:index',
        'page'        => 1,
    )));
    
    return $collection;
    

By adding page to the defaults key, the {page} placeholder is no longer required. The URL /blog will match this route and the value of the page parameter will be set to 1. The URL /blog/2 will also match, giving the page parameter a value of 2. Perfect.

URL Route Parameters
/blog blog {page} = 1
/blog/1 blog {page} = 1
/blog/2 blog {page} = 2

警告

Of course, you can have more than one optional placeholder (e.g. /blog/{slug}/{page}), but everything after an optional placeholder must be optional. For example, /{page}/blog is a valid path, but page will always be required (i.e. simply /blog will not match this route).

小技巧

Routes with optional parameters at the end will not match on requests with a trailing slash (i.e. /blog/ will not match, /blog will match).

Adding Requirements

Take a quick look at the routes that have been created so far:

  • Annotations
    // src/AppBundle/Controller/BlogController.php
    
    // ...
    class BlogController extends Controller
    {
        /**
         * @Route("/blog/{page}", defaults={"page" = 1})
         */
        public function indexAction($page)
        {
            // ...
        }
    
        /**
         * @Route("/blog/{slug}")
         */
        public function showAction($slug)
        {
            // ...
        }
    }
    
  • YAML
    # app/config/routing.yml
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AppBundle:Blog:index, page: 1 }
    
    blog_show:
        path:      /blog/{slug}
        defaults:  { _controller: AppBundle:Blog:show }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog" path="/blog/{page}">
            <default key="_controller">AppBundle:Blog:index</default>
            <default key="page">1</default>
        </route>
    
        <route id="blog_show" path="/blog/{slug}">
            <default key="_controller">AppBundle:Blog:show</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog', new Route('/blog/{page}', array(
        '_controller' => 'AppBundle:Blog:index',
        'page'        => 1,
    )));
    
    $collection->add('blog_show', new Route('/blog/{show}', array(
        '_controller' => 'AppBundle:Blog:show',
    )));
    
    return $collection;
    

Can you spot the problem? Notice that both routes have patterns that match URLs that look like /blog/*. The Symfony router will always choose the first matching route it finds. In other words, the blog_show route will never be matched. Instead, a URL like /blog/my-blog-post will match the first route (blog) and return a nonsense value of my-blog-post to the {page} parameter.

URL Route Parameters
/blog/2 blog {page} = 2
/blog/my-blog-post blog {page} = "my-blog-post"

The answer to the problem is to add route requirements. The routes in this example would work perfectly if the /blog/{page} path only matched URLs where the {page} portion is an integer. Fortunately, regular expression requirements can easily be added for each parameter. For example:

  • Annotations
    // src/AppBundle/Controller/BlogController.php
    
    // ...
    
    /**
     * @Route("/blog/{page}", defaults={"page": 1}, requirements={
     *     "page": "\d+"
     * })
     */
    public function indexAction($page)
    {
        // ...
    }
    
  • YAML
    # app/config/routing.yml
    blog:
        path:      /blog/{page}
        defaults:  { _controller: AppBundle:Blog:index, page: 1 }
        requirements:
            page:  \d+
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog" path="/blog/{page}">
            <default key="_controller">AppBundle:Blog:index</default>
            <default key="page">1</default>
            <requirement key="page">\d+</requirement>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog', new Route('/blog/{page}', array(
        '_controller' => 'AppBundle:Blog:index',
        'page'        => 1,
    ), array(
        'page' => '\d+',
    )));
    
    return $collection;
    

The \d+ requirement is a regular expression that says that the value of the {page} parameter must be a digit (i.e. a number). The blog route will still match on a URL like /blog/2 (because 2 is a number), but it will no longer match a URL like /blog/my-blog-post (because my-blog-post is not a number).

As a result, a URL like /blog/my-blog-post will now properly match the blog_show route.

URL Route Parameters
/blog/2 blog {page} = 2
/blog/my-blog-post blog_show {slug} = my-blog-post
/blog/2-my-blog-post blog_show {slug} = 2-my-blog-post

Since the parameter requirements are regular expressions, the complexity and flexibility of each requirement is entirely up to you. Suppose the homepage of your application is available in two different languages, based on the URL:

  • Annotations
    // src/AppBundle/Controller/MainController.php
    
    // ...
    class MainController extends Controller
    {
        /**
         * @Route("/{_locale}", defaults={"_locale": "en"}, requirements={
         *     "_locale": "en|fr"
         * })
         */
        public function homepageAction($_locale)
        {
        }
    }
    
  • YAML
    # app/config/routing.yml
    homepage:
        path:      /{_locale}
        defaults:  { _controller: AppBundle:Main:homepage, _locale: en }
        requirements:
            _locale:  en|fr
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="homepage" path="/{_locale}">
            <default key="_controller">AppBundle:Main:homepage</default>
            <default key="_locale">en</default>
            <requirement key="_locale">en|fr</requirement>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('homepage', new Route('/{_locale}', array(
        '_controller' => 'AppBundle:Main:homepage',
        '_locale'     => 'en',
    ), array(
        '_locale' => 'en|fr',
    )));
    
    return $collection;
    

For incoming requests, the {_locale} portion of the URL is matched against the regular expression (en|fr).

Path Parameters
/ {_locale} = "en"
/en {_locale} = "en"
/fr {_locale} = "fr"
/es won’t match this route
Adding HTTP Method Requirements

In addition to the URL, you can also match on the method of the incoming request (i.e. GET, HEAD, POST, PUT, DELETE). Suppose you have a contact form with two controllers - one for displaying the form (on a GET request) and one for processing the form when it’s submitted (on a POST request). This can be accomplished with the following route configuration:

  • Annotations
    // src/AppBundle/Controller/MainController.php
    namespace AppBundle\Controller;
    
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
    // ...
    
    class MainController extends Controller
    {
        /**
         * @Route("/contact")
         * @Method("GET")
         */
        public function contactAction()
        {
            // ... display contact form
        }
    
        /**
         * @Route("/contact")
         * @Method("POST")
         */
        public function processContactAction()
        {
            // ... process contact form
        }
    }
    
  • YAML
    # app/config/routing.yml
    contact:
        path:     /contact
        defaults: { _controller: AppBundle:Main:contact }
        methods:  [GET]
    
    contact_process:
        path:     /contact
        defaults: { _controller: AppBundle:Main:processContact }
        methods:  [POST]
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="contact" path="/contact" methods="GET">
            <default key="_controller">AppBundle:Main:contact</default>
        </route>
    
        <route id="contact_process" path="/contact" methods="POST">
            <default key="_controller">AppBundle:Main:processContact</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('contact', new Route('/contact', array(
        '_controller' => 'AppBundle:Main:contact',
    ), array(), array(), '', array(), array('GET')));
    
    $collection->add('contact_process', new Route('/contact', array(
        '_controller' => 'AppBundle:Main:processContact',
    ), array(), array(), '', array(), array('POST')));
    
    return $collection;
    

2.2 新版功能: The methods option was introduced in Symfony 2.2. Use the _method requirement in older versions.

Despite the fact that these two routes have identical paths (/contact), the first route will match only GET requests and the second route will match only POST requests. This means that you can display the form and submit the form via the same URL, while using distinct controllers for the two actions.

注解

If no methods are specified, the route will match on all methods.

Adding a Host Requirement

2.2 新版功能: Host matching support was introduced in Symfony 2.2

You can also match on the HTTP host of the incoming request. For more information, see How to Match a Route Based on the Host in the Routing component documentation.

Advanced Routing Example

At this point, you have everything you need to create a powerful routing structure in Symfony. The following is an example of just how flexible the routing system can be:

  • Annotations
    // src/AppBundle/Controller/ArticleController.php
    
    // ...
    class ArticleController extends Controller
    {
        /**
         * @Route(
         *     "/articles/{_locale}/{year}/{title}.{_format}",
         *     defaults={"_format": "html"},
         *     requirements={
         *         "_locale": "en|fr",
         *         "_format": "html|rss",
         *         "year": "\d+"
         *     }
         * )
         */
        public function showAction($_locale, $year, $title)
        {
        }
    }
    
  • YAML
    # app/config/routing.yml
    article_show:
      path:     /articles/{_locale}/{year}/{title}.{_format}
      defaults: { _controller: AppBundle:Article:show, _format: html }
      requirements:
          _locale:  en|fr
          _format:  html|rss
          year:     \d+
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="article_show"
            path="/articles/{_locale}/{year}/{title}.{_format}">
    
            <default key="_controller">AppBundle:Article:show</default>
            <default key="_format">html</default>
            <requirement key="_locale">en|fr</requirement>
            <requirement key="_format">html|rss</requirement>
            <requirement key="year">\d+</requirement>
    
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add(
        'article_show',
        new Route('/articles/{_locale}/{year}/{title}.{_format}', array(
            '_controller' => 'AppBundle:Article:show',
            '_format'     => 'html',
        ), array(
            '_locale' => 'en|fr',
            '_format' => 'html|rss',
            'year'    => '\d+',
        ))
    );
    
    return $collection;
    

As you’ve seen, this route will only match if the {_locale} portion of the URL is either en or fr and if the {year} is a number. This route also shows how you can use a dot between placeholders instead of a slash. URLs matching this route might look like:

  • /articles/en/2010/my-post
  • /articles/fr/2010/my-post.rss
  • /articles/en/2013/my-latest-post.html

注解

Sometimes you want to make certain parts of your routes globally configurable. Symfony provides you with a way to do this by leveraging service container parameters. Read more about this in “How to Use Service Container Parameters in your Routes”.

Special Routing Parameters

As you’ve seen, each routing parameter or default value is eventually available as an argument in the controller method. Additionally, there are three parameters that are special: each adds a unique piece of functionality inside your application:

_controller
As you’ve seen, this parameter is used to determine which controller is executed when the route is matched.
_format
Used to set the request format (read more).
_locale
Used to set the locale on the request (read more).
Controller Naming Pattern

Every route must have a _controller parameter, which dictates which controller should be executed when that route is matched. This parameter uses a simple string pattern called the logical controller name, which Symfony maps to a specific PHP method and class. The pattern has three parts, each separated by a colon:

bundle:controller:action

For example, a _controller value of AppBundle:Blog:show means:

Bundle Controller Class Method Name
AppBundle BlogController showAction

The controller might look like this:

// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogController extends Controller
{
    public function showAction($slug)
    {
        // ...
    }
}

Notice that Symfony adds the string Controller to the class name (Blog => BlogController) and Action to the method name (show => showAction).

You could also refer to this controller using its fully-qualified class name and method: Acme\BlogBundle\Controller\BlogController::showAction. But if you follow some simple conventions, the logical name is more concise and allows more flexibility.

注解

In addition to using the logical name or the fully-qualified class name, Symfony supports a third way of referring to a controller. This method uses just one colon separator (e.g. service_name:indexAction) and refers to the controller as a service (see How to Define Controllers as Services).

Route Parameters and Controller Arguments

The route parameters (e.g. {slug}) are especially important because each is made available as an argument to the controller method:

public function showAction($slug)
{
    // ...
}

In reality, the entire defaults collection is merged with the parameter values to form a single array. Each key of that array is available as an argument on the controller.

In other words, for each argument of your controller method, Symfony looks for a route parameter of that name and assigns its value to that argument. In the advanced example above, any combination (in any order) of the following variables could be used as arguments to the showAction() method:

  • $_locale
  • $year
  • $title
  • $_format
  • $_controller
  • $_route

Since the placeholders and defaults collection are merged together, even the $_controller variable is available. For a more detailed discussion, see 作为控制器参数的路由占位符.

小技巧

The special $_route variable is set to the name of the route that was matched.

You can even add extra information to your route definition and access it within your controller. For more information on this topic, see How to Pass Extra Information from a Route to a Controller.

Including External Routing Resources

All routes are loaded via a single configuration file - usually app/config/routing.yml (see Creating Routes above). However, if you use routing annotations, you’ll need to point the router to the controllers with the annotations. This can be done by “importing” directories into the routing configuration:

  • YAML
    # app/config/routing.yml
    app:
        resource: "@AppBundle/Controller/"
        type:     annotation # required to enable the Annotation reader for this resource
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <!-- the type is required to enable the annotation reader for this resource -->
        <import resource="@AppBundle/Controller/" type="annotation"/>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection(
        // second argument is the type, which is required to enable
        // the annotation reader for this resource
        $loader->import("@AppBundle/Controller/", "annotation")
    );
    
    return $collection;
    

注解

When importing resources from YAML, the key (e.g. app) is meaningless. Just be sure that it’s unique so no other lines override it.

The resource key loads the given routing resource. In this example the resource is a directory, where the @AppBundle shortcut syntax resolves to the full path of the AppBundle. When pointing to a directory, all files in that directory are parsed and put into the routing.

注解

You can also include other routing configuration files, this is often used to import the routing of third party bundles:

  • YAML
    # app/config/routing.yml
    app:
        resource: "@AcmeOtherBundle/Resources/config/routing.yml"
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <import resource="@AcmeOtherBundle/Resources/config/routing.xml" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection(
        $loader->import("@AcmeOtherBundle/Resources/config/routing.php")
    );
    
    return $collection;
    
Prefixing Imported Routes

You can also choose to provide a “prefix” for the imported routes. For example, suppose you want to prefix all routes in the AppBundle with /site (e.g. /site/blog/{slug} instead of /blog/{slug}):

  • YAML
    # app/config/routing.yml
    app:
        resource: "@AppBundle/Controller/"
        type:     annotation
        prefix:   /site
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <import
            resource="@AppBundle/Controller/"
            type="annotation"
            prefix="/site" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $app = $loader->import('@AppBundle/Controller/', 'annotation');
    $app->addPrefix('/site');
    
    $collection = new RouteCollection();
    $collection->addCollection($app);
    
    return $collection;
    

The path of each route being loaded from the new routing resource will now be prefixed with the string /site.

Adding a Host Requirement to Imported Routes

2.2 新版功能: Host matching support was introduced in Symfony 2.2

You can set the host regex on imported routes. For more information, see Using Host Matching of Imported Routes.

Visualizing & Debugging Routes

While adding and customizing routes, it’s helpful to be able to visualize and get detailed information about your routes. A great way to see every route in your application is via the router:debug console command. Execute the command by running the following from the root of your project.

$ php app/console router:debug

This command will print a helpful list of all the configured routes in your application:

homepage              ANY       /
contact               GET       /contact
contact_process       POST      /contact
article_show          ANY       /articles/{_locale}/{year}/{title}.{_format}
blog                  ANY       /blog/{page}
blog_show             ANY       /blog/{slug}

You can also get very specific information on a single route by including the route name after the command:

$ php app/console router:debug article_show

Likewise, if you want to test whether a URL matches a given route, you can use the router:match console command:

$ php app/console router:match /blog/my-latest-post

This command will print which route the URL matches.

Route "blog_show" matches
Generating URLs

The routing system should also be used to generate URLs. In reality, routing is a bidirectional system: mapping the URL to a controller+parameters and a route+parameters back to a URL. The match() and generate() methods form this bidirectional system. Take the blog_show example route from earlier:

$params = $this->get('router')->match('/blog/my-blog-post');
// array(
//     'slug'        => 'my-blog-post',
//     '_controller' => 'AppBundle:Blog:show',
// )

$uri = $this->get('router')->generate('blog_show', array(
    'slug' => 'my-blog-post'
));
// /blog/my-blog-post

To generate a URL, you need to specify the name of the route (e.g. blog_show) and any wildcards (e.g. slug = my-blog-post) used in the path for that route. With this information, any URL can easily be generated:

class MainController extends Controller
{
    public function showAction($slug)
    {
        // ...

        $url = $this->generateUrl(
            'blog_show',
            array('slug' => 'my-blog-post')
        );
    }
}

注解

In controllers that don’t extend Symfony’s base Controller, you can use the router service’s generate() method:

use Symfony\Component\DependencyInjection\ContainerAware;

class MainController extends ContainerAware
{
    public function showAction($slug)
    {
        // ...

        $url = $this->container->get('router')->generate(
            'blog_show',
            array('slug' => 'my-blog-post')
        );
    }
}

In an upcoming section, you’ll learn how to generate URLs from inside templates.

小技巧

If the frontend of your application uses Ajax requests, you might want to be able to generate URLs in JavaScript based on your routing configuration. By using the FOSJsRoutingBundle, you can do exactly that:

var url = Routing.generate(
    'blog_show',
    {"slug": 'my-blog-post'}
);

For more information, see the documentation for that bundle.

Generating URLs with Query Strings

The generate method takes an array of wildcard values to generate the URI. But if you pass extra ones, they will be added to the URI as a query string:

$this->get('router')->generate('blog', array(
    'page' => 2,
    'category' => 'Symfony'
));
// /blog/2?category=Symfony
Generating URLs from a Template

The most common place to generate a URL is from within a template when linking between pages in your application. This is done just as before, but using a template helper function:

  • Twig
    <a href="{{ path('blog_show', {'slug': 'my-blog-post'}) }}">
      Read this blog post.
    </a>
    
  • PHP
    <a href="<?php echo $view['router']->generate('blog_show', array(
        'slug' => 'my-blog-post',
    )) ?>">
        Read this blog post.
    </a>
    
Generating Absolute URLs

By default, the router will generate relative URLs (e.g. /blog). From a controller, simply pass true to the third argument of the generateUrl() method:

$this->generateUrl('blog_show', array('slug' => 'my-blog-post'), true);
// http://www.example.com/blog/my-blog-post

From a template, in Twig, simply use the url() function (which generates an absolute URL) rather than the path() function (which generates a relative URL). In PHP, pass true to generate():

  • Twig
    <a href="{{ url('blog_show', {'slug': 'my-blog-post'}) }}">
      Read this blog post.
    </a>
    
  • PHP
    <a href="<?php echo $view['router']->generate('blog_show', array(
        'slug' => 'my-blog-post',
    ), true) ?>">
        Read this blog post.
    </a>
    

注解

The host that’s used when generating an absolute URL is automatically detected using the current Request object. When generating absolute URLs from outside the web context (for instance in a console command) this doesn’t work. See How to Generate URLs and Send Emails from the Console to learn how to solve this problem.

Summary

Routing is a system for mapping the URL of incoming requests to the controller function that should be called to process the request. It both allows you to specify beautiful URLs and keeps the functionality of your application decoupled from those URLs. Routing is a bidirectional mechanism, meaning that it should also be used to generate URLs.

Creating and Using Templates

As you know, the controller is responsible for handling each request that comes into a Symfony application. In reality, the controller delegates most of the heavy work to other places so that code can be tested and reused. When a controller needs to generate HTML, CSS or any other content, it hands the work off to the templating engine. In this chapter, you’ll learn how to write powerful templates that can be used to return content to the user, populate email bodies, and more. You’ll learn shortcuts, clever ways to extend templates and how to reuse template code.

注解

How to render templates is covered in the controller page of the book.

Templates

A template is simply a text file that can generate any text-based format (HTML, XML, CSV, LaTeX ...). The most familiar type of template is a PHP template - a text file parsed by PHP that contains a mix of text and PHP code:

<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to Symfony!</title>
    </head>
    <body>
        <h1><?php echo $page_title ?></h1>

        <ul id="navigation">
            <?php foreach ($navigation as $item): ?>
                <li>
                    <a href="<?php echo $item->getHref() ?>">
                        <?php echo $item->getCaption() ?>
                    </a>
                </li>
            <?php endforeach ?>
        </ul>
    </body>
</html>

But Symfony packages an even more powerful templating language called Twig. Twig allows you to write concise, readable templates that are more friendly to web designers and, in several ways, more powerful than PHP templates:

<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to Symfony!</title>
    </head>
    <body>
        <h1>{{ page_title }}</h1>

        <ul id="navigation">
            {% for item in navigation %}
                <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
            {% endfor %}
        </ul>
    </body>
</html>

Twig defines three types of special syntax:

{{ ... }}
“Says something”: prints a variable or the result of an expression to the template.
{% ... %}
“Does something”: a tag that controls the logic of the template; it is used to execute statements such as for-loops for example.
{# ... #}
“Comment something”: it’s the equivalent of the PHP /* comment */ syntax. It’s used to add single or multi-line comments. The content of the comments isn’t included in the rendered pages.

Twig also contains filters, which modify content before being rendered. The following makes the title variable all uppercase before rendering it:

{{ title|upper }}

Twig comes with a long list of tags and filters that are available by default. You can even add your own extensions to Twig as needed.

小技巧

Registering a Twig extension is as easy as creating a new service and tagging it with twig.extension tag.

As you’ll see throughout the documentation, Twig also supports functions and new functions can be easily added. For example, the following uses a standard for tag and the cycle function to print ten div tags, with alternating odd, even classes:

{% for i in 0..10 %}
    <div class="{{ cycle(['odd', 'even'], i) }}">
      <!-- some HTML here -->
    </div>
{% endfor %}

Throughout this chapter, template examples will be shown in both Twig and PHP.

小技巧

If you do choose to not use Twig and you disable it, you’ll need to implement your own exception handler via the kernel.exception event.

Twig Template Caching

Twig is fast. Each Twig template is compiled down to a native PHP class that is rendered at runtime. The compiled classes are located in the app/cache/{environment}/twig directory (where {environment} is the environment, such as dev or prod) and in some cases can be useful while debugging. See Environments for more information on environments.

When debug mode is enabled (common in the dev environment), a Twig template will be automatically recompiled when changes are made to it. This means that during development you can happily make changes to a Twig template and instantly see the changes without needing to worry about clearing any cache.

When debug mode is disabled (common in the prod environment), however, you must clear the Twig cache directory so that the Twig templates will regenerate. Remember to do this when deploying your application.

Template Inheritance and Layouts

More often than not, templates in a project share common elements, like the header, footer, sidebar or more. In Symfony, this problem is thought about differently: a template can be decorated by another one. This works exactly the same as PHP classes: template inheritance allows you to build a base “layout” template that contains all the common elements of your site defined as blocks (think “PHP class with base methods”). A child template can extend the base layout and override any of its blocks (think “PHP subclass that overrides certain methods of its parent class”).

First, build a base layout file:

  • Twig
    {# app/Resources/views/base.html.twig #}
    <!DOCTYPE html>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <title>{% block title %}Test Application{% endblock %}</title>
        </head>
        <body>
            <div id="sidebar">
                {% block sidebar %}
                    <ul>
                          <li><a href="/">Home</a></li>
                          <li><a href="/blog">Blog</a></li>
                    </ul>
                {% endblock %}
            </div>
    
            <div id="content">
                {% block body %}{% endblock %}
            </div>
        </body>
    </html>
    
  • PHP
    <!-- app/Resources/views/base.html.php -->
    <!DOCTYPE html>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <title><?php $view['slots']->output('title', 'Test Application') ?></title>
        </head>
        <body>
            <div id="sidebar">
                <?php if ($view['slots']->has('sidebar')): ?>
                    <?php $view['slots']->output('sidebar') ?>
                <?php else: ?>
                    <ul>
                        <li><a href="/">Home</a></li>
                        <li><a href="/blog">Blog</a></li>
                    </ul>
                <?php endif ?>
            </div>
    
            <div id="content">
                <?php $view['slots']->output('body') ?>
            </div>
        </body>
    </html>
    

注解

Though the discussion about template inheritance will be in terms of Twig, the philosophy is the same between Twig and PHP templates.

This template defines the base HTML skeleton document of a simple two-column page. In this example, three {% block %} areas are defined (title, sidebar and body). Each block may be overridden by a child template or left with its default implementation. This template could also be rendered directly. In that case the title, sidebar and body blocks would simply retain the default values used in this template.

A child template might look like this:

  • Twig
    {# app/Resources/views/Blog/index.html.twig #}
    {% extends 'base.html.twig' %}
    
    {% block title %}My cool blog posts{% endblock %}
    
    {% block body %}
        {% for entry in blog_entries %}
            <h2>{{ entry.title }}</h2>
            <p>{{ entry.body }}</p>
        {% endfor %}
    {% endblock %}
    
  • PHP
    <!-- app/Resources/views/Blog/index.html.php -->
    <?php $view->extend('base.html.php') ?>
    
    <?php $view['slots']->set('title', 'My cool blog posts') ?>
    
    <?php $view['slots']->start('body') ?>
        <?php foreach ($blog_entries as $entry): ?>
            <h2><?php echo $entry->getTitle() ?></h2>
            <p><?php echo $entry->getBody() ?></p>
        <?php endforeach ?>
    <?php $view['slots']->stop() ?>
    

注解

The parent template is identified by a special string syntax (base.html.twig). This path is relative to the app/Resources/views directory of the project. You could also use the logical name equivalent: ::base.html.twig. This naming convention is explained fully in Template Naming and Locations.

The key to template inheritance is the {% extends %} tag. This tells the templating engine to first evaluate the base template, which sets up the layout and defines several blocks. The child template is then rendered, at which point the title and body blocks of the parent are replaced by those from the child. Depending on the value of blog_entries, the output might look like this:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>My cool blog posts</title>
    </head>
    <body>
        <div id="sidebar">
            <ul>
                <li><a href="/">Home</a></li>
                <li><a href="/blog">Blog</a></li>
            </ul>
        </div>

        <div id="content">
            <h2>My first post</h2>
            <p>The body of the first post.</p>

            <h2>Another post</h2>
            <p>The body of the second post.</p>
        </div>
    </body>
</html>

Notice that since the child template didn’t define a sidebar block, the value from the parent template is used instead. Content within a {% block %} tag in a parent template is always used by default.

You can use as many levels of inheritance as you want. In the next section, a common three-level inheritance model will be explained along with how templates are organized inside a Symfony project.

When working with template inheritance, here are some tips to keep in mind:

  • If you use {% extends %} in a template, it must be the first tag in that template;

  • The more {% block %} tags you have in your base templates, the better. Remember, child templates don’t have to define all parent blocks, so create as many blocks in your base templates as you want and give each a sensible default. The more blocks your base templates have, the more flexible your layout will be;

  • If you find yourself duplicating content in a number of templates, it probably means you should move that content to a {% block %} in a parent template. In some cases, a better solution may be to move the content to a new template and include it (see Including other Templates);

  • If you need to get the content of a block from the parent template, you can use the {{ parent() }} function. This is useful if you want to add to the contents of a parent block instead of completely overriding it:

    {% block sidebar %}
        <h3>Table of Contents</h3>
    
        {# ... #}
    
        {{ parent() }}
    {% endblock %}
    
Template Naming and Locations

2.2 新版功能: Namespaced path support was introduced in 2.2, allowing for template names like @AcmeDemo/layout.html.twig. See How to Use and Register Namespaced Twig Paths for more details.

By default, templates can live in two different locations:

app/Resources/views/
The applications views directory can contain application-wide base templates (i.e. your application’s layouts and templates of the application bundle) as well as templates that override third party bundle templates (see Overriding Bundle Templates).
path/to/bundle/Resources/views/
Each third party bundle houses its templates in its Resources/views/ directory (and subdirectories). When you plan to share your bundle, you should put the templates in the bundle instead of the app/ directory.

Most of the templates you’ll use live in the app/Resources/views/ directory. The path you’ll use will be relative to this directory. For example, to render/extend app/Resources/views/base.html.twig, you’ll use the base.html.twig path and to render/extend app/Resources/views/Blog/index.html.twig, you’ll use the Blog/index.html.twig path.

Referencing Templates in a Bundle

Symfony uses a bundle:directory:filename string syntax for templates that live inside a bundle. This allows for several types of templates, each which lives in a specific location:

  • AcmeBlogBundle:Blog:index.html.twig: This syntax is used to specify a template for a specific page. The three parts of the string, each separated by a colon (:), mean the following:

    • AcmeBlogBundle: (bundle) the template lives inside the AcmeBlogBundle (e.g. src/Acme/BlogBundle);
    • Blog: (directory) indicates that the template lives inside the Blog subdirectory of Resources/views;
    • index.html.twig: (filename) the actual name of the file is index.html.twig.

    Assuming that the AcmeBlogBundle lives at src/Acme/BlogBundle, the final path to the layout would be src/Acme/BlogBundle/Resources/views/Blog/index.html.twig.

  • AcmeBlogBundle::layout.html.twig: This syntax refers to a base template that’s specific to the AcmeBlogBundle. Since the middle, “directory”, portion is missing (e.g. Blog), the template lives at Resources/views/layout.html.twig inside AcmeBlogBundle. Yes, there are 2 colons in the middle of the string when the “controller” subdirectory part is missing.

In the Overriding Bundle Templates section, you’ll find out how each template living inside the AcmeBlogBundle, for example, can be overridden by placing a template of the same name in the app/Resources/AcmeBlogBundle/views/ directory. This gives the power to override templates from any vendor bundle.

小技巧

Hopefully the template naming syntax looks familiar - it’s similar to the naming convention used to refer to Controller Naming Pattern.

Template Suffix

Every template name also has two extensions that specify the format and engine for that template.

Filename Format Engine
Blog/index.html.twig HTML Twig
Blog/index.html.php HTML PHP
Blog/index.css.twig CSS Twig

By default, any Symfony template can be written in either Twig or PHP, and the last part of the extension (e.g. .twig or .php) specifies which of these two engines should be used. The first part of the extension, (e.g. .html, .css, etc) is the final format that the template will generate. Unlike the engine, which determines how Symfony parses the template, this is simply an organizational tactic used in case the same resource needs to be rendered as HTML (index.html.twig), XML (index.xml.twig), or any other format. For more information, read the Template Formats section.

注解

The available “engines” can be configured and even new engines added. See Templating Configuration for more details.

Tags and Helpers

You already understand the basics of templates, how they’re named and how to use template inheritance. The hardest parts are already behind you. In this section, you’ll learn about a large group of tools available to help perform the most common template tasks such as including other templates, linking to pages and including images.

Symfony comes bundled with several specialized Twig tags and functions that ease the work of the template designer. In PHP, the templating system provides an extensible helper system that provides useful features in a template context.

You’ve already seen a few built-in Twig tags ({% block %} & {% extends %}) as well as an example of a PHP helper ($view['slots']). Here you will learn a few more.

Including other Templates

You’ll often want to include the same template or code fragment on several pages. For example, in an application with “news articles”, the template code displaying an article might be used on the article detail page, on a page displaying the most popular articles, or in a list of the latest articles.

When you need to reuse a chunk of PHP code, you typically move the code to a new PHP class or function. The same is true for templates. By moving the reused template code into its own template, it can be included from any other template. First, create the template that you’ll need to reuse.

  • Twig
    {# app/Resources/views/Article/articleDetails.html.twig #}
    <h2>{{ article.title }}</h2>
    <h3 class="byline">by {{ article.authorName }}</h3>
    
    <p>
        {{ article.body }}
    </p>
    
  • PHP
    <!-- app/Resources/views/Article/articleDetails.html.php -->
    <h2><?php echo $article->getTitle() ?></h2>
    <h3 class="byline">by <?php echo $article->getAuthorName() ?></h3>
    
    <p>
        <?php echo $article->getBody() ?>
    </p>
    

Including this template from any other template is simple:

  • Twig
    {# app/Resources/views/Article/list.html.twig #}
    {% extends 'layout.html.twig' %}
    
    {% block body %}
        <h1>Recent Articles<h1>
    
        {% for article in articles %}
            {{ include('Article/articleDetails.html.twig', { 'article': article }) }}
        {% endfor %}
    {% endblock %}
    
  • PHP
    <!-- app/Resources/Article/list.html.php -->
    <?php $view->extend('layout.html.php') ?>
    
    <?php $view['slots']->start('body') ?>
        <h1>Recent Articles</h1>
    
        <?php foreach ($articles as $article): ?>
            <?php echo $view->render(
                'Article/articleDetails.html.php',
                array('article' => $article)
            ) ?>
        <?php endforeach ?>
    <?php $view['slots']->stop() ?>
    

The template is included using the {{ include() }} function. Notice that the template name follows the same typical convention. The articleDetails.html.twig template uses an article variable, which we pass to it. In this case, you could avoid doing this entirely, as all of the variables available in list.html.twig are also available in articleDetails.html.twig (unless you set with_context to false).

小技巧

The {'article': article} syntax is the standard Twig syntax for hash maps (i.e. an array with named keys). If you needed to pass in multiple elements, it would look like this: {'foo': foo, 'bar': bar}.

2.2 新版功能: The include() function is a new Twig feature that’s available in Symfony 2.2. Prior, the {% include %} tag tag was used.

Embedding Controllers

In some cases, you need to do more than include a simple template. Suppose you have a sidebar in your layout that contains the three most recent articles. Retrieving the three articles may include querying the database or performing other heavy logic that can’t be done from within a template.

The solution is to simply embed the result of an entire controller from your template. First, create a controller that renders a certain number of recent articles:

// src/AppBundle/Controller/ArticleController.php
namespace AppBundle\Controller;

// ...

class ArticleController extends Controller
{
    public function recentArticlesAction($max = 3)
    {
        // make a database call or other logic
        // to get the "$max" most recent articles
        $articles = ...;

        return $this->render(
            'Article/recentList.html.twig',
            array('articles' => $articles)
        );
    }
}

The recentList template is perfectly straightforward:

  • Twig
    {# app/Resources/views/Article/recentList.html.twig #}
    {% for article in articles %}
        <a href="/article/{{ article.slug }}">
            {{ article.title }}
        </a>
    {% endfor %}
    
  • PHP
    <!-- app/Resources/views/Article/recentList.html.php -->
    <?php foreach ($articles as $article): ?>
        <a href="/article/<?php echo $article->getSlug() ?>">
            <?php echo $article->getTitle() ?>
        </a>
    <?php endforeach ?>
    

注解

Notice that the article URL is hardcoded in this example (e.g. /article/*slug*). This is a bad practice. In the next section, you’ll learn how to do this correctly.

To include the controller, you’ll need to refer to it using the standard string syntax for controllers (i.e. bundle:controller:action):

  • Twig
    {# app/Resources/views/base.html.twig #}
    
    {# ... #}
    <div id="sidebar">
        {{ render(controller(
            'AcmeArticleBundle:Article:recentArticles',
            { 'max': 3 }
        )) }}
    </div>
    
  • PHP
    <!-- app/Resources/views/base.html.php -->
    
    <!-- ... -->
    <div id="sidebar">
        <?php echo $view['actions']->render(
            new \Symfony\Component\HttpKernel\Controller\ControllerReference(
                'AcmeArticleBundle:Article:recentArticles',
                array('max' => 3)
            )
        ) ?>
    </div>
    

Whenever you find that you need a variable or a piece of information that you don’t have access to in a template, consider rendering a controller. Controllers are fast to execute and promote good code organization and reuse. Of course, like all controllers, they should ideally be “skinny”, meaning that as much code as possible lives in reusable services.

Asynchronous Content with hinclude.js

2.1 新版功能: hinclude.js support was introduced in Symfony 2.1

Controllers can be embedded asynchronously using the hinclude.js JavaScript library. As the embedded content comes from another page (or controller for that matter), Symfony uses a version of the standard render function to configure hinclude tags:

  • Twig
    {{ render_hinclude(controller('...')) }}
    {{ render_hinclude(url('...')) }}
    
  • PHP
    <?php echo $view['actions']->render(
        new ControllerReference('...'),
        array('renderer' => 'hinclude')
    ) ?>
    
    <?php echo $view['actions']->render(
        $view['router']->generate('...'),
        array('renderer' => 'hinclude')
    ) ?>
    

注解

hinclude.js needs to be included in your page to work.

注解

When using a controller instead of a URL, you must enable the Symfony fragments configuration:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        fragments: { path: /_fragment }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <!-- ... -->
        <framework:config>
            <framework:fragments path="/_fragment" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'fragments' => array('path' => '/_fragment'),
    ));
    

Default content (while loading or if JavaScript is disabled) can be set globally in your application configuration:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        templating:
            hinclude_default_template: hinclude.html.twig
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <!-- ... -->
        <framework:config>
            <framework:templating hinclude-default-template="hinclude.html.twig" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'templating'      => array(
            'hinclude_default_template' => array(
                'hinclude.html.twig',
            ),
        ),
    ));
    

2.2 新版功能: Default templates per render function was introduced in Symfony 2.2

You can define default templates per render function (which will override any global default template that is defined):

  • Twig
    {{ render_hinclude(controller('...'),  {
        'default': 'Default/content.html.twig'
    }) }}
    
  • PHP
    <?php echo $view['actions']->render(
        new ControllerReference('...'),
        array(
            'renderer' => 'hinclude',
            'default' => 'Default/content.html.twig',
        )
    ) ?>
    

Or you can also specify a string to display as the default content:

  • Twig
    {{ render_hinclude(controller('...'), {'default': 'Loading...'}) }}
    
  • PHP
    <?php echo $view['actions']->render(
        new ControllerReference('...'),
        array(
            'renderer' => 'hinclude',
            'default' => 'Loading...',
        )
    ) ?>
    
Linking to Pages

Creating links to other pages in your application is one of the most common jobs for a template. Instead of hardcoding URLs in templates, use the path Twig function (or the router helper in PHP) to generate URLs based on the routing configuration. Later, if you want to modify the URL of a particular page, all you’ll need to do is change the routing configuration; the templates will automatically generate the new URL.

First, link to the “_welcome” page, which is accessible via the following routing configuration:

  • YAML
    # app/config/routing.yml
    _welcome:
        path:     /
        defaults: { _controller: AppBundle:Welcome:index }
    
  • XML
    <!-- app/config/routing.yml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="_welcome" path="/">
            <default key="_controller">AppBundle:Welcome:index</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\Route;
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->add('_welcome', new Route('/', array(
        '_controller' => 'AppBundle:Welcome:index',
    )));
    
    return $collection;
    

To link to the page, just use the path Twig function and refer to the route:

  • Twig
    <a href="{{ path('_welcome') }}">Home</a>
    
  • PHP
    <a href="<?php echo $view['router']->generate('_welcome') ?>">Home</a>
    

As expected, this will generate the URL /. Now, for a more complicated route:

  • YAML
    # app/config/routing.yml
    article_show:
        path:     /article/{slug}
        defaults: { _controller: AppBundle:Article:show }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="article_show" path="/article/{slug}">
            <default key="_controller">AppBundle:Article:show</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\Route;
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->add('article_show', new Route('/article/{slug}', array(
        '_controller' => 'AppBundle:Article:show',
    )));
    
    return $collection;
    

In this case, you need to specify both the route name (article_show) and a value for the {slug} parameter. Using this route, revisit the recentList template from the previous section and link to the articles correctly:

  • Twig
    {# app/Resources/views/Article/recentList.html.twig #}
    {% for article in articles %}
        <a href="{{ path('article_show', {'slug': article.slug}) }}">
            {{ article.title }}
        </a>
    {% endfor %}
    
  • PHP
    <!-- app/Resources/views/Article/recentList.html.php -->
    <?php foreach ($articles in $article): ?>
        <a href="<?php echo $view['router']->generate('article_show', array(
            'slug' => $article->getSlug(),
        )) ?>">
            <?php echo $article->getTitle() ?>
        </a>
    <?php endforeach ?>
    

小技巧

You can also generate an absolute URL by using the url Twig function:

<a href="{{ url('_welcome') }}">Home</a>

The same can be done in PHP templates by passing a third argument to the generate() method:

<a href="<?php echo $view['router']->generate(
    '_welcome',
    array(),
    true
) ?>">Home</a>
Linking to Assets

Templates also commonly refer to images, JavaScript, stylesheets and other assets. Of course you could hard-code the path to these assets (e.g. /images/logo.png), but Symfony provides a more dynamic option via the asset Twig function:

  • Twig
    <img src="{{ asset('images/logo.png') }}" alt="Symfony!" />
    
    <link href="{{ asset('css/blog.css') }}" rel="stylesheet" type="text/css" />
    
  • PHP
    <img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" alt="Symfony!" />
    
    <link href="<?php echo $view['assets']->getUrl('css/blog.css') ?>" rel="stylesheet" type="text/css" />
    

The asset function’s main purpose is to make your application more portable. If your application lives at the root of your host (e.g. http://example.com), then the rendered paths should be /images/logo.png. But if your application lives in a subdirectory (e.g. http://example.com/my_app), each asset path should render with the subdirectory (e.g. /my_app/images/logo.png). The asset function takes care of this by determining how your application is being used and generating the correct paths accordingly.

Additionally, if you use the asset function, Symfony can automatically append a query string to your asset, in order to guarantee that updated static assets won’t be cached when deployed. For example, /images/logo.png might look like /images/logo.png?v2. For more information, see the assets_version configuration option.

Including Stylesheets and JavaScripts in Twig

No site would be complete without including JavaScript files and stylesheets. In Symfony, the inclusion of these assets is handled elegantly by taking advantage of Symfony’s template inheritance.

小技巧

This section will teach you the philosophy behind including stylesheet and JavaScript assets in Symfony. Symfony also packages another library, called Assetic, which follows this philosophy but allows you to do much more interesting things with those assets. For more information on using Assetic see How to Use Assetic for Asset Management.

Start by adding two blocks to your base template that will hold your assets: one called stylesheets inside the head tag and another called javascripts just above the closing body tag. These blocks will contain all of the stylesheets and JavaScripts that you’ll need throughout your site:

  • Twig
    {# app/Resources/views/base.html.twig #}
    <html>
        <head>
            {# ... #}
    
            {% block stylesheets %}
                <link href="{{ asset('css/main.css') }}" rel="stylesheet" />
            {% endblock %}
        </head>
        <body>
            {# ... #}
    
            {% block javascripts %}
                <script src="{{ asset('js/main.js') }}"></script>
            {% endblock %}
        </body>
    </html>
    
  • PHP
    // app/Resources/views/base.html.php
    <html>
        <head>
            <?php ... ?>
    
            <?php $view['slots']->start('stylesheets') ?>
                <link href="<?php echo $view['assets']->getUrl('css/main.css') ?>" rel="stylesheet" />
            <?php $view['slots']->stop() ?>
        </head>
        <body>
            <?php ... ?>
    
            <?php $view['slots']->start('javascripts') ?>
                <script src="<?php echo $view['assets']->getUrl('js/main.js') ?>"></script>
            <?php $view['slots']->stop() ?>
        </body>
    </html>
    

That’s easy enough! But what if you need to include an extra stylesheet or JavaScript from a child template? For example, suppose you have a contact page and you need to include a contact.css stylesheet just on that page. From inside that contact page’s template, do the following:

  • Twig
    {# app/Resources/views/Contact/contact.html.twig #}
    {% extends 'base.html.twig' %}
    
    {% block stylesheets %}
        {{ parent() }}
    
        <link href="{{ asset('css/contact.css') }}" rel="stylesheet" />
    {% endblock %}
    
    {# ... #}
    
  • PHP
    // app/Resources/views/Contact/contact.html.twig
    <?php $view->extend('base.html.php') ?>
    
    <?php $view['slots']->start('stylesheets') ?>
        <link href="<?php echo $view['assets']->getUrl('css/contact.css') ?>" rel="stylesheet" />
    <?php $view['slots']->stop() ?>
    

In the child template, you simply override the stylesheets block and put your new stylesheet tag inside of that block. Of course, since you want to add to the parent block’s content (and not actually replace it), you should use the parent() Twig function to include everything from the stylesheets block of the base template.

You can also include assets located in your bundles’ Resources/public folder. You will need to run the php app/console assets:install target [--symlink] command, which moves (or symlinks) files into the correct location. (target is by default “web”).

<link href="{{ asset('bundles/acmedemo/css/contact.css') }}" rel="stylesheet" />

The end result is a page that includes both the main.css and contact.css stylesheets.

Global Template Variables

During each request, Symfony will set a global template variable app in both Twig and PHP template engines by default. The app variable is a GlobalVariables instance which will give you access to some application specific variables automatically:

app.security
The security context.
app.user
The current user object.
app.request
The request object.
app.session
The session object.
app.environment
The current environment (dev, prod, etc).
app.debug
True if in debug mode. False otherwise.
  • Twig
    <p>Username: {{ app.user.username }}</p>
    {% if app.debug %}
        <p>Request method: {{ app.request.method }}</p>
        <p>Application Environment: {{ app.environment }}</p>
    {% endif %}
    
  • PHP
    <p>Username: <?php echo $app->getUser()->getUsername() ?></p>
    <?php if ($app->getDebug()): ?>
        <p>Request method: <?php echo $app->getRequest()->getMethod() ?></p>
        <p>Application Environment: <?php echo $app->getEnvironment() ?></p>
    <?php endif ?>
    

小技巧

You can add your own global template variables. See the cookbook example on Global Variables.

Configuring and Using the templating Service

The heart of the template system in Symfony is the templating Engine. This special object is responsible for rendering templates and returning their content. When you render a template in a controller, for example, you’re actually using the templating engine service. For example:

return $this->render('Article/index.html.twig');

is equivalent to:

use Symfony\Component\HttpFoundation\Response;

$engine = $this->container->get('templating');
$content = $engine->render('Article/index.html.twig');

return $response = new Response($content);

The templating engine (or “service”) is preconfigured to work automatically inside Symfony. It can, of course, be configured further in the application configuration file:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        templating: { engines: ['twig'] }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <!-- ... -->
        <framework:config>
            <framework:templating>
                <framework:engine>twig</framework:engine>
            </framework:templating>
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
    
        'templating' => array(
            'engines' => array('twig'),
        ),
    ));
    

Several configuration options are available and are covered in the Configuration Appendix.

注解

The twig engine is mandatory to use the webprofiler (as well as many third-party bundles).

Overriding Bundle Templates

The Symfony community prides itself on creating and maintaining high quality bundles (see KnpBundles.com) for a large number of different features. Once you use a third-party bundle, you’ll likely need to override and customize one or more of its templates.

Suppose you’ve installed the imaginary open-source AcmeBlogBundle in your project. And while you’re really happy with everything, you want to override the blog “list” page to customize the markup specifically for your application. By digging into the Blog controller of the AcmeBlogBundle, you find the following:

public function indexAction()
{
    // some logic to retrieve the blogs
    $blogs = ...;

    $this->render(
        'AcmeBlogBundle:Blog:index.html.twig',
        array('blogs' => $blogs)
    );
}

When the AcmeBlogBundle:Blog:index.html.twig is rendered, Symfony actually looks in two different locations for the template:

  1. app/Resources/AcmeBlogBundle/views/Blog/index.html.twig
  2. src/Acme/BlogBundle/Resources/views/Blog/index.html.twig

To override the bundle template, just copy the index.html.twig template from the bundle to app/Resources/AcmeBlogBundle/views/Blog/index.html.twig (the app/Resources/AcmeBlogBundle directory won’t exist, so you’ll need to create it). You’re now free to customize the template.

警告

If you add a template in a new location, you may need to clear your cache (php app/console cache:clear), even if you are in debug mode.

This logic also applies to base bundle templates. Suppose also that each template in AcmeBlogBundle inherits from a base template called AcmeBlogBundle::layout.html.twig. Just as before, Symfony will look in the following two places for the template:

  1. app/Resources/AcmeBlogBundle/views/layout.html.twig
  2. src/Acme/BlogBundle/Resources/views/layout.html.twig

Once again, to override the template, just copy it from the bundle to app/Resources/AcmeBlogBundle/views/layout.html.twig. You’re now free to customize this copy as you see fit.

If you take a step back, you’ll see that Symfony always starts by looking in the app/Resources/{BUNDLE_NAME}/views/ directory for a template. If the template doesn’t exist there, it continues by checking inside the Resources/views directory of the bundle itself. This means that all bundle templates can be overridden by placing them in the correct app/Resources subdirectory.

注解

You can also override templates from within a bundle by using bundle inheritance. For more information, see How to Use Bundle Inheritance to Override Parts of a Bundle.

Overriding Core Templates

Since the Symfony framework itself is just a bundle, core templates can be overridden in the same way. For example, the core TwigBundle contains a number of different “exception” and “error” templates that can be overridden by copying each from the Resources/views/Exception directory of the TwigBundle to, you guessed it, the app/Resources/TwigBundle/views/Exception directory.

Three-level Inheritance

One common way to use inheritance is to use a three-level approach. This method works perfectly with the three different types of templates that were just covered:

  • Create a app/Resources/views/base.html.twig file that contains the main layout for your application (like in the previous example). Internally, this template is called base.html.twig;

  • Create a template for each “section” of your site. For example, the blog functionality would have a template called Blog/layout.html.twig that contains only blog section-specific elements;

    {# app/Resources/views/Blog/layout.html.twig #}
    {% extends 'base.html.twig' %}
    
    {% block body %}
        <h1>Blog Application</h1>
    
        {% block content %}{% endblock %}
    {% endblock %}
    
  • Create individual templates for each page and make each extend the appropriate section template. For example, the “index” page would be called something close to Blog/index.html.twig and list the actual blog posts.

    {# app/Resources/views/Blog/index.html.twig #}
    {% extends 'Blog/layout.html.twig' %}
    
    {% block content %}
        {% for entry in blog_entries %}
            <h2>{{ entry.title }}</h2>
            <p>{{ entry.body }}</p>
        {% endfor %}
    {% endblock %}
    

Notice that this template extends the section template (Blog/layout.html.twig) which in turn extends the base application layout (base.html.twig). This is the common three-level inheritance model.

When building your application, you may choose to follow this method or simply make each page template extend the base application template directly (e.g. {% extends 'base.html.twig' %}). The three-template model is a best-practice method used by vendor bundles so that the base template for a bundle can be easily overridden to properly extend your application’s base layout.

Output Escaping

When generating HTML from a template, there is always a risk that a template variable may output unintended HTML or dangerous client-side code. The result is that dynamic content could break the HTML of the resulting page or allow a malicious user to perform a Cross Site Scripting (XSS) attack. Consider this classic example:

  • Twig
    Hello {{ name }}
    
  • PHP
    Hello <?php echo $name ?>
    

Imagine the user enters the following code for their name:

<script>alert('hello!')</script>

Without any output escaping, the resulting template will cause a JavaScript alert box to pop up:

Hello <script>alert('hello!')</script>

And while this seems harmless, if a user can get this far, that same user should also be able to write JavaScript that performs malicious actions inside the secure area of an unknowing, legitimate user.

The answer to the problem is output escaping. With output escaping on, the same template will render harmlessly, and literally print the script tag to the screen:

Hello &lt;script&gt;alert(&#39;helloe&#39;)&lt;/script&gt;

The Twig and PHP templating systems approach the problem in different ways. If you’re using Twig, output escaping is on by default and you’re protected. In PHP, output escaping is not automatic, meaning you’ll need to manually escape where necessary.

Output Escaping in Twig

If you’re using Twig templates, then output escaping is on by default. This means that you’re protected out-of-the-box from the unintentional consequences of user-submitted code. By default, the output escaping assumes that content is being escaped for HTML output.

In some cases, you’ll need to disable output escaping when you’re rendering a variable that is trusted and contains markup that should not be escaped. Suppose that administrative users are able to write articles that contain HTML code. By default, Twig will escape the article body.

To render it normally, add the raw filter:

{{ article.body|raw }}

You can also disable output escaping inside a {% block %} area or for an entire template. For more information, see Output Escaping in the Twig documentation.

Output Escaping in PHP

Output escaping is not automatic when using PHP templates. This means that unless you explicitly choose to escape a variable, you’re not protected. To use output escaping, use the special escape() view method:

Hello <?php echo $view->escape($name) ?>

By default, the escape() method assumes that the variable is being rendered within an HTML context (and thus the variable is escaped to be safe for HTML). The second argument lets you change the context. For example, to output something in a JavaScript string, use the js context:

var myMsg = 'Hello <?php echo $view->escape($name, 'js') ?>';
Debugging

When using PHP, you can use var_dump if you need to quickly find the value of a variable passed. This is useful, for example, inside your controller. The same can be achieved when using Twig thanks to the Debug extension.

Template parameters can then be dumped using the dump function:

{# app/Resources/views/Article/recentList.html.twig #}
{{ dump(articles) }}

{% for article in articles %}
    <a href="/article/{{ article.slug }}">
        {{ article.title }}
    </a>
{% endfor %}

The variables will only be dumped if Twig’s debug setting (in config.yml) is true. By default this means that the variables will be dumped in the dev environment but not the prod environment.

Syntax Checking

You can check for syntax errors in Twig templates using the twig:lint console command:

# You can check by filename:
$ php app/console twig:lint app/Resources/views/Article/recentList.html.twig

# or by directory:
$ php app/console twig:lint app/Resources/views
Template Formats

Templates are a generic way to render content in any format. And while in most cases you’ll use templates to render HTML content, a template can just as easily generate JavaScript, CSS, XML or any other format you can dream of.

For example, the same “resource” is often rendered in several formats. To render an article index page in XML, simply include the format in the template name:

  • XML template name: Article/index.xml.twig
  • XML template filename: index.xml.twig

In reality, this is nothing more than a naming convention and the template isn’t actually rendered differently based on its format.

In many cases, you may want to allow a single controller to render multiple different formats based on the “request format”. For that reason, a common pattern is to do the following:

public function indexAction(Request $request)
{
    $format = $request->getRequestFormat();

    return $this->render('Blog/index.'.$format.'.twig');
}

The getRequestFormat on the Request object defaults to html, but can return any other format based on the format requested by the user. The request format is most often managed by the routing, where a route can be configured so that /contact sets the request format to html while /contact.xml sets the format to xml. For more information, see the Advanced Example in the Routing chapter.

To create links that include the format parameter, include a _format key in the parameter hash:

  • Twig
    <a href="{{ path('article_show', {'id': 123, '_format': 'pdf'}) }}">
        PDF Version
    </a>
    
  • PHP
    <a href="<?php echo $view['router']->generate('article_show', array(
        'id' => 123,
        '_format' => 'pdf',
    )) ?>">
        PDF Version
    </a>
    
Final Thoughts

The templating engine in Symfony is a powerful tool that can be used each time you need to generate presentational content in HTML, XML or any other format. And though templates are a common way to generate content in a controller, their use is not mandatory. The Response object returned by a controller can be created with or without the use of a template:

// creates a Response object whose content is the rendered template
$response = $this->render('Article/index.html.twig');

// creates a Response object whose content is simple text
$response = new Response('response content');

Symfony’s templating engine is very flexible and two different template renderers are available by default: the traditional PHP templates and the sleek and powerful Twig templates. Both support a template hierarchy and come packaged with a rich set of helper functions capable of performing the most common tasks.

Overall, the topic of templating should be thought of as a powerful tool that’s at your disposal. In some cases, you may not need to render a template, and in Symfony, that’s absolutely fine.

Databases and Doctrine

One of the most common and challenging tasks for any application involves persisting and reading information to and from a database. Although the Symfony full-stack framework doesn’t integrate any ORM by default, the Symfony Standard Edition, which is the most widely used distribution, comes integrated with Doctrine, a library whose sole goal is to give you powerful tools to make this easy. In this chapter, you’ll learn the basic philosophy behind Doctrine and see how easy working with a database can be.

注解

Doctrine is totally decoupled from Symfony and using it is optional. This chapter is all about the Doctrine ORM, which aims to let you map objects to a relational database (such as MySQL, PostgreSQL or Microsoft SQL). If you prefer to use raw database queries, this is easy, and explained in the “How to Use Doctrine DBAL” cookbook entry.

You can also persist data to MongoDB using Doctrine ODM library. For more information, read the “DoctrineMongoDBBundle” documentation.

A Simple Example: A Product

The easiest way to understand how Doctrine works is to see it in action. In this section, you’ll configure your database, create a Product object, persist it to the database and fetch it back out.

Configuring the Database

Before you really begin, you’ll need to configure your database connection information. By convention, this information is usually configured in an app/config/parameters.yml file:

# app/config/parameters.yml
parameters:
    database_driver:    pdo_mysql
    database_host:      localhost
    database_name:      test_project
    database_user:      root
    database_password:  password

# ...

注解

Defining the configuration via parameters.yml is just a convention. The parameters defined in that file are referenced by the main configuration file when setting up Doctrine:

  • YAML
    # app/config/config.yml
    doctrine:
        dbal:
            driver:   "%database_driver%"
            host:     "%database_host%"
            dbname:   "%database_name%"
            user:     "%database_user%"
            password: "%database_password%"
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/doctrine
            http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <doctrine:config>
            <doctrine:dbal
                driver="%database_driver%"
                host="%database_host%"
                dbname="%database_name%"
                user="%database_user%"
                password="%database_password%" />
        </doctrine:config>
    </container>
    
  • PHP
    // app/config/config.php
    $configuration->loadFromExtension('doctrine', array(
        'dbal' => array(
            'driver'   => '%database_driver%',
            'host'     => '%database_host%',
            'dbname'   => '%database_name%',
            'user'     => '%database_user%',
            'password' => '%database_password%',
        ),
    ));
    

By separating the database information into a separate file, you can easily keep different versions of the file on each server. You can also easily store database configuration (or any sensitive information) outside of your project, like inside your Apache configuration, for example. For more information, see How to Set external Parameters in the Service Container.

Now that Doctrine knows about your database, you can have it create the database for you:

$ php app/console doctrine:database:create

注解

If you want to use SQLite as your database, you need to set the path where your database file should be stored:

  • YAML
    # app/config/config.yml
    doctrine:
        dbal:
            driver: pdo_sqlite
            path: "%kernel.root_dir%/sqlite.db"
            charset: UTF8
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/doctrine
            http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <doctrine:config>
            <doctrine:dbal
                driver="pdo_sqlite"
                path="%kernel.root_dir%/sqlite.db"
                charset="UTF-8" />
        </doctrine:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
            'driver'  => 'pdo_sqlite',
            'path'    => '%kernel.root_dir%/sqlite.db',
            'charset' => 'UTF-8',
        ),
    ));
    
Creating an Entity Class

Suppose you’re building an application where products need to be displayed. Without even thinking about Doctrine or databases, you already know that you need a Product object to represent those products. Create this class inside the Entity directory of your AppBundle:

// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;

class Product
{
    protected $name;
    protected $price;
    protected $description;
}

The class - often called an “entity”, meaning a basic class that holds data - is simple and helps fulfill the business requirement of needing products in your application. This class can’t be persisted to a database yet - it’s just a simple PHP class.

小技巧

Once you learn the concepts behind Doctrine, you can have Doctrine create simple entity classes for you. This will ask you interactive questions to help you build any entity:

$ php app/console doctrine:generate:entity
Add Mapping Information

Doctrine allows you to work with databases in a much more interesting way than just fetching rows of a column-based table into an array. Instead, Doctrine allows you to persist entire objects to the database and fetch entire objects out of the database. This works by mapping a PHP class to a database table, and the properties of that PHP class to columns on the table:

_images/doctrine_image_1.png

For Doctrine to be able to do this, you just have to create “metadata”, or configuration that tells Doctrine exactly how the Product class and its properties should be mapped to the database. This metadata can be specified in a number of different formats including YAML, XML or directly inside the Product class via annotations:

  • Annotations
    // src/AppBundle/Entity/Product.php
    namespace AppBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity
     * @ORM\Table(name="product")
     */
    class Product
    {
        /**
         * @ORM\Column(type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        protected $id;
    
        /**
         * @ORM\Column(type="string", length=100)
         */
        protected $name;
    
        /**
         * @ORM\Column(type="decimal", scale=2)
         */
        protected $price;
    
        /**
         * @ORM\Column(type="text")
         */
        protected $description;
    }
    
  • YAML
    # src/AppBundle/Resources/config/doctrine/Product.orm.yml
    AppBundle\Entity\Product:
        type: entity
        table: product
        id:
            id:
                type: integer
                generator: { strategy: AUTO }
        fields:
            name:
                type: string
                length: 100
            price:
                type: decimal
                scale: 2
            description:
                type: text
    
  • XML
    <!-- src/AppBundle/Resources/config/doctrine/Product.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="AppBundle\Entity\Product" table="product">
            <id name="id" type="integer">
                <generator strategy="AUTO" />
            </id>
            <field name="name" type="string" length="100" />
            <field name="price" type="decimal" scale="2" />
            <field name="description" type="text" />
        </entity>
    </doctrine-mapping>
    

注解

A bundle can accept only one metadata definition format. For example, it’s not possible to mix YAML metadata definitions with annotated PHP entity class definitions.

小技巧

The table name is optional and if omitted, will be determined automatically based on the name of the entity class.

Doctrine allows you to choose from a wide variety of different field types, each with their own options. For information on the available field types, see the Doctrine Field Types Reference section.

参见

You can also check out Doctrine’s Basic Mapping Documentation for all details about mapping information. If you use annotations, you’ll need to prepend all annotations with ORM\ (e.g. ORM\Column(...)), which is not shown in Doctrine’s documentation. You’ll also need to include the use Doctrine\ORM\Mapping as ORM; statement, which imports the ORM annotations prefix.

警告

Be careful that your class name and properties aren’t mapped to a protected SQL keyword (such as group or user). For example, if your entity class name is Group, then, by default, your table name will be group, which will cause an SQL error in some engines. See Doctrine’s Reserved SQL keywords documentation on how to properly escape these names. Alternatively, if you’re free to choose your database schema, simply map to a different table name or column name. See Doctrine’s Persistent classes and Property Mapping documentation.

注解

When using another library or program (e.g. Doxygen) that uses annotations, you should place the @IgnoreAnnotation annotation on the class to indicate which annotations Symfony should ignore.

For example, to prevent the @fn annotation from throwing an exception, add the following:

/**
 * @IgnoreAnnotation("fn")
 */
class Product
// ...
Generating Getters and Setters

Even though Doctrine now knows how to persist a Product object to the database, the class itself isn’t really useful yet. Since Product is just a regular PHP class, you need to create getter and setter methods (e.g. getName(), setName()) in order to access its properties (since the properties are protected). Fortunately, Doctrine can do this for you by running:

$ php app/console doctrine:generate:entities AppBundle/Entity/Product

This command makes sure that all the getters and setters are generated for the Product class. This is a safe command - you can run it over and over again: it only generates getters and setters that don’t exist (i.e. it doesn’t replace your existing methods).

警告

Keep in mind that Doctrine’s entity generator produces simple getters/setters. You should check generated entities and adjust getter/setter logic to your own needs.

You can also generate all known entities (i.e. any PHP class with Doctrine mapping information) of a bundle or an entire namespace:

# generates all entities in the AppBundle
$ php app/console doctrine:generate:entities AppBundle

# generates all entities of bundles in the Acme namespace
$ php app/console doctrine:generate:entities Acme

注解

Doctrine doesn’t care whether your properties are protected or private, or whether you have a getter or setter function for a property. The getters and setters are generated here only because you’ll need them to interact with your PHP object.

Creating the Database Tables/Schema

You now have a usable Product class with mapping information so that Doctrine knows exactly how to persist it. Of course, you don’t yet have the corresponding product table in your database. Fortunately, Doctrine can automatically create all the database tables needed for every known entity in your application. To do this, run:

$ php app/console doctrine:schema:update --force

小技巧

Actually, this command is incredibly powerful. It compares what your database should look like (based on the mapping information of your entities) with how it actually looks, and generates the SQL statements needed to update the database to where it should be. In other words, if you add a new property with mapping metadata to Product and run this task again, it will generate the “alter table” statement needed to add that new column to the existing product table.

An even better way to take advantage of this functionality is via migrations, which allow you to generate these SQL statements and store them in migration classes that can be run systematically on your production server in order to track and migrate your database schema safely and reliably.

Your database now has a fully-functional product table with columns that match the metadata you’ve specified.

Persisting Objects to the Database

Now that you have a mapped Product entity and corresponding product table, you’re ready to persist data to the database. From inside a controller, this is pretty easy. Add the following method to the DefaultController of the bundle:

// src/AppBundle/Controller/DefaultController.php

// ...
use AppBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

// ...
public function createAction()
{
    $product = new Product();
    $product->setName('A Foo Bar');
    $product->setPrice('19.99');
    $product->setDescription('Lorem ipsum dolor');

    $em = $this->getDoctrine()->getManager();

    $em->persist($product);
    $em->flush();

    return new Response('Created product id '.$product->getId());
}

注解

If you’re following along with this example, you’ll need to create a route that points to this action to see it work.

小技巧

This article shows working with Doctrine from within a controller by using the getDoctrine() method of the controller. This method is a shortcut to get the doctrine service. You can work with Doctrine anywhere else by injecting that service in the service. See Service Container for more on creating your own services.

Take a look at the previous example in more detail:

  • lines 10-13 In this section, you instantiate and work with the $product object like any other, normal PHP object.
  • line 15 This line fetches Doctrine’s entity manager object, which is responsible for handling the process of persisting and fetching objects to and from the database.
  • line 16 The persist() method tells Doctrine to “manage” the $product object. This does not actually cause a query to be made to the database (yet).
  • line 17 When the flush() method is called, Doctrine looks through all of the objects that it’s managing to see if they need to be persisted to the database. In this example, the $product object has not been persisted yet, so the entity manager executes an INSERT query and a row is created in the product table.

注解

In fact, since Doctrine is aware of all your managed entities, when you call the flush() method, it calculates an overall changeset and executes the queries in the correct order. It utilizes cached prepared statement to slightly improve the performance. For example, if you persist a total of 100 Product objects and then subsequently call flush(), Doctrine will execute 100 INSERT queries using a single prepared statement object.

When creating or updating objects, the workflow is always the same. In the next section, you’ll see how Doctrine is smart enough to automatically issue an UPDATE query if the record already exists in the database.

小技巧

Doctrine provides a library that allows you to programmatically load testing data into your project (i.e. “fixture data”). For information, see the “DoctrineFixturesBundle” documentation.

Fetching Objects from the Database

Fetching an object back out of the database is even easier. For example, suppose you’ve configured a route to display a specific Product based on its id value:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AppBundle:Product')
        ->find($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    // ... do something, like pass the $product object into a template
}

小技巧

You can achieve the equivalent of this without writing any code by using the @ParamConverter shortcut. See the FrameworkExtraBundle documentation for more details.

When you query for a particular type of object, you always use what’s known as its “repository”. You can think of a repository as a PHP class whose only job is to help you fetch entities of a certain class. You can access the repository object for an entity class via:

$repository = $this->getDoctrine()
    ->getRepository('AppBundle:Product');

注解

The AppBundle:Product string is a shortcut you can use anywhere in Doctrine instead of the full class name of the entity (i.e. AppBundle\Entity\Product). As long as your entity lives under the Entity namespace of your bundle, this will work.

Once you have your repository, you have access to all sorts of helpful methods:

// query by the primary key (usually "id")
$product = $repository->find($id);

// dynamic method names to find based on a column value
$product = $repository->findOneById($id);
$product = $repository->findOneByName('foo');

// find *all* products
$products = $repository->findAll();

// find a group of products based on an arbitrary column value
$products = $repository->findByPrice(19.99);

注解

Of course, you can also issue complex queries, which you’ll learn more about in the Querying for Objects section.

You can also take advantage of the useful findBy and findOneBy methods to easily fetch objects based on multiple conditions:

// query for one product matching by name and price
$product = $repository->findOneBy(
    array('name' => 'foo', 'price' => 19.99)
);

// query for all products matching the name, ordered by price
$products = $repository->findBy(
    array('name' => 'foo'),
    array('price' => 'ASC')
);

小技巧

When you render any page, you can see how many queries were made in the bottom right corner of the web debug toolbar.

_images/doctrine_web_debug_toolbar.png

If you click the icon, the profiler will open, showing you the exact queries that were made.

Updating an Object

Once you’ve fetched an object from Doctrine, updating it is easy. Suppose you have a route that maps a product id to an update action in a controller:

public function updateAction($id)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository('AppBundle:Product')->find($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    $product->setName('New product name!');
    $em->flush();

    return $this->redirect($this->generateUrl('homepage'));
}

Updating an object involves just three steps:

  1. fetching the object from Doctrine;
  2. modifying the object;
  3. calling flush() on the entity manager

Notice that calling $em->persist($product) isn’t necessary. Recall that this method simply tells Doctrine to manage or “watch” the $product object. In this case, since you fetched the $product object from Doctrine, it’s already managed.

Deleting an Object

Deleting an object is very similar, but requires a call to the remove() method of the entity manager:

$em->remove($product);
$em->flush();

As you might expect, the remove() method notifies Doctrine that you’d like to remove the given object from the database. The actual DELETE query, however, isn’t actually executed until the flush() method is called.

Querying for Objects

You’ve already seen how the repository object allows you to run basic queries without any work:

$repository->find($id);

$repository->findOneByName('Foo');

Of course, Doctrine also allows you to write more complex queries using the Doctrine Query Language (DQL). DQL is similar to SQL except that you should imagine that you’re querying for one or more objects of an entity class (e.g. Product) instead of querying for rows on a table (e.g. product).

When querying in Doctrine, you have two options: writing pure Doctrine queries or using Doctrine’s Query Builder.

Querying for Objects Using Doctrine’s Query Builder

Imagine that you want to query for products, but only return products that cost more than 19.99, ordered from cheapest to most expensive. You can use Doctrine’s QueryBuilder for this:

$repository = $this->getDoctrine()
    ->getRepository('AppBundle:Product');

$query = $repository->createQueryBuilder('p')
    ->where('p.price > :price')
    ->setParameter('price', '19.99')
    ->orderBy('p.price', 'ASC')
    ->getQuery();

$products = $query->getResult();

The QueryBuilder object contains every method necessary to build your query. By calling the getQuery() method, the query builder returns a normal Query object, which can be used to get the result of the query.

小技巧

Take note of the setParameter() method. When working with Doctrine, it’s always a good idea to set any external values as “placeholders” (:price in the example above) as it prevents SQL injection attacks.

The getResult() method returns an array of results. To get only one result, you can use getSingleResult() (which throws an exception if there is no result) or getOneOrNullResult():

$product = $query->getOneOrNullResult();

For more information on Doctrine’s Query Builder, consult Doctrine’s Query Builder documentation.

Querying for Objects with DQL

Instead of using the QueryBuilder, you can alternatively write the queries directly using DQL:

$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
    'SELECT p
    FROM AppBundle:Product p
    WHERE p.price > :price
    ORDER BY p.price ASC'
)->setParameter('price', '19.99');

$products = $query->getResult();

If you’re comfortable with SQL, then DQL should feel very natural. The biggest difference is that you need to think in terms of “objects” instead of rows in a database. For this reason, you select from the AppBundle:Product object and then alias it as p (as you see, this is equal to what you already did in the previous section).

The DQL syntax is incredibly powerful, allowing you to easily join between entities (the topic of relations will be covered later), group, etc. For more information, see the official Doctrine Query Language documentation.

Custom Repository Classes

In the previous sections, you began constructing and using more complex queries from inside a controller. In order to isolate, test and reuse these queries, it’s a good practice to create a custom repository class for your entity and add methods with your query logic there.

To do this, add the name of the repository class to your mapping definition:

  • Annotations
    // src/AppBundle/Entity/Product.php
    namespace AppBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity(repositoryClass="AppBundle\Entity\ProductRepository")
     */
    class Product
    {
        //...
    }
    
  • YAML
    # src/AppBundle/Resources/config/doctrine/Product.orm.yml
    AppBundle\Entity\Product:
        type: entity
        repositoryClass: AppBundle\Entity\ProductRepository
        # ...
    
  • XML
    <!-- src/AppBundle/Resources/config/doctrine/Product.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity
            name="AppBundle\Entity\Product"
            repository-class="AppBundle\Entity\ProductRepository">
    
            <!-- ... -->
        </entity>
    </doctrine-mapping>
    

Doctrine can generate the repository class for you by running the same command used earlier to generate the missing getter and setter methods:

$ php app/console doctrine:generate:entities AppBundle

Next, add a new method - findAllOrderedByName() - to the newly generated repository class. This method will query for all the Product entities, ordered alphabetically.

// src/AppBundle/Entity/ProductRepository.php
namespace AppBundle\Entity;

use Doctrine\ORM\EntityRepository;

class ProductRepository extends EntityRepository
{
    public function findAllOrderedByName()
    {
        return $this->getEntityManager()
            ->createQuery(
                'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC'
            )
            ->getResult();
    }
}

小技巧

The entity manager can be accessed via $this->getEntityManager() from inside the repository.

You can use this new method just like the default finder methods of the repository:

$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('AppBundle:Product')
    ->findAllOrderedByName();

注解

When using a custom repository class, you still have access to the default finder methods such as find() and findAll().

Entity Relationships/Associations

Suppose that the products in your application all belong to exactly one “category”. In this case, you’ll need a Category object and a way to relate a Product object to a Category object. Start by creating the Category entity. Since you know that you’ll eventually need to persist the class through Doctrine, you can let Doctrine create the class for you.

$ php app/console doctrine:generate:entity \
    --entity="AppBundle:Category" \
    --fields="name:string(255)"

This task generates the Category entity for you, with an id field, a name field and the associated getter and setter functions.

Relationship Mapping Metadata

To relate the Category and Product entities, start by creating a products property on the Category class:

  • Annotations
    // src/AppBundle/Entity/Category.php
    
    // ...
    use Doctrine\Common\Collections\ArrayCollection;
    
    class Category
    {
        // ...
    
        /**
         * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
         */
        protected $products;
    
        public function __construct()
        {
            $this->products = new ArrayCollection();
        }
    }
    
  • YAML
    # src/AppBundle/Resources/config/doctrine/Category.orm.yml
    AppBundle\Entity\Category:
        type: entity
        # ...
        oneToMany:
            products:
                targetEntity: Product
                mappedBy: category
        # don't forget to init the collection in the __construct() method
        # of the entity
    
  • XML
    <!-- src/AppBundle/Resources/config/doctrine/Category.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="AppBundle\Entity\Category">
            <!-- ... -->
            <one-to-many
                field="products"
                target-entity="Product"
                mapped-by="category" />
    
            <!--
                don't forget to init the collection in
                the __construct() method of the entity
            -->
        </entity>
    </doctrine-mapping>
    

First, since a Category object will relate to many Product objects, a products array property is added to hold those Product objects. Again, this isn’t done because Doctrine needs it, but instead because it makes sense in the application for each Category to hold an array of Product objects.

注解

The code in the __construct() method is important because Doctrine requires the $products property to be an ArrayCollection object. This object looks and acts almost exactly like an array, but has some added flexibility. If this makes you uncomfortable, don’t worry. Just imagine that it’s an array and you’ll be in good shape.

小技巧

The targetEntity value in the decorator used above can reference any entity with a valid namespace, not just entities defined in the same namespace. To relate to an entity defined in a different class or bundle, enter a full namespace as the targetEntity.

Next, since each Product class can relate to exactly one Category object, you’ll want to add a $category property to the Product class:

  • Annotations
    // src/AppBundle/Entity/Product.php
    
    // ...
    class Product
    {
        // ...
    
        /**
         * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
         * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
         */
        protected $category;
    }
    
  • YAML
    # src/AppBundle/Resources/config/doctrine/Product.orm.yml
    AppBundle\Entity\Product:
        type: entity
        # ...
        manyToOne:
            category:
                targetEntity: Category
                inversedBy: products
                joinColumn:
                    name: category_id
                    referencedColumnName: id
    
  • XML
    <!-- src/AppBundle/Resources/config/doctrine/Product.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="AppBundle\Entity\Product">
            <!-- ... -->
            <many-to-one
                field="category"
                target-entity="Category"
                inversed-by="products"
                join-column="category">
    
                <join-column name="category_id" referenced-column-name="id" />
            </many-to-one>
        </entity>
    </doctrine-mapping>
    

Finally, now that you’ve added a new property to both the Category and Product classes, tell Doctrine to generate the missing getter and setter methods for you:

$ php app/console doctrine:generate:entities AppBundle

Ignore the Doctrine metadata for a moment. You now have two classes - Category and Product with a natural one-to-many relationship. The Category class holds an array of Product objects and the Product object can hold one Category object. In other words - you’ve built your classes in a way that makes sense for your needs. The fact that the data needs to be persisted to a database is always secondary.

Now, look at the metadata above the $category property on the Product class. The information here tells Doctrine that the related class is Category and that it should store the id of the category record on a category_id field that lives on the product table. In other words, the related Category object will be stored on the $category property, but behind the scenes, Doctrine will persist this relationship by storing the category’s id value on a category_id column of the product table.

_images/doctrine_image_2.png

The metadata above the $products property of the Category object is less important, and simply tells Doctrine to look at the Product.category property to figure out how the relationship is mapped.

Before you continue, be sure to tell Doctrine to add the new category table, and product.category_id column, and new foreign key:

$ php app/console doctrine:schema:update --force

注解

This task should only be really used during development. For a more robust method of systematically updating your production database, read about migrations.

More Information on Associations

This section has been an introduction to one common type of entity relationship, the one-to-many relationship. For more advanced details and examples of how to use other types of relations (e.g. one-to-one, many-to-many), see Doctrine’s Association Mapping Documentation.

注解

If you’re using annotations, you’ll need to prepend all annotations with ORM\ (e.g. ORM\OneToMany), which is not reflected in Doctrine’s documentation. You’ll also need to include the use Doctrine\ORM\Mapping as ORM; statement, which imports the ORM annotations prefix.

Configuration

Doctrine is highly configurable, though you probably won’t ever need to worry about most of its options. To find out more about configuring Doctrine, see the Doctrine section of the config reference.

Lifecycle Callbacks

Sometimes, you need to perform an action right before or after an entity is inserted, updated, or deleted. These types of actions are known as “lifecycle” callbacks, as they’re callback methods that you need to execute during different stages of the lifecycle of an entity (e.g. the entity is inserted, updated, deleted, etc).

If you’re using annotations for your metadata, start by enabling the lifecycle callbacks. This is not necessary if you’re using YAML or XML for your mapping.

/**
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks()
 */
class Product
{
    // ...
}

Now, you can tell Doctrine to execute a method on any of the available lifecycle events. For example, suppose you want to set a createdAt date column to the current date, only when the entity is first persisted (i.e. inserted):

  • Annotations
    // src/AppBundle/Entity/Product.php
    
    /**
     * @ORM\PrePersist
     */
    public function setCreatedAtValue()
    {
        $this->createdAt = new \DateTime();
    }
    
  • YAML
    # src/AppBundle/Resources/config/doctrine/Product.orm.yml
    AppBundle\Entity\Product:
        type: entity
        # ...
        lifecycleCallbacks:
            prePersist: [setCreatedAtValue]
    
  • XML
    <!-- src/AppBundle/Resources/config/doctrine/Product.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="AppBundle\Entity\Product">
            <!-- ... -->
            <lifecycle-callbacks>
                <lifecycle-callback type="prePersist" method="setCreatedAtValue" />
            </lifecycle-callbacks>
        </entity>
    </doctrine-mapping>
    

注解

The above example assumes that you’ve created and mapped a createdAt property (not shown here).

Now, right before the entity is first persisted, Doctrine will automatically call this method and the createdAt field will be set to the current date.

There are several other lifecycle events that you can hook into. For more information on other lifecycle events and lifecycle callbacks in general, see Doctrine’s Lifecycle Events documentation.

Doctrine Field Types Reference

Doctrine comes with numerous field types available. Each of these maps a PHP data type to a specific column type in whatever database you’re using. For each field type, the Column can be configured further, setting the length, nullable behavior, name and other options. To see a list of all available types and more information, see Doctrine’s Mapping Types documentation.

Summary

With Doctrine, you can focus on your objects and how they’re used in your application and worry about database persistence second. This is because Doctrine allows you to use any PHP object to hold your data and relies on mapping metadata information to map an object’s data to a particular database table.

And even though Doctrine revolves around a simple concept, it’s incredibly powerful, allowing you to create complex queries and subscribe to events that allow you to take different actions as objects go through their persistence lifecycle.

Learn more

For more information about Doctrine, see the Doctrine section of the cookbook. Some useful articles might be:

Databases and Propel

One of the most common and challenging tasks for any application involves persisting and reading information to and from a database. Symfony does not come integrated with any ORMs but the Propel integration is easy. To install Propel, read Working With Symfony2 on the Propel documentation.

A Simple Example: A Product

In this section, you’ll configure your database, create a Product object, persist it to the database and fetch it back out.

Configuring the Database

Before you can start, you’ll need to configure your database connection information. By convention, this information is usually configured in an app/config/parameters.yml file:

# app/config/parameters.yml
parameters:
    database_driver:   mysql
    database_host:     localhost
    database_name:     test_project
    database_user:     root
    database_password: password
    database_charset:  UTF8

注解

Defining the configuration via parameters.yml is just a convention. The parameters defined in that file are referenced by the main configuration file when setting up Propel:

These parameters defined in parameters.yml can now be included in the configuration file (config.yml):

propel:
    dbal:
        driver:   "%database_driver%"
        user:     "%database_user%"
        password: "%database_password%"
        dsn:      "%database_driver%:host=%database_host%;dbname=%database_name%;charset=%database_charset%"

Now that Propel knows about your database, Symfony can create the database for you:

$ php app/console propel:database:create

注解

In this example, you have one configured connection, named default. If you want to configure more than one connection, read the PropelBundle configuration section.

Creating a Model Class

In the Propel world, ActiveRecord classes are known as models because classes generated by Propel contain some business logic.

注解

For people who use Symfony with Doctrine2, models are equivalent to entities.

Suppose you’re building an application where products need to be displayed. First, create a schema.xml file inside the Resources/config directory of your AcmeStoreBundle:

<?xml version="1.0" encoding="UTF-8" ?>
<database
    name="default"
    namespace="Acme\StoreBundle\Model"
    defaultIdMethod="native">

    <table name="product">
        <column
            name="id"
            type="integer"
            required="true"
            primaryKey="true"
            autoIncrement="true" />

        <column
            name="name"
            type="varchar"
            primaryString="true"
            size="100" />
        <column
            name="price"
            type="decimal" />

        <column
            name="description"
            type="longvarchar" />
    </table>
</database>
Building the Model

After creating your schema.xml, generate your model from it by running:

$ php app/console propel:model:build

This generates each model class to quickly develop your application in the Model/ directory of the AcmeStoreBundle bundle.

Creating the Database Tables/Schema

Now you have a usable Product class and all you need to persist it. Of course, you don’t yet have the corresponding product table in your database. Fortunately, Propel can automatically create all the database tables needed for every known model in your application. To do this, run:

$ php app/console propel:sql:build
$ php app/console propel:sql:insert --force

Your database now has a fully-functional product table with columns that match the schema you’ve specified.

小技巧

You can run the last three commands combined by using the following command: php app/console propel:build --insert-sql.

Persisting Objects to the Database

Now that you have a Product object and corresponding product table, you’re ready to persist data to the database. From inside a controller, this is pretty easy. Add the following method to the DefaultController of the bundle:

// src/Acme/StoreBundle/Controller/DefaultController.php

// ...
use Acme\StoreBundle\Model\Product;
use Symfony\Component\HttpFoundation\Response;

public function createAction()
{
    $product = new Product();
    $product->setName('A Foo Bar');
    $product->setPrice(19.99);
    $product->setDescription('Lorem ipsum dolor');

    $product->save();

    return new Response('Created product id '.$product->getId());
}

In this piece of code, you instantiate and work with the $product object. When you call the save() method on it, you persist it to the database. No need to use other services, the object knows how to persist itself.

注解

If you’re following along with this example, you’ll need to create a route that points to this action to see it in action.

Fetching Objects from the Database

Fetching an object back from the database is even easier. For example, suppose you’ve configured a route to display a specific Product based on its id value:

// ...
use Acme\StoreBundle\Model\ProductQuery;

public function showAction($id)
{
    $product = ProductQuery::create()
        ->findPk($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    // ... do something, like pass the $product object into a template
}
Updating an Object

Once you’ve fetched an object from Propel, updating it is easy. Suppose you have a route that maps a product id to an update action in a controller:

// ...
use Acme\StoreBundle\Model\ProductQuery;

public function updateAction($id)
{
    $product = ProductQuery::create()
        ->findPk($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    $product->setName('New product name!');
    $product->save();

    return $this->redirect($this->generateUrl('homepage'));
}

Updating an object involves just three steps:

  1. fetching the object from Propel (line 6 - 13);
  2. modifying the object (line 15);
  3. saving it (line 16).
Deleting an Object

Deleting an object is very similar to updating, but requires a call to the delete() method on the object:

$product->delete();
Querying for Objects

Propel provides generated Query classes to run both basic and complex queries without any work:

\Acme\StoreBundle\Model\ProductQuery::create()->findPk($id);

\Acme\StoreBundle\Model\ProductQuery::create()
    ->filterByName('Foo')
    ->findOne();

Imagine that you want to query for products which cost more than 19.99, ordered from cheapest to most expensive. From inside a controller, do the following:

$products = \Acme\StoreBundle\Model\ProductQuery::create()
    ->filterByPrice(array('min' => 19.99))
    ->orderByPrice()
    ->find();

In one line, you get your products in a powerful oriented object way. No need to waste your time with SQL or whatever, Symfony offers fully object oriented programming and Propel respects the same philosophy by providing an awesome abstraction layer.

If you want to reuse some queries, you can add your own methods to the ProductQuery class:

// src/Acme/StoreBundle/Model/ProductQuery.php
class ProductQuery extends BaseProductQuery
{
    public function filterByExpensivePrice()
    {
        return $this
            ->filterByPrice(array('min' => 1000));
    }
}

But note that Propel generates a lot of methods for you and a simple findAllOrderedByName() can be written without any effort:

\Acme\StoreBundle\Model\ProductQuery::create()
    ->orderByName()
    ->find();
Relationships/Associations

Suppose that the products in your application all belong to exactly one “category”. In this case, you’ll need a Category object and a way to relate a Product object to a Category object.

Start by adding the category definition in your schema.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<database
    name="default"
    namespace="Acme\StoreBundle\Model"
    defaultIdMethod="native">

    <table name="product">
        <column
            name="id"
            type="integer"
            required="true"
            primaryKey="true"
            autoIncrement="true" />

        <column
            name="name"
            type="varchar"
            primaryString="true"
            size="100" />

        <column
            name="price"
            type="decimal" />

        <column
            name="description"
            type="longvarchar" />

        <column
            name="category_id"
            type="integer" />

        <foreign-key foreignTable="category">
            <reference local="category_id" foreign="id" />
        </foreign-key>
    </table>

    <table name="category">
        <column
            name="id"
            type="integer"
            required="true"
            primaryKey="true"
            autoIncrement="true" />

        <column
            name="name"
            type="varchar"
            primaryString="true"
            size="100" />
   </table>
</database>

Create the classes:

$ php app/console propel:model:build

Assuming you have products in your database, you don’t want to lose them. Thanks to migrations, Propel will be able to update your database without losing existing data.

$ php app/console propel:migration:generate-diff
$ php app/console propel:migration:migrate

Your database has been updated, you can continue writing your application.

More Information on Associations

You will find more information on relations by reading the dedicated chapter on Relationships.

Lifecycle Callbacks

Sometimes, you need to perform an action right before or after an object is inserted, updated, or deleted. These types of actions are known as “lifecycle” callbacks or “hooks”, as they’re callback methods that you need to execute during different stages of the lifecycle of an object (e.g. the object is inserted, updated, deleted, etc).

To add a hook, just add a new method to the object class:

// src/Acme/StoreBundle/Model/Product.php

// ...
class Product extends BaseProduct
{
    public function preInsert(\PropelPDO $con = null)
    {
        // do something before the object is inserted
    }
}

Propel provides the following hooks:

preInsert()
Code executed before insertion of a new object.
postInsert()
Code executed after insertion of a new object.
preUpdate()
Code executed before update of an existing object.
postUpdate()
Code executed after update of an existing object.
preSave()
Code executed before saving an object (new or existing).
postSave()
Code executed after saving an object (new or existing).
preDelete()
Code executed before deleting an object.
postDelete()
Code executed after deleting an object.
Behaviors

All bundled behaviors in Propel are working with Symfony. To get more information about how to use Propel behaviors, look at the Behaviors reference section.

Commands

You should read the dedicated section for Propel commands in Symfony2.

Testing

Whenever you write a new line of code, you also potentially add new bugs. To build better and more reliable applications, you should test your code using both functional and unit tests.

The PHPUnit Testing Framework

Symfony integrates with an independent library - called PHPUnit - to give you a rich testing framework. This chapter won’t cover PHPUnit itself, but it has its own excellent documentation.

注解

It’s recommended to use the latest stable PHPUnit version (you will have to use version 4.2 or higher to test the Symfony core code itself).

Each test - whether it’s a unit test or a functional test - is a PHP class that should live in the Tests/ subdirectory of your bundles. If you follow this rule, then you can run all of your application’s tests with the following command:

# specify the configuration directory on the command line
$ phpunit -c app/

The -c option tells PHPUnit to look in the app/ directory for a configuration file. If you’re curious about the PHPUnit options, check out the app/phpunit.xml.dist file.

小技巧

Code coverage can be generated with the --coverage-html option.

Unit Tests

A unit test is usually a test against a specific PHP class. If you want to test the overall behavior of your application, see the section about Functional Tests.

Writing Symfony unit tests is no different from writing standard PHPUnit unit tests. Suppose, for example, that you have an incredibly simple class called Calculator in the Utility/ directory of your bundle:

// src/Acme/DemoBundle/Utility/Calculator.php
namespace Acme\DemoBundle\Utility;

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

To test this, create a CalculatorTest file in the Tests/Utility directory of your bundle:

// src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
namespace Acme\DemoBundle\Tests\Utility;

use Acme\DemoBundle\Utility\Calculator;

class CalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testAdd()
    {
        $calc = new Calculator();
        $result = $calc->add(30, 12);

        // assert that your calculator added the numbers correctly!
        $this->assertEquals(42, $result);
    }
}

注解

By convention, the Tests/ sub-directory should replicate the directory of your bundle. So, if you’re testing a class in your bundle’s Utility/ directory, put the test in the Tests/Utility/ directory.

Just like in your real application - autoloading is automatically enabled via the bootstrap.php.cache file (as configured by default in the app/phpunit.xml.dist file).

Running tests for a given file or directory is also very easy:

# run all tests in the Utility directory
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/

# run tests for the Calculator class
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php

# run all tests for the entire Bundle
$ phpunit -c app src/Acme/DemoBundle/
Functional Tests

Functional tests check the integration of the different layers of an application (from the routing to the views). They are no different from unit tests as far as PHPUnit is concerned, but they have a very specific workflow:

  • Make a request;
  • Test the response;
  • Click on a link or submit a form;
  • Test the response;
  • Rinse and repeat.
Your First Functional Test

Functional tests are simple PHP files that typically live in the Tests/Controller directory of your bundle. If you want to test the pages handled by your DemoController class, start by creating a new DemoControllerTest.php file that extends a special WebTestCase class.

For example, the Symfony Standard Edition provides a simple functional test for its DemoController (DemoControllerTest) that reads as follows:

// src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
namespace Acme\DemoBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DemoControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/demo/hello/Fabien');

        $this->assertGreaterThan(
            0,
            $crawler->filter('html:contains("Hello Fabien")')->count()
        );
    }
}

小技巧

To run your functional tests, the WebTestCase class bootstraps the kernel of your application. In most cases, this happens automatically. However, if your kernel is in a non-standard directory, you’ll need to modify your phpunit.xml.dist file to set the KERNEL_DIR environment variable to the directory of your kernel:

<phpunit>
    <!-- ... -->
    <php>
        <server name="KERNEL_DIR" value="/path/to/your/app/" />
    </php>
    <!-- ... -->
</phpunit>

The createClient() method returns a client, which is like a browser that you’ll use to crawl your site:

$crawler = $client->request('GET', '/demo/hello/Fabien');

The request() method (see more about the request method) returns a Crawler object which can be used to select elements in the Response, click on links, and submit forms.

小技巧

The Crawler only works when the response is an XML or an HTML document. To get the raw content response, call $client->getResponse()->getContent().

Click on a link by first selecting it with the Crawler using either an XPath expression or a CSS selector, then use the Client to click on it. For example, the following code finds all links with the text Greet, then selects the second one, and ultimately clicks on it:

$link = $crawler->filter('a:contains("Greet")')->eq(1)->link();

$crawler = $client->click($link);

Submitting a form is very similar; select a form button, optionally override some form values, and submit the corresponding form:

$form = $crawler->selectButton('submit')->form();

// set some values
$form['name'] = 'Lucas';
$form['form_name[subject]'] = 'Hey there!';

// submit the form
$crawler = $client->submit($form);

小技巧

The form can also handle uploads and contains methods to fill in different types of form fields (e.g. select() and tick()). For details, see the Forms section below.

Now that you can easily navigate through an application, use assertions to test that it actually does what you expect it to. Use the Crawler to make assertions on the DOM:

// Assert that the response matches a given CSS selector.
$this->assertGreaterThan(0, $crawler->filter('h1')->count());

Or, test against the Response content directly if you just want to assert that the content contains some text, or if the Response is not an XML/HTML document:

$this->assertRegExp(
    '/Hello Fabien/',
    $client->getResponse()->getContent()
);
Working with the Test Client

The Test Client simulates an HTTP client like a browser and makes requests into your Symfony application:

$crawler = $client->request('GET', '/hello/Fabien');

The request() method takes the HTTP method and a URL as arguments and returns a Crawler instance.

小技巧

Hardcoding the request URLs is a best practice for functional tests. If the test generates URLs using the Symfony router, it won’t detect any change made to the application URLs which may impact the end users.

Use the Crawler to find DOM elements in the Response. These elements can then be used to click on links and submit forms:

$link = $crawler->selectLink('Go elsewhere...')->link();
$crawler = $client->click($link);

$form = $crawler->selectButton('validate')->form();
$crawler = $client->submit($form, array('name' => 'Fabien'));

The click() and submit() methods both return a Crawler object. These methods are the best way to browse your application as it takes care of a lot of things for you, like detecting the HTTP method from a form and giving you a nice API for uploading files.

小技巧

You will learn more about the Link and Form objects in the Crawler section below.

The request method can also be used to simulate form submissions directly or perform more complex requests:

// Directly submit a form (but using the Crawler is easier!)
$client->request('POST', '/submit', array('name' => 'Fabien'));

// Submit a raw JSON string in the request body
$client->request(
    'POST',
    '/submit',
    array(),
    array(),
    array('CONTENT_TYPE' => 'application/json'),
    '{"name":"Fabien"}'
);

// Form submission with a file upload
use Symfony\Component\HttpFoundation\File\UploadedFile;

$photo = new UploadedFile(
    '/path/to/photo.jpg',
    'photo.jpg',
    'image/jpeg',
    123
);
$client->request(
    'POST',
    '/submit',
    array('name' => 'Fabien'),
    array('photo' => $photo)
);

// Perform a DELETE requests, and pass HTTP headers
$client->request(
    'DELETE',
    '/post/12',
    array(),
    array(),
    array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
);

Last but not least, you can force each request to be executed in its own PHP process to avoid any side-effects when working with several clients in the same script:

$client->insulate();
Browsing

The Client supports many operations that can be done in a real browser:

$client->back();
$client->forward();
$client->reload();

// Clears all cookies and the history
$client->restart();
Accessing internal Objects

2.3 新版功能: The getInternalRequest() and getInternalResponse() methods were introduced in Symfony 2.3.

If you use the client to test your application, you might want to access the client’s internal objects:

$history   = $client->getHistory();
$cookieJar = $client->getCookieJar();

You can also get the objects related to the latest request:

// the HttpKernel request instance
$request  = $client->getRequest();

// the BrowserKit request instance
$request  = $client->getInternalRequest();

// the HttpKernel response instance
$response = $client->getResponse();

// the BrowserKit response instance
$response = $client->getInternalResponse();

$crawler  = $client->getCrawler();

If your requests are not insulated, you can also access the Container and the Kernel:

$container = $client->getContainer();
$kernel    = $client->getKernel();
Accessing the Container

It’s highly recommended that a functional test only tests the Response. But under certain very rare circumstances, you might want to access some internal objects to write assertions. In such cases, you can access the dependency injection container:

$container = $client->getContainer();

Be warned that this does not work if you insulate the client or if you use an HTTP layer. For a list of services available in your application, use the container:debug console task.

小技巧

If the information you need to check is available from the profiler, use it instead.

Accessing the Profiler Data

On each request, you can enable the Symfony profiler to collect data about the internal handling of that request. For example, the profiler could be used to verify that a given page executes less than a certain number of database queries when loading.

To get the Profiler for the last request, do the following:

// enable the profiler for the very next request
$client->enableProfiler();

$crawler = $client->request('GET', '/profiler');

// get the profile
$profile = $client->getProfile();

For specific details on using the profiler inside a test, see the How to Use the Profiler in a Functional Test cookbook entry.

Redirecting

When a request returns a redirect response, the client does not follow it automatically. You can examine the response and force a redirection afterwards with the followRedirect() method:

$crawler = $client->followRedirect();

If you want the client to automatically follow all redirects, you can force him with the followRedirects() method:

$client->followRedirects();

If you pass false to the followRedirects() method, the redirects will no longer be followed:

$client->followRedirects(false);
The Crawler

A Crawler instance is returned each time you make a request with the Client. It allows you to traverse HTML documents, select nodes, find links and forms.

Traversing

Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML document. For example, the following finds all input[type=submit] elements, selects the last one on the page, and then selects its immediate parent element:

$newCrawler = $crawler->filter('input[type=submit]')
    ->last()
    ->parents()
    ->first()
;

Many other methods are also available:

filter('h1.title')
Nodes that match the CSS selector.
filterXpath('h1')
Nodes that match the XPath expression.
eq(1)
Node for the specified index.
first()
First node.
last()
Last node.
siblings()
Siblings.
nextAll()
All following siblings.
previousAll()
All preceding siblings.
parents()
Returns the parent nodes.
children()
Returns children nodes.
reduce($lambda)
Nodes for which the callable does not return false.

Since each of these methods returns a new Crawler instance, you can narrow down your node selection by chaining the method calls:

$crawler
    ->filter('h1')
    ->reduce(function ($node, $i) {
        if (!$node->getAttribute('class')) {
            return false;
        }
    })
    ->first()
;

小技巧

Use the count() function to get the number of nodes stored in a Crawler: count($crawler)

Extracting Information

The Crawler can extract information from the nodes:

// Returns the attribute value for the first node
$crawler->attr('class');

// Returns the node value for the first node
$crawler->text();

// Extracts an array of attributes for all nodes
// (_text returns the node value)
// returns an array for each element in crawler,
// each with the value and href
$info = $crawler->extract(array('_text', 'href'));

// Executes a lambda for each node and return an array of results
$data = $crawler->each(function ($node, $i) {
    return $node->attr('href');
});
Forms

Just like links, you select forms with the selectButton() method:

$buttonCrawlerNode = $crawler->selectButton('submit');

注解

Notice that you select form buttons and not forms as a form can have several buttons; if you use the traversing API, keep in mind that you must look for a button.

The selectButton() method can select button tags and submit input tags. It uses several parts of the buttons to find them:

  • The value attribute value;
  • The id or alt attribute value for images;
  • The id or name attribute value for button tags.

Once you have a Crawler representing a button, call the form() method to get a Form instance for the form wrapping the button node:

$form = $buttonCrawlerNode->form();

When calling the form() method, you can also pass an array of field values that overrides the default ones:

$form = $buttonCrawlerNode->form(array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

And if you want to simulate a specific HTTP method for the form, pass it as a second argument:

$form = $buttonCrawlerNode->form(array(), 'DELETE');

The Client can submit Form instances:

$client->submit($form);

The field values can also be passed as a second argument of the submit() method:

$client->submit($form, array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

For more complex situations, use the Form instance as an array to set the value of each field individually:

// Change the value of a field
$form['name'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';

There is also a nice API to manipulate the values of the fields according to their type:

// Select an option or a radio
$form['country']->select('France');

// Tick a checkbox
$form['like_symfony']->tick();

// Upload a file
$form['photo']->upload('/path/to/lucas.jpg');

小技巧

You can get the values that will be submitted by calling the getValues() method on the Form object. The uploaded files are available in a separate array returned by getFiles(). The getPhpValues() and getPhpFiles() methods also return the submitted values, but in the PHP format (it converts the keys with square brackets notation - e.g. my_form[subject] - to PHP arrays).

Testing Configuration

The Client used by functional tests creates a Kernel that runs in a special test environment. Since Symfony loads the app/config/config_test.yml in the test environment, you can tweak any of your application’s settings specifically for testing.

For example, by default, the Swift Mailer is configured to not actually deliver emails in the test environment. You can see this under the swiftmailer configuration option:

  • YAML
    # app/config/config_test.yml
    
    # ...
    swiftmailer:
        disable_delivery: true
    
  • XML
    <!-- app/config/config_test.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/swiftmailer
            http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd">
    
        <!-- ... -->
        <swiftmailer:config disable-delivery="true" />
    </container>
    
  • PHP
    // app/config/config_test.php
    
    // ...
    $container->loadFromExtension('swiftmailer', array(
        'disable_delivery' => true,
    ));
    

You can also use a different environment entirely, or override the default debug mode (true) by passing each as options to the createClient() method:

$client = static::createClient(array(
    'environment' => 'my_test_env',
    'debug'       => false,
));

If your application behaves according to some HTTP headers, pass them as the second argument of createClient():

$client = static::createClient(array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

You can also override HTTP headers on a per request basis:

$client->request('GET', '/', array(), array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

小技巧

The test client is available as a service in the container in the test environment (or wherever the framework.test option is enabled). This means you can override the service entirely if you need to.

PHPUnit Configuration

Each application has its own PHPUnit configuration, stored in the app/phpunit.xml.dist file. You can edit this file to change the defaults or create an app/phpunit.xml file to set up a configuration for your local machine only.

小技巧

Store the app/phpunit.xml.dist file in your code repository and ignore the app/phpunit.xml file.

By default, only the tests from your own custom bundles stored in the standard directories src/*/*Bundle/Tests, src/*/Bundle/*Bundle/Tests, src/*Bundle/Tests are run by the phpunit command, as configured in the app/phpunit.xml.dist file:

<!-- app/phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>../src/*/*Bundle/Tests</directory>
            <directory>../src/*/Bundle/*Bundle/Tests</directory>
            <directory>../src/*Bundle/Tests</directory>
        </testsuite>
    </testsuites>
    <!-- ... -->
</phpunit>

But you can easily add more directories. For instance, the following configuration adds tests from a custom lib/tests directory:

<!-- app/phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <testsuites>
        <testsuite name="Project Test Suite">
            <!-- ... --->
            <directory>../lib/tests</directory>
        </testsuite>
    </testsuites>
    <!-- ... --->
</phpunit>

To include other directories in the code coverage, also edit the <filter> section:

<!-- app/phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <filter>
        <whitelist>
            <!-- ... -->
            <directory>../lib</directory>
            <exclude>
                <!-- ... -->
                <directory>../lib/tests</directory>
            </exclude>
        </whitelist>
    </filter>
    <!-- ... --->
</phpunit>

Validation

Validation is a very common task in web applications. Data entered in forms needs to be validated. Data also needs to be validated before it is written into a database or passed to a web service.

Symfony ships with a Validator component that makes this task easy and transparent. This component is based on the JSR303 Bean Validation specification.

The Basics of Validation

The best way to understand validation is to see it in action. To start, suppose you’ve created a plain-old-PHP object that you need to use somewhere in your application:

// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;

class Author
{
    public $name;
}

So far, this is just an ordinary class that serves some purpose inside your application. The goal of validation is to tell you if the data of an object is valid. For this to work, you’ll configure a list of rules (called constraints) that the object must follow in order to be valid. These rules can be specified via a number of different formats (YAML, XML, annotations, or PHP).

For example, to guarantee that the $name property is not empty, add the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            name:
                - NotBlank: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\NotBlank()
         */
        public $name;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="name">
                <constraint name="NotBlank" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\NotBlank;
    
    class Author
    {
        public $name;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('name', new NotBlank());
        }
    }
    

小技巧

Protected and private properties can also be validated, as well as “getter” methods (see Constraint Targets).

Using the validator Service

Next, to actually validate an Author object, use the validate method on the validator service (class Validator). The job of the validator is easy: to read the constraints (i.e. rules) of a class and verify if the data on the object satisfies those constraints. If validation fails, a non-empty list of errors (class ConstraintViolationList) is returned. Take this simple example from inside a controller:

// ...
use Symfony\Component\HttpFoundation\Response;
use Acme\BlogBundle\Entity\Author;

public function indexAction()
{
    $author = new Author();
    // ... do something to the $author object

    $validator = $this->get('validator');
    $errors = $validator->validate($author);

    if (count($errors) > 0) {
        /*
         * Uses a __toString method on the $errors variable which is a
         * ConstraintViolationList object. This gives us a nice string
         * for debugging
         */
        $errorsString = (string) $errors;

        return new Response($errorsString);
    }

    return new Response('The author is valid! Yes!');
}

If the $name property is empty, you will see the following error message:

Acme\BlogBundle\Author.name:
    This value should not be blank

If you insert a value into the name property, the happy success message will appear.

小技巧

Most of the time, you won’t interact directly with the validator service or need to worry about printing out the errors. Most of the time, you’ll use validation indirectly when handling submitted form data. For more information, see the Validation and Forms.

You could also pass the collection of errors into a template.

if (count($errors) > 0) {
    return $this->render('AcmeBlogBundle:Author:validate.html.twig', array(
        'errors' => $errors,
    ));
}

Inside the template, you can output the list of errors exactly as needed:

  • Twig
    {# src/Acme/BlogBundle/Resources/views/Author/validate.html.twig #}
    <h3>The author has the following errors</h3>
    <ul>
    {% for error in errors %}
        <li>{{ error.message }}</li>
    {% endfor %}
    </ul>
    
  • PHP
    <!-- src/Acme/BlogBundle/Resources/views/Author/validate.html.php -->
    <h3>The author has the following errors</h3>
    <ul>
    <?php foreach ($errors as $error): ?>
        <li><?php echo $error->getMessage() ?></li>
    <?php endforeach ?>
    </ul>
    

注解

Each validation error (called a “constraint violation”), is represented by a ConstraintViolation object.

Validation and Forms

The validator service can be used at any time to validate any object. In reality, however, you’ll usually work with the validator indirectly when working with forms. Symfony’s form library uses the validator service internally to validate the underlying object after values have been submitted. The constraint violations on the object are converted into FieldError objects that can easily be displayed with your form. The typical form submission workflow looks like the following from inside a controller:

// ...
use Acme\BlogBundle\Entity\Author;
use Acme\BlogBundle\Form\AuthorType;
use Symfony\Component\HttpFoundation\Request;

public function updateAction(Request $request)
{
    $author = new Author();
    $form = $this->createForm(new AuthorType(), $author);

    $form->handleRequest($request);

    if ($form->isValid()) {
        // the validation passed, do something with the $author object

        return $this->redirect($this->generateUrl(...));
    }

    return $this->render('BlogBundle:Author:form.html.twig', array(
        'form' => $form->createView(),
    ));
}

注解

This example uses an AuthorType form class, which is not shown here.

For more information, see the Forms chapter.

Configuration

The Symfony validator is enabled by default, but you must explicitly enable annotations if you’re using the annotation method to specify your constraints:

  • YAML
    # app/config/config.yml
    framework:
        validation: { enable_annotations: true }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:validation enable-annotations="true" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'validation' => array(
            'enable_annotations' => true,
        ),
    ));
    
Constraints

The validator is designed to validate objects against constraints (i.e. rules). In order to validate an object, simply map one or more constraints to its class and then pass it to the validator service.

Behind the scenes, a constraint is simply a PHP object that makes an assertive statement. In real life, a constraint could be: “The cake must not be burned”. In Symfony, constraints are similar: they are assertions that a condition is true. Given a value, a constraint will tell you if that value adheres to the rules of the constraint.

Supported Constraints

Symfony packages many of the most commonly-needed constraints:

Basic Constraints

These are the basic constraints: use them to assert very basic things about the value of properties or the return value of methods on your object.

String Constraints
Number Constraints
Date Constraints
File Constraints
Financial and other Number Constraints
Other Constraints

You can also create your own custom constraints. This topic is covered in the “How to Create a custom Validation Constraint” article of the cookbook.

Constraint Configuration

Some constraints, like NotBlank, are simple whereas others, like the Choice constraint, have several configuration options available. Suppose that the Author class has another property, gender that can be set to either “male” or “female”:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            gender:
                - Choice: { choices: [male, female], message: Choose a valid gender. }
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Choice(
         *     choices = { "male", "female" },
         *     message = "Choose a valid gender."
         * )
         */
        public $gender;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="gender">
                <constraint name="Choice">
                    <option name="choices">
                        <value>male</value>
                        <value>female</value>
                    </option>
                    <option name="message">Choose a valid gender.</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\Choice;
    
    class Author
    {
        public $gender;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('gender', new Choice(array(
                'choices' => array('male', 'female'),
                'message' => 'Choose a valid gender.',
            )));
        }
    }
    

The options of a constraint can always be passed in as an array. Some constraints, however, also allow you to pass the value of one, “default”, option in place of the array. In the case of the Choice constraint, the choices options can be specified in this way.

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            gender:
                - Choice: [male, female]
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Choice({"male", "female"})
         */
        protected $gender;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="gender">
                <constraint name="Choice">
                    <value>male</value>
                    <value>female</value>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\Choice;
    
    class Author
    {
        protected $gender;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint(
                'gender',
                new Choice(array('male', 'female'))
            );
        }
    }
    

This is purely meant to make the configuration of the most common option of a constraint shorter and quicker.

If you’re ever unsure of how to specify an option, either check the API documentation for the constraint or play it safe by always passing in an array of options (the first method shown above).

Translation Constraint Messages

For information on translating the constraint messages, see Translating Constraint Messages.

Constraint Targets

Constraints can be applied to a class property (e.g. name) or a public getter method (e.g. getFullName). The first is the most common and easy to use, but the second allows you to specify more complex validation rules.

Properties

Validating class properties is the most basic validation technique. Symfony allows you to validate private, protected or public properties. The next listing shows you how to configure the $firstName property of an Author class to have at least 3 characters.

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            firstName:
                - NotBlank: ~
                - Length:
                    min: 3
    
  • Annotations
    // Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\NotBlank()
         * @Assert\Length(min = "3")
         */
        private $firstName;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="firstName">
                <constraint name="NotBlank" />
                <constraint name="Length">
                    <option name="min">3</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\Length;
    
    class Author
    {
        private $firstName;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', new NotBlank());
            $metadata->addPropertyConstraint(
                'firstName',
                new Length(array("min" => 3)));
        }
    }
    
Getters

Constraints can also be applied to the return value of a method. Symfony allows you to add a constraint to any public method whose name starts with “get” or “is”. In this guide, both of these types of methods are referred to as “getters”.

The benefit of this technique is that it allows you to validate your object dynamically. For example, suppose you want to make sure that a password field doesn’t match the first name of the user (for security reasons). You can do this by creating an isPasswordLegal method, and then asserting that this method must return true:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        getters:
            passwordLegal:
                - "True": { message: "The password cannot match your first name" }
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\True(message = "The password cannot match your first name")
         */
        public function isPasswordLegal()
        {
            // return true or false
        }
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <getter property="passwordLegal">
                <constraint name="True">
                    <option name="message">The password cannot match your first name</option>
                </constraint>
            </getter>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\True;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addGetterConstraint('passwordLegal', new True(array(
                'message' => 'The password cannot match your first name',
            )));
        }
    }
    

Now, create the isPasswordLegal() method, and include the logic you need:

public function isPasswordLegal()
{
    return $this->firstName != $this->password;
}

注解

The keen-eyed among you will have noticed that the prefix of the getter (“get” or “is”) is omitted in the mapping. This allows you to move the constraint to a property with the same name later (or vice versa) without changing your validation logic.

Classes

Some constraints apply to the entire class being validated. For example, the Callback constraint is a generic constraint that’s applied to the class itself. When that class is validated, methods specified by that constraint are simply executed so that each can provide more custom validation.

Validation Groups

So far, you’ve been able to add constraints to a class and ask whether or not that class passes all the defined constraints. In some cases, however, you’ll need to validate an object against only some constraints on that class. To do this, you can organize each constraint into one or more “validation groups”, and then apply validation against just one group of constraints.

For example, suppose you have a User class, which is used both when a user registers and when a user updates their contact information later:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\User:
        properties:
            email:
                - Email: { groups: [registration] }
            password:
                - NotBlank: { groups: [registration] }
                - Length: { min: 7, groups: [registration] }
            city:
                - Length:
                    min: 2
    
  • Annotations
    // src/Acme/BlogBundle/Entity/User.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Security\Core\User\UserInterface;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User implements UserInterface
    {
        /**
        * @Assert\Email(groups={"registration"})
        */
        private $email;
    
        /**
        * @Assert\NotBlank(groups={"registration"})
        * @Assert\Length(min=7, groups={"registration"})
        */
        private $password;
    
        /**
        * @Assert\Length(min = "2")
        */
        private $city;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\User">
            <property name="email">
                <constraint name="Email">
                    <option name="groups">
                        <value>registration</value>
                    </option>
                </constraint>
            </property>
            <property name="password">
                <constraint name="NotBlank">
                    <option name="groups">
                        <value>registration</value>
                    </option>
                </constraint>
                <constraint name="Length">
                    <option name="min">7</option>
                    <option name="groups">
                        <value>registration</value>
                    </option>
                </constraint>
            </property>
            <property name="city">
                <constraint name="Length">
                    <option name="min">7</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/User.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\Email;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\Length;
    
    class User
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('email', new Email(array(
                'groups' => array('registration'),
            )));
    
            $metadata->addPropertyConstraint('password', new NotBlank(array(
                'groups' => array('registration'),
            )));
            $metadata->addPropertyConstraint('password', new Length(array(
                'min'  => 7,
                'groups' => array('registration')
            )));
    
            $metadata->addPropertyConstraint(
                'city',
                Length(array("min" => 3)));
        }
    }
    

With this configuration, there are three validation groups:

Default
Contains the constraints in the current class and all referenced classes that belong to no other group.
User
Equivalent to all constraints of the User object in the Default group. This is always the name of the class. The difference between this and Default is explained below.
registration
Contains the constraints on the email and password fields only.

Constraints in the Default group of a class are the constraints that have either no explicit group configured or that are configured to a group equal to the class name or the string Default.

警告

When validating just the User object, there is no difference between the Default group and the User group. But, there is a difference if User has embedded objects. For example, imagine User has an address property that contains some Address object and that you’ve added the Valid constraint to this property so that it’s validated when you validate the User object.

If you validate User using the Default group, then any constraints on the Address class that are in the Default group will be used. But, if you validate User using the User validation group, then only constraints on the Address class with the User group will be validated.

In other words, the Default group and the class name group (e.g. User) are identical, except when the class is embedded in another object that’s actually the one being validated.

If you have inheritance (e.g. User extends BaseUser) and you validate with the class name of the subclass (i.e. User), then all constraints in the User and BaseUser will be validated. However, if you validate using the base class (i.e. BaseUser), then only the default constraints in the BaseUser class will be validated.

To tell the validator to use a specific group, pass one or more group names as the second argument to the validate() method:

$errors = $validator->validate($author, array('registration'));

If no groups are specified, all constraints that belong in group Default will be applied.

Of course, you’ll usually work with validation indirectly through the form library. For information on how to use validation groups inside forms, see Validation Groups.

Group Sequence

In some cases, you want to validate your groups by steps. To do this, you can use the GroupSequence feature. In this case, an object defines a group sequence, which determines the order groups should be validated.

For example, suppose you have a User class and want to validate that the username and the password are different only if all other validation passes (in order to avoid multiple error messages).

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\User:
        group_sequence:
            - User
            - Strict
        getters:
            passwordLegal:
                - "True":
                    message: "The password cannot match your username"
                    groups: [Strict]
        properties:
            username:
                - NotBlank: ~
            password:
                - NotBlank: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/User.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Security\Core\User\UserInterface;
    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @Assert\GroupSequence({"User", "Strict"})
     */
    class User implements UserInterface
    {
        /**
        * @Assert\NotBlank
        */
        private $username;
    
        /**
        * @Assert\NotBlank
        */
        private $password;
    
        /**
         * @Assert\True(message="The password cannot match your username", groups={"Strict"})
         */
        public function isPasswordLegal()
        {
            return ($this->username !== $this->password);
        }
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\User">
            <property name="username">
                <constraint name="NotBlank" />
            </property>
            <property name="password">
                <constraint name="NotBlank" />
            </property>
            <getter property="passwordLegal">
                <constraint name="True">
                    <option name="message">The password cannot match your username</option>
                    <option name="groups">
                        <value>Strict</value>
                    </option>
                </constraint>
            </getter>
            <group-sequence>
                <value>User</value>
                <value>Strict</value>
            </group-sequence>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/User.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint(
                'username',
                new Assert\NotBlank()
            );
            $metadata->addPropertyConstraint(
                'password',
                new Assert\NotBlank()
            );
    
            $metadata->addGetterConstraint(
                'passwordLegal',
                new Assert\True(array(
                    'message' => 'The password cannot match your first name',
                    'groups'  => array('Strict'),
                ))
            );
    
            $metadata->setGroupSequence(array('User', 'Strict'));
        }
    }
    

In this example, it will first validate all constraints in the group User (which is the same as the Default group). Only if all constraints in that group are valid, the second group, Strict, will be validated.

警告

As you have already seen in the previous section, the Default group and the group containing the class name (e.g. User) were identical. However, when using Group Sequences, they are no longer identical. The Default group will now reference the group sequence, instead of all constraints that do not belong to any group.

This means that you have to use the {ClassName} (e.g. User) group when specifying a group sequence. When using Default, you get an infinite recursion (as the Default group references the group sequence, which will contain the Default group which references the same group sequence, ...).

Group Sequence Providers

Imagine a User entity which can be a normal user or a premium user. When it’s a premium user, some extra constraints should be added to the user entity (e.g. the credit card details). To dynamically determine which groups should be activated, you can create a Group Sequence Provider. First, create the entity and a new constraint group called Premium:

  • YAML
    # src/Acme/DemoBundle/Resources/config/validation.yml
    Acme\DemoBundle\Entity\User:
        properties:
            name:
                - NotBlank: ~
            creditCard:
                - CardScheme:
                    schemes: [VISA]
                    groups: [Premium]
    
  • Annotations
    // src/Acme/DemoBundle/Entity/User.php
    namespace Acme\DemoBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        // ...
    
        /**
         * @Assert\NotBlank()
         */
        private $name;
    
        /**
         * @Assert\CardScheme(
         *     schemes={"VISA"},
         *     groups={"Premium"},
         * )
         */
        private $creditCard;
    }
    
  • XML
    <!-- src/Acme/DemoBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\DemoBundle\Entity\User">
            <property name="name">
                <constraint name="NotBlank" />
            </property>
    
            <property name="creditCard">
                <constraint name="CardScheme">
                    <option name="schemes">
                        <value>VISA</value>
                    </option>
                    <option name="groups">
                        <value>Premium</value>
                    </option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/DemoBundle/Entity/User.php
    namespace Acme\DemoBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    
    class User
    {
        private $name;
        private $creditCard;
    
        // ...
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('name', new Assert\NotBlank());
            $metadata->addPropertyConstraint('creditCard', new Assert\CardScheme(
                'schemes' => array('VISA'),
                'groups'  => array('Premium'),
            ));
        }
    }
    

Now, change the User class to implement GroupSequenceProviderInterface and add the getGroupSequence(), which should return an array of groups to use:

// src/Acme/DemoBundle/Entity/User.php
namespace Acme\DemoBundle\Entity;

// ...
use Symfony\Component\Validator\GroupSequenceProviderInterface;

class User implements GroupSequenceProviderInterface
{
    // ...

    public function getGroupSequence()
    {
        $groups = array('User');

        if ($this->isPremium()) {
            $groups[] = 'Premium';
        }

        return $groups;
    }
}

At last, you have to notify the Validator component that your User class provides a sequence of groups to be validated:

  • YAML
    # src/Acme/DemoBundle/Resources/config/validation.yml
    Acme\DemoBundle\Entity\User:
        group_sequence_provider: true
    
  • Annotations
    // src/Acme/DemoBundle/Entity/User.php
    namespace Acme\DemoBundle\Entity;
    
    // ...
    
    /**
     * @Assert\GroupSequenceProvider
     */
    class User implements GroupSequenceProviderInterface
    {
        // ...
    }
    
  • XML
    <!-- src/Acme/DemoBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
            http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\DemoBundle\Entity\User">
            <group-sequence-provider />
            <!-- ... -->
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/DemoBundle/Entity/User.php
    namespace Acme\DemoBundle\Entity;
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    
    class User implements GroupSequenceProviderInterface
    {
        // ...
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->setGroupSequenceProvider(true);
            // ...
        }
    }
    
Validating Values and Arrays

So far, you’ve seen how you can validate entire objects. But sometimes, you just want to validate a simple value - like to verify that a string is a valid email address. This is actually pretty easy to do. From inside a controller, it looks like this:

use Symfony\Component\Validator\Constraints\Email;
// ...

public function addEmailAction($email)
{
    $emailConstraint = new Email();
    // all constraint "options" can be set this way
    $emailConstraint->message = 'Invalid email address';

    // use the validator to validate the value
    $errorList = $this->get('validator')->validateValue(
        $email,
        $emailConstraint
    );

    if (count($errorList) == 0) {
        // this IS a valid email address, do something
    } else {
        // this is *not* a valid email address
        $errorMessage = $errorList[0]->getMessage();

        // ... do something with the error
    }

    // ...
}

By calling validateValue on the validator, you can pass in a raw value and the constraint object that you want to validate that value against. A full list of the available constraints - as well as the full class name for each constraint - is available in the constraints reference section.

The validateValue method returns a ConstraintViolationList object, which acts just like an array of errors. Each error in the collection is a ConstraintViolation object, which holds the error message on its getMessage method.

Final Thoughts

The Symfony validator is a powerful tool that can be leveraged to guarantee that the data of any object is “valid”. The power behind validation lies in “constraints”, which are rules that you can apply to properties or getter methods of your object. And while you’ll most commonly use the validation framework indirectly when using forms, remember that it can be used anywhere to validate any object.

Learn more from the Cookbook

Forms

Dealing with HTML forms is one of the most common - and challenging - tasks for a web developer. Symfony integrates a Form component that makes dealing with forms easy. In this chapter, you’ll build a complex form from the ground up, learning the most important features of the form library along the way.

注解

The Symfony Form component is a standalone library that can be used outside of Symfony projects. For more information, see the Form component documentation on GitHub.

Creating a Simple Form

Suppose you’re building a simple todo list application that will need to display “tasks”. Because your users will need to edit and create tasks, you’re going to need to build a form. But before you begin, first focus on the generic Task class that represents and stores the data for a single task:

// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;

class Task
{
    protected $task;
    protected $dueDate;

    public function getTask()
    {
        return $this->task;
    }

    public function setTask($task)
    {
        $this->task = $task;
    }

    public function getDueDate()
    {
        return $this->dueDate;
    }

    public function setDueDate(\DateTime $dueDate = null)
    {
        $this->dueDate = $dueDate;
    }
}

This class is a “plain-old-PHP-object” because, so far, it has nothing to do with Symfony or any other library. It’s quite simply a normal PHP object that directly solves a problem inside your application (i.e. the need to represent a task in your application). Of course, by the end of this chapter, you’ll be able to submit data to a Task instance (via an HTML form), validate its data, and persist it to the database.

Building the Form

Now that you’ve created a Task class, the next step is to create and render the actual HTML form. In Symfony, this is done by building a form object and then rendering it in a template. For now, this can all be done from inside a controller:

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use AppBundle\Entity\Task;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    public function newAction(Request $request)
    {
        // create a task and give it some dummy data for this example
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));

        $form = $this->createFormBuilder($task)
            ->add('task', 'text')
            ->add('dueDate', 'date')
            ->add('save', 'submit', array('label' => 'Create Task'))
            ->getForm();

        return $this->render('Default/new.html.twig', array(
            'form' => $form->createView(),
        ));
    }
}

小技巧

This example shows you how to build your form directly in the controller. Later, in the “Creating Form Classes” section, you’ll learn how to build your form in a standalone class, which is recommended as your form becomes reusable.

Creating a form requires relatively little code because Symfony form objects are built with a “form builder”. The form builder’s purpose is to allow you to write simple form “recipes”, and have it do all the heavy-lifting of actually building the form.

In this example, you’ve added two fields to your form - task and dueDate - corresponding to the task and dueDate properties of the Task class. You’ve also assigned each a “type” (e.g. text, date), which, among other things, determines which HTML form tag(s) is rendered for that field.

Finally, you added a submit button with a custom label for submitting the form to the server.

2.3 新版功能: Support for submit buttons was introduced in Symfony 2.3. Before that, you had to add buttons to the form’s HTML manually.

Symfony comes with many built-in types that will be discussed shortly (see Built-in Field Types).

Rendering the Form

Now that the form has been created, the next step is to render it. This is done by passing a special form “view” object to your template (notice the $form->createView() in the controller above) and using a set of form helper functions:

  • Twig
    {# app/Resources/views/Default/new.html.twig #}
    {{ form_start(form) }}
    {{ form_widget(form) }}
    {{ form_end(form) }}
    
  • PHP
    <!-- app/Resources/views/Default/new.html.php -->
    <?php echo $view['form']->start($form) ?>
    <?php echo $view['form']->widget($form) ?>
    <?php echo $view['form']->end($form) ?>
    
_images/form-simple.png

注解

This example assumes that you submit the form in a “POST” request and to the same URL that it was displayed in. You will learn later how to change the request method and the target URL of the form.

That’s it! Just three lines are needed to render the complete form:

form_start(form)
Renders the start tag of the form, including the correct enctype attribute when using file uploads.
form_widget(form)
Renders all the fields, which includes the field element itself, a label and any validation error messages for the field.
form_end(form)
Renders the end tag of the form and any fields that have not yet been rendered, in case you rendered each field yourself. This is useful for rendering hidden fields and taking advantage of the automatic CSRF Protection.

参见

As easy as this is, it’s not very flexible (yet). Usually, you’ll want to render each form field individually so you can control how the form looks. You’ll learn how to do that in the “Rendering a Form in a Template” section.

Before moving on, notice how the rendered task input field has the value of the task property from the $task object (i.e. “Write a blog post”). This is the first job of a form: to take data from an object and translate it into a format that’s suitable for being rendered in an HTML form.

小技巧

The form system is smart enough to access the value of the protected task property via the getTask() and setTask() methods on the Task class. Unless a property is public, it must have a “getter” and “setter” method so that the Form component can get and put data onto the property. For a Boolean property, you can use an “isser” or “hasser” method (e.g. isPublished() or hasReminder()) instead of a getter (e.g. getPublished() or getReminder()).

Handling Form Submissions

The second job of a form is to translate user-submitted data back to the properties of an object. To make this happen, the submitted data from the user must be written into the form. Add the following functionality to your controller:

// ...
use Symfony\Component\HttpFoundation\Request;

public function newAction(Request $request)
{
    // just setup a fresh $task object (remove the dummy data)
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task', 'text')
        ->add('dueDate', 'date')
        ->add('save', 'submit', array('label' => 'Create Task'))
        ->getForm();

    $form->handleRequest($request);

    if ($form->isValid()) {
        // perform some action, such as saving the task to the database

        return $this->redirect($this->generateUrl('task_success'));
    }

    // ...
}

2.3 新版功能: The handleRequest() method was introduced in Symfony 2.3. Previously, the $request was passed to the submit method - a strategy which is deprecated and will be removed in Symfony 3.0. For details on that method, see Passing a Request to Form::submit() (Deprecated).

This controller follows a common pattern for handling forms, and has three possible paths:

  1. When initially loading the page in a browser, the form is simply created and rendered. handleRequest() recognizes that the form was not submitted and does nothing. isValid() returns false if the form was not submitted.

  2. When the user submits the form, handleRequest() recognizes this and immediately writes the submitted data back into the task and dueDate properties of the $task object. Then this object is validated. If it is invalid (validation is covered in the next section), isValid() returns false again, so the form is rendered together with all validation errors;

    注解

    You can use the method isSubmitted() to check whether a form was submitted, regardless of whether or not the submitted data is actually valid.

  3. When the user submits the form with valid data, the submitted data is again written into the form, but this time isValid() returns true. Now you have the opportunity to perform some actions using the $task object (e.g. persisting it to the database) before redirecting the user to some other page (e.g. a “thank you” or “success” page).

    注解

    Redirecting a user after a successful form submission prevents the user from being able to hit the “Refresh” button of their browser and re-post the data.

参见

If you need more control over exactly when your form is submitted or which data is passed to it, you can use the submit() for this. Read more about it in the cookbook.

Submitting Forms with Multiple Buttons

2.3 新版功能: Support for buttons in forms was introduced in Symfony 2.3.

When your form contains more than one submit button, you will want to check which of the buttons was clicked to adapt the program flow in your controller. To do this, add a second button with the caption “Save and add” to your form:

$form = $this->createFormBuilder($task)
    ->add('task', 'text')
    ->add('dueDate', 'date')
    ->add('save', 'submit', array('label' => 'Create Task'))
    ->add('saveAndAdd', 'submit', array('label' => 'Save and Add'))
    ->getForm();

In your controller, use the button’s isClicked() method for querying if the “Save and add” button was clicked:

if ($form->isValid()) {
    // ... perform some action, such as saving the task to the database

    $nextAction = $form->get('saveAndAdd')->isClicked()
        ? 'task_new'
        : 'task_success';

    return $this->redirect($this->generateUrl($nextAction));
}
Form Validation

In the previous section, you learned how a form can be submitted with valid or invalid data. In Symfony, validation is applied to the underlying object (e.g. Task). In other words, the question isn’t whether the “form” is valid, but whether or not the $task object is valid after the form has applied the submitted data to it. Calling $form->isValid() is a shortcut that asks the $task object whether or not it has valid data.

Validation is done by adding a set of rules (called constraints) to a class. To see this in action, add validation constraints so that the task field cannot be empty and the dueDate field cannot be empty and must be a valid DateTime object.

  • YAML
    # AppBundle/Resources/config/validation.yml
    AppBundle\Entity\Task:
        properties:
            task:
                - NotBlank: ~
            dueDate:
                - NotBlank: ~
                - Type: \DateTime
    
  • Annotations
    // AppBundle/Entity/Task.php
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Task
    {
        /**
         * @Assert\NotBlank()
         */
        public $task;
    
        /**
         * @Assert\NotBlank()
         * @Assert\Type("\DateTime")
         */
        protected $dueDate;
    }
    
  • XML
    <!-- AppBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
            http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="AppBundle\Entity\Task">
            <property name="task">
                <constraint name="NotBlank" />
            </property>
            <property name="dueDate">
                <constraint name="NotBlank" />
                <constraint name="Type">\DateTime</constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // AppBundle/Entity/Task.php
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\Type;
    
    class Task
    {
        // ...
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('task', new NotBlank());
    
            $metadata->addPropertyConstraint('dueDate', new NotBlank());
            $metadata->addPropertyConstraint(
                'dueDate',
                new Type('\DateTime')
            );
        }
    }
    

That’s it! If you re-submit the form with invalid data, you’ll see the corresponding errors printed out with the form.

Validation is a very powerful feature of Symfony and has its own dedicated chapter.

Validation Groups

If your object takes advantage of validation groups, you’ll need to specify which validation group(s) your form should use:

$form = $this->createFormBuilder($users, array(
    'validation_groups' => array('registration'),
))->add(...);

If you’re creating form classes (a good practice), then you’ll need to add the following to the setDefaultOptions() method:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => array('registration'),
    ));
}

In both of these cases, only the registration validation group will be used to validate the underlying object.

Disabling Validation

2.3 新版功能: The ability to set validation_groups to false was introduced in Symfony 2.3.

Sometimes it is useful to suppress the validation of a form altogether. For these cases you can set the validation_groups option to false:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => false,
    ));
}

Note that when you do that, the form will still run basic integrity checks, for example whether an uploaded file was too large or whether non-existing fields were submitted. If you want to suppress validation, you can use the POST_SUBMIT event.

Groups based on the Submitted Data

If you need some advanced logic to determine the validation groups (e.g. based on submitted data), you can set the validation_groups option to an array callback:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => array(
            'AppBundle\Entity\Client',
            'determineValidationGroups',
        ),
    ));
}

This will call the static method determineValidationGroups() on the Client class after the form is submitted, but before validation is executed. The Form object is passed as an argument to that method (see next example). You can also define whole logic inline by using a Closure:

use Acme\AcmeBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function(FormInterface $form) {
            $data = $form->getData();
            if (Client::TYPE_PERSON == $data->getType()) {
                return array('person');
            }

            return array('company');
        },
    ));
}

Using the validation_groups option overrides the default validation group which is being used. If you want to validate the default constraints of the entity as well you have to adjust the option as follows:

use Acme\AcmeBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function(FormInterface $form) {
            $data = $form->getData();
            if (Client::TYPE_PERSON == $data->getType()) {
                return array('Default', 'person');
            }

            return array('Default', 'company');
        },
    ));
}

You can find more information about how the validation groups and the default constraints work in the book section about validation groups.

Groups based on the Clicked Button

2.3 新版功能: Support for buttons in forms was introduced in Symfony 2.3.

When your form contains multiple submit buttons, you can change the validation group depending on which button is used to submit the form. For example, consider a form in a wizard that lets you advance to the next step or go back to the previous step. Also assume that when returning to the previous step, the data of the form should be saved, but not validated.

First, we need to add the two buttons to the form:

$form = $this->createFormBuilder($task)
    // ...
    ->add('nextStep', 'submit')
    ->add('previousStep', 'submit')
    ->getForm();

Then, we configure the button for returning to the previous step to run specific validation groups. In this example, we want it to suppress validation, so we set its validation_groups option to false:

$form = $this->createFormBuilder($task)
    // ...
    ->add('previousStep', 'submit', array(
        'validation_groups' => false,
    ))
    ->getForm();

Now the form will skip your validation constraints. It will still validate basic integrity constraints, such as checking whether an uploaded file was too large or whether you tried to submit text in a number field.

Built-in Field Types

Symfony comes standard with a large group of field types that cover all of the common form fields and data types you’ll encounter:

Date and Time Fields
Other Fields
Field Groups
Hidden Fields
Base Fields

You can also create your own custom field types. This topic is covered in the “How to Create a Custom Form Field Type” article of the cookbook.

Field Type Options

Each field type has a number of options that can be used to configure it. For example, the dueDate field is currently being rendered as 3 select boxes. However, the date field can be configured to be rendered as a single text box (where the user would enter the date as a string in the box):

->add('dueDate', 'date', array('widget' => 'single_text'))
_images/form-simple2.png

Each field type has a number of different options that can be passed to it. Many of these are specific to the field type and details can be found in the documentation for each type.

Field Type Guessing

Now that you’ve added validation metadata to the Task class, Symfony already knows a bit about your fields. If you allow it, Symfony can “guess” the type of your field and set it up for you. In this example, Symfony can guess from the validation rules that both the task field is a normal text field and the dueDate field is a date field:

public function newAction()
{
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task')
        ->add('dueDate', null, array('widget' => 'single_text'))
        ->add('save', 'submit')
        ->getForm();
}

The “guessing” is activated when you omit the second argument to the add() method (or if you pass null to it). If you pass an options array as the third argument (done for dueDate above), these options are applied to the guessed field.

警告

If your form uses a specific validation group, the field type guesser will still consider all validation constraints when guessing your field types (including constraints that are not part of the validation group(s) being used).

Field Type Options Guessing

In addition to guessing the “type” for a field, Symfony can also try to guess the correct values of a number of field options.

小技巧

When these options are set, the field will be rendered with special HTML attributes that provide for HTML5 client-side validation. However, it doesn’t generate the equivalent server-side constraints (e.g. Assert\Length). And though you’ll need to manually add your server-side validation, these field type options can then be guessed from that information.

required
The required option can be guessed based on the validation rules (i.e. is the field NotBlank or NotNull) or the Doctrine metadata (i.e. is the field nullable). This is very useful, as your client-side validation will automatically match your validation rules.
max_length
If the field is some sort of text field, then the max_length option can be guessed from the validation constraints (if Length or Range is used) or from the Doctrine metadata (via the field’s length).

注解

These field options are only guessed if you’re using Symfony to guess the field type (i.e. omit or pass null as the second argument to add()).

If you’d like to change one of the guessed values, you can override it by passing the option in the options field array:

->add('task', null, array('max_length' => 4))
Rendering a Form in a Template

So far, you’ve seen how an entire form can be rendered with just one line of code. Of course, you’ll usually need much more flexibility when rendering:

  • Twig
    {# app/Resources/views/Default/new.html.twig #}
    {{ form_start(form) }}
        {{ form_errors(form) }}
    
        {{ form_row(form.task) }}
        {{ form_row(form.dueDate) }}
    {{ form_end(form) }}
    
  • PHP
    <!-- app/Resources/views/Default/newAction.html.php -->
    <?php echo $view['form']->start($form) ?>
        <?php echo $view['form']->errors($form) ?>
    
        <?php echo $view['form']->row($form['task']) ?>
        <?php echo $view['form']->row($form['dueDate']) ?>
    <?php echo $view['form']->end($form) ?>
    

You already know the form_start() and form_end() functions, but what do the other functions do?

form_errors(form)
Renders any errors global to the whole form (field-specific errors are displayed next to each field).
form_row(form.dueDate)
Renders the label, any errors, and the HTML form widget for the given field (e.g. dueDate) inside, by default, a div element.

The majority of the work is done by the form_row helper, which renders the label, errors and HTML form widget of each field inside a div tag by default. In the Form Theming section, you’ll learn how the form_row output can be customized on many different levels.

小技巧

You can access the current data of your form via form.vars.value:

  • Twig
    {{ form.vars.value.task }}
    
  • PHP
    <?php echo $form->vars['value']->getTask() ?>
    
Rendering each Field by Hand

The form_row helper is great because you can very quickly render each field of your form (and the markup used for the “row” can be customized as well). But since life isn’t always so simple, you can also render each field entirely by hand. The end-product of the following is the same as when you used the form_row helper:

  • Twig
    {{ form_start(form) }}
        {{ form_errors(form) }}
    
        <div>
            {{ form_label(form.task) }}
            {{ form_errors(form.task) }}
            {{ form_widget(form.task) }}
        </div>
    
        <div>
            {{ form_label(form.dueDate) }}
            {{ form_errors(form.dueDate) }}
            {{ form_widget(form.dueDate) }}
        </div>
    
        <div>
            {{ form_widget(form.save) }}
        </div>
    
    {{ form_end(form) }}
    
  • PHP
    <?php echo $view['form']->start($form) ?>
    
        <?php echo $view['form']->errors($form) ?>
    
        <div>
            <?php echo $view['form']->label($form['task']) ?>
            <?php echo $view['form']->errors($form['task']) ?>
            <?php echo $view['form']->widget($form['task']) ?>
        </div>
    
        <div>
            <?php echo $view['form']->label($form['dueDate']) ?>
            <?php echo $view['form']->errors($form['dueDate']) ?>
            <?php echo $view['form']->widget($form['dueDate']) ?>
        </div>
    
        <div>
            <?php echo $view['form']->widget($form['save']) ?>
        </div>
    
    <?php echo $view['form']->end($form) ?>
    

If the auto-generated label for a field isn’t quite right, you can explicitly specify it:

  • Twig
    {{ form_label(form.task, 'Task Description') }}
    
  • PHP
    <?php echo $view['form']->label($form['task'], 'Task Description') ?>
    

Some field types have additional rendering options that can be passed to the widget. These options are documented with each type, but one common options is attr, which allows you to modify attributes on the form element. The following would add the task_field class to the rendered input text field:

  • Twig
    {{ form_widget(form.task, {'attr': {'class': 'task_field'}}) }}
    
  • PHP
    <?php echo $view['form']->widget($form['task'], array(
        'attr' => array('class' => 'task_field'),
    )) ?>
    

If you need to render form fields “by hand” then you can access individual values for fields such as the id, name and label. For example to get the id:

  • Twig
    {{ form.task.vars.id }}
    
  • PHP
    <?php echo $form['task']->vars['id']?>
    

To get the value used for the form field’s name attribute you need to use the full_name value:

  • Twig
    {{ form.task.vars.full_name }}
    
  • PHP
    <?php echo $form['task']->vars['full_name'] ?>
    
Twig Template Function Reference

If you’re using Twig, a full reference of the form rendering functions is available in the reference manual. Read this to know everything about the helpers available and the options that can be used with each.

Changing the Action and Method of a Form

So far, the form_start() helper has been used to render the form’s start tag and we assumed that each form is submitted to the same URL in a POST request. Sometimes you want to change these parameters. You can do so in a few different ways. If you build your form in the controller, you can use setAction() and setMethod():

$form = $this->createFormBuilder($task)
    ->setAction($this->generateUrl('target_route'))
    ->setMethod('GET')
    ->add('task', 'text')
    ->add('dueDate', 'date')
    ->add('save', 'submit')
    ->getForm();

注解

This example assumes that you’ve created a route called target_route that points to the controller that processes the form.

In Creating Form Classes you will learn how to move the form building code into separate classes. When using an external form class in the controller, you can pass the action and method as form options:

$form = $this->createForm(new TaskType(), $task, array(
    'action' => $this->generateUrl('target_route'),
    'method' => 'GET',
));

Finally, you can override the action and method in the template by passing them to the form() or the form_start() helper:

  • Twig
    {# app/Resources/views/Default/new.html.twig #}
    {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }}
    
  • PHP
    <!-- app/Resources/views/Default/newAction.html.php -->
    <?php echo $view['form']->start($form, array(
        'action' => $view['router']->generate('target_route'),
        'method' => 'GET',
    )) ?>
    

注解

If the form’s method is not GET or POST, but PUT, PATCH or DELETE, Symfony will insert a hidden field with the name _method that stores this method. The form will be submitted in a normal POST request, but Symfony’s router is capable of detecting the _method parameter and will interpret it as a PUT, PATCH or DELETE request. Read the cookbook chapter “How to Use HTTP Methods beyond GET and POST in Routes” for more information.

Creating Form Classes

As you’ve seen, a form can be created and used directly in a controller. However, a better practice is to build the form in a separate, standalone PHP class, which can then be reused anywhere in your application. Create a new class that will house the logic for building the task form:

// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', 'submit');
    }

    public function getName()
    {
        return 'task';
    }
}

This new class contains all the directions needed to create the task form (note that the getName() method should return a unique identifier for this form “type”). It can be used to quickly build a form object in the controller:

// src/AppBundle/Controller/DefaultController.php

// add this new use statement at the top of the class
use AppBundle\Form\Type\TaskType;

public function newAction()
{
    $task = ...;
    $form = $this->createForm(new TaskType(), $task);

    // ...
}

Placing the form logic into its own class means that the form can be easily reused elsewhere in your project. This is the best way to create forms, but the choice is ultimately up to you.

小技巧

When mapping forms to objects, all fields are mapped. Any fields on the form that do not exist on the mapped object will cause an exception to be thrown.

In cases where you need extra fields in the form (for example: a “do you agree with these terms” checkbox) that will not be mapped to the underlying object, you need to set the mapped option to false:

use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('task')
        ->add('dueDate', null, array('mapped' => false))
        ->add('save', 'submit');
}

Additionally, if there are any fields on the form that aren’t included in the submitted data, those fields will be explicitly set to null.

The field data can be accessed in a controller with:

$form->get('dueDate')->getData();

In addition, the data of an unmapped field can also be modified directly:

$form->get('dueDate')->setData(new \DateTime());
Defining your Forms as Services

Defining your form type as a service is a good practice and makes it really easy to use in your application.

注解

Services and the service container will be handled later on in this book. Things will be more clear after reading that chapter.

  • YAML
    # src/AppBundle/Resources/config/services.yml
    services:
        acme_demo.form.type.task:
            class: AppBundle\Form\Type\TaskType
            tags:
                - { name: form.type, alias: task }
    
  • XML
    <!-- src/AppBundle/Resources/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="acme_demo.form.type.task"
                class="AppBundle\Form\Type\TaskType">
    
                <tag name="form.type" alias="task" />
            </service>
        </services>
    </container>
    
  • PHP
    // src/AppBundle/Resources/config/services.php
    $container
        ->register(
            'acme_demo.form.type.task',
            'AppBundle\Form\Type\TaskType'
        )
        ->addTag('form.type', array(
            'alias' => 'task',
        ))
    ;
    

That’s it! Now you can use your form type directly in a controller:

// src/AppBundle/Controller/DefaultController.php
// ...

public function newAction()
{
    $task = ...;
    $form = $this->createForm('task', $task);

    // ...
}

or even use from within the form type of another form:

// src/AppBundle/Form/Type/ListType.php
// ...

class ListType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // ...

        $builder->add('someTask', 'task');
    }
}

Read Creating your Field Type as a Service for more information.

Forms and Doctrine

The goal of a form is to translate data from an object (e.g. Task) to an HTML form and then translate user-submitted data back to the original object. As such, the topic of persisting the Task object to the database is entirely unrelated to the topic of forms. But, if you’ve configured the Task class to be persisted via Doctrine (i.e. you’ve added mapping metadata for it), then persisting it after a form submission can be done when the form is valid:

if ($form->isValid()) {
    $em = $this->getDoctrine()->getManager();
    $em->persist($task);
    $em->flush();

    return $this->redirect($this->generateUrl('task_success'));
}

If, for some reason, you don’t have access to your original $task object, you can fetch it from the form:

$task = $form->getData();

For more information, see the Doctrine ORM chapter.

The key thing to understand is that when the form is submitted, the submitted data is transferred to the underlying object immediately. If you want to persist that data, you simply need to persist the object itself (which already contains the submitted data).

Embedded Forms

Often, you’ll want to build a form that will include fields from many different objects. For example, a registration form may contain data belonging to a User object as well as many Address objects. Fortunately, this is easy and natural with the Form component.

Embedding a Single Object

Suppose that each Task belongs to a simple Category object. Start, of course, by creating the Category object:

// src/AppBundle/Entity/Category.php
namespace AppBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Category
{
    /**
     * @Assert\NotBlank()
     */
    public $name;
}

Next, add a new category property to the Task class:

// ...

class Task
{
    // ...

    /**
     * @Assert\Type(type="AppBundle\Entity\Category")
     * @Assert\Valid()
     */
    protected $category;

    // ...

    public function getCategory()
    {
        return $this->category;
    }

    public function setCategory(Category $category = null)
    {
        $this->category = $category;
    }
}

小技巧

The Valid Constraint has been added to the property category. This cascades the validation to the corresponding entity. If you omit this constraint the child entity would not be validated.

Now that your application has been updated to reflect the new requirements, create a form class so that a Category object can be modified by the user:

// src/AppBundle/Form/Type/CategoryType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class CategoryType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Category',
        ));
    }

    public function getName()
    {
        return 'category';
    }
}

The end goal is to allow the Category of a Task to be modified right inside the task form itself. To accomplish this, add a category field to the TaskType object whose type is an instance of the new CategoryType class:

use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('category', new CategoryType());
}

The fields from CategoryType can now be rendered alongside those from the TaskType class.

Render the Category fields in the same way as the original Task fields:

  • Twig
    {# ... #}
    
    <h3>Category</h3>
    <div class="category">
        {{ form_row(form.category.name) }}
    </div>
    
    {# ... #}
    
  • PHP
    <!-- ... -->
    
    <h3>Category</h3>
    <div class="category">
        <?php echo $view['form']->row($form['category']['name']) ?>
    </div>
    
    <!-- ... -->
    

When the user submits the form, the submitted data for the Category fields are used to construct an instance of Category, which is then set on the category field of the Task instance.

The Category instance is accessible naturally via $task->getCategory() and can be persisted to the database or used however you need.

Embedding a Collection of Forms

You can also embed a collection of forms into one form (imagine a Category form with many Product sub-forms). This is done by using the collection field type.

For more information see the “How to Embed a Collection of Forms” cookbook entry and the collection field type reference.

Form Theming

Every part of how a form is rendered can be customized. You’re free to change how each form “row” renders, change the markup used to render errors, or even customize how a textarea tag should be rendered. Nothing is off-limits, and different customizations can be used in different places.

Symfony uses templates to render each and every part of a form, such as label tags, input tags, error messages and everything else.

In Twig, each form “fragment” is represented by a Twig block. To customize any part of how a form renders, you just need to override the appropriate block.

In PHP, each form “fragment” is rendered via an individual template file. To customize any part of how a form renders, you just need to override the existing template by creating a new one.

To understand how this works, customize the form_row fragment and add a class attribute to the div element that surrounds each row. To do this, create a new template file that will store the new markup:

  • Twig
    {# app/Resources/views/Form/fields.html.twig #}
    {% block form_row %}
    {% spaceless %}
        <div class="form_row">
            {{ form_label(form) }}
            {{ form_errors(form) }}
            {{ form_widget(form) }}
        </div>
    {% endspaceless %}
    {% endblock form_row %}
    
  • PHP
    <!-- app/Resources/views/Form/form_row.html.php -->
    <div class="form_row">
        <?php echo $view['form']->label($form, $label) ?>
        <?php echo $view['form']->errors($form) ?>
        <?php echo $view['form']->widget($form, $parameters) ?>
    </div>
    

The form_row form fragment is used when rendering most fields via the form_row function. To tell the Form component to use your new form_row fragment defined above, add the following to the top of the template that renders the form:

  • Twig
    {# app/Resources/views/Default/new.html.twig #}
    {% form_theme form 'Form/fields.html.twig' %}
    
    {% form_theme form 'Form/fields.html.twig' 'Form/fields2.html.twig' %}
    
    {# ... render the form #}
    
  • PHP
    <!-- app/Resources/views/Default/new.html.php -->
    <?php $view['form']->setTheme($form, array('Form')) ?>
    
    <?php $view['form']->setTheme($form, array('Form', 'Form2')) ?>
    
    <!-- ... render the form -->
    

The form_theme tag (in Twig) “imports” the fragments defined in the given template and uses them when rendering the form. In other words, when the form_row function is called later in this template, it will use the form_row block from your custom theme (instead of the default form_row block that ships with Symfony).

Your custom theme does not have to override all the blocks. When rendering a block which is not overridden in your custom theme, the theming engine will fall back to the global theme (defined at the bundle level).

If several custom themes are provided they will be searched in the listed order before falling back to the global theme.

To customize any portion of a form, you just need to override the appropriate fragment. Knowing exactly which block or file to override is the subject of the next section.

For a more extensive discussion, see How to Customize Form Rendering.

Form Fragment Naming

In Symfony, every part of a form that is rendered - HTML form elements, errors, labels, etc. - is defined in a base theme, which is a collection of blocks in Twig and a collection of template files in PHP.

In Twig, every block needed is defined in a single template file (e.g. form_div_layout.html.twig) that lives inside the Twig Bridge. Inside this file, you can see every block needed to render a form and every default field type.

In PHP, the fragments are individual template files. By default they are located in the Resources/views/Form directory of the framework bundle (view on GitHub).

Each fragment name follows the same basic pattern and is broken up into two pieces, separated by a single underscore character (_). A few examples are:

  • form_row - used by form_row to render most fields;
  • textarea_widget - used by form_widget to render a textarea field type;
  • form_errors - used by form_errors to render errors for a field;

Each fragment follows the same basic pattern: type_part. The type portion corresponds to the field type being rendered (e.g. textarea, checkbox, date, etc) whereas the part portion corresponds to what is being rendered (e.g. label, widget, errors, etc). By default, there are 4 possible parts of a form that can be rendered:

label (e.g. form_label) renders the field’s label
widget (e.g. form_widget) renders the field’s HTML representation
errors (e.g. form_errors) renders the field’s errors
row (e.g. form_row) renders the field’s entire row (label, widget & errors)

注解

There are actually 2 other parts - rows and rest - but you should rarely if ever need to worry about overriding them.

By knowing the field type (e.g. textarea) and which part you want to customize (e.g. widget), you can construct the fragment name that needs to be overridden (e.g. textarea_widget).

Template Fragment Inheritance

In some cases, the fragment you want to customize will appear to be missing. For example, there is no textarea_errors fragment in the default themes provided with Symfony. So how are the errors for a textarea field rendered?

The answer is: via the form_errors fragment. When Symfony renders the errors for a textarea type, it looks first for a textarea_errors fragment before falling back to the form_errors fragment. Each field type has a parent type (the parent type of textarea is text, its parent is form), and Symfony uses the fragment for the parent type if the base fragment doesn’t exist.

So, to override the errors for only textarea fields, copy the form_errors fragment, rename it to textarea_errors and customize it. To override the default error rendering for all fields, copy and customize the form_errors fragment directly.

小技巧

The “parent” type of each field type is available in the form type reference for each field type.

Global Form Theming

In the above example, you used the form_theme helper (in Twig) to “import” the custom form fragments into just that form. You can also tell Symfony to import form customizations across your entire project.

Twig

To automatically include the customized blocks from the fields.html.twig template created earlier in all templates, modify your application configuration file:

  • YAML
    # app/config/config.yml
    twig:
        form:
            resources:
                - 'Form/fields.html.twig'
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:twig="http://symfony.com/schema/dic/twig"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/twig http://symfony.com/schema/dic/twig/twig-1.0.xsd">
    
        <twig:config>
            <twig:form>
                <twig:resource>Form/fields.html.twig</twig:resource>
            </twig:form>
            <!-- ... -->
        </twig:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'form' => array(
            'resources' => array(
                'Form/fields.html.twig',
            ),
        ),
        // ...
    ));
    

Any blocks inside the fields.html.twig template are now used globally to define form output.

PHP

To automatically include the customized templates from the app/Resources/views/Form directory created earlier in all templates, modify your application configuration file:

  • YAML
    # app/config/config.yml
    framework:
        templating:
            form:
                resources:
                    - 'Form'
    # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:templating>
                <framework:form>
                    <framework:resource>Form</framework:resource>
                </framework:form>
            </framework:templating>
            <!-- ... -->
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'templating' => array(
            'form' => array(
                'resources' => array(
                    'Form',
                ),
            ),
        )
        // ...
    ));
    

Any fragments inside the app/Resources/views/Form directory are now used globally to define form output.

CSRF Protection

CSRF - or Cross-site request forgery - is a method by which a malicious user attempts to make your legitimate users unknowingly submit data that they don’t intend to submit. Fortunately, CSRF attacks can be prevented by using a CSRF token inside your forms.

The good news is that, by default, Symfony embeds and validates CSRF tokens automatically for you. This means that you can take advantage of the CSRF protection without doing anything. In fact, every form in this chapter has taken advantage of the CSRF protection!

CSRF protection works by adding a hidden field to your form - called _token by default - that contains a value that only you and your user knows. This ensures that the user - not some other entity - is submitting the given data. Symfony automatically validates the presence and accuracy of this token.

The _token field is a hidden field and will be automatically rendered if you include the form_end() function in your template, which ensures that all un-rendered fields are output.

The CSRF token can be customized on a form-by-form basis. For example:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class TaskType extends AbstractType
{
    // ...

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'      => 'AppBundle\Entity\Task',
            'csrf_protection' => true,
            'csrf_field_name' => '_token',
            // a unique key to help generate the secret token
            'intention'       => 'task_item',
        ));
    }

    // ...
}

To disable CSRF protection, set the csrf_protection option to false. Customizations can also be made globally in your project. For more information, see the form configuration reference section.

注解

The intention option is optional but greatly enhances the security of the generated token by making it different for each form.

警告

CSRF tokens are meant to be different for every user. This is why you need to be cautious if you try to cache pages with forms including this kind of protection. For more information, see Caching Pages that Contain CSRF Protected Forms.

Using a Form without a Class

In most cases, a form is tied to an object, and the fields of the form get and store their data on the properties of that object. This is exactly what you’ve seen so far in this chapter with the Task class.

But sometimes, you may just want to use a form without a class, and get back an array of the submitted data. This is actually really easy:

// make sure you've imported the Request namespace above the class
use Symfony\Component\HttpFoundation\Request;
// ...

public function contactAction(Request $request)
{
    $defaultData = array('message' => 'Type your message here');
    $form = $this->createFormBuilder($defaultData)
        ->add('name', 'text')
        ->add('email', 'email')
        ->add('message', 'textarea')
        ->add('send', 'submit')
        ->getForm();

    $form->handleRequest($request);

    if ($form->isValid()) {
        // data is an array with "name", "email", and "message" keys
        $data = $form->getData();
    }

    // ... render the form
}

By default, a form actually assumes that you want to work with arrays of data, instead of an object. There are exactly two ways that you can change this behavior and tie the form to an object instead:

  1. Pass an object when creating the form (as the first argument to createFormBuilder or the second argument to createForm);
  2. Declare the data_class option on your form.

If you don’t do either of these, then the form will return the data as an array. In this example, since $defaultData is not an object (and no data_class option is set), $form->getData() ultimately returns an array.

小技巧

You can also access POST values (in this case “name”) directly through the request object, like so:

$request->request->get('name');

Be advised, however, that in most cases using the getData() method is a better choice, since it returns the data (usually an object) after it’s been transformed by the form framework.

Adding Validation

The only missing piece is validation. Usually, when you call $form->isValid(), the object is validated by reading the constraints that you applied to that class. If your form is mapped to an object (i.e. you’re using the data_class option or passing an object to your form), this is almost always the approach you want to use. See Validation for more details.

But if the form is not mapped to an object and you instead want to retrieve a simple array of your submitted data, how can you add constraints to the data of your form?

The answer is to setup the constraints yourself, and attach them to the individual fields. The overall approach is covered a bit more in the validation chapter, but here’s a short example:

2.1 新版功能: The constraints option, which accepts a single constraint or an array of constraints (before 2.1, the option was called validation_constraint, and only accepted a single constraint) was introduced in Symfony 2.1.

use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

$builder
   ->add('firstName', 'text', array(
       'constraints' => new Length(array('min' => 3)),
   ))
   ->add('lastName', 'text', array(
       'constraints' => array(
           new NotBlank(),
           new Length(array('min' => 3)),
       ),
   ))
;

小技巧

If you are using validation groups, you need to either reference the Default group when creating the form, or set the correct group on the constraint you are adding.

new NotBlank(array('groups' => array('create', 'update'))
Final Thoughts

You now know all of the building blocks necessary to build complex and functional forms for your application. When building forms, keep in mind that the first goal of a form is to translate data from an object (Task) to an HTML form so that the user can modify that data. The second goal of a form is to take the data submitted by the user and to re-apply it to the object.

There’s still much more to learn about the powerful world of forms, such as how to handle file uploads with Doctrine or how to create a form where a dynamic number of sub-forms can be added (e.g. a todo list where you can keep adding more fields via JavaScript before submitting). See the cookbook for these topics. Also, be sure to lean on the field type reference documentation, which includes examples of how to use each field type and its options.

Security

Symfony’s security system is incredibly powerful, but it can also be confusing to set up. In this chapter, you’ll learn how to set up your application’s security step-by-step, from configuring your firewall and how you load users to denying access and fetching the User object. Depending on what you need, sometimes the initial setup can be tough. But once it’s done, Symfony’s security system is both flexible and (hopefully) fun to work with.

Since there’s a lot to talk about, this chapter is organized into a few big sections:

  1. Initial security.yml setup (authentication);
  2. Denying access to your app (authorization);
  3. Fetching the current User object

These are followed by a number of small (but still captivating) sections, like logging out and encoding user passwords.

1) Initial security.yml Setup (Authentication)

The security system is configured in app/config/security.yml. The default configuration looks like this:

  • YAML
    # app/config/security.yml
    security:
        providers:
            in_memory:
                memory: ~
    
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
    
            default:
                anonymous: ~
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory />
            </provider>
    
            <firewall name="dev"
                pattern="^/(_(profiler|wdt)|css|images|js)/"
                security=false />
    
            <firewall name="default">
                <anonymous />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => array(),
            ),
        ),
        'firewalls' => array(
            'dev' => array(
                'pattern'    => '^/(_(profiler|wdt)|css|images|js)/',
                'security'   => false,
            ),
            'default' => array(
                'anonymous'  => null,
            ),
        ),
    ));
    

The firewalls key is the heart of your security configuration. The dev firewall isn’t important, it just makes sure that Symfony’s development tools - which live under URLs like /_profiler and /_wdt aren’t blocked by your security.

All other URLs will be handled by the default firewall (no pattern key means it matches all URLs). You can think of the firewall like your security system, and so it usually makes sense to have just one main firewall. But this does not mean that every URL requires authentication - the anonymous key takes care of this. In fact, if you go to the homepage right now, you’ll have access and you’ll see that you’re “authenticated” as anon.. Don’t be fooled by the “Yes” next to Authenticated, you’re just an anonymous user:

_images/security_anonymous_wdt.png

You’ll learn later how to deny access to certain URLs or controllers.

小技巧

Security is highly configurable and there’s a Security Configuration Reference that shows all of the options with some extra explanation.

A) Configuring how your Users will Authenticate

The main job of a firewall is to configure how your users will authenticate. Will they use a login form? Http Basic? An API token? All of the above?

Let’s start with Http Basic (the old-school pop-up) and work up from there. To activate this, add the http_basic key under your firewall:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            # ...
            default:
                anonymous: ~
                http_basic: ~
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="default">
                <anonymous />
                <http-basic />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'default' => array(
                'anonymous'  => null,
                'http_basic' => null,
            ),
        ),
    ));
    

Simple! To try this, you need to require the user to be logged in to see a page. To make things interesting, create a new page at /admin. For example, if you use annotations, create something like this:

// src/AppBundle/Controller/DefaultController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    /**
     * @Route("/admin")
     */
    public function adminAction()
    {
        return new Response('Admin page!');
    }
}

Next, add an access_control entry to security.yml that requires the user to be logged in to access this URL:

  • YAML
    # app/config/security.yml
    security:
        # ...
        firewalls:
            # ...
    
        access_control:
            # require ROLE_ADMIN for /admin*
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="default">
                <!-- ... -->
            </firewall>
    
            <access-control>
                <!-- require ROLE_ADMIN for /admin* -->
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'default' => array(
                // ...
            ),
        ),
       'access_control' => array(
           // require ROLE_ADMIN for /admin*
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

注解

You’ll learn more about this ROLE_ADMIN thing and denying access later in the 2) Denying Access, Roles and other Authorization section.

Great! Now, if you go to /admin, you’ll see the HTTP Basic popup:

_images/security_http_basic_popup.png

But who can you login as? Where do users come from?

小技巧

Want to use a traditional login form? Great! See How to Build a Traditional Login Form. What other methods are supported? See the Configuration Reference or build your own.

B) Configuring how Users are Loaded

When you type in your username, Symfony needs to load that user’s information from somewhere. This is called a “user provider”, and you’re in charge of configuring it. Symfony has a built-in way to load users from the database, or you can create your own user provider.

The easiest (but most limited) way, is to configure Symfony to load hardcoded users directly from the security.yml file itself. This is called an “in memory” provider, but it’s better to think of it as an “in configuration” provider:

  • YAML
    # app/config/security.yml
    security:
        providers:
            in_memory:
                memory:
                    users:
                        ryan:
                            password: ryanpass
                            roles: 'ROLE_USER'
                        admin:
                            password: kitten
                            roles: 'ROLE_ADMIN'
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="ryanpass" roles="ROLE_USER" />
                    <user name="admin" password="kitten" roles="ROLE_ADMIN" />
                </memory>
            </provider>
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array(
                            'password' => 'ryanpass',
                            'roles' => 'ROLE_USER',
                        ),
                        'admin' => array(
                            'password' => 'kitten',
                            'roles' => 'ROLE_ADMIN',
                        ),
                    ),
                ),
            ),
        ),
        // ...
    ));
    

Like with firewalls, you can have multiple providers, but you’ll probably only need one. If you do have multiple, you can configure which one provider to use for your firewall under its provider key (e.g. provider: in_memory).

Try to login using username admin and password kitten. You should see an error!

No encoder has been configured for account “SymfonyComponentSecurityCoreUserUser”

To fix this, add an encoders key:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Symfony\Component\Security\Core\User\User: plaintext
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <encoder class="Symfony\Component\Security\Core\User\User"
                algorithm="plaintext" />
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => 'plaintext',
        ),
        // ...
    ));
    

User providers load user information and put it into a User object. If you load users from the database or some other source, you’ll use your own custom User class. But when you use the “in memory” provider, it gives you a Symfony\Component\Security\Core\User\User object.

Whatever your User class is, you need to tell Symfony what algorithm was used to encode the passwords. In this case, the passwords are just plaintext, but in a second, you’ll change this to use bcrypt.

If you refresh now, you’ll be logged in! The web debug toolbar even tells you who you are and what roles you have:

_images/symfony_loggedin_wdt.png

Because this URL requires ROLE_ADMIN, if you had logged in as ryan, this would deny you access. More on that later (Securing URL patterns (access_control)).

Loading Users from the Database

If you’d like to load your users via the Doctrine ORM, that’s easy! See How to Load Security Users from the Database (the Entity Provider) for all the details.

C) Encoding the User’s Password

Whether your users are stored in security.yml, in a database or somewhere else, you’ll want to encode their passwords. The best algorithm to use is bcrypt:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Symfony\Component\Security\Core\User\User:
                algorithm: bcrypt
                cost: 12
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <encoder class="Symfony\Component\Security\Core\User\User"
                algorithm="bcrypt"
                cost="12" />
    
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => array(
                'algorithm' => 'plaintext',
                'cost' => 12,
            )
        ),
        // ...
    ));
    

警告

If you’re using PHP 5.4 or lower, you’ll need to install the ircmaxell/password-compat library via Composer in order to be able to use the bcrypt encoder:

{
    "require": {
        ...
        "ircmaxell/password-compat": "~1.0.3"
    }
}

Of course, your user’s passwords now need to be encoded with this exact algorithm. For hardcoded users, you can use an online tool, which will give you something like this:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        providers:
            in_memory:
                memory:
                    users:
                        ryan:
                            password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli
                            roles: 'ROLE_USER'
                        admin:
                            password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G
                            roles: 'ROLE_ADMIN'
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli" roles="ROLE_USER" />
                    <user name="admin" password="$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G" roles="ROLE_ADMIN" />
                </memory>
            </provider>
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array(
                            'password' => '$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli',
                            'roles' => 'ROLE_USER',
                        ),
                        'admin' => array(
                            'password' => '$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G',
                            'roles' => 'ROLE_ADMIN',
                        ),
                    ),
                ),
            ),
        ),
        // ...
    ));
    

Everything will now work exactly like before. But if you have dynamic users (e.g. from a database), how can you programmatically encode the password before inserting them into the database? Don’t worry, see Dynamically Encoding a Password for details.

小技巧

Supported algorithms for this method depend on your PHP version, but include the algorithms returned by the PHP function hash_algos as well as a few others (e.g. bcrypt). See the encoders key in the Security Reference Section for examples.

D) Configuration Done!

Congratulations! You now have a working authentication system that uses Http Basic and loads users right from the security.yml file.

Your next steps depend on your setup:

2) Denying Access, Roles and other Authorization

Users can now login to your app using http_basic or some other method. Great! Now, you need to learn how to deny access and work with the User object. This is called authorization, and its job is to decide if a user can access some resource (a URL, a model object, a method call, ...).

The process of authorization has two different sides:

  1. The user receives a specific set of roles when logging in (e.g. ROLE_ADMIN).
  2. You add code so that a resource (e.g. URL, controller) requires a specific “attribute” (most commonly a role like ROLE_ADMIN) in order to be accessed.

小技巧

In addition to roles (e.g. ROLE_ADMIN), you can protect a resource using other attributes/strings (e.g. EDIT) and use voters or Symfony’s ACL system to give these meaning. This might come in handy if you need to check if user A can “EDIT” some object B (e.g. a Product with id 5). See Access Control Lists (ACLs): Securing individual Database Objects.

Roles

When a user logs in, they receive a set of roles (e.g. ROLE_ADMIN). In the example above, these are hardcoded into security.yml. If you’re loading users from the database, these are probably stored on a column in your table.

警告

All roles you assign to a user must begin with the ROLE_ prefix. Otherwise, they won’t be handled by Symfony’s security system in the normal way (i.e. unless you’re doing something advanced, assigning a role like FOO to a user and then checking for FOO as described below will not work).

Roles are simple, and are basically strings that you invent and use as needed. For example, if you need to start limiting access to the blog admin section of your website, you could protect that section using a ROLE_BLOG_ADMIN role. This role doesn’t need to be defined anywhere - you can just start using it.

小技巧

Make sure every user has at least one role, or your user will look like they’re not authenticated. A common convention is to give every user ROLE_USER.

You can also specify a role hierarchy where some roles automatically mean that you also have other roles.

Add Code to Deny Access

There are two ways to deny access to something:

  1. access_control in security.yml allows you to protect URL patterns (e.g. /admin/*). This is easy, but less flexible;
  2. in your code via the security.context service.
Securing URL patterns (access_control)

The most basic way to secure part of your application is to secure an entire URL pattern. You saw this earlier, where anything matching the regular expression ^/admin requires the ROLE_ADMIN role:

  • YAML
    # app/config/security.yml
    security:
        # ...
        firewalls:
            # ...
    
        access_control:
            # require ROLE_ADMIN for /admin*
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="default">
                <!-- ... -->
            </firewall>
    
            <access-control>
                <!-- require ROLE_ADMIN for /admin* -->
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'default' => array(
                // ...
            ),
        ),
       'access_control' => array(
           // require ROLE_ADMIN for /admin*
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

This is great for securing entire sections, but you’ll also probably want to secure your individual controllers as well.

You can define as many URL patterns as you need - each is a regular expression. BUT, only one will be matched. Symfony will look at each starting at the top, and stop as soon as it finds one access_control entry that matches the URL.

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
            <access-control>
                <rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'access_control' => array(
            array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'),
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

Prepending the path with ^ means that only URLs beginning with the pattern are matched. For example, a path of simply /admin (without the ^) would match /admin/foo but would also match URLs like /foo/admin.

Securing Controllers and other Code

You can easily deny access from inside a controller:

// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function helloAction($name)
{
    if (!$this->get('security.context')->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }

    // ...
}

That’s it! If the user isn’t logged in yet, they will be asked to login (e.g. redirected to the login page). If they are logged in, they’ll be shown the 403 access denied page (which you can customize).

Access Control in Templates

If you want to check if the current user has a role inside a template, use the built-in helper function:

  • Twig
    {% if is_granted('ROLE_ADMIN') %}
        <a href="...">Delete</a>
    {% endif %}
    
  • PHP
    <?php if ($view['security']->isGranted('ROLE_ADMIN')): ?>
        <a href="...">Delete</a>
    <?php endif ?>
    

If you use this function and are not behind a firewall, an exception will be thrown. Again, it’s almost always a good idea to have a main firewall that covers all URLs (as has been shown in this chapter).

警告

Be careful with this in your layout or on your error pages! Because of some internal Symfony details, to avoid broken error pages in the prod environment, wrap calls in these templates with a check for app.user:

{% if app.user and is_granted('ROLE_ADMIN') %}
Securing other Services

In fact, anything in Symfony can be protected by doing something similar to this. For example, suppose you have a service (i.e. a PHP class) whose job is to send emails. You can restrict use of this class - no matter where it’s being used from - to only certain users.

For more information see How to Secure any Service or Method in your Application.

Checking to see if a User is Logged In (IS_AUTHENTICATED_FULLY)

So far, you’ve checked access based on roles - those strings that start with ROLE_ and are assigned to users. But if you only want to check if a user is logged in (you don’t care about roles), then you can see IS_AUTHENTICATED_FULLY:

// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function helloAction($name)
{
    if (!$this->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw new AccessDeniedException();
    }

    // ...
}

小技巧

You can of course also use this in access_control.

IS_AUTHENTICATED_FULLY isn’t a role, but it kind of acts like one, and every user that has successfully logged in will have this. In fact, there are three special attributes like this:

  • IS_AUTHENTICATED_REMEMBERED: All logged in users have this, even if they are logged in because of a “remember me cookie”. Even if you don’t use the remember me functionality, you can use this to check if the user is logged in.
  • IS_AUTHENTICATED_FULLY: This is similar to IS_AUTHENTICATED_REMEMBERED, but stronger. Users who are logged in only because of a “remember me cookie” will have IS_AUTHENTICATED_REMEMBERED but will not have IS_AUTHENTICATED_FULLY.
  • IS_AUTHENTICATED_ANONYMOUSLY: All users (even anonymous ones) have this - this is useful when whitelisting URLs to guarantee access - some details are in How Does the Security access_control Work?.
Access Control Lists (ACLs): Securing individual Database Objects

Imagine you are designing a blog where users can comment on your posts. You also want a user to be able to edit their own comments, but not those of other users. Also, as the admin user, you yourself want to be able to edit all comments.

To accomplish this you have 2 options:

  • Voters allow you to use business logic (e.g. the user can edit this post because they were the creator) to determine access. You’ll probably want this option - it’s flexible enough to solve the above situation.
  • ACLs allow you to create a database structure where you can assign any arbitrary user any access (e.g. EDIT, VIEW) to any object in your system. Use this if you need an admin user to be able to grant customized access across your system via some admin interface.

In both cases, you’ll still deny access using methods similar to what was shown above.

Retrieving the User Object

After authentication, the User object of the current user can be accessed via the security.context service. From inside a controller, this will look like:

public function indexAction()
{
    if (!$this->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw new AccessDeniedException();
    }

    $user = $this->getUser();

    // the above is a shortcut for this
    $user = $this->get('security.context')->getToken()->getUser();
}

小技巧

The user will be an object and the class of that object will depend on your user provider.

Now you can call whatever methods are on your User object. For example, if your User object has a getFirstName() method, you could use that:

use Symfony\Component\HttpFoundation\Response;

public function indexAction()
{
    // ...

    return new Response('Well hi there '.$user->getFirstName());
}
Always Check if the User is Logged In

It’s important to check if the user is authenticated first. If they’re not, $user will either be null or the string anon.. Wait, what? Yes, this is a quirk. If you’re not logged in, the user is technically the string anon., though the getUser() controller shortcut converts this to null for convenience.

The point is this: always check to see if the user is logged in before using the User object, and use the isGranted method (or access_control) to do this:

// yay! Use this to see if the user is logged in
if (!$this->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY')) {
    throw new AccessDeniedException();
}

// boo :(. Never check for the User object to see if they're logged in
if ($this->getUser()) {

}
Retrieving the User in a Template

In a Twig Template this object can be accessed via the app.user key:

  • Twig
    {% if is_granted('IS_AUTHENTICATED_FULLY') %}
        <p>Username: {{ app.user.username }}</p>
    {% endif %}
    
  • PHP
    <?php if ($view['security']->isGranted('IS_AUTHENTICATED_FULLY')): ?>
        <p>Username: <?php echo $app->getUser()->getUsername() ?></p>
    <?php endif; ?>
    
Logging Out

Usually, you’ll also want your users to be able to log out. Fortunately, the firewall can handle this automatically for you when you activate the logout config parameter:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                logout:
                    path:   /logout
                    target: /
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="secured_area" pattern="^/">
                <!-- ... -->
                <logout path="/logout" target="/" />
            </firewall>
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'logout' => array('path' => 'logout', 'target' => '/'),
            ),
        ),
        // ...
    ));
    

Next, you’ll need to create a route for this URL (but not a controller):

  • YAML
    # app/config/routing.yml
    logout:
        path:   /logout
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="logout" path="/logout" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('logout', new Route('/logout', array()));
    
    return $collection;
    

And that’s it! By sending a user to /logout (or whatever you configure the path to be), Symfony will un-authenticate the current user.

Once the user has been logged out, they will be redirected to whatever path is defined by the target parameter above (e.g. the homepage).

小技巧

If you need to do something more interesting after logging out, you can specify a logout success handler by adding a success_handler key and pointing it to a service id of a class that implements LogoutSuccessHandlerInterface. See Security Configuration Reference.

Dynamically Encoding a Password

If, for example, you’re storing users in the database, you’ll need to encode the users’ passwords before inserting them. No matter what algorithm you configure for your user object, the hashed password can always be determined in the following way from a controller:

$factory = $this->get('security.encoder_factory');
// whatever *your* User object is
$user = new AppBundle\Entity\User();

$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword('ryanpass', $user->getSalt());
$user->setPassword($password);

In order for this to work, just make sure that you have the encoder for your user class (e.g. AppBundle\Entity\User) configured under the encoders key in app/config/security.yml.

The $encoder object also has an isPasswordValid method, which takes the User object as the first argument and the plain password to check as the second argument.

警告

When you allow a user to submit a plaintext password (e.g. registration form, change password form), you must have validation that guarantees that the password is 4096 characters or fewer. Read more details in How to implement a simple Registration Form.

Hierarchical Roles

Instead of associating many roles to users, you can define role inheritance rules by creating a role hierarchy:

  • YAML
    # app/config/security.yml
    security:
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <role id="ROLE_ADMIN">ROLE_USER</role>
            <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'role_hierarchy' => array(
            'ROLE_ADMIN'       => 'ROLE_USER',
            'ROLE_SUPER_ADMIN' => array(
                'ROLE_ADMIN',
                'ROLE_ALLOWED_TO_SWITCH',
            ),
        ),
    ));
    

In the above configuration, users with ROLE_ADMIN role will also have the ROLE_USER role. The ROLE_SUPER_ADMIN role has ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH and ROLE_USER (inherited from ROLE_ADMIN).

Stateless Authentication

By default, Symfony relies on a cookie (the Session) to persist the security context of the user. But if you use certificates or HTTP authentication for instance, persistence is not needed as credentials are available for each request. In that case, and if you don’t need to store anything else between requests, you can activate the stateless authentication (which means that no cookie will be ever created by Symfony):

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                http_basic: ~
                stateless:  true
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall stateless="true">
                <http-basic />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array('http_basic' => array(), 'stateless' => true),
        ),
    ));
    

注解

If you use a form login, Symfony will create a cookie even if you set stateless to true.

Final Words

Woh! Nice work! You now know more than the basics of security. The hardest parts are when you have custom requirements: like a custom authentication strategy (e.g. API tokens), complex authorization logic and many other things (because security is complex!).

Fortunately, there are a lot of Security Cookbook Articles aimed at describing many of these situations. Also, see the Security Reference Section. Many of the options don’t have specific details, but seeing the full possible configuration tree may be useful.

Good luck!

HTTP Cache

The nature of rich web applications means that they’re dynamic. No matter how efficient your application, each request will always contain more overhead than serving a static file.

And for most Web applications, that’s fine. Symfony is lightning fast, and unless you’re doing some serious heavy-lifting, each request will come back quickly without putting too much stress on your server.

But as your site grows, that overhead can become a problem. The processing that’s normally performed on every request should be done only once. This is exactly what caching aims to accomplish.

Caching on the Shoulders of Giants

The most effective way to improve performance of an application is to cache the full output of a page and then bypass the application entirely on each subsequent request. Of course, this isn’t always possible for highly dynamic websites, or is it? In this chapter, you’ll see how the Symfony cache system works and why this is the best possible approach.

The Symfony cache system is different because it relies on the simplicity and power of the HTTP cache as defined in the HTTP specification. Instead of reinventing a caching methodology, Symfony embraces the standard that defines basic communication on the Web. Once you understand the fundamental HTTP validation and expiration caching models, you’ll be ready to master the Symfony cache system.

For the purposes of learning how to cache with Symfony, the subject is covered in four steps:

  1. A gateway cache, or reverse proxy, is an independent layer that sits in front of your application. The reverse proxy caches responses as they’re returned from your application and answers requests with cached responses before they hit your application. Symfony provides its own reverse proxy, but any reverse proxy can be used.
  2. HTTP cache headers are used to communicate with the gateway cache and any other caches between your application and the client. Symfony provides sensible defaults and a powerful interface for interacting with the cache headers.
  3. HTTP expiration and validation are the two models used for determining whether cached content is fresh (can be reused from the cache) or stale (should be regenerated by the application).
  4. Edge Side Includes (ESI) allow HTTP cache to be used to cache page fragments (even nested fragments) independently. With ESI, you can even cache an entire page for 60 minutes, but an embedded sidebar for only 5 minutes.

Since caching with HTTP isn’t unique to Symfony, many articles already exist on the topic. If you’re new to HTTP caching, Ryan Tomayko’s article Things Caches Do is highly recommended . Another in-depth resource is Mark Nottingham’s Cache Tutorial.

Caching with a Gateway Cache

When caching with HTTP, the cache is separated from your application entirely and sits between your application and the client making the request.

The job of the cache is to accept requests from the client and pass them back to your application. The cache will also receive responses back from your application and forward them on to the client. The cache is the “middle-man” of the request-response communication between the client and your application.

Along the way, the cache will store each response that is deemed “cacheable” (See Introduction to HTTP Caching). If the same resource is requested again, the cache sends the cached response to the client, ignoring your application entirely.

This type of cache is known as a HTTP gateway cache and many exist such as Varnish, Squid in reverse proxy mode, and the Symfony reverse proxy.

Types of Caches

But a gateway cache isn’t the only type of cache. In fact, the HTTP cache headers sent by your application are consumed and interpreted by up to three different types of caches:

  • Browser caches: Every browser comes with its own local cache that is mainly useful for when you hit “back” or for images and other assets. The browser cache is a private cache as cached resources aren’t shared with anyone else;
  • Proxy caches: A proxy is a shared cache as many people can be behind a single one. It’s usually installed by large corporations and ISPs to reduce latency and network traffic;
  • Gateway caches: Like a proxy, it’s also a shared cache but on the server side. Installed by network administrators, it makes websites more scalable, reliable and performant.

小技巧

Gateway caches are sometimes referred to as reverse proxy caches, surrogate caches, or even HTTP accelerators.

注解

The significance of private versus shared caches will become more obvious when caching responses containing content that is specific to exactly one user (e.g. account information) is discussed.

Each response from your application will likely go through one or both of the first two cache types. These caches are outside of your control but follow the HTTP cache directions set in the response.

Symfony Reverse Proxy

Symfony comes with a reverse proxy (also called a gateway cache) written in PHP. Enable it and cacheable responses from your application will start to be cached right away. Installing it is just as easy. Each new Symfony application comes with a pre-configured caching kernel (AppCache) that wraps the default one (AppKernel). The caching Kernel is the reverse proxy.

To enable caching, modify the code of a front controller to use the caching kernel:

// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
require_once __DIR__.'/../app/AppCache.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// wrap the default AppKernel with the AppCache one
$kernel = new AppCache($kernel);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

The caching kernel will immediately act as a reverse proxy - caching responses from your application and returning them to the client.

小技巧

The cache kernel has a special getLog() method that returns a string representation of what happened in the cache layer. In the development environment, use it to debug and validate your cache strategy:

error_log($kernel->getLog());

The AppCache object has a sensible default configuration, but it can be finely tuned via a set of options you can set by overriding the getOptions() method:

// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

class AppCache extends HttpCache
{
    protected function getOptions()
    {
        return array(
            'debug'                  => false,
            'default_ttl'            => 0,
            'private_headers'        => array('Authorization', 'Cookie'),
            'allow_reload'           => false,
            'allow_revalidate'       => false,
            'stale_while_revalidate' => 2,
            'stale_if_error'         => 60,
        );
    }
}

小技巧

Unless overridden in getOptions(), the debug option will be set to automatically be the debug value of the wrapped AppKernel.

Here is a list of the main options:

default_ttl
The number of seconds that a cache entry should be considered fresh when no explicit freshness information is provided in a response. Explicit Cache-Control or Expires headers override this value (default: 0).
private_headers
Set of request headers that trigger “private” Cache-Control behavior on responses that don’t explicitly state whether the response is public or private via a Cache-Control directive (default: Authorization and Cookie).
allow_reload
Specifies whether the client can force a cache reload by including a Cache-Control “no-cache” directive in the request. Set it to true for compliance with RFC 2616 (default: false).
allow_revalidate
Specifies whether the client can force a cache revalidate by including a Cache-Control “max-age=0” directive in the request. Set it to true for compliance with RFC 2616 (default: false).
stale_while_revalidate
Specifies the default number of seconds (the granularity is the second as the Response TTL precision is a second) during which the cache can immediately return a stale response while it revalidates it in the background (default: 2); this setting is overridden by the stale-while-revalidate HTTP Cache-Control extension (see RFC 5861).
stale_if_error
Specifies the default number of seconds (the granularity is the second) during which the cache can serve a stale response when an error is encountered (default: 60). This setting is overridden by the stale-if-error HTTP Cache-Control extension (see RFC 5861).

If debug is true, Symfony automatically adds an X-Symfony-Cache header to the response containing useful information about cache hits and misses.

注解

The performance of the Symfony reverse proxy is independent of the complexity of the application. That’s because the application kernel is only booted when the request needs to be forwarded to it.

Introduction to HTTP Caching

To take advantage of the available cache layers, your application must be able to communicate which responses are cacheable and the rules that govern when/how that cache should become stale. This is done by setting HTTP cache headers on the response.

小技巧

Keep in mind that “HTTP” is nothing more than the language (a simple text language) that web clients (e.g. browsers) and web servers use to communicate with each other. HTTP caching is the part of that language that allows clients and servers to exchange information related to caching.

HTTP specifies four response cache headers that are looked at here:

  • Cache-Control
  • Expires
  • ETag
  • Last-Modified

The most important and versatile header is the Cache-Control header, which is actually a collection of various cache information.

注解

Each of the headers will be explained in full detail in the HTTP Expiration, Validation and Invalidation section.

The Cache-Control Header

The Cache-Control header is unique in that it contains not one, but various pieces of information about the cacheability of a response. Each piece of information is separated by a comma:

Cache-Control: private, max-age=0, must-revalidate

Cache-Control: max-age=3600, must-revalidate

Symfony provides an abstraction around the Cache-Control header to make its creation more manageable:

// ...

use Symfony\Component\HttpFoundation\Response;

$response = new Response();

// mark the response as either public or private
$response->setPublic();
$response->setPrivate();

// set the private or shared max age
$response->setMaxAge(600);
$response->setSharedMaxAge(600);

// set a custom Cache-Control directive
$response->headers->addCacheControlDirective('must-revalidate', true);

小技巧

If you need to set cache headers for many different controller actions, you might want to look into the FOSHttpCacheBundle. It provides a way to define cache headers based on the URL pattern and other request properties.

Public vs Private Responses

Both gateway and proxy caches are considered “shared” caches as the cached content is shared by more than one user. If a user-specific response were ever mistakenly stored by a shared cache, it might be returned later to any number of different users. Imagine if your account information were cached and then returned to every subsequent user who asked for their account page!

To handle this situation, every response may be set to be public or private:

public
Indicates that the response may be cached by both private and shared caches.
private
Indicates that all or part of the response message is intended for a single user and must not be cached by a shared cache.

Symfony conservatively defaults each response to be private. To take advantage of shared caches (like the Symfony reverse proxy), the response will need to be explicitly set as public.

Safe Methods

HTTP caching only works for “safe” HTTP methods (like GET and HEAD). Being safe means that you never change the application’s state on the server when serving the request (you can of course log information, cache data, etc). This has two very reasonable consequences:

  • You should never change the state of your application when responding to a GET or HEAD request. Even if you don’t use a gateway cache, the presence of proxy caches mean that any GET or HEAD request may or may not actually hit your server;
  • Don’t expect PUT, POST or DELETE methods to cache. These methods are meant to be used when mutating the state of your application (e.g. deleting a blog post). Caching them would prevent certain requests from hitting and mutating your application.
Caching Rules and Defaults

HTTP 1.1 allows caching anything by default unless there is an explicit Cache-Control header. In practice, most caches do nothing when requests have a cookie, an authorization header, use a non-safe method (i.e. PUT, POST, DELETE), or when responses have a redirect status code.

Symfony automatically sets a sensible and conservative Cache-Control header when none is set by the developer by following these rules:

  • If no cache header is defined (Cache-Control, Expires, ETag or Last-Modified), Cache-Control is set to no-cache, meaning that the response will not be cached;
  • If Cache-Control is empty (but one of the other cache headers is present), its value is set to private, must-revalidate;
  • But if at least one Cache-Control directive is set, and no public or private directives have been explicitly added, Symfony adds the private directive automatically (except when s-maxage is set).
HTTP Expiration, Validation and Invalidation

The HTTP specification defines two caching models:

  • With the expiration model, you simply specify how long a response should be considered “fresh” by including a Cache-Control and/or an Expires header. Caches that understand expiration will not make the same request until the cached version reaches its expiration time and becomes “stale”;
  • When pages are really dynamic (i.e. their representation changes often), the validation model is often necessary. With this model, the cache stores the response, but asks the server on each request whether or not the cached response is still valid. The application uses a unique response identifier (the Etag header) and/or a timestamp (the Last-Modified header) to check if the page has changed since being cached.

The goal of both models is to never generate the same response twice by relying on a cache to store and return “fresh” responses. To achieve long caching times but still provide updated content immediately, cache invalidation is sometimes used.

Expiration

The expiration model is the more efficient and straightforward of the two caching models and should be used whenever possible. When a response is cached with an expiration, the cache will store the response and return it directly without hitting the application until it expires.

The expiration model can be accomplished using one of two, nearly identical, HTTP headers: Expires or Cache-Control.

Expiration with the Expires Header

According to the HTTP specification, “the Expires header field gives the date/time after which the response is considered stale.” The Expires header can be set with the setExpires() Response method. It takes a DateTime instance as an argument:

$date = new DateTime();
$date->modify('+600 seconds');

$response->setExpires($date);

The resulting HTTP header will look like this:

Expires: Thu, 01 Mar 2011 16:00:00 GMT

注解

The setExpires() method automatically converts the date to the GMT timezone as required by the specification.

Note that in HTTP versions before 1.1 the origin server wasn’t required to send the Date header. Consequently, the cache (e.g. the browser) might need to rely on the local clock to evaluate the Expires header making the lifetime calculation vulnerable to clock skew. Another limitation of the Expires header is that the specification states that “HTTP/1.1 servers should not send Expires dates more than one year in the future.”

Expiration with the Cache-Control Header

Because of the Expires header limitations, most of the time, you should use the Cache-Control header instead. Recall that the Cache-Control header is used to specify many different cache directives. For expiration, there are two directives, max-age and s-maxage. The first one is used by all caches, whereas the second one is only taken into account by shared caches:

// Sets the number of seconds after which the response
// should no longer be considered fresh
$response->setMaxAge(600);

// Same as above but only for shared caches
$response->setSharedMaxAge(600);

The Cache-Control header would take on the following format (it may have additional directives):

Cache-Control: max-age=600, s-maxage=600
Validation

When a resource needs to be updated as soon as a change is made to the underlying data, the expiration model falls short. With the expiration model, the application won’t be asked to return the updated response until the cache finally becomes stale.

The validation model addresses this issue. Under this model, the cache continues to store responses. The difference is that, for each request, the cache asks the application if the cached response is still valid or if it needs to be regenerated. If the cache is still valid, your application should return a 304 status code and no content. This tells the cache that it’s ok to return the cached response.

Under this model, you only save CPU if you’re able to determine that the cached response is still valid by doing less work than generating the whole page again (see below for an implementation example).

小技巧

The 304 status code means “Not Modified”. It’s important because with this status code the response does not contain the actual content being requested. Instead, the response is simply a light-weight set of directions that tells the cache that it should use its stored version.

Like with expiration, there are two different HTTP headers that can be used to implement the validation model: ETag and Last-Modified.

Validation with the ETag Header

The ETag header is a string header (called the “entity-tag”) that uniquely identifies one representation of the target resource. It’s entirely generated and set by your application so that you can tell, for example, if the /about resource that’s stored by the cache is up-to-date with what your application would return. An ETag is like a fingerprint and is used to quickly compare if two different versions of a resource are equivalent. Like fingerprints, each ETag must be unique across all representations of the same resource.

To see a simple implementation, generate the ETag as the md5 of the content:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $response = $this->render('MyBundle:Main:index.html.twig');
    $response->setETag(md5($response->getContent()));
    $response->setPublic(); // make sure the response is public/cacheable
    $response->isNotModified($request);

    return $response;
}

The isNotModified() method compares the If-None-Match sent with the Request with the ETag header set on the Response. If the two match, the method automatically sets the Response status code to 304.

注解

The cache sets the If-None-Match header on the request to the ETag of the original cached response before sending the request back to the app. This is how the cache and server communicate with each other and decide whether or not the resource has been updated since it was cached.

This algorithm is simple enough and very generic, but you need to create the whole Response before being able to compute the ETag, which is sub-optimal. In other words, it saves on bandwidth, but not CPU cycles.

In the Optimizing your Code with Validation section, you’ll see how validation can be used more intelligently to determine the validity of a cache without doing so much work.

小技巧

Symfony also supports weak ETags by passing true as the second argument to the setETag() method.

Validation with the Last-Modified Header

The Last-Modified header is the second form of validation. According to the HTTP specification, “The Last-Modified header field indicates the date and time at which the origin server believes the representation was last modified.” In other words, the application decides whether or not the cached content has been updated based on whether or not it’s been updated since the response was cached.

For instance, you can use the latest update date for all the objects needed to compute the resource representation as the value for the Last-Modified header value:

use Symfony\Component\HttpFoundation\Request;

public function showAction($articleSlug, Request $request)
{
    // ...

    $articleDate = new \DateTime($article->getUpdatedAt());
    $authorDate = new \DateTime($author->getUpdatedAt());

    $date = $authorDate > $articleDate ? $authorDate : $articleDate;

    $response->setLastModified($date);
    // Set response as public. Otherwise it will be private by default.
    $response->setPublic();

    if ($response->isNotModified($request)) {
        return $response;
    }

    // ... do more work to populate the response with the full content

    return $response;
}

The isNotModified() method compares the If-Modified-Since header sent by the request with the Last-Modified header set on the response. If they are equivalent, the Response will be set to a 304 status code.

注解

The cache sets the If-Modified-Since header on the request to the Last-Modified of the original cached response before sending the request back to the app. This is how the cache and server communicate with each other and decide whether or not the resource has been updated since it was cached.

Optimizing your Code with Validation

The main goal of any caching strategy is to lighten the load on the application. Put another way, the less you do in your application to return a 304 response, the better. The Response::isNotModified() method does exactly that by exposing a simple and efficient pattern:

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

public function showAction($articleSlug, Request $request)
{
    // Get the minimum information to compute
    // the ETag or the Last-Modified value
    // (based on the Request, data is retrieved from
    // a database or a key-value store for instance)
    $article = ...;

    // create a Response with an ETag and/or a Last-Modified header
    $response = new Response();
    $response->setETag($article->computeETag());
    $response->setLastModified($article->getPublishedAt());

    // Set response as public. Otherwise it will be private by default.
    $response->setPublic();

    // Check that the Response is not modified for the given Request
    if ($response->isNotModified($request)) {
        // return the 304 Response immediately
        return $response;
    }

    // do more work here - like retrieving more data
    $comments = ...;

    // or render a template with the $response you've already started
    return $this->render(
        'MyBundle:MyController:article.html.twig',
        array('article' => $article, 'comments' => $comments),
        $response
    );
}

When the Response is not modified, the isNotModified() automatically sets the response status code to 304, removes the content, and removes some headers that must not be present for 304 responses (see setNotModified()).

Varying the Response

So far, it’s been assumed that each URI has exactly one representation of the target resource. By default, HTTP caching is done by using the URI of the resource as the cache key. If two people request the same URI of a cacheable resource, the second person will receive the cached version.

Sometimes this isn’t enough and different versions of the same URI need to be cached based on one or more request header values. For instance, if you compress pages when the client supports it, any given URI has two representations: one when the client supports compression, and one when it does not. This determination is done by the value of the Accept-Encoding request header.

In this case, you need the cache to store both a compressed and uncompressed version of the response for the particular URI and return them based on the request’s Accept-Encoding value. This is done by using the Vary response header, which is a comma-separated list of different headers whose values trigger a different representation of the requested resource:

Vary: Accept-Encoding, User-Agent

小技巧

This particular Vary header would cache different versions of each resource based on the URI and the value of the Accept-Encoding and User-Agent request header.

The Response object offers a clean interface for managing the Vary header:

// set one vary header
$response->setVary('Accept-Encoding');

// set multiple vary headers
$response->setVary(array('Accept-Encoding', 'User-Agent'));

The setVary() method takes a header name or an array of header names for which the response varies.

Expiration and Validation

You can of course use both validation and expiration within the same Response. As expiration wins over validation, you can easily benefit from the best of both worlds. In other words, by using both expiration and validation, you can instruct the cache to serve the cached content, while checking back at some interval (the expiration) to verify that the content is still valid.

小技巧

You can also define HTTP caching headers for expiration and validation by using annotations. See the FrameworkExtraBundle documentation.

More Response Methods

The Response class provides many more methods related to the cache. Here are the most useful ones:

// Marks the Response stale
$response->expire();

// Force the response to return a proper 304 response with no content
$response->setNotModified();

Additionally, most cache-related HTTP headers can be set via the single setCache() method:

// Set cache settings in one call
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));
Cache Invalidation
“There are only two hard things in Computer Science: cache invalidation and naming things.” – Phil Karlton

Once an URL is cached by a gateway cache, the cache will not ask the application for that content anymore. This allows the cache to provide fast responses and reduces the load on your application. However, you risk delivering outdated content. A way out of this dilemma is to use long cache lifetimes, but to actively notify the gateway cache when content changes. Reverse proxies usually provide a channel to receive such notifications, typically through special HTTP requests.

警告

While cache invalidation is powerful, avoid it when possible. If you fail to invalidate something, outdated caches will be served for a potentially long time. Instead, use short cache lifetimes or use the validation model, and adjust your controllers to perform efficient validation checks as explained in Optimizing your Code with Validation.

Furthermore, since invalidation is a topic specific to each type of reverse proxy, using this concept will tie you to a specific reverse proxy or need additional efforts to support different proxies.

Sometimes, however, you need that extra performance you can get when explicitly invalidating. For invalidation, your application needs to detect when content changes and tell the cache to remove the URLs which contain that data from its cache.

小技巧

If you want to use cache invalidation, have a look at the FOSHttpCacheBundle. This bundle provides services to help with various cache invalidation concepts, and also documents the configuration for the a couple of common caching proxies.

If one content corresponds to one URL, the PURGE model works well. You send a request to the cache proxy with the HTTP method PURGE (using the word “PURGE” is a convention, technically this can be any string) instead of GET and make the cache proxy detect this and remove the data from the cache instead of going to the application to get a response.

Here is how you can configure the Symfony reverse proxy to support the PURGE HTTP method:

// app/AppCache.php

// ...
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class AppCache extends HttpCache
{
    protected function invalidate(Request $request, $catch = false)
    {
        if ('PURGE' !== $request->getMethod()) {
            return parent::invalidate($request, $catch);
        }

        if ('127.0.0.1' !== $request->getClientIp()) {
            return new Response(
                'Invalid HTTP method',
                Response::HTTP_BAD_REQUEST
            );
        }

        $response = new Response();
        if ($this->getStore()->purge($request->getUri())) {
            $response->setStatusCode(200, 'Purged');
        } else {
            $response->setStatusCode(200, 'Not found');
        }

        return $response;
    }
}

警告

You must protect the PURGE HTTP method somehow to avoid random people purging your cached data.

Purge instructs the cache to drop a resource in all its variants (according to the Vary header, see above). An alternative to purging is refreshing a content. Refreshing means that the caching proxy is instructed to discard its local cache and fetch the content again. This way, the new content is already available in the cache. The drawback of refreshing is that variants are not invalidated.

In many applications, the same content bit is used on various pages with different URLs. More flexible concepts exist for those cases:

  • Banning invalidates responses matching regular expressions on the URL or other criteria;
  • Cache tagging lets you add a tag for each content used in a response so that you can invalidate all URLs containing a certain content.
Using Edge Side Includes

Gateway caches are a great way to make your website perform better. But they have one limitation: they can only cache whole pages. If you can’t cache whole pages or if parts of a page has “more” dynamic parts, you are out of luck. Fortunately, Symfony provides a solution for these cases, based on a technology called ESI, or Edge Side Includes. Akamai wrote this specification almost 10 years ago, and it allows specific parts of a page to have a different caching strategy than the main page.

The ESI specification describes tags you can embed in your pages to communicate with the gateway cache. Only one tag is implemented in Symfony, include, as this is the only useful one outside of Akamai context:

<!DOCTYPE html>
<html>
    <body>
        <!-- ... some content -->

        <!-- Embed the content of another page here -->
        <esi:include src="http://..." />

        <!-- ... more content -->
    </body>
</html>

注解

Notice from the example that each ESI tag has a fully-qualified URL. An ESI tag represents a page fragment that can be fetched via the given URL.

When a request is handled, the gateway cache fetches the entire page from its cache or requests it from the backend application. If the response contains one or more ESI tags, these are processed in the same way. In other words, the gateway cache either retrieves the included page fragment from its cache or requests the page fragment from the backend application again. When all the ESI tags have been resolved, the gateway cache merges each into the main page and sends the final content to the client.

All of this happens transparently at the gateway cache level (i.e. outside of your application). As you’ll see, if you choose to take advantage of ESI tags, Symfony makes the process of including them almost effortless.

Using ESI in Symfony

First, to use ESI, be sure to enable it in your application configuration:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        esi: { enabled: true }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/symfony"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <!-- ... -->
            <framework:esi enabled="true" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'esi' => array('enabled' => true),
    ));
    

Now, suppose you have a page that is relatively static, except for a news ticker at the bottom of the content. With ESI, you can cache the news ticker independent of the rest of the page.

public function indexAction()
{
    $response = $this->render('MyBundle:MyController:index.html.twig');
    // set the shared max age - which also marks the response as public
    $response->setSharedMaxAge(600);

    return $response;
}

In this example, the full-page cache has a lifetime of ten minutes. Next, include the news ticker in the template by embedding an action. This is done via the render helper (See Embedding Controllers for more details).

As the embedded content comes from another page (or controller for that matter), Symfony uses the standard render helper to configure ESI tags:

  • Twig
    {# you can use a controller reference #}
    {{ render_esi(controller('...:news', { 'maxPerPage': 5 })) }}
    
    {# ... or a URL #}
    {{ render_esi(url('latest_news', { 'maxPerPage': 5 })) }}
    
  • PHP
    <?php echo $view['actions']->render(
        new \Symfony\Component\HttpKernel\Controller\ControllerReference('...:news', array('maxPerPage' => 5)),
        array('strategy' => 'esi'))
    ?>
    
    <?php echo $view['actions']->render(
        $view['router']->generate('latest_news', array('maxPerPage' => 5), true),
        array('strategy' => 'esi'),
    ) ?>
    

By using the esi renderer (via the render_esi Twig function), you tell Symfony that the action should be rendered as an ESI tag. You might be wondering why you would want to use a helper instead of just writing the ESI tag yourself. That’s because using a helper makes your application work even if there is no gateway cache installed.

小技巧

As you’ll see below, the maxPerPage variable you pass is available as an argument to your controller (i.e. $maxPerPage). The variables passed through render_esi also become part of the cache key so that you have unique caches for each combination of variables and values.

When using the default render function (or setting the renderer to inline), Symfony merges the included page content into the main one before sending the response to the client. But if you use the esi renderer (i.e. call render_esi), and if Symfony detects that it’s talking to a gateway cache that supports ESI, it generates an ESI include tag. But if there is no gateway cache or if it does not support ESI, Symfony will just merge the included page content within the main one as it would have done if you had used render.

注解

Symfony detects if a gateway cache supports ESI via another Akamai specification that is supported out of the box by the Symfony reverse proxy.

The embedded action can now specify its own caching rules, entirely independent of the master page.

public function newsAction($maxPerPage)
{
    // ...

    $response->setSharedMaxAge(60);
}

With ESI, the full page cache will be valid for 600 seconds, but the news component cache will only last for 60 seconds.

When using a controller reference, the ESI tag should reference the embedded action as an accessible URL so the gateway cache can fetch it independently of the rest of the page. Symfony takes care of generating a unique URL for any controller reference and it is able to route them properly thanks to the FragmentListener that must be enabled in your configuration:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        fragments: { path: /_fragment }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/framework"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <!-- ... -->
        <framework:config>
            <framework:fragments path="/_fragment" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'fragments' => array('path' => '/_fragment'),
    ));
    

One great advantage of the ESI renderer is that you can make your application as dynamic as needed and at the same time, hit the application as little as possible.

小技巧

The listener only responds to local IP addresses or trusted proxies.

注解

Once you start using ESI, remember to always use the s-maxage directive instead of max-age. As the browser only ever receives the aggregated resource, it is not aware of the sub-components, and so it will obey the max-age directive and cache the entire page. And you don’t want that.

The render_esi helper supports two other useful options:

alt
Used as the alt attribute on the ESI tag, which allows you to specify an alternative URL to be used if the src cannot be found.
ignore_errors
If set to true, an onerror attribute will be added to the ESI with a value of continue indicating that, in the event of a failure, the gateway cache will simply remove the ESI tag silently.
Summary

Symfony was designed to follow the proven rules of the road: HTTP. Caching is no exception. Mastering the Symfony cache system means becoming familiar with the HTTP cache models and using them effectively. This means that, instead of relying only on Symfony documentation and code examples, you have access to a world of knowledge related to HTTP caching and gateway caches such as Varnish.

Learn more from the Cookbook

Translations

The term “internationalization” (often abbreviated i18n) refers to the process of abstracting strings and other locale-specific pieces out of your application into a layer where they can be translated and converted based on the user’s locale (i.e. language and country). For text, this means wrapping each with a function capable of translating the text (or “message”) into the language of the user:

// text will *always* print out in English
echo 'Hello World';

// text can be translated into the end-user's language or
// default to English
echo $translator->trans('Hello World');

注解

The term locale refers roughly to the user’s language and country. It can be any string that your application uses to manage translations and other format differences (e.g. currency format). The ISO 639-1 language code, an underscore (_), then the ISO 3166-1 alpha-2 country code (e.g. fr_FR for French/France) is recommended.

In this chapter, you’ll learn how to use the Translation component in the Symfony framework. You can read the Translation component documentation to learn even more. Overall, the process has several steps:

  1. Enable and configure Symfony’s translation service;
  2. Abstract strings (i.e. “messages”) by wrapping them in calls to the Translator (“Basic Translation”);
  3. Create translation resources/files for each supported locale that translate each message in the application;
  4. Determine, set and manage the user’s locale for the request and optionally on the user’s entire session.
Configuration

Translations are handled by a translator service that uses the user’s locale to lookup and return translated messages. Before using it, enable the translator in your configuration:

  • YAML
    # app/config/config.yml
    framework:
        translator: { fallback: en }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:translator fallback="en" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'translator' => array('fallback' => 'en'),
    ));
    

See Fallback Translation Locales for details on the fallback key and what Symfony does when it doesn’t find a translation.

The locale used in translations is the one stored on the request. This is typically set via a _locale attribute on your routes (see The Locale and the URL).

Basic Translation

Translation of text is done through the translator service (Translator). To translate a block of text (called a message), use the trans() method. Suppose, for example, that you’re translating a simple message from inside a controller:

// ...
use Symfony\Component\HttpFoundation\Response;

public function indexAction()
{
    $translated = $this->get('translator')->trans('Symfony is great');

    return new Response($translated);
}

When this code is executed, Symfony will attempt to translate the message “Symfony is great” based on the locale of the user. For this to work, you need to tell Symfony how to translate the message via a “translation resource”, which is usually a file that contains a collection of translations for a given locale. This “dictionary” of translations can be created in several different formats, XLIFF being the recommended format:

  • XML
    <!-- messages.fr.xliff -->
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>Symfony is great</source>
                    <target>J'aime Symfony</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • YAML
    # messages.fr.yml
    Symfony is great: J'aime Symfony
    
  • PHP
    // messages.fr.php
    return array(
        'Symfony is great' => 'J\'aime Symfony',
    );
    

For information on where these files should be located, see Translation Resource/File Names and Locations.

Now, if the language of the user’s locale is French (e.g. fr_FR or fr_BE), the message will be translated into J'aime Symfony. You can also translate the message inside your templates.

The Translation Process

To actually translate the message, Symfony uses a simple process:

  • The locale of the current user, which is stored on the request is determined;
  • A catalog (e.g. big collection) of translated messages is loaded from translation resources defined for the locale (e.g. fr_FR). Messages from the fallback locale are also loaded and added to the catalog if they don’t already exist. The end result is a large “dictionary” of translations.
  • If the message is located in the catalog, the translation is returned. If not, the translator returns the original message.

When using the trans() method, Symfony looks for the exact string inside the appropriate message catalog and returns it (if it exists).

Message Placeholders

Sometimes, a message containing a variable needs to be translated:

use Symfony\Component\HttpFoundation\Response;

public function indexAction($name)
{
    $translated = $this->get('translator')->trans('Hello '.$name);

    return new Response($translated);
}

However, creating a translation for this string is impossible since the translator will try to look up the exact message, including the variable portions (e.g. “Hello Ryan” or “Hello Fabien”).

For details on how to handle this situation, see Message Placeholders in the components documentation. For how to do this in templates, see Twig Templates.

Pluralization

Another complication is when you have translations that may or may not be plural, based on some variable:

There is one apple.
There are 5 apples.

To handle this, use the transChoice() method or the transchoice tag/filter in your template.

For much more information, see Pluralization in the Translation component documentation.

Translations in Templates

Most of the time, translation occurs in templates. Symfony provides native support for both Twig and PHP templates.

Twig Templates

Symfony provides specialized Twig tags (trans and transchoice) to help with message translation of static blocks of text:

{% trans %}Hello %name%{% endtrans %}

{% transchoice count %}
    {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}

The transchoice tag automatically gets the %count% variable from the current context and passes it to the translator. This mechanism only works when you use a placeholder following the %var% pattern.

警告

The %var% notation of placeholders is required when translating in Twig templates using the tag.

小技巧

If you need to use the percent character (%) in a string, escape it by doubling it: {% trans %}Percent: %percent%%%{% endtrans %}

You can also specify the message domain and pass some additional variables:

{% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}

{% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %}

{% transchoice count with {'%name%': 'Fabien'} from "app" %}
    {0} %name%, there are no apples|{1} %name%, there is one apple|]1,Inf] %name%, there are %count% apples
{% endtranschoice %}

The trans and transchoice filters can be used to translate variable texts and complex expressions:

{{ message|trans }}

{{ message|transchoice(5) }}

{{ message|trans({'%name%': 'Fabien'}, "app") }}

{{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }}

小技巧

Using the translation tags or filters have the same effect, but with one subtle difference: automatic output escaping is only applied to translations using a filter. In other words, if you need to be sure that your translated message is not output escaped, you must apply the raw filter after the translation filter:

{# text translated between tags is never escaped #}
{% trans %}
    <h3>foo</h3>
{% endtrans %}

{% set message = '<h3>foo</h3>' %}

{# strings and variables translated via a filter are escaped by default #}
{{ message|trans|raw }}
{{ '<h3>bar</h3>'|trans|raw }}

小技巧

You can set the translation domain for an entire Twig template with a single tag:

{% trans_default_domain "app" %}

Note that this only influences the current template, not any “included” template (in order to avoid side effects).

2.1 新版功能: The trans_default_domain tag was introduced in Symfony 2.1.

PHP Templates

The translator service is accessible in PHP templates through the translator helper:

<?php echo $view['translator']->trans('Symfony is great') ?>

<?php echo $view['translator']->transChoice(
    '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10)
) ?>
Translation Resource/File Names and Locations

Symfony looks for message files (i.e. translations) in the following locations:

  • the app/Resources/translations directory;
  • the app/Resources/<bundle name>/translations directory;
  • the Resources/translations/ directory inside of any bundle.

The locations are listed here with the highest priority first. That is, you can override the translation messages of a bundle in any of the top 2 directories.

The override mechanism works at a key level: only the overridden keys need to be listed in a higher priority message file. When a key is not found in a message file, the translator will automatically fall back to the lower priority message files.

The filename of the translation files is also important: each message file must be named according to the following path: domain.locale.loader:

  • domain: An optional way to organize messages into groups (e.g. admin, navigation or the default messages) - see Using Message Domains;
  • locale: The locale that the translations are for (e.g. en_GB, en, etc);
  • loader: How Symfony should load and parse the file (e.g. xliff, php, yml, etc).

The loader can be the name of any registered loader. By default, Symfony provides many loaders, including:

  • xliff: XLIFF file;
  • php: PHP file;
  • yml: YAML file.

The choice of which loader to use is entirely up to you and is a matter of taste. The recommended option is to use xliff for translations. For more options, see Loading Message Catalogs.

注解

You can also store translations in a database, or any other storage by providing a custom class implementing the LoaderInterface interface. See the translation.loader tag for more information.

警告

Each time you create a new translation resource (or install a bundle that includes a translation resource), be sure to clear your cache so that Symfony can discover the new translation resources:

$ php app/console cache:clear
Fallback Translation Locales

Imagine that the user’s locale is fr_FR and that you’re translating the key Symfony is great. To find the French translation, Symfony actually checks translation resources for several locales:

  1. First, Symfony looks for the translation in a fr_FR translation resource (e.g. messages.fr_FR.xliff);
  2. If it wasn’t found, Symfony looks for the translation in a fr translation resource (e.g. messages.fr.xliff);
  3. If the translation still isn’t found, Symfony uses the fallback configuration parameter, which defaults to en (see Configuration).
Handling the User’s Locale

The locale of the current user is stored in the request and is accessible via the request object:

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $locale = $request->getLocale();

    $request->setLocale('en_US');
}

小技巧

Read Making the Locale “Sticky” during a User’s Session to learn how to store the user’s locale in the session.

See the The Locale and the URL section below about setting the locale via routing.

The Locale and the URL

Since you can store the locale of the user in the session, it may be tempting to use the same URL to display a resource in different languages based on the user’s locale. For example, http://www.example.com/contact could show content in English for one user and French for another user. Unfortunately, this violates a fundamental rule of the Web: that a particular URL returns the same resource regardless of the user. To further muddy the problem, which version of the content would be indexed by search engines?

A better policy is to include the locale in the URL. This is fully-supported by the routing system using the special _locale parameter:

  • YAML
    # app/config/routing.yml
    contact:
        path:     /{_locale}/contact
        defaults: { _controller: AppBundle:Contact:index }
        requirements:
            _locale: en|fr|de
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="contact" path="/{_locale}/contact">
            <default key="_controller">AppBundle:Contact:index</default>
            <requirement key="_locale">en|fr|de</requirement>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('contact', new Route(
        '/{_locale}/contact',
        array(
            '_controller' => 'AppBundle:Contact:index',
        ),
        array(
            '_locale'     => 'en|fr|de',
        )
    ));
    
    return $collection;
    

When using the special _locale parameter in a route, the matched locale will automatically be set on the Request and can be retrieved via the getLocale() method. In other words, if a user visits the URI /fr/contact, the locale fr will automatically be set as the locale for the current request.

You can now use the locale to create routes to other translated pages in your application.

小技巧

Read How to Use Service Container Parameters in your Routes to learn how to avoid hardcoding the _locale requirement in all your routes.

Setting a default Locale

What if the user’s locale hasn’t been determined? You can guarantee that a locale is set on each user’s request by defining a default_locale for the framework:

  • YAML
    # app/config/config.yml
    framework:
        default_locale: en
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config default-locale="en" />
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'default_locale' => 'en',
    ));
    

2.1 新版功能: The default_locale parameter was defined under the session key originally, however, as of 2.1 this has been moved. This is because the locale is now set on the request instead of the session.

Translating Constraint Messages

If you’re using validation constraints with the form framework, then translating the error messages is easy: simply create a translation resource for the validators domain.

To start, suppose you’ve created a plain-old-PHP object that you need to use somewhere in your application:

// src/AppBundle/Entity/Author.php
namespace AppBundle\Entity;

class Author
{
    public $name;
}

Add constraints though any of the supported methods. Set the message option to the translation source text. For example, to guarantee that the $name property is not empty, add the following:

  • Annotations
    // src/AppBundle/Entity/Author.php
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\NotBlank(message = "author.name.not_blank")
         */
        public $name;
    }
    
  • YAML
    # src/AppBundle/Resources/config/validation.yml
    AppBundle\Entity\Author:
        properties:
            name:
                - NotBlank: { message: "author.name.not_blank" }
    
  • XML
    <!-- src/AppBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
            http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="AppBundle\Entity\Author">
            <property name="name">
                <constraint name="NotBlank">
                    <option name="message">author.name.not_blank</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/AppBundle/Entity/Author.php
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\NotBlank;
    
    class Author
    {
        public $name;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('name', new NotBlank(array(
                'message' => 'author.name.not_blank',
            )));
        }
    }
    

Create a translation file under the validators catalog for the constraint messages, typically in the Resources/translations/ directory of the bundle.

  • XML
    <!-- validators.en.xliff -->
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>author.name.not_blank</source>
                    <target>Please enter an author name.</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • YAML
    # validators.en.yml
    author.name.not_blank: Please enter an author name.
    
  • PHP
    // validators.en.php
    return array(
        'author.name.not_blank' => 'Please enter an author name.',
    );
    
Translating Database Content

The translation of database content should be handled by Doctrine through the Translatable Extension or the Translatable Behavior (PHP 5.4+). For more information, see the documentation for these libraries.

Summary

With the Symfony Translation component, creating an internationalized application no longer needs to be a painful process and boils down to just a few basic steps:

  • Abstract messages in your application by wrapping each in either the trans() or transChoice() methods (learn about this in Using the Translator);
  • Translate each message into multiple locales by creating translation message files. Symfony discovers and processes each file because its name follows a specific convention;
  • Manage the user’s locale, which is stored on the request, but can also be set on the user’s session.

Service Container

A modern PHP application is full of objects. One object may facilitate the delivery of email messages while another may allow you to persist information into a database. In your application, you may create an object that manages your product inventory, or another object that processes data from a third-party API. The point is that a modern application does many things and is organized into many objects that handle each task.

This chapter is about a special PHP object in Symfony that helps you instantiate, organize and retrieve the many objects of your application. This object, called a service container, will allow you to standardize and centralize the way objects are constructed in your application. The container makes your life easier, is super fast, and emphasizes an architecture that promotes reusable and decoupled code. Since all core Symfony classes use the container, you’ll learn how to extend, configure and use any object in Symfony. In large part, the service container is the biggest contributor to the speed and extensibility of Symfony.

Finally, configuring and using the service container is easy. By the end of this chapter, you’ll be comfortable creating your own objects via the container and customizing objects from any third-party bundle. You’ll begin writing code that is more reusable, testable and decoupled, simply because the service container makes writing good code so easy.

小技巧

If you want to know a lot more after reading this chapter, check out the DependencyInjection component documentation.

What is a Service?

Put simply, a Service is any PHP object that performs some sort of “global” task. It’s a purposefully-generic name used in computer science to describe an object that’s created for a specific purpose (e.g. delivering emails). Each service is used throughout your application whenever you need the specific functionality it provides. You don’t have to do anything special to make a service: simply write a PHP class with some code that accomplishes a specific task. Congratulations, you’ve just created a service!

注解

As a rule, a PHP object is a service if it is used globally in your application. A single Mailer service is used globally to send email messages whereas the many Message objects that it delivers are not services. Similarly, a Product object is not a service, but an object that persists Product objects to a database is a service.

So what’s the big deal then? The advantage of thinking about “services” is that you begin to think about separating each piece of functionality in your application into a series of services. Since each service does just one job, you can easily access each service and use its functionality wherever you need it. Each service can also be more easily tested and configured since it’s separated from the other functionality in your application. This idea is called service-oriented architecture and is not unique to Symfony or even PHP. Structuring your application around a set of independent service classes is a well-known and trusted object-oriented best-practice. These skills are key to being a good developer in almost any language.

What is a Service Container?

A Service Container (or dependency injection container) is simply a PHP object that manages the instantiation of services (i.e. objects).

For example, suppose you have a simple PHP class that delivers email messages. Without a service container, you must manually create the object whenever you need it:

use Acme\HelloBundle\Mailer;

$mailer = new Mailer('sendmail');
$mailer->send('ryan@example.com', ...);

This is easy enough. The imaginary Mailer class allows you to configure the method used to deliver the email messages (e.g. sendmail, smtp, etc). But what if you wanted to use the mailer service somewhere else? You certainly don’t want to repeat the mailer configuration every time you need to use the Mailer object. What if you needed to change the transport from sendmail to smtp everywhere in the application? You’d need to hunt down every place you create a Mailer service and change it.

Creating/Configuring Services in the Container

A better answer is to let the service container create the Mailer object for you. In order for this to work, you must teach the container how to create the Mailer service. This is done via configuration, which can be specified in YAML, XML or PHP:

  • YAML
    # app/config/config.yml
    services:
        my_mailer:
            class:        Acme\HelloBundle\Mailer
            arguments:    [sendmail]
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer" class="Acme\HelloBundle\Mailer">
                <argument>sendmail</argument>
            </service>
        </services>
    </container>
    
  • PHP
    // app/config/config.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition('my_mailer', new Definition(
        'Acme\HelloBundle\Mailer',
        array('sendmail')
    ));
    

注解

When Symfony initializes, it builds the service container using the application configuration (app/config/config.yml by default). The exact file that’s loaded is dictated by the AppKernel::registerContainerConfiguration() method, which loads an environment-specific configuration file (e.g. config_dev.yml for the dev environment or config_prod.yml for prod).

An instance of the Acme\HelloBundle\Mailer object is now available via the service container. The container is available in any traditional Symfony controller where you can access the services of the container via the get() shortcut method:

class HelloController extends Controller
{
    // ...

    public function sendEmailAction()
    {
        // ...
        $mailer = $this->get('my_mailer');
        $mailer->send('ryan@foobar.net', ...);
    }
}

When you ask for the my_mailer service from the container, the container constructs the object and returns it. This is another major advantage of using the service container. Namely, a service is never constructed until it’s needed. If you define a service and never use it on a request, the service is never created. This saves memory and increases the speed of your application. This also means that there’s very little or no performance hit for defining lots of services. Services that are never used are never constructed.

As a bonus, the Mailer service is only created once and the same instance is returned each time you ask for the service. This is almost always the behavior you’ll need (it’s more flexible and powerful), but you’ll learn later how you can configure a service that has multiple instances in the “How to Work with Scopes” cookbook article.

注解

In this example, the controller extends Symfony’s base Controller, which gives you access to the service container itself. You can then use the get method to locate and retrieve the my_mailer service from the service container. You can also define your controllers as services. This is a bit more advanced and not necessary, but it allows you to inject only the services you need into your controller.

Service Parameters

The creation of new services (i.e. objects) via the container is pretty straightforward. Parameters make defining services more organized and flexible:

  • YAML
    # app/config/config.yml
    parameters:
        my_mailer.transport:  sendmail
    
    services:
        my_mailer:
            class:        Acme\HelloBundle\Mailer
            arguments:    ["%my_mailer.transport%"]
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="my_mailer.transport">sendmail</parameter>
        </parameters>
    
        <services>
            <service id="my_mailer" class="Acme\HelloBundle\Mailer">
                <argument>%my_mailer.transport%</argument>
            </service>
        </services>
    </container>
    
  • PHP
    // app/config/config.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('my_mailer.transport', 'sendmail');
    
    $container->setDefinition('my_mailer', new Definition(
        'Acme\HelloBundle\Mailer',
        array('%my_mailer.transport%')
    ));
    

The end result is exactly the same as before - the difference is only in how you defined the service. By surrounding the my_mailer.transport string in percent (%) signs, the container knows to look for a parameter with that name. When the container is built, it looks up the value of each parameter and uses it in the service definition.

注解

If you want to use a string that starts with an @ sign as a parameter value (e.g. a very safe mailer password) in a YAML file, you need to escape it by adding another @ sign (this only applies to the YAML format):

# app/config/parameters.yml
parameters:
    # This will be parsed as string "@securepass"
    mailer_password: "@@securepass"

注解

The percent sign inside a parameter or argument, as part of the string, must be escaped with another percent sign:

<argument type="string">http://symfony.com/?foo=%%s&bar=%%d</argument>

警告

You may receive a ScopeWideningInjectionException when passing the request service as an argument. To understand this problem better and learn how to solve it, refer to the cookbook article How to Work with Scopes.

The purpose of parameters is to feed information into services. Of course there was nothing wrong with defining the service without using any parameters. Parameters, however, have several advantages:

  • separation and organization of all service “options” under a single parameters key;
  • parameter values can be used in multiple service definitions;
  • when creating a service in a bundle (this follows shortly), using parameters allows the service to be easily customized in your application.

The choice of using or not using parameters is up to you. High-quality third-party bundles will always use parameters as they make the service stored in the container more configurable. For the services in your application, however, you may not need the flexibility of parameters.

Array Parameters

Parameters can also contain array values. See Array Parameters.

Importing other Container Configuration Resources

小技巧

In this section, service configuration files are referred to as resources. This is to highlight the fact that, while most configuration resources will be files (e.g. YAML, XML, PHP), Symfony is so flexible that configuration could be loaded from anywhere (e.g. a database or even via an external web service).

The service container is built using a single configuration resource (app/config/config.yml by default). All other service configuration (including the core Symfony and third-party bundle configuration) must be imported from inside this file in one way or another. This gives you absolute flexibility over the services in your application.

External service configuration can be imported in two different ways. The first - and most common method - is via the imports directive. Later, you’ll learn about the second method, which is the flexible and preferred method for importing service configuration from third-party bundles.

Importing Configuration with imports

So far, you’ve placed your my_mailer service container definition directly in the application configuration file (e.g. app/config/config.yml). Of course, since the Mailer class itself lives inside the AcmeHelloBundle, it makes more sense to put the my_mailer container definition inside the bundle as well.

First, move the my_mailer container definition into a new container resource file inside AcmeHelloBundle. If the Resources or Resources/config directories don’t exist, create them.

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    parameters:
        my_mailer.transport:  sendmail
    
    services:
        my_mailer:
            class:        Acme\HelloBundle\Mailer
            arguments:    ["%my_mailer.transport%"]
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="my_mailer.transport">sendmail</parameter>
        </parameters>
    
        <services>
            <service id="my_mailer" class="Acme\HelloBundle\Mailer">
                <argument>%my_mailer.transport%</argument>
            </service>
        </services>
    </container>
    
  • PHP
    // src/Acme/HelloBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('my_mailer.transport', 'sendmail');
    
    $container->setDefinition('my_mailer', new Definition(
        'Acme\HelloBundle\Mailer',
        array('%my_mailer.transport%')
    ));
    

The definition itself hasn’t changed, only its location. Of course the service container doesn’t know about the new resource file. Fortunately, you can easily import the resource file using the imports key in the application configuration.

  • YAML
    # app/config/config.yml
    imports:
        - { resource: "@AcmeHelloBundle/Resources/config/services.yml" }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <imports>
            <import resource="@AcmeHelloBundle/Resources/config/services.xml"/>
        </imports>
    </container>
    
  • PHP
    // app/config/config.php
    $loader->import('@AcmeHelloBundle/Resources/config/services.php');
    

注解

Due to the way in which parameters are resolved, you cannot use them to build paths in imports dynamically. This means that something like the following doesn’t work:

  • YAML
    # app/config/config.yml
    imports:
        - { resource: "%kernel.root_dir%/parameters.yml" }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <imports>
            <import resource="%kernel.root_dir%/parameters.yml" />
        </imports>
    </container>
    
  • PHP
    // app/config/config.php
    $loader->import('%kernel.root_dir%/parameters.yml');
    

The imports directive allows your application to include service container configuration resources from any other location (most commonly from bundles). The resource location, for files, is the absolute path to the resource file. The special @AcmeHelloBundle syntax resolves the directory path of the AcmeHelloBundle bundle. This helps you specify the path to the resource without worrying later if you move the AcmeHelloBundle to a different directory.

Importing Configuration via Container Extensions

When developing in Symfony, you’ll most commonly use the imports directive to import container configuration from the bundles you’ve created specifically for your application. Third-party bundle container configuration, including Symfony core services, are usually loaded using another method that’s more flexible and easy to configure in your application.

Here’s how it works. Internally, each bundle defines its services very much like you’ve seen so far. Namely, a bundle uses one or more configuration resource files (usually XML) to specify the parameters and services for that bundle. However, instead of importing each of these resources directly from your application configuration using the imports directive, you can simply invoke a service container extension inside the bundle that does the work for you. A service container extension is a PHP class created by the bundle author to accomplish two things:

  • import all service container resources needed to configure the services for the bundle;
  • provide semantic, straightforward configuration so that the bundle can be configured without interacting with the flat parameters of the bundle’s service container configuration.

In other words, a service container extension configures the services for a bundle on your behalf. And as you’ll see in a moment, the extension provides a sensible, high-level interface for configuring the bundle.

Take the FrameworkBundle - the core Symfony framework bundle - as an example. The presence of the following code in your application configuration invokes the service container extension inside the FrameworkBundle:

  • YAML
    # app/config/config.yml
    framework:
        secret:          xxxxxxxxxx
        form:            true
        csrf_protection: true
        router:        { resource: "%kernel.root_dir%/config/routing.yml" }
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config secret="xxxxxxxxxx">
            <framework:form />
            <framework:csrf-protection />
            <framework:router resource="%kernel.root_dir%/config/routing.xml" />
            <!-- ... -->
        </framework>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'secret'          => 'xxxxxxxxxx',
        'form'            => array(),
        'csrf-protection' => array(),
        'router'          => array(
            'resource' => '%kernel.root_dir%/config/routing.php',
        ),
    
        // ...
    ));
    

When the configuration is parsed, the container looks for an extension that can handle the framework configuration directive. The extension in question, which lives in the FrameworkBundle, is invoked and the service configuration for the FrameworkBundle is loaded. If you remove the framework key from your application configuration file entirely, the core Symfony services won’t be loaded. The point is that you’re in control: the Symfony framework doesn’t contain any magic or perform any actions that you don’t have control over.

Of course you can do much more than simply “activate” the service container extension of the FrameworkBundle. Each extension allows you to easily customize the bundle, without worrying about how the internal services are defined.

In this case, the extension allows you to customize the error_handler, csrf_protection, router configuration and much more. Internally, the FrameworkBundle uses the options specified here to define and configure the services specific to it. The bundle takes care of creating all the necessary parameters and services for the service container, while still allowing much of the configuration to be easily customized. As a bonus, most service container extensions are also smart enough to perform validation - notifying you of options that are missing or the wrong data type.

When installing or configuring a bundle, see the bundle’s documentation for how the services for the bundle should be installed and configured. The options available for the core bundles can be found inside the Reference Guide.

注解

Natively, the service container only recognizes the parameters, services, and imports directives. Any other directives are handled by a service container extension.

If you want to expose user friendly configuration in your own bundles, read the “How to Load Service Configuration inside a Bundle” cookbook recipe.

Referencing (Injecting) Services

So far, the original my_mailer service is simple: it takes just one argument in its constructor, which is easily configurable. As you’ll see, the real power of the container is realized when you need to create a service that depends on one or more other services in the container.

As an example, suppose you have a new service, NewsletterManager, that helps to manage the preparation and delivery of an email message to a collection of addresses. Of course the my_mailer service is already really good at delivering email messages, so you’ll use it inside NewsletterManager to handle the actual delivery of the messages. This pretend class might look something like this:

// src/Acme/HelloBundle/Newsletter/NewsletterManager.php
namespace Acme\HelloBundle\Newsletter;

use Acme\HelloBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Without using the service container, you can create a new NewsletterManager fairly easily from inside a controller:

use Acme\HelloBundle\Newsletter\NewsletterManager;

// ...

public function sendNewsletterAction()
{
    $mailer = $this->get('my_mailer');
    $newsletter = new NewsletterManager($mailer);
    // ...
}

This approach is fine, but what if you decide later that the NewsletterManager class needs a second or third constructor argument? What if you decide to refactor your code and rename the class? In both cases, you’d need to find every place where the NewsletterManager is instantiated and modify it. Of course, the service container gives you a much more appealing option:

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    services:
        my_mailer:
            # ...
    
        newsletter_manager:
            class:     Acme\HelloBundle\Newsletter\NewsletterManager
            arguments: ["@my_mailer"]
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
            <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="Acme\HelloBundle\Newsletter\NewsletterManager">
                <argument type="service" id="my_mailer"/>
            </service>
        </services>
    </container>
    
  • PHP
    // src/Acme/HelloBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('my_mailer', ...);
    
    $container->setDefinition('newsletter_manager', new Definition(
        'Acme\HelloBundle\Newsletter\NewsletterManager',
        array(new Reference('my_mailer'))
    ));
    

In YAML, the special @my_mailer syntax tells the container to look for a service named my_mailer and to pass that object into the constructor of NewsletterManager. In this case, however, the specified service my_mailer must exist. If it does not, an exception will be thrown. You can mark your dependencies as optional - this will be discussed in the next section.

Using references is a very powerful tool that allows you to create independent service classes with well-defined dependencies. In this example, the newsletter_manager service needs the my_mailer service in order to function. When you define this dependency in the service container, the container takes care of all the work of instantiating the classes.

Optional Dependencies: Setter Injection

Injecting dependencies into the constructor in this manner is an excellent way of ensuring that the dependency is available to use. If you have optional dependencies for a class, then “setter injection” may be a better option. This means injecting the dependency using a method call rather than through the constructor. The class would look like this:

namespace Acme\HelloBundle\Newsletter;

use Acme\HelloBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Injecting the dependency by the setter method just needs a change of syntax:

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    services:
        my_mailer:
            # ...
    
        newsletter_manager:
            class:     Acme\HelloBundle\Newsletter\NewsletterManager
            calls:
                - [setMailer, ["@my_mailer"]]
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
            <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="Acme\HelloBundle\Newsletter\NewsletterManager">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
            </service>
        </services>
    </container>
    
  • PHP
    // src/Acme/HelloBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('my_mailer', ...);
    
    $container->setDefinition('newsletter_manager', new Definition(
        'Acme\HelloBundle\Newsletter\NewsletterManager'
    ))->addMethodCall('setMailer', array(
        new Reference('my_mailer'),
    ));
    

注解

The approaches presented in this section are called “constructor injection” and “setter injection”. The Symfony service container also supports “property injection”.

Making References optional

Sometimes, one of your services may have an optional dependency, meaning that the dependency is not required for your service to work properly. In the example above, the my_mailer service must exist, otherwise an exception will be thrown. By modifying the newsletter_manager service definition, you can make this reference optional. The container will then inject it if it exists and do nothing if it doesn’t:

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    services:
        newsletter_manager:
            class:     Acme\HelloBundle\Newsletter\NewsletterManager
            arguments: ["@?my_mailer"]
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
            <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="Acme\HelloBundle\Newsletter\NewsletterManager">
                <argument type="service" id="my_mailer" on-invalid="ignore" />
            </service>
        </services>
    </container>
    
  • PHP
    // src/Acme/HelloBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    $container->setDefinition('my_mailer', ...);
    
    $container->setDefinition('newsletter_manager', new Definition(
        'Acme\HelloBundle\Newsletter\NewsletterManager',
        array(
            new Reference(
                'my_mailer',
                ContainerInterface::IGNORE_ON_INVALID_REFERENCE
            )
        )
    ));
    

In YAML, the special @? syntax tells the service container that the dependency is optional. Of course, the NewsletterManager must also be rewritten to allow for an optional dependency:

public function __construct(Mailer $mailer = null)
{
    // ...
}
Core Symfony and Third-Party Bundle Services

Since Symfony and all third-party bundles configure and retrieve their services via the container, you can easily access them or even use them in your own services. To keep things simple, Symfony by default does not require that controllers be defined as services. Furthermore, Symfony injects the entire service container into your controller. For example, to handle the storage of information on a user’s session, Symfony provides a session service, which you can access inside a standard controller as follows:

public function indexAction($bar)
{
    $session = $this->get('session');
    $session->set('foo', $bar);

    // ...
}

In Symfony, you’ll constantly use services provided by the Symfony core or other third-party bundles to perform tasks such as rendering templates (templating), sending emails (mailer), or accessing information on the request (request).

You can take this a step further by using these services inside services that you’ve created for your application. Beginning by modifying the NewsletterManager to use the real Symfony mailer service (instead of the pretend my_mailer). Also pass the templating engine service to the NewsletterManager so that it can generate the email content via a template:

namespace Acme\HelloBundle\Newsletter;

use Symfony\Component\Templating\EngineInterface;

class NewsletterManager
{
    protected $mailer;

    protected $templating;

    public function __construct(
        \Swift_Mailer $mailer,
        EngineInterface $templating
    ) {
        $this->mailer = $mailer;
        $this->templating = $templating;
    }

    // ...
}

Configuring the service container is easy:

  • YAML
    # src/Acme/HelloBundle/Resources/config/services.yml
    services:
        newsletter_manager:
            class:     Acme\HelloBundle\Newsletter\NewsletterManager
            arguments: ["@mailer", "@templating"]
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <service id="newsletter_manager" class="Acme\HelloBundle\Newsletter\NewsletterManager">
            <argument type="service" id="mailer"/>
            <argument type="service" id="templating"/>
        </service>
    </container>
    
  • PHP
    // src/Acme/HelloBundle/Resources/config/services.php
    $container->setDefinition('newsletter_manager', new Definition(
        'Acme\HelloBundle\Newsletter\NewsletterManager',
        array(
            new Reference('mailer'),
            new Reference('templating'),
        )
    ));
    

The newsletter_manager service now has access to the core mailer and templating services. This is a common way to create services specific to your application that leverage the power of different services within the framework.

小技巧

Be sure that the swiftmailer entry appears in your application configuration. As was mentioned in Importing Configuration via Container Extensions, the swiftmailer key invokes the service extension from the SwiftmailerBundle, which registers the mailer service.

Tags

In the same way that a blog post on the Web might be tagged with things such as “Symfony” or “PHP”, services configured in your container can also be tagged. In the service container, a tag implies that the service is meant to be used for a specific purpose. Take the following example:

  • YAML
    # app/config/services.yml
    services:
        foo.twig.extension:
            class: Acme\HelloBundle\Extension\FooExtension
            public: false
            tags:
                -  { name: twig.extension }
    
  • XML
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <service
            id="foo.twig.extension"
            class="Acme\HelloBundle\Extension\FooExtension"
            public="false">
    
            <tag name="twig.extension" />
        </service>
    </container>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $definition = new Definition('Acme\HelloBundle\Extension\FooExtension');
    $definition->setPublic(false);
    $definition->addTag('twig.extension');
    $container->setDefinition('foo.twig.extension', $definition);
    

The twig.extension tag is a special tag that the TwigBundle uses during configuration. By giving the service this twig.extension tag, the bundle knows that the foo.twig.extension service should be registered as a Twig extension with Twig. In other words, Twig finds all services tagged with twig.extension and automatically registers them as extensions.

Tags, then, are a way to tell Symfony or other third-party bundles that your service should be registered or used in some special way by the bundle.

For a list of all the tags available in the core Symfony Framework, check out The Dependency Injection Tags. Each of these has a different effect on your service and many tags require additional arguments (beyond just the name parameter).

Debugging Services

You can find out what services are registered with the container using the console. To show all services and the class for each service, run:

$ php app/console container:debug

By default, only public services are shown, but you can also view private services:

$ php app/console container:debug --show-private

注解

If a private service is only used as an argument to just one other service, it won’t be displayed by the container:debug command, even when using the --show-private option. See Inline Private Services for more details.

You can get more detailed information about a particular service by specifying its id:

$ php app/console container:debug my_mailer

Performance

Symfony is fast, right out of the box. Of course, if you really need speed, there are many ways that you can make Symfony even faster. In this chapter, you’ll explore many of the most common and powerful ways to make your Symfony application even faster.

Use a Byte Code Cache (e.g. APC)

One of the best (and easiest) things that you should do to improve your performance is to use a “byte code cache”. The idea of a byte code cache is to remove the need to constantly recompile the PHP source code. There are a number of byte code caches available, some of which are open source. The most widely used byte code cache is probably APC

Using a byte code cache really has no downside, and Symfony has been architected to perform really well in this type of environment.

Further Optimizations

Byte code caches usually monitor the source files for changes. This ensures that if the source of a file changes, the byte code is recompiled automatically. This is really convenient, but obviously adds overhead.

For this reason, some byte code caches offer an option to disable these checks. Obviously, when disabling these checks, it will be up to the server admin to ensure that the cache is cleared whenever any source files change. Otherwise, the updates you’ve made won’t be seen.

For example, to disable these checks in APC, simply add apc.stat=0 to your php.ini configuration.

Use Composer’s Class Map Functionality

By default, the Symfony standard edition uses Composer’s autoloader in the autoload.php file. This autoloader is easy to use, as it will automatically find any new classes that you’ve placed in the registered directories.

Unfortunately, this comes at a cost, as the loader iterates over all configured namespaces to find a particular file, making file_exists calls until it finally finds the file it’s looking for.

The simplest solution is to tell Composer to build a “class map” (i.e. a big array of the locations of all the classes). This can be done from the command line, and might become part of your deploy process:

$ composer dump-autoload --optimize

Internally, this builds the big class map array in vendor/composer/autoload_classmap.php.

Caching the Autoloader with APC

Another solution is to cache the location of each class after it’s located the first time. Symfony comes with a class - ApcClassLoader - that does exactly this. To use it, just adapt your front controller file. If you’re using the Standard Distribution, this code should already be available as comments in this file:

// app.php
// ...

$loader = require_once __DIR__.'/../app/bootstrap.php.cache';

// Use APC for autoloading to improve performance
// Change 'sf2' by the prefix you want in order
// to prevent key conflict with another application
/*
$loader = new ApcClassLoader('sf2', $loader);
$loader->register(true);
*/

// ...

For more details, see Cache a Class Loader.

注解

When using the APC autoloader, if you add new classes, they will be found automatically and everything will work the same as before (i.e. no reason to “clear” the cache). However, if you change the location of a particular namespace or prefix, you’ll need to flush your APC cache. Otherwise, the autoloader will still be looking at the old location for all classes inside that namespace.

Use Bootstrap Files

To ensure optimal flexibility and code reuse, Symfony applications leverage a variety of classes and 3rd party components. But loading all of these classes from separate files on each request can result in some overhead. To reduce this overhead, the Symfony Standard Edition provides a script to generate a so-called bootstrap file, consisting of multiple classes definitions in a single file. By including this file (which contains a copy of many of the core classes), Symfony no longer needs to include any of the source files containing those classes. This will reduce disc IO quite a bit.

If you’re using the Symfony Standard Edition, then you’re probably already using the bootstrap file. To be sure, open your front controller (usually app.php) and check to make sure that the following line exists:

require_once __DIR__.'/../app/bootstrap.php.cache';

Note that there are two disadvantages when using a bootstrap file:

  • the file needs to be regenerated whenever any of the original sources change (i.e. when you update the Symfony source or vendor libraries);
  • when debugging, one will need to place break points inside the bootstrap file.

If you’re using the Symfony Standard Edition, the bootstrap file is automatically rebuilt after updating the vendor libraries via the composer install command.

Bootstrap Files and Byte Code Caches

Even when using a byte code cache, performance will improve when using a bootstrap file since there will be fewer files to monitor for changes. Of course if this feature is disabled in the byte code cache (e.g. apc.stat=0 in APC), there is no longer a reason to use a bootstrap file.

Internals

Looks like you want to understand how Symfony works and how to extend it. That makes me very happy! This section is an in-depth explanation of the Symfony internals.

注解

You only need to read this section if you want to understand how Symfony works behind the scenes, or if you want to extend Symfony.

Overview

The Symfony code is made of several independent layers. Each layer is built on top of the previous one.

小技巧

Autoloading is not managed by the framework directly; it’s done by using Composer’s autoloader (vendor/autoload.php), which is included in the app/autoload.php file.

HttpFoundation Component

The deepest level is the HttpFoundation component. HttpFoundation provides the main objects needed to deal with HTTP. It is an object-oriented abstraction of some native PHP functions and variables:

  • The Request class abstracts the main PHP global variables like $_GET, $_POST, $_COOKIE, $_FILES, and $_SERVER;
  • The Response class abstracts some PHP functions like header(), setcookie(), and echo;
  • The Session class and SessionStorageInterface interface abstract session management session_*() functions.

注解

Read more about the HttpFoundation component.

HttpKernel Component

On top of HttpFoundation is the HttpKernel component. HttpKernel handles the dynamic part of HTTP; it is a thin wrapper on top of the Request and Response classes to standardize the way requests are handled. It also provides extension points and tools that makes it the ideal starting point to create a Web framework without too much overhead.

It also optionally adds configurability and extensibility, thanks to the DependencyInjection component and a powerful plugin system (bundles).

参见

Read more about the HttpKernel component, Dependency Injection and Bundles.

FrameworkBundle

The FrameworkBundle bundle is the bundle that ties the main components and libraries together to make a lightweight and fast MVC framework. It comes with a sensible default configuration and conventions to ease the learning curve.

Kernel

The HttpKernel class is the central class of Symfony and is responsible for handling client requests. Its main goal is to “convert” a Request object to a Response object.

Every Symfony Kernel implements HttpKernelInterface:

function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
Controllers

To convert a Request to a Response, the Kernel relies on a “Controller”. A Controller can be any valid PHP callable.

The Kernel delegates the selection of what Controller should be executed to an implementation of ControllerResolverInterface:

public function getController(Request $request);

public function getArguments(Request $request, $controller);

The getController() method returns the Controller (a PHP callable) associated with the given Request. The default implementation (ControllerResolver) looks for a _controller request attribute that represents the controller name (a “class::method” string, like Bundle\BlogBundle\PostController:indexAction).

小技巧

The default implementation uses the RouterListener to define the _controller Request attribute (see kernel.request Event).

The getArguments() method returns an array of arguments to pass to the Controller callable. The default implementation automatically resolves the method arguments, based on the Request attributes.

Handling Requests

The handle() method takes a Request and always returns a Response. To convert the Request, handle() relies on the Resolver and an ordered chain of Event notifications (see the next section for more information about each Event):

  1. Before doing anything else, the kernel.request event is notified – if one of the listeners returns a Response, it jumps to step 8 directly;
  2. The Resolver is called to determine the Controller to execute;
  3. Listeners of the kernel.controller event can now manipulate the Controller callable the way they want (change it, wrap it, ...);
  4. The Kernel checks that the Controller is actually a valid PHP callable;
  5. The Resolver is called to determine the arguments to pass to the Controller;
  6. The Kernel calls the Controller;
  7. If the Controller does not return a Response, listeners of the kernel.view event can convert the Controller return value to a Response;
  8. Listeners of the kernel.response event can manipulate the Response (content and headers);
  9. The Response is returned;
  10. Listeners of the kernel.terminate event can perform tasks after the Response has been served.

If an Exception is thrown during processing, the kernel.exception is notified and listeners are given a chance to convert the Exception to a Response. If that works, the kernel.response event is notified; if not, the Exception is re-thrown.

If you don’t want Exceptions to be caught (for embedded requests for instance), disable the kernel.exception event by passing false as the third argument to the handle() method.

Internal Requests

At any time during the handling of a request (the ‘master’ one), a sub-request can be handled. You can pass the request type to the handle() method (its second argument):

  • HttpKernelInterface::MASTER_REQUEST;
  • HttpKernelInterface::SUB_REQUEST.

The type is passed to all events and listeners can act accordingly (some processing must only occur on the master request).

Events

Each event thrown by the Kernel is a subclass of KernelEvent. This means that each event has access to the same basic information:

getRequestType()
Returns the type of the request (HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST).
getKernel()
Returns the Kernel handling the request.
getRequest()
Returns the current Request being handled.
getRequestType()

The getRequestType() method allows listeners to know the type of the request. For instance, if a listener must only be active for master requests, add the following code at the beginning of your listener method:

use Symfony\Component\HttpKernel\HttpKernelInterface;

if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
    // return immediately
    return;
}

小技巧

If you are not yet familiar with the Symfony EventDispatcher, read the EventDispatcher component documentation section first.

kernel.request Event

Event Class: GetResponseEvent

The goal of this event is to either return a Response object immediately or setup variables so that a Controller can be called after the event. Any listener can return a Response object via the setResponse() method on the event. In this case, all other listeners won’t be called.

This event is used by the FrameworkBundle to populate the _controller Request attribute, via the RouterListener. RequestListener uses a RouterInterface object to match the Request and determine the Controller name (stored in the _controller Request attribute).

参见

Read more on the kernel.request event.

kernel.controller Event

Event Class: FilterControllerEvent

This event is not used by the FrameworkBundle, but can be an entry point used to modify the controller that should be executed:

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

public function onKernelController(FilterControllerEvent $event)
{
    $controller = $event->getController();
    // ...

    // the controller can be changed to any PHP callable
    $event->setController($controller);
}

参见

Read more on the kernel.controller event.

kernel.view Event

Event Class: GetResponseForControllerResultEvent

This event is not used by the FrameworkBundle, but it can be used to implement a view sub-system. This event is called only if the Controller does not return a Response object. The purpose of the event is to allow some other return value to be converted into a Response.

The value returned by the Controller is accessible via the getControllerResult method:

use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;

public function onKernelView(GetResponseForControllerResultEvent $event)
{
    $val = $event->getControllerResult();
    $response = new Response();

    // ... some how customize the Response from the return value

    $event->setResponse($response);
}

参见

Read more on the kernel.view event.

kernel.response Event

Event Class: FilterResponseEvent

The purpose of this event is to allow other systems to modify or replace the Response object after its creation:

public function onKernelResponse(FilterResponseEvent $event)
{
    $response = $event->getResponse();

    // ... modify the response object
}

The FrameworkBundle registers several listeners:

ProfilerListener
Collects data for the current request.
WebDebugToolbarListener
Injects the Web Debug Toolbar.
ResponseListener
Fixes the Response Content-Type based on the request format.
EsiListener
Adds a Surrogate-Control HTTP header when the Response needs to be parsed for ESI tags.

参见

Read more on the kernel.response event.

kernel.terminate Event

Event Class: PostResponseEvent

The purpose of this event is to perform “heavier” tasks after the response was already served to the client.

参见

Read more on the kernel.terminate event.

kernel.exception Event

Event Class: GetResponseForExceptionEvent

The FrameworkBundle registers an ExceptionListener that forwards the Request to a given Controller (the value of the exception_listener.controller parameter – must be in the class::method notation).

A listener on this event can create and set a Response object, create and set a new Exception object, or do nothing:

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;

public function onKernelException(GetResponseForExceptionEvent $event)
{
    $exception = $event->getException();
    $response = new Response();
    // setup the Response object based on the caught exception
    $event->setResponse($response);

    // you can alternatively set a new Exception
    // $exception = new \Exception('Some special exception');
    // $event->setException($exception);
}

注解

As Symfony ensures that the Response status code is set to the most appropriate one depending on the exception, setting the status on the response won’t work. If you want to overwrite the status code (which you should not without a good reason), set the X-Status-Code header:

return new Response(
    'Error',
    404 // ignored,
    array('X-Status-Code' => 200)
);

参见

Read more on the kernel.exception event.

The EventDispatcher

The EventDispatcher is a standalone component that is responsible for much of the underlying logic and flow behind a Symfony request. For more information, see the EventDispatcher component documentation.

Profiler

When enabled, the Symfony profiler collects useful information about each request made to your application and store them for later analysis. Use the profiler in the development environment to help you to debug your code and enhance performance; use it in the production environment to explore problems after the fact.

You rarely have to deal with the profiler directly as Symfony provides visualizer tools like the Web Debug Toolbar and the Web Profiler. If you use the Symfony Standard Edition, the profiler, the web debug toolbar, and the web profiler are all already configured with sensible settings.

注解

The profiler collects information for all requests (simple requests, redirects, exceptions, Ajax requests, ESI requests; and for all HTTP methods and all formats). It means that for a single URL, you can have several associated profiling data (one per external request/response pair).

Visualizing Profiling Data
Using the Web Debug Toolbar

In the development environment, the web debug toolbar is available at the bottom of all pages. It displays a good summary of the profiling data that gives you instant access to a lot of useful information when something does not work as expected.

If the summary provided by the Web Debug Toolbar is not enough, click on the token link (a string made of 13 random characters) to access the Web Profiler.

注解

If the token is not clickable, it means that the profiler routes are not registered (see below for configuration information).

Analyzing Profiling Data with the Web Profiler

The Web Profiler is a visualization tool for profiling data that you can use in development to debug your code and enhance performance; but it can also be used to explore problems that occur in production. It exposes all information collected by the profiler in a web interface.

Accessing the Profiling information

You don’t need to use the default visualizer to access the profiling information. But how can you retrieve profiling information for a specific request after the fact? When the profiler stores data about a Request, it also associates a token with it; this token is available in the X-Debug-Token HTTP header of the Response:

$profile = $container->get('profiler')->loadProfileFromResponse($response);

$profile = $container->get('profiler')->loadProfile($token);

小技巧

When the profiler is enabled but not the web debug toolbar, or when you want to get the token for an Ajax request, use a tool like Firebug to get the value of the X-Debug-Token HTTP header.

Use the find() method to access tokens based on some criteria:

// get the latest 10 tokens
$tokens = $container->get('profiler')->find('', '', 10, '', '');

// get the latest 10 tokens for all URL containing /admin/
$tokens = $container->get('profiler')->find('', '/admin/', 10, '', '');

// get the latest 10 tokens for local requests
$tokens = $container->get('profiler')->find('127.0.0.1', '', 10, '', '');

// get the latest 10 tokens for requests that happened between 2 and 4 days ago
$tokens = $container->get('profiler')
    ->find('', '', 10, '4 days ago', '2 days ago');

If you want to manipulate profiling data on a different machine than the one where the information were generated, use the export() and import() methods:

// on the production machine
$profile = $container->get('profiler')->loadProfile($token);
$data = $profiler->export($profile);

// on the development machine
$profiler->import($data);
Configuration

The default Symfony configuration comes with sensible settings for the profiler, the web debug toolbar, and the web profiler. Here is for instance the configuration for the development environment:

  • YAML
    # load the profiler
    framework:
        profiler: { only_exceptions: false }
    
    # enable the web profiler
    web_profiler:
        toolbar: true
        intercept_redirects: true
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:webprofiler="http://symfony.com/schema/dic/webprofiler"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/webprofiler
            http://symfony.com/schema/dic/webprofiler/webprofiler-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <!-- load the profiler -->
        <framework:config>
            <framework:profiler only-exceptions="false" />
        </framework:config>
    
        <!-- enable the web profiler -->
        <webprofiler:config
            toolbar="true"
            intercept-redirects="true" />
    </container>
    
  • PHP
    // load the profiler
    $container->loadFromExtension('framework', array(
        'profiler' => array('only_exceptions' => false),
    ));
    
    // enable the web profiler
    $container->loadFromExtension('web_profiler', array(
        'toolbar'             => true,
        'intercept_redirects' => true,
    ));
    

When only_exceptions is set to true, the profiler only collects data when an exception is thrown by the application.

When intercept_redirects is set to true, the web profiler intercepts the redirects and gives you the opportunity to look at the collected data before following the redirect.

If you enable the web profiler, you also need to mount the profiler routes:

  • YAML
    _profiler:
        resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
        prefix:   /_profiler
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <import
            resource="@WebProfilerBundle/Resources/config/routing/profiler.xml"
            prefix="/_profiler" />
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    
    $profiler = $loader->import(
        '@WebProfilerBundle/Resources/config/routing/profiler.xml'
    );
    $profiler->addPrefix('/_profiler');
    
    $collection = new RouteCollection();
    $collection->addCollection($profiler);
    

As the profiler adds some overhead, you might want to enable it only under certain circumstances in the production environment. The only_exceptions settings limits profiling to exceptions, but what if you want to get information when the client IP comes from a specific address, or for a limited portion of the website? You can use a Profiler Matcher, learn more about that in “How to Use Matchers to Enable the Profiler Conditionally”.

Cookbook

The Cookbook

Assetic

How to Use Assetic for Asset Management

Assetic combines two major ideas: assets and filters. The assets are files such as CSS, JavaScript and image files. The filters are things that can be applied to these files before they are served to the browser. This allows a separation between the asset files stored in the application and the files actually presented to the user.

Without Assetic, you just serve the files that are stored in the application directly:

  • Twig
    <script src="{{ asset('js/script.js') }}"></script>
    
  • PHP
    <script src="<?php echo $view['assets']->getUrl('js/script.js') ?>"></script>
    

But with Assetic, you can manipulate these assets however you want (or load them from anywhere) before serving them. This means you can:

  • Minify and combine all of your CSS and JS files
  • Run all (or just some) of your CSS or JS files through some sort of compiler, such as LESS, SASS or CoffeeScript
  • Run image optimizations on your images
Assets

Using Assetic provides many advantages over directly serving the files. The files do not need to be stored where they are served from and can be drawn from various sources such as from within a bundle.

You can use Assetic to process CSS stylesheets, JavaScript files and images. The philosophy behind adding either is basically the same, but with a slightly different syntax.

Including JavaScript Files

To include JavaScript files, use the javascripts tag in any template:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

注解

If you’re using the default block names from the Symfony Standard Edition, the javascripts tag will most commonly live in the javascripts block:

{# ... #}
{% block javascripts %}
    {% javascripts '@AppBundle/Resources/public/js/*' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
{% endblock %}
{# ... #}

小技巧

You can also include CSS Stylesheets: see Including CSS Stylesheets.

In this example, all of the files in the Resources/public/js/ directory of the AppBundle will be loaded and served from a different location. The actual rendered tag might simply look like:

<script src="/app_dev.php/js/abcd123.js"></script>

This is a key point: once you let Assetic handle your assets, the files are served from a different location. This will cause problems with CSS files that reference images by their relative path. See Fixing CSS Paths with the cssrewrite Filter.

Including CSS Stylesheets

To bring in CSS stylesheets, you can use the same methodologies seen above, except with the stylesheets tag:

  • Twig
    {% stylesheets 'bundles/app/css/*' filter='cssrewrite' %}
        <link rel="stylesheet" href="{{ asset_url }}" />
    {% endstylesheets %}
    
  • PHP
    <?php foreach ($view['assetic']->stylesheets(
        array('bundles/app/css/*'),
        array('cssrewrite')
    ) as $url): ?>
        <link rel="stylesheet" href="<?php echo $view->escape($url) ?>" />
    <?php endforeach ?>
    

注解

If you’re using the default block names from the Symfony Standard Edition, the stylesheets tag will most commonly live in the stylesheets block:

{# ... #}
{% block stylesheets %}
    {% stylesheets 'bundles/app/css/*' filter='cssrewrite' %}
        <link rel="stylesheet" href="{{ asset_url }}" />
    {% endstylesheets %}
{% endblock %}
{# ... #}

But because Assetic changes the paths to your assets, this will break any background images (or other paths) that uses relative paths, unless you use the cssrewrite filter.

注解

Notice that in the original example that included JavaScript files, you referred to the files using a path like @AppBundle/Resources/public/file.js, but that in this example, you referred to the CSS files using their actual, publicly-accessible path: bundles/app/css. You can use either, except that there is a known issue that causes the cssrewrite filter to fail when using the @AppBundle syntax for CSS Stylesheets.

Including Images

To include an image you can use the image tag.

  • Twig
    {% image '@AppBundle/Resources/public/images/example.jpg' %}
        <img src="{{ asset_url }}" alt="Example" />
    {% endimage %}
    
  • PHP
    <?php foreach ($view['assetic']->image(
        array('@AppBundle/Resources/public/images/example.jpg')
    ) as $url): ?>
        <img src="<?php echo $view->escape($url) ?>" alt="Example" />
    <?php endforeach ?>
    

You can also use Assetic for image optimization. More information in How to Use Assetic for Image Optimization with Twig Functions.

Fixing CSS Paths with the cssrewrite Filter

Since Assetic generates new URLs for your assets, any relative paths inside your CSS files will break. To fix this, make sure to use the cssrewrite filter with your stylesheets tag. This parses your CSS files and corrects the paths internally to reflect the new location.

You can see an example in the previous section.

警告

When using the cssrewrite filter, don’t refer to your CSS files using the @AppBundle syntax. See the note in the above section for details.

Combining Assets

One feature of Assetic is that it will combine many files into one. This helps to reduce the number of HTTP requests, which is great for front end performance. It also allows you to maintain the files more easily by splitting them into manageable parts. This can help with re-usability as you can easily split project-specific files from those which can be used in other applications, but still serve them as a single file:

  • Twig
    {% javascripts
        '@AppBundle/Resources/public/js/*'
        '@AcmeBarBundle/Resources/public/js/form.js'
        '@AcmeBarBundle/Resources/public/js/calendar.js' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array(
            '@AppBundle/Resources/public/js/*',
            '@AcmeBarBundle/Resources/public/js/form.js',
            '@AcmeBarBundle/Resources/public/js/calendar.js',
        )
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

In the dev environment, each file is still served individually, so that you can debug problems more easily. However, in the prod environment (or more specifically, when the debug flag is false), this will be rendered as a single script tag, which contains the contents of all of the JavaScript files.

小技巧

If you’re new to Assetic and try to use your application in the prod environment (by using the app.php controller), you’ll likely see that all of your CSS and JS breaks. Don’t worry! This is on purpose. For details on using Assetic in the prod environment, see Dumping Asset Files.

And combining files doesn’t only apply to your files. You can also use Assetic to combine third party assets, such as jQuery, with your own into a single file:

  • Twig
    {% javascripts
        '@AppBundle/Resources/public/js/thirdparty/jquery.js'
        '@AppBundle/Resources/public/js/*' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array(
            '@AppBundle/Resources/public/js/thirdparty/jquery.js',
            '@AppBundle/Resources/public/js/*',
        )
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    
Using Named Assets

AsseticBundle configuration directives allow you to define named asset sets. You can do so by defining the input files, filters and output files in your configuration under the assetic section. Read more in the assetic config reference.

  • YAML
    # app/config/config.yml
    assetic:
        assets:
            jquery_and_ui:
                inputs:
                    - '@AppBundle/Resources/public/js/thirdparty/jquery.js'
                    - '@AppBundle/Resources/public/js/thirdparty/jquery.ui.js'
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:assetic="http://symfony.com/schema/dic/assetic">
    
        <assetic:config>
            <assetic:asset name="jquery_and_ui">
                <assetic:input>@AppBundle/Resources/public/js/thirdparty/jquery.js</assetic:input>
                <assetic:input>@AppBundle/Resources/public/js/thirdparty/jquery.ui.js</assetic:input>
            </assetic:asset>
        </assetic:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'assets' => array(
            'jquery_and_ui' => array(
                'inputs' => array(
                    '@AppBundle/Resources/public/js/thirdparty/jquery.js',
                    '@AppBundle/Resources/public/js/thirdparty/jquery.ui.js',
                ),
            ),
        ),
    );
    

After you have defined the named assets, you can reference them in your templates with the @named_asset notation:

  • Twig
    {% javascripts
        '@jquery_and_ui'
        '@AppBundle/Resources/public/js/*' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array(
            '@jquery_and_ui',
            '@AppBundle/Resources/public/js/*',
        )
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    
Filters

Once they’re managed by Assetic, you can apply filters to your assets before they are served. This includes filters that compress the output of your assets for smaller file sizes (and better front-end optimization). Other filters can compile JavaScript file from CoffeeScript files and process SASS into CSS. In fact, Assetic has a long list of available filters.

Many of the filters do not do the work directly, but use existing third-party libraries to do the heavy-lifting. This means that you’ll often need to install a third-party library to use a filter. The great advantage of using Assetic to invoke these libraries (as opposed to using them directly) is that instead of having to run them manually after you work on the files, Assetic will take care of this for you and remove this step altogether from your development and deployment processes.

To use a filter, you first need to specify it in the Assetic configuration. Adding a filter here doesn’t mean it’s being used - it just means that it’s available to use (you’ll use the filter below).

For example to use the UglifyJS JavaScript minifier the following config should be added:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            uglifyjs2:
                bin: /usr/local/bin/uglifyjs
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="uglifyjs2"
            bin="/usr/local/bin/uglifyjs" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'uglifyjs2' => array(
                'bin' => '/usr/local/bin/uglifyjs',
            ),
        ),
    ));
    

Now, to actually use the filter on a group of JavaScript files, add it into your template:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' filter='uglifyjs2' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*'),
        array('uglifyjs2')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

A more detailed guide about configuring and using Assetic filters as well as details of Assetic’s debug mode can be found in How to Minify CSS/JS Files (Using UglifyJS and UglifyCSS).

Controlling the URL Used

If you wish to, you can control the URLs that Assetic produces. This is done from the template and is relative to the public document root:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' output='js/compiled/main.js' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*'),
        array(),
        array('output' => 'js/compiled/main.js')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

注解

Symfony also contains a method for cache busting, where the final URL generated by Assetic contains a query parameter that can be incremented via configuration on each deployment. For more information, see the assets_version configuration option.

Dumping Asset Files

In the dev environment, Assetic generates paths to CSS and JavaScript files that don’t physically exist on your computer. But they render nonetheless because an internal Symfony controller opens the files and serves back the content (after running any filters).

This kind of dynamic serving of processed assets is great because it means that you can immediately see the new state of any asset files you change. It’s also bad, because it can be quite slow. If you’re using a lot of filters, it might be downright frustrating.

Fortunately, Assetic provides a way to dump your assets to real files, instead of being generated dynamically.

Dumping Asset Files in the prod Environment

In the prod environment, your JS and CSS files are represented by a single tag each. In other words, instead of seeing each JavaScript file you’re including in your source, you’ll likely just see something like this:

<script src="/js/abcd123.js"></script>

Moreover, that file does not actually exist, nor is it dynamically rendered by Symfony (as the asset files are in the dev environment). This is on purpose - letting Symfony generate these files dynamically in a production environment is just too slow.

Instead, each time you use your app in the prod environment (and therefore, each time you deploy), you should run the following task:

$ php app/console assetic:dump --env=prod --no-debug

This will physically generate and write each file that you need (e.g. /js/abcd123.js). If you update any of your assets, you’ll need to run this again to regenerate the file.

Dumping Asset Files in the dev Environment

By default, each asset path generated in the dev environment is handled dynamically by Symfony. This has no disadvantage (you can see your changes immediately), except that assets can load noticeably slow. If you feel like your assets are loading too slowly, follow this guide.

First, tell Symfony to stop trying to process these files dynamically. Make the following change in your config_dev.yml file:

  • YAML
    # app/config/config_dev.yml
    assetic:
        use_controller: false
    
  • XML
    <!-- app/config/config_dev.xml -->
    <assetic:config use-controller="false" />
    
  • PHP
    // app/config/config_dev.php
    $container->loadFromExtension('assetic', array(
        'use_controller' => false,
    ));
    

Next, since Symfony is no longer generating these assets for you, you’ll need to dump them manually. To do so, run the following:

$ php app/console assetic:dump

This physically writes all of the asset files you need for your dev environment. The big disadvantage is that you need to run this each time you update an asset. Fortunately, by passing the --watch option, the command will automatically regenerate assets as they change:

$ php app/console assetic:dump --watch

Since running this command in the dev environment may generate a bunch of files, it’s usually a good idea to point your generated asset files to some isolated directory (e.g. /js/compiled), to keep things organized:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' output='js/compiled/main.js' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*'),
        array(),
        array('output' => 'js/compiled/main.js')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    
How to Minify CSS/JS Files (Using UglifyJS and UglifyCSS)

UglifyJS is a JavaScript parser/compressor/beautifier toolkit. It can be used to combine and minify JavaScript assets so that they require less HTTP requests and make your site load faster. UglifyCSS is a CSS compressor/beautifier that is very similar to UglifyJS.

In this cookbook, the installation, configuration and usage of UglifyJS is shown in detail. UglifyCSS works pretty much the same way and is only talked about briefly.

Install UglifyJS

UglifyJS is available as an Node.js npm module and can be installed using npm. First, you need to install Node.js. Afterwards you can install UglifyJS using npm:

$ npm install -g uglify-js

This command will install UglifyJS globally and you may need to run it as a root user.

注解

It’s also possible to install UglifyJS inside your project only. To do this, install it without the -g option and specify the path where to put the module:

$ cd /path/to/symfony
$ mkdir app/Resources/node_modules
$ npm install uglify-js --prefix app/Resources

It is recommended that you install UglifyJS in your app/Resources folder and add the node_modules folder to version control. Alternatively, you can create an npm package.json file and specify your dependencies there.

Depending on your installation method, you should either be able to execute the uglifyjs executable globally, or execute the physical file that lives in the node_modules directory:

$ uglifyjs --help

$ ./app/Resources/node_modules/.bin/uglifyjs --help
Configure the uglifyjs2 Filter

Now we need to configure Symfony to use the uglifyjs2 filter when processing your JavaScripts:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            uglifyjs2:
                # the path to the uglifyjs executable
                bin: /usr/local/bin/uglifyjs
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <!-- bin: the path to the uglifyjs executable -->
        <assetic:filter
            name="uglifyjs2"
            bin="/usr/local/bin/uglifyjs" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'uglifyjs2' => array(
                // the path to the uglifyjs executable
                'bin' => '/usr/local/bin/uglifyjs',
            ),
        ),
    ));
    

注解

The path where UglifyJS is installed may vary depending on your system. To find out where npm stores the bin folder, you can use the following command:

$ npm bin -g

It should output a folder on your system, inside which you should find the UglifyJS executable.

If you installed UglifyJS locally, you can find the bin folder inside the node_modules folder. It’s called .bin in this case.

You now have access to the uglifyjs2 filter in your application.

Configure the node Binary

Assetic tries to find the node binary automatically. If it cannot be found, you can configure its location using the node key:

  • YAML
    # app/config/config.yml
    assetic:
        # the path to the node executable
        node: /usr/bin/nodejs
        filters:
            uglifyjs2:
                # the path to the uglifyjs executable
                bin: /usr/local/bin/uglifyjs
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config
        node="/usr/bin/nodejs" >
        <assetic:filter
            name="uglifyjs2"
            bin="/usr/local/bin/uglifyjs" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'node' => '/usr/bin/nodejs',
        'uglifyjs2' => array(
                // the path to the uglifyjs executable
                'bin' => '/usr/local/bin/uglifyjs',
            ),
    ));
    
Minify your Assets

In order to use UglifyJS on your assets, you need to apply it to them. Since your assets are a part of the view layer, this work is done in your templates:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' filter='uglifyjs2' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*'),
        array('uglifyj2s')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

注解

The above example assumes that you have a bundle called AppBundle and your JavaScript files are in the Resources/public/js directory under your bundle. This isn’t important however - you can include your JavaScript files no matter where they are.

With the addition of the uglifyjs2 filter to the asset tags above, you should now see minified JavaScripts coming over the wire much faster.

Disable Minification in Debug Mode

Minified JavaScripts are very difficult to read, let alone debug. Because of this, Assetic lets you disable a certain filter when your application is in debug (e.g. app_dev.php) mode. You can do this by prefixing the filter name in your template with a question mark: ?. This tells Assetic to only apply this filter when debug mode is off (e.g. app.php):

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' filter='?uglifyjs2' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*'),
        array('?uglifyjs2')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

To try this out, switch to your prod environment (app.php). But before you do, don’t forget to clear your cache and dump your assetic assets.

小技巧

Instead of adding the filter to the asset tags, you can also globally enable it by adding the apply_to attribute to the filter configuration, for example in the uglifyjs2 filter apply_to: "\.js$". To only have the filter applied in production, add this to the config_prod file rather than the common config file. For details on applying filters by file extension, see Filtering Based on a File Extension.

Install, Configure and Use UglifyCSS

The usage of UglifyCSS works the same way as UglifyJS. First, make sure the node package is installed:

$ npm install -g uglifycss

Next, add the configuration for this filter:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            uglifycss:
                bin: /usr/local/bin/uglifycss
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="uglifycss"
            bin="/usr/local/bin/uglifycss" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'uglifycss' => array(
                'bin' => '/usr/local/bin/uglifycss',
            ),
        ),
    ));
    

To use the filter for your CSS files, add the filter to the Assetic stylesheets helper:

  • Twig
    {% stylesheets 'bundles/App/css/*' filter='uglifycss' filter='cssrewrite' %}
         <link rel="stylesheet" href="{{ asset_url }}" />
    {% endstylesheets %}
    
  • PHP
    <?php foreach ($view['assetic']->stylesheets(
        array('bundles/App/css/*'),
        array('uglifycss'),
        array('cssrewrite')
    ) as $url): ?>
        <link rel="stylesheet" href="<?php echo $view->escape($url) ?>" />
    <?php endforeach ?>
    

Just like with the uglifyjs2 filter, if you prefix the filter name with ? (i.e. ?uglifycss), the minification will only happen when you’re not in debug mode.

How to Minify JavaScripts and Stylesheets with YUI Compressor

Yahoo! provides an excellent utility for minifying JavaScripts and stylesheets so they travel over the wire faster, the YUI Compressor. Thanks to Assetic, you can take advantage of this tool very easily.

警告

The YUI Compressor is no longer maintained by Yahoo but by an independent volunteer. Moreover, Yahoo has decided to stop all new development on YUI and to move to other modern alternatives such as Node.js.

That’s why you are strongly advised to avoid using YUI utilities unless strictly necessary. Read How to Minify CSS/JS Files (Using UglifyJS and UglifyCSS) for a modern and up-to-date alternative.

Download the YUI Compressor JAR

The YUI Compressor is written in Java and distributed as a JAR. Download the JAR from the Yahoo! site and save it to app/Resources/java/yuicompressor.jar.

Configure the YUI Filters

Now you need to configure two Assetic filters in your application, one for minifying JavaScripts with the YUI Compressor and one for minifying stylesheets:

  • YAML
    # app/config/config.yml
    assetic:
        # java: "/usr/bin/java"
        filters:
            yui_css:
                jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
            yui_js:
                jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="yui_css"
            jar="%kernel.root_dir%/Resources/java/yuicompressor.jar" />
        <assetic:filter
            name="yui_js"
            jar="%kernel.root_dir%/Resources/java/yuicompressor.jar" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        // 'java' => '/usr/bin/java',
        'filters' => array(
            'yui_css' => array(
                'jar' => '%kernel.root_dir%/Resources/java/yuicompressor.jar',
            ),
            'yui_js' => array(
                'jar' => '%kernel.root_dir%/Resources/java/yuicompressor.jar',
            ),
        ),
    ));
    

注解

Windows users need to remember to update config to proper Java location. In Windows7 x64 bit by default it’s C:\Program Files (x86)\Java\jre6\bin\java.exe.

You now have access to two new Assetic filters in your application: yui_css and yui_js. These will use the YUI Compressor to minify stylesheets and JavaScripts, respectively.

Minify your Assets

You have YUI Compressor configured now, but nothing is going to happen until you apply one of these filters to an asset. Since your assets are a part of the view layer, this work is done in your templates:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' filter='yui_js' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*'),
        array('yui_js')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

注解

The above example assumes that you have a bundle called AppBundle and your JavaScript files are in the Resources/public/js directory under your bundle. This isn’t important however - you can include your JavaScript files no matter where they are.

With the addition of the yui_js filter to the asset tags above, you should now see minified JavaScripts coming over the wire much faster. The same process can be repeated to minify your stylesheets.

  • Twig
    {% stylesheets '@AppBundle/Resources/public/css/*' filter='yui_css' %}
        <link rel="stylesheet" type="text/css" media="screen" href="{{ asset_url }}" />
    {% endstylesheets %}
    
  • PHP
    <?php foreach ($view['assetic']->stylesheets(
        array('@AppBundle/Resources/public/css/*'),
        array('yui_css')
    ) as $url): ?>
        <link rel="stylesheet" type="text/css" media="screen" href="<?php echo $view->escape($url) ?>" />
    <?php endforeach ?>
    
Disable Minification in Debug Mode

Minified JavaScripts and Stylesheets are very difficult to read, let alone debug. Because of this, Assetic lets you disable a certain filter when your application is in debug mode. You can do this by prefixing the filter name in your template with a question mark: ?. This tells Assetic to only apply this filter when debug mode is off.

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/*' filter='?yui_js' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/*'),
        array('?yui_js')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

小技巧

Instead of adding the filter to the asset tags, you can also globally enable it by adding the apply_to attribute to the filter configuration, for example in the yui_js filter apply_to: "\.js$". To only have the filter applied in production, add this to the config_prod file rather than the common config file. For details on applying filters by file extension, see Filtering Based on a File Extension.

How to Use Assetic for Image Optimization with Twig Functions

Amongst its many filters, Assetic has four filters which can be used for on-the-fly image optimization. This allows you to get the benefits of smaller file sizes without having to use an image editor to process each image. The results are cached and can be dumped for production so there is no performance hit for your end users.

Using Jpegoptim

Jpegoptim is a utility for optimizing JPEG files. To use it with Assetic, add the following to the Assetic config:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            jpegoptim:
                bin: path/to/jpegoptim
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="jpegoptim"
            bin="path/to/jpegoptim" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'jpegoptim' => array(
                'bin' => 'path/to/jpegoptim',
            ),
        ),
    ));
    

注解

Notice that to use jpegoptim, you must have it already installed on your system. The bin option points to the location of the compiled binary.

It can now be used from a template:

  • Twig
    {% image '@AppBundle/Resources/public/images/example.jpg'
        filter='jpegoptim' output='/images/example.jpg' %}
        <img src="{{ asset_url }}" alt="Example"/>
    {% endimage %}
    
  • PHP
    <?php foreach ($view['assetic']->image(
        array('@AppBundle/Resources/public/images/example.jpg'),
        array('jpegoptim')
    ) as $url): ?>
        <img src="<?php echo $view->escape($url) ?>" alt="Example"/>
    <?php endforeach ?>
    
Removing all EXIF Data

By default, running this filter only removes some of the meta information stored in the file. Any EXIF data and comments are not removed, but you can remove these by using the strip_all option:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            jpegoptim:
                bin: path/to/jpegoptim
                strip_all: true
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="jpegoptim"
            bin="path/to/jpegoptim"
            strip_all="true" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'jpegoptim' => array(
                'bin'       => 'path/to/jpegoptim',
                'strip_all' => 'true',
            ),
        ),
    ));
    
Lowering maximum Quality

The quality level of the JPEG is not affected by default. You can gain further file size reductions by setting the max quality setting lower than the current level of the images. This will of course be at the expense of image quality:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            jpegoptim:
                bin: path/to/jpegoptim
                max: 70
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="jpegoptim"
            bin="path/to/jpegoptim"
            max="70" />
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'jpegoptim' => array(
                'bin' => 'path/to/jpegoptim',
                'max' => '70',
            ),
        ),
    ));
    
Shorter Syntax: Twig Function

If you’re using Twig, it’s possible to achieve all of this with a shorter syntax by enabling and using a special Twig function. Start by adding the following config:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            jpegoptim:
                bin: path/to/jpegoptim
        twig:
            functions:
                jpegoptim: ~
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="jpegoptim"
            bin="path/to/jpegoptim" />
        <assetic:twig>
            <assetic:twig_function
                name="jpegoptim" />
        </assetic:twig>
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'jpegoptim' => array(
                'bin' => 'path/to/jpegoptim',
            ),
        ),
        'twig' => array(
            'functions' => array('jpegoptim'),
            ),
        ),
    ));
    

The Twig template can now be changed to the following:

<img src="{{ jpegoptim('@AppBundle/Resources/public/images/example.jpg') }}" alt="Example"/>

You can specify the output directory in the config in the following way:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            jpegoptim:
                bin: path/to/jpegoptim
        twig:
            functions:
                jpegoptim: { output: images/*.jpg }
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="jpegoptim"
            bin="path/to/jpegoptim" />
        <assetic:twig>
            <assetic:twig_function
                name="jpegoptim"
                output="images/*.jpg" />
        </assetic:twig>
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'jpegoptim' => array(
                'bin' => 'path/to/jpegoptim',
            ),
        ),
        'twig' => array(
            'functions' => array(
                'jpegoptim' => array(
                    output => 'images/*.jpg'
                ),
            ),
        ),
    ));
    
How to Apply an Assetic Filter to a specific File Extension

Assetic filters can be applied to individual files, groups of files or even, as you’ll see here, files that have a specific extension. To show you how to handle each option, suppose that you want to use Assetic’s CoffeeScript filter, which compiles CoffeeScript files into JavaScript.

The main configuration is just the paths to coffee, node and node_modules. An example configuration might look like this:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            coffee:
                bin:        /usr/bin/coffee
                node:       /usr/bin/node
                node_paths: [/usr/lib/node_modules/]
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="coffee"
            bin="/usr/bin/coffee/"
            node="/usr/bin/node/">
            <assetic:node-path>/usr/lib/node_modules/</assetic:node-path>
        </assetic:filter>
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'coffee' => array(
                'bin'  => '/usr/bin/coffee',
                'node' => '/usr/bin/node',
                'node_paths' => array('/usr/lib/node_modules/'),
            ),
        ),
    ));
    
Filter a single File

You can now serve up a single CoffeeScript file as JavaScript from within your templates:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/example.coffee' filter='coffee' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array('@AppBundle/Resources/public/js/example.coffee'),
        array('coffee')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

This is all that’s needed to compile this CoffeeScript file and serve it as the compiled JavaScript.

Filter multiple Files

You can also combine multiple CoffeeScript files into a single output file:

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/example.coffee'
                   '@AppBundle/Resources/public/js/another.coffee'
        filter='coffee' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array(
            '@AppBundle/Resources/public/js/example.coffee',
            '@AppBundle/Resources/public/js/another.coffee',
        ),
        array('coffee')
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

Both the files will now be served up as a single file compiled into regular JavaScript.

Filtering Based on a File Extension

One of the great advantages of using Assetic is reducing the number of asset files to lower HTTP requests. In order to make full use of this, it would be good to combine all your JavaScript and CoffeeScript files together since they will ultimately all be served as JavaScript. Unfortunately just adding the JavaScript files to the files to be combined as above will not work as the regular JavaScript files will not survive the CoffeeScript compilation.

This problem can be avoided by using the apply_to option in the config, which allows you to specify which filter should always be applied to particular file extensions. In this case you can specify that the coffee filter is applied to all .coffee files:

  • YAML
    # app/config/config.yml
    assetic:
        filters:
            coffee:
                bin:        /usr/bin/coffee
                node:       /usr/bin/node
                node_paths: [/usr/lib/node_modules/]
                apply_to:   "\.coffee$"
    
  • XML
    <!-- app/config/config.xml -->
    <assetic:config>
        <assetic:filter
            name="coffee"
            bin="/usr/bin/coffee"
            node="/usr/bin/node"
            apply_to="\.coffee$" />
            <assetic:node-paths>/usr/lib/node_modules/</assetic:node-path>
    </assetic:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('assetic', array(
        'filters' => array(
            'coffee' => array(
                'bin'      => '/usr/bin/coffee',
                'node'     => '/usr/bin/node',
                'node_paths' => array('/usr/lib/node_modules/'),
                'apply_to' => '\.coffee$',
            ),
        ),
    ));
    

With this, you no longer need to specify the coffee filter in the template. You can also list regular JavaScript files, all of which will be combined and rendered as a single JavaScript file (with only the .coffee files being run through the CoffeeScript filter):

  • Twig
    {% javascripts '@AppBundle/Resources/public/js/example.coffee'
                   '@AppBundle/Resources/public/js/another.coffee'
                   '@AppBundle/Resources/public/js/regular.js' %}
        <script src="{{ asset_url }}"></script>
    {% endjavascripts %}
    
  • PHP
    <?php foreach ($view['assetic']->javascripts(
        array(
            '@AppBundle/Resources/public/js/example.coffee',
            '@AppBundle/Resources/public/js/another.coffee',
            '@AppBundle/Resources/public/js/regular.js',
        )
    ) as $url): ?>
        <script src="<?php echo $view->escape($url) ?>"></script>
    <?php endforeach ?>
    

Bundles

How to Install 3rd Party Bundles

Most bundles provide their own installation instructions. However, the basic steps for installing a bundle are the same:

A) Add Composer Dependencies

Dependencies are managed with Composer, so if Composer is new to you, learn some basics in their documentation. This has 2 steps:

1) Find out the Name of the Bundle on Packagist

The README for a bundle (e.g. FOSUserBundle) usually tells you its name (e.g. friendsofsymfony/user-bundle). If it doesn’t, you can search for the library on the Packagist.org site.

注解

Looking for bundles? Try searching at KnpBundles.com: the unofficial archive of Symfony Bundles.

2) Install the Bundle via Composer

Now that you know the package name, you can install it via Composer:

$ composer require friendsofsymfony/user-bundle

This will choose the best version for your project, add it to composer.json and download the library into the vendor/ directory. If you need a specific version, add a : and the version right after the library name (see composer require).

B) Enable the Bundle

At this point, the bundle is installed in your Symfony project (in vendor/friendsofsymfony/) and the autoloader recognizes its classes. The only thing you need to do now is register the bundle in AppKernel:

// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    // ...

    public function registerBundles()
    {
        $bundles = array(
            // ...,
            new FOS\UserBundle\FOSUserBundle(),
        );

        // ...
    }
}
C) Configure the Bundle

It’s pretty common for a bundle to need some additional setup or configuration in app/config/config.yml. The bundle’s documentation will tell you about the configuration, but you can also get a reference of the bundle’s config via the config:dump-reference command.

For instance, in order to look the reference of the assetic config you can use this:

$ app/console config:dump-reference AsseticBundle

or this:

$ app/console config:dump-reference assetic

The output will look like this:

assetic:
    debug:                %kernel.debug%
    use_controller:
        enabled:              %kernel.debug%
        profiler:             false
    read_from:            %kernel.root_dir%/../web
    write_to:             %assetic.read_from%
    java:                 /usr/bin/java
    node:                 /usr/local/bin/node
    node_paths:           []
    # ...
Other Setup

At this point, check the README file of your brand new bundle to see what to do next. Have fun!

Best Practices for Reusable Bundles

There are 2 types of bundles:

  • Application-specific bundles: only used to build your application;
  • Reusable bundles: meant to be shared across many projects.

This article is all about how to structure your reusable bundles so that they’re easy to configure and extend. Many of these recommendations do not apply to application bundles because you’ll want to keep those as simple as possible. For application bundles, just follow the practices shown throughout the book and cookbook.

参见

The best practices for application-specific bundles are discussed in The Symfony Framework Best Practices.

Bundle Name

A bundle is also a PHP namespace. The namespace must follow the technical interoperability standards for PHP 5.3 namespaces and class names: it starts with a vendor segment, followed by zero or more category segments, and it ends with the namespace short name, which must end with a Bundle suffix.

A namespace becomes a bundle as soon as you add a bundle class to it. The bundle class name must follow these simple rules:

  • Use only alphanumeric characters and underscores;
  • Use a CamelCased name;
  • Use a descriptive and short name (no more than 2 words);
  • Prefix the name with the concatenation of the vendor (and optionally the category namespaces);
  • Suffix the name with Bundle.

Here are some valid bundle namespaces and class names:

Namespace Bundle Class Name
Acme\Bundle\BlogBundle AcmeBlogBundle
Acme\Bundle\Social\BlogBundle AcmeSocialBlogBundle
Acme\BlogBundle AcmeBlogBundle

By convention, the getName() method of the bundle class should return the class name.

注解

If you share your bundle publicly, you must use the bundle class name as the name of the repository (AcmeBlogBundle and not BlogBundle for instance).

注解

Symfony core Bundles do not prefix the Bundle class with Symfony and always add a Bundle sub-namespace; for example: FrameworkBundle.

Each bundle has an alias, which is the lower-cased short version of the bundle name using underscores (acme_hello for AcmeHelloBundle, or acme_social_blog for Acme\Social\BlogBundle for instance). This alias is used to enforce uniqueness within a bundle (see below for some usage examples).

Directory Structure

The basic directory structure of a HelloBundle must read as follows:

XXX/...
    HelloBundle/
        HelloBundle.php
        Controller/
        Resources/
            meta/
                LICENSE
            config/
            doc/
                index.rst
            translations/
            views/
            public/
        Tests/

The XXX directory(ies) reflects the namespace structure of the bundle.

The following files are mandatory:

  • HelloBundle.php;
  • Resources/meta/LICENSE: The full license for the code;
  • Resources/doc/index.rst: The root file for the Bundle documentation.

注解

These conventions ensure that automated tools can rely on this default structure to work.

The depth of sub-directories should be kept to the minimal for most used classes and files (2 levels at a maximum). More levels can be defined for non-strategic, less-used files.

The bundle directory is read-only. If you need to write temporary files, store them under the cache/ or log/ directory of the host application. Tools can generate files in the bundle directory structure, but only if the generated files are going to be part of the repository.

The following classes and files have specific emplacements:

Type Directory
Commands Command/
Controllers Controller/
Service Container Extensions DependencyInjection/
Event Listeners EventListener/
Configuration Resources/config/
Web Resources Resources/public/
Translation files Resources/translations/
Templates Resources/views/
Unit and Functional Tests Tests/

注解

When building a reusable bundle, model classes should be placed in the Model namespace. See How to Provide Model Classes for several Doctrine Implementations for how to handle the mapping with a compiler pass.

Classes

The bundle directory structure is used as the namespace hierarchy. For instance, a HelloController controller is stored in Bundle/HelloBundle/Controller/HelloController.php and the fully qualified class name is Bundle\HelloBundle\Controller\HelloController.

All classes and files must follow the Symfony coding standards.

Some classes should be seen as facades and should be as short as possible, like Commands, Helpers, Listeners, and Controllers.

Classes that connect to the event dispatcher should be suffixed with Listener.

Exceptions classes should be stored in an Exception sub-namespace.

Vendors

A bundle must not embed third-party PHP libraries. It should rely on the standard Symfony autoloading instead.

A bundle should not embed third-party libraries written in JavaScript, CSS, or any other language.

Tests

A bundle should come with a test suite written with PHPUnit and stored under the Tests/ directory. Tests should follow the following principles:

  • The test suite must be executable with a simple phpunit command run from a sample application;
  • The functional tests should only be used to test the response output and some profiling information if you have some;
  • The tests should cover at least 95% of the code base.

注解

A test suite must not contain AllTests.php scripts, but must rely on the existence of a phpunit.xml.dist file.

Documentation

All classes and functions must come with full PHPDoc.

Extensive documentation should also be provided in the reStructuredText format, under the Resources/doc/ directory; the Resources/doc/index.rst file is the only mandatory file and must be the entry point for the documentation.

Installation Instructions

In order to ease the installation of third-party bundles, consider using the following standardized instructions in your README.md file.

Installation
============

Step 1: Download the Bundle
---------------------------

Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:

```bash
$ composer require <package-name> "~1"
```

This command requires you to have Composer installed globally, as explained
in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
of the Composer documentation.

Step 2: Enable the Bundle
-------------------------

Then, enable the bundle by adding the following line in the `app/AppKernel.php`
file of your project:

```php
<?php
// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...

            new <vendor>\<bundle-name>\<bundle-long-name>(),
        );

        // ...
    }

    // ...
}
```

This template assumes that your bundle is in its 1.x version. If not, change the "~1" installation version accordingly ("~2", "~3", etc.)

Optionally, you can add more installation steps (Step 3, Step 4, etc.) to explain other required installation tasks, such as registering routes or dumping assets.

Routing

If the bundle provides routes, they must be prefixed with the bundle alias. For an AcmeBlogBundle for instance, all routes must be prefixed with acme_blog_.

Templates

If a bundle provides templates, they must use Twig. A bundle must not provide a main layout, except if it provides a full working application.

Translation Files

If a bundle provides message translations, they must be defined in the XLIFF format; the domain should be named after the bundle name (bundle.hello).

A bundle must not override existing messages from another bundle.

Configuration

To provide more flexibility, a bundle can provide configurable settings by using the Symfony built-in mechanisms.

For simple configuration settings, rely on the default parameters entry of the Symfony configuration. Symfony parameters are simple key/value pairs; a value being any valid PHP value. Each parameter name should start with the bundle alias, though this is just a best-practice suggestion. The rest of the parameter name will use a period (.) to separate different parts (e.g. acme_hello.email.from).

The end user can provide values in any configuration file:

  • YAML
    # app/config/config.yml
    parameters:
        acme_hello.email.from: fabien@example.com
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="acme_hello.email.from">fabien@example.com</parameter>
    </parameters>
    
  • PHP
    // app/config/config.php
    $container->setParameter('acme_hello.email.from', 'fabien@example.com');
    
  • INI
    ; app/config/config.ini
    [parameters]
    acme_hello.email.from = fabien@example.com
    

Retrieve the configuration parameters in your code from the container:

$container->getParameter('acme_hello.email.from');

Even if this mechanism is simple enough, you are highly encouraged to use the semantic configuration described in the cookbook.

注解

If you are defining services, they should also be prefixed with the bundle alias.

How to Use Bundle Inheritance to Override Parts of a Bundle

When working with third-party bundles, you’ll probably come across a situation where you want to override a file in that third-party bundle with a file in one of your own bundles. Symfony gives you a very convenient way to override things like controllers, templates, and other files in a bundle’s Resources/ directory.

For example, suppose that you’re installing the FOSUserBundle, but you want to override its base layout.html.twig template, as well as one of its controllers. Suppose also that you have your own AcmeUserBundle where you want the overridden files to live. Start by registering the FOSUserBundle as the “parent” of your bundle:

// src/Acme/UserBundle/AcmeUserBundle.php
namespace Acme\UserBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeUserBundle extends Bundle
{
    public function getParent()
    {
        return 'FOSUserBundle';
    }
}

By making this simple change, you can now override several parts of the FOSUserBundle simply by creating a file with the same name.

注解

Despite the method name, there is no parent/child relationship between the bundles, it is just a way to extend and override an existing bundle.

Overriding Controllers

Suppose you want to add some functionality to the registerAction of a RegistrationController that lives inside FOSUserBundle. To do so, just create your own RegistrationController.php file, override the bundle’s original method, and change its functionality:

// src/Acme/UserBundle/Controller/RegistrationController.php
namespace Acme\UserBundle\Controller;

use FOS\UserBundle\Controller\RegistrationController as BaseController;

class RegistrationController extends BaseController
{
    public function registerAction()
    {
        $response = parent::registerAction();

        // ... do custom stuff
        return $response;
    }
}

小技巧

Depending on how severely you need to change the behavior, you might call parent::registerAction() or completely replace its logic with your own.

注解

Overriding controllers in this way only works if the bundle refers to the controller using the standard FOSUserBundle:Registration:register syntax in routes and templates. This is the best practice.

Overriding Resources: Templates, Routing, etc

Most resources can also be overridden, simply by creating a file in the same location as your parent bundle.

For example, it’s very common to need to override the FOSUserBundle’s layout.html.twig template so that it uses your application’s base layout. Since the file lives at Resources/views/layout.html.twig in the FOSUserBundle, you can create your own file in the same location of AcmeUserBundle. Symfony will ignore the file that lives inside the FOSUserBundle entirely, and use your file instead.

The same goes for routing files and some other resources.

注解

The overriding of resources only works when you refer to resources with the @FOSUserBundle/Resources/config/routing/security.xml method. If you refer to resources without using the @BundleName shortcut, they can’t be overridden in this way.

警告

Translation and validation files do not work in the same way as described above. Read “Translations” if you want to learn how to override translations and see “Validation Metadata” for tricks to override the validation.

How to Override any Part of a Bundle

This document is a quick reference for how to override different parts of third-party bundles.

Templates

For information on overriding templates, see

Routing

Routing is never automatically imported in Symfony. If you want to include the routes from any bundle, then they must be manually imported from somewhere in your application (e.g. app/config/routing.yml).

The easiest way to “override” a bundle’s routing is to never import it at all. Instead of importing a third-party bundle’s routing, simply copy that routing file into your application, modify it, and import it instead.

Controllers

Assuming the third-party bundle involved uses non-service controllers (which is almost always the case), you can easily override controllers via bundle inheritance. For more information, see How to Use Bundle Inheritance to Override Parts of a Bundle. If the controller is a service, see the next section on how to override it.

Services & Configuration

In order to override/extend a service, there are two options. First, you can set the parameter holding the service’s class name to your own class by setting it in app/config/config.yml. This of course is only possible if the class name is defined as a parameter in the service config of the bundle containing the service. For example, to override the class used for Symfony’s translator service, you would override the translator.class parameter. Knowing exactly which parameter to override may take some research. For the translator, the parameter is defined and used in the Resources/config/translation.xml file in the core FrameworkBundle:

  • YAML
    # app/config/config.yml
    parameters:
        translator.class: Acme\HelloBundle\Translation\Translator
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="translator.class">Acme\HelloBundle\Translation\Translator</parameter>
    </parameters>
    
  • PHP
    // app/config/config.php
    $container->setParameter('translator.class', 'Acme\HelloBundle\Translation\Translator');
    

Secondly, if the class is not available as a parameter, you want to make sure the class is always overridden when your bundle is used or if you need to modify something beyond just the class name, you should use a compiler pass:

// src/Acme/DemoBundle/DependencyInjection/Compiler/OverrideServiceCompilerPass.php
namespace Acme\DemoBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class OverrideServiceCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition('original-service-id');
        $definition->setClass('Acme\DemoBundle\YourService');
    }
}

In this example you fetch the service definition of the original service, and set its class name to your own class.

See How to Work with Compiler Passes in Bundles for information on how to use compiler passes. If you want to do something beyond just overriding the class - like adding a method call - you can only use the compiler pass method.

Entities & Entity Mapping

Due to the way Doctrine works, it is not possible to override entity mapping of a bundle. However, if a bundle provides a mapped superclass (such as the User entity in the FOSUserBundle) one can override attributes and associations. Learn more about this feature and its limitations in the Doctrine documentation.

Forms

In order to override a form type, it has to be registered as a service (meaning it is tagged as form.type). You can then override it as you would override any service as explained in Services & Configuration. This, of course, will only work if the type is referred to by its alias rather than being instantiated, e.g.:

$builder->add('name', 'custom_type');

rather than:

$builder->add('name', new CustomType());
Validation Metadata

Symfony loads all validation configuration files from every bundle and combines them into one validation metadata tree. This means you are able to add new constraints to a property, but you cannot override them.

To override this, the 3rd party bundle needs to have configuration for validation groups. For instance, the FOSUserBundle has this configuration. To create your own validation, add the constraints to a new validation group:

  • YAML
    # src/Acme/UserBundle/Resources/config/validation.yml
    FOS\UserBundle\Model\User:
        properties:
            plainPassword:
                - NotBlank:
                    groups: [AcmeValidation]
                - Length:
                    min: 6
                    minMessage: fos_user.password.short
                    groups: [AcmeValidation]
    
  • XML
    <!-- src/Acme/UserBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
            http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="FOS\UserBundle\Model\User">
            <property name="plainPassword">
                <constraint name="NotBlank">
                    <option name="groups">
                        <value>AcmeValidation</value>
                    </option>
                </constraint>
    
                <constraint name="Length">
                    <option name="min">6</option>
                    <option name="minMessage">fos_user.password.short</option>
                    <option name="groups">
                        <value>AcmeValidation</value>
                    </option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    

Now, update the FOSUserBundle configuration, so it uses your validation groups instead of the original ones.

Translations

Translations are not related to bundles, but to domains. That means that you can override the translations from any translation file, as long as it is in the correct domain.

警告

The last translation file always wins. That means that you need to make sure that the bundle containing your translations is loaded after any bundle whose translations you’re overriding. This is done in AppKernel.

The file that always wins is the one that is placed in app/Resources/translations, as those files are always loaded last.

How to Remove the AcmeDemoBundle

The Symfony Standard Edition comes with a complete demo that lives inside a bundle called AcmeDemoBundle. It is a great boilerplate to refer to while starting a project, but you’ll probably want to eventually remove it.

小技巧

This article uses the AcmeDemoBundle as an example, but you can use these steps to remove any bundle.

1. Unregister the Bundle in the AppKernel

To disconnect the bundle from the framework, you should remove the bundle from the AppKernel::registerBundles() method. The bundle is normally found in the $bundles array but the AcmeDemoBundle is only registered in the development environment and you can find it inside the if statement below:

// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(...);

        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            // comment or remove this line:
            // $bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
            // ...
        }
    }
}
2. Remove Bundle Configuration

Now that Symfony doesn’t know about the bundle, you need to remove any configuration and routing configuration inside the app/config directory that refers to the bundle.

2.1 Remove Bundle Routing

The routing for the AcmeDemoBundle can be found in app/config/routing_dev.yml. Remove the _acme_demo entry at the bottom of this file.

2.2 Remove Bundle Configuration

Some bundles contain configuration in one of the app/config/config*.yml files. Be sure to remove the related configuration from these files. You can quickly spot bundle configuration by looking for a acme_demo (or whatever the name of the bundle is, e.g. fos_user for the FOSUserBundle) string in the configuration files.

The AcmeDemoBundle doesn’t have configuration. However, the bundle is used in the configuration for the app/config/security.yml file. You can use it as a boilerplate for your own security, but you can also remove everything: it doesn’t matter to Symfony if you remove it or not.

3. Remove the Bundle from the Filesystem

Now you have removed every reference to the bundle in your application, you should remove the bundle from the filesystem. The bundle is located in the src/Acme/DemoBundle directory. You should remove this directory and you can remove the Acme directory as well.

小技巧

If you don’t know the location of a bundle, you can use the getPath() method to get the path of the bundle:

echo $this->container->get('kernel')->getBundle('AcmeDemoBundle')->getPath();
3.1 Remove Bundle Assets

Remove the assets of the bundle in the web/ directory (e.g. web/bundles/acmedemo for the AcmeDemoBundle).

4. Remove Integration in other Bundles

注解

This doesn’t apply to the AcmeDemoBundle - no other bundles depend on it, so you can skip this step.

Some bundles rely on other bundles, if you remove one of the two, the other will probably not work. Be sure that no other bundles, third party or self-made, rely on the bundle you are about to remove.

小技巧

If one bundle relies on another, in most cases it means that it uses some services from the bundle. Searching for the bundle alias string may help you spot them (e.g. acme_demo for bundles depending on AcmeDemoBundle).

小技巧

If a third party bundle relies on another bundle, you can find that bundle mentioned in the composer.json file included in the bundle directory.

How to Load Service Configuration inside a Bundle

In Symfony, you’ll find yourself using many services. These services can be registered in the app/config directory of your application. But when you want to decouple the bundle for use in other projects, you want to include the service configuration in the bundle itself. This article will teach you how to do that.

Creating an Extension Class

In order to load service configuration, you have to create a Dependency Injection Extension for your bundle. This class has some conventions in order to be detected automatically. But you’ll later see how you can change it to your own preferences. By default, the Extension has to comply with the following conventions:

  • It has to live in the DependencyInjection namespace of the bundle;
  • The name is equal to the bundle name with the Bundle suffix replaced by Extension (e.g. the Extension class of the AppBundle would be called AppExtension and the one for AcmeHelloBundle would be called AcmeHelloExtension).

The Extension class should implement the ExtensionInterface, but usually you would simply extend the Extension class:

// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AcmeHelloExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        // ... you'll load the files here later
    }
}
Manually Registering an Extension Class

When not following the conventions, you will have to manually register your extension. To do this, you should override the Bundle::getContainerExtension() method to return the instance of the extension:

// ...
use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass;

class AcmeHelloBundle extends Bundle
{
    public function getContainerExtension()
    {
        return new UnconventionalExtensionClass();
    }
}

Since the new Extension class name doesn’t follow the naming conventions, you should also override Extension::getAlias() to return the correct DI alias. The DI alias is the name used to refer to the bundle in the container (e.g. in the app/config/config.yml file). By default, this is done by removing the Extension prefix and converting the class name to underscores (e.g. AcmeHelloExtension‘s DI alias is acme_hello).

Using the load() Method

In the load() method, all services and parameters related to this extension will be loaded. This method doesn’t get the actual container instance, but a copy. This container only has the parameters from the actual container. After loading the services and parameters, the copy will be merged into the actual container, to ensure all services and parameters are also added to the actual container.

In the load() method, you can use PHP code to register service definitions, but it is more common if you put these definitions in a configuration file (using the Yaml, XML or PHP format). Luckily, you can use the file loaders in the extension!

For instance, assume you have a file called services.xml in the Resources/config directory of your bundle, your load method looks like:

use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;

// ...
public function load(array $configs, ContainerBuilder $container)
{
    $loader = new XmlFileLoader(
        $container,
        new FileLocator(__DIR__.'/../Resources/config')
    );
    $loader->load('services.xml');
}

Other available loaders are the YamlFileLoader, PhpFileLoader and IniFileLoader.

注解

The IniFileLoader can only be used to load parameters and it can only load them as strings.

Using Configuration to Change the Services

The Extension is also the class that handles the configuration for that particular bundle (e.g. the configuration in app/config/config.yml). To read more about it, see the “How to Create Friendly Configuration for a Bundle” article.

How to Create Friendly Configuration for a Bundle

If you open your application configuration file (usually app/config/config.yml), you’ll see a number of different configuration “namespaces”, such as framework, twig and doctrine. Each of these configures a specific bundle, allowing you to configure things at a high level and then let the bundle make all the low-level, complex changes based on your settings.

For example, the following tells the FrameworkBundle to enable the form integration, which involves the definition of quite a few services as well as integration of other related components:

  • YAML
    framework:
        form: true
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:form />
        </framework:config>
    </container>
    
  • PHP
    $container->loadFromExtension('framework', array(
        'form' => true,
    ));
    
Using the Bundle Extension

The basic idea is that instead of having the user override individual parameters, you let the user configure just a few, specifically created, options. As the bundle developer, you then parse through that configuration and load correct services and parameters inside an “Extension” class.

As an example, imagine you are creating a social bundle, which provides integration with Twitter and such. To be able to reuse your bundle, you have to make the client_id and client_secret variables configurable. Your bundle configuration would look like:

  • YAML
    # app/config/config.yml
    acme_social:
        twitter:
            client_id: 123
            client_secret: $ecret
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" ?>
    
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:acme-social="http://example.org/dic/schema/acme_social"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
       <acme-social:config>
           <twitter client-id="123" client-secret="$ecret" />
       </acme-social:config>
    
       <!-- ... -->
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('acme_social', array(
        'client_id'     => 123,
        'client_secret' => '$ecret',
    ));
    

参见

Read more about the extension in How to Load Service Configuration inside a Bundle.

小技巧

If a bundle provides an Extension class, then you should not generally override any service container parameters from that bundle. The idea is that if an Extension class is present, every setting that should be configurable should be present in the configuration made available by that class. In other words, the extension class defines all the public configuration settings for which backward compatibility will be maintained.

参见

For parameter handling within a Dependency Injection class see Using Parameters within a Dependency Injection Class.

Processing the $configs Array

First things first, you have to create an extension class as explained in How to Load Service Configuration inside a Bundle.

Whenever a user includes the acme_social key (which is the DI alias) in a configuration file, the configuration under it is added to an array of configurations and passed to the load() method of your extension (Symfony automatically converts XML and YAML to an array).

For the configuration example in the previous section, the array passed to your load() method will look like this:

array(
    array(
        'twitter' => array(
            'client_id' => 123,
            'client_secret' => '$ecret',
        ),
    ),
)

Notice that this is an array of arrays, not just a single flat array of the configuration values. This is intentional, as it allows Symfony to parse several configuration resources. For example, if acme_social appears in another configuration file - say config_dev.yml - with different values beneath it, the incoming array might look like this:

array(
    // values from config.yml
    array(
        'twitter' => array(
            'client_id' => 123,
            'client_secret' => '$secret',
        ),
    ),
    // values from config_dev.yml
    array(
        'twitter' => array(
            'client_id' => 456,
        ),
    ),
)

The order of the two arrays depends on which one is set first.

But don’t worry! Symfony’s Config component will help you merge these values, provide defaults and give the user validation errors on bad configuration. Here’s how it works. Create a Configuration class in the DependencyInjection directory and build a tree that defines the structure of your bundle’s configuration.

The Configuration class to handle the sample configuration looks like:

// src/Acme/SocialBundle/DependencyInjection/Configuration.php
namespace Acme\SocialBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_social');

        $rootNode
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;

        return $treeBuilder;
    }
}

参见

The Configuration class can be much more complicated than shown here, supporting “prototype” nodes, advanced validation, XML-specific normalization and advanced merging. You can read more about this in the Config component documentation. You can also see it in action by checking out some of the core Configuration classes, such as the one from the FrameworkBundle Configuration or the TwigBundle Configuration.

This class can now be used in your load() method to merge configurations and force validation (e.g. if an additional option was passed, an exception will be thrown):

public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();

    $config = $this->processConfiguration($configuration, $configs);
    // ...
}

The processConfiguration() method uses the configuration tree you’ve defined in the Configuration class to validate, normalize and merge all of the configuration arrays together.

小技巧

Instead of calling processConfiguration() in your extension each time you provide some configuration options, you might want to use the ConfigurableExtension to do this automatically for you:

// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;

class AcmeHelloExtension extends ConfigurableExtension
{
    // note that this method is called loadInternal and not load
    protected function loadInternal(array $mergedConfig, ContainerBuilder $container)
    {
        // ...
    }
}

This class uses the getConfiguration() method to get the Configuration instance, you should override it if your Configuration class is not called Configuration or if it is not placed in the same namespace as the extension.

Modifying the Configuration of Another Bundle

If you have multiple bundles that depend on each other, it may be useful to allow one Extension class to modify the configuration passed to another bundle’s Extension class, as if the end-developer has actually placed that configuration in their app/config/config.yml file. This can be achieved using a prepend extension. For more details, see How to Simplify Configuration of multiple Bundles.

Dump the Configuration

The config:dump-reference command dumps the default configuration of a bundle in the console using the Yaml format.

As long as your bundle’s configuration is located in the standard location (YourBundle\DependencyInjection\Configuration) and does not require arguments to be passed to the constructor it will work automatically. If you have something different, your Extension class must override the Extension::getConfiguration() method and return an instance of your Configuration.

Supporting XML

Symfony allows people to provide the configuration in three different formats: Yaml, XML and PHP. Both Yaml and PHP use the same syntax and are supported by default when using the Config component. Supporting XML requires you to do some more things. But when sharing your bundle with others, it is recommended that you follow these steps.

Make your Config Tree ready for XML

The Config component provides some methods by default to allow it to correctly process XML configuration. See “Normalization” of the component documentation. However, you can do some optional things as well, this will improve the experience of using XML configuration:

Choosing an XML Namespace

In XML, the XML namespace is used to determine which elements belong to the configuration of a specific bundle. The namespace is returned from the Extension::getNamespace() method. By convention, the namespace is a URL (it doesn’t have to be a valid URL nor does it need to exists). By default, the namespace for a bundle is http://example.org/dic/schema/DI_ALIAS, where DI_ALIAS is the DI alias of the extension. You might want to change this to a more professional URL:

// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php

// ...
class AcmeHelloExtension extends Extension
{
    // ...

    public function getNamespace()
    {
        return 'http://acme_company.com/schema/dic/hello';
    }
}
Providing an XML Schema

XML has a very useful feature called XML schema. This allows you to describe all possible elements and attributes and their values in an XML Schema Definition (an xsd file). This XSD file is used by IDEs for auto completion and it is used by the Config component to validate the elements.

In order to use the schema, the XML configuration file must provide an xsi:schemaLocation attribute pointing to the XSD file for a certain XML namespace. This location always starts with the XML namespace. This XML namespace is then replaced with the XSD validation base path returned from Extension::getXsdValidationBasePath() method. This namespace is then followed by the rest of the path from the base path to the file itself.

By convention, the XSD file lives in the Resources/config/schema, but you can place it anywhere you like. You should return this path as the base path:

// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php

// ...
class AcmeHelloExtension extends Extension
{
    // ...

    public function getXsdValidationBasePath()
    {
        return __DIR__.'/../Resources/config/schema';
    }
}

Assume the XSD file is called hello-1.0.xsd, the schema location will be http://acme_company.com/schema/dic/hello/hello-1.0.xsd:

<!-- app/config/config.xml -->
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:acme-hello="http://acme_company.com/schema/dic/hello"
    xsi:schemaLocation="http://acme_company.com/schema/dic/hello
        http://acme_company.com/schema/dic/hello/hello-1.0.xsd">

    <acme-hello:config>
        <!-- ... -->
    </acme-hello:config>

    <!-- ... -->
</container>
How to Simplify Configuration of multiple Bundles

When building reusable and extensible applications, developers are often faced with a choice: either create a single large bundle or multiple smaller bundles. Creating a single bundle has the drawback that it’s impossible for users to choose to remove functionality they are not using. Creating multiple bundles has the drawback that configuration becomes more tedious and settings often need to be repeated for various bundles.

Using the below approach, it is possible to remove the disadvantage of the multiple bundle approach by enabling a single Extension to prepend the settings for any bundle. It can use the settings defined in the app/config/config.yml to prepend settings just as if they would have been written explicitly by the user in the application configuration.

For example, this could be used to configure the entity manager name to use in multiple bundles. Or it can be used to enable an optional feature that depends on another bundle being loaded as well.

To give an Extension the power to do this, it needs to implement PrependExtensionInterface:

// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AcmeHelloExtension extends Extension implements PrependExtensionInterface
{
    // ...

    public function prepend(ContainerBuilder $container)
    {
        // ...
    }
}

Inside the prepend() method, developers have full access to the ContainerBuilder instance just before the load() method is called on each of the registered bundle Extensions. In order to prepend settings to a bundle extension developers can use the prependExtensionConfig() method on the ContainerBuilder instance. As this method only prepends settings, any other settings done explicitly inside the app/config/config.yml would override these prepended settings.

The following example illustrates how to prepend a configuration setting in multiple bundles as well as disable a flag in multiple bundles in case a specific other bundle is not registered:

public function prepend(ContainerBuilder $container)
{
    // get all bundles
    $bundles = $container->getParameter('kernel.bundles');
    // determine if AcmeGoodbyeBundle is registered
    if (!isset($bundles['AcmeGoodbyeBundle'])) {
        // disable AcmeGoodbyeBundle in bundles
        $config = array('use_acme_goodbye' => false);
        foreach ($container->getExtensions() as $name => $extension) {
            switch ($name) {
                case 'acme_something':
                case 'acme_other':
                    // set use_acme_goodbye to false in the config of
                    // acme_something and acme_other note that if the user manually
                    // configured use_acme_goodbye to true in the app/config/config.yml
                    // then the setting would in the end be true and not false
                    $container->prependExtensionConfig($name, $config);
                    break;
            }
        }
    }

    // process the configuration of AcmeHelloExtension
    $configs = $container->getExtensionConfig($this->getAlias());
    // use the Configuration class to generate a config array with
    // the settings "acme_hello"
    $config = $this->processConfiguration(new Configuration(), $configs);

    // check if entity_manager_name is set in the "acme_hello" configuration
    if (isset($config['entity_manager_name'])) {
        // prepend the acme_something settings with the entity_manager_name
        $config = array('entity_manager_name' => $config['entity_manager_name']);
        $container->prependExtensionConfig('acme_something', $config);
    }
}

The above would be the equivalent of writing the following into the app/config/config.yml in case AcmeGoodbyeBundle is not registered and the entity_manager_name setting for acme_hello is set to non_default:

  • YAML
    # app/config/config.yml
    acme_something:
        # ...
        use_acme_goodbye: false
        entity_manager_name: non_default
    
    acme_other:
        # ...
        use_acme_goodbye: false
    
  • XML
    <!-- app/config/config.xml -->
    <acme-something:config use-acme-goodbye="false">
        <acme-something:entity-manager-name>non_default</acme-something:entity-manager-name>
    </acme-something:config>
    
    <acme-other:config use-acme-goodbye="false" />
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('acme_something', array(
        // ...
        'use_acme_goodbye' => false,
        'entity_manager_name' => 'non_default',
    ));
    $container->loadFromExtension('acme_other', array(
        // ...
        'use_acme_goodbye' => false,
    ));
    

Cache

How to Use Varnish to Speed up my Website

Because Symfony’s cache uses the standard HTTP cache headers, the Symfony Reverse Proxy can easily be replaced with any other reverse proxy. Varnish is a powerful, open-source, HTTP accelerator capable of serving cached content fast and including support for Edge Side Includes.

Make Symfony Trust the Reverse Proxy

For ESI to work correctly and for the X-FORWARDED headers to be used, you need to configure Varnish as a trusted proxy.

Routing and X-FORWARDED Headers

To ensure that the Symfony Router generates URLs correctly with Varnish, a X-Forwarded-Port header must be present for Symfony to use the correct port number.

This port depends on your setup. Lets say that external connections come in on the default HTTP port 80. For HTTPS connections, there is another proxy (as Varnish does not do HTTPS itself) on the default HTTPS port 443 that handles the SSL termination and forwards the requests as HTTP requests to Varnish with a X-Forwarded-Proto header. In this case, you need to add the following configuration snippet:

sub vcl_recv {
    if (req.http.X-Forwarded-Proto == "https" ) {
        set req.http.X-Forwarded-Port = "443";
    } else {
        set req.http.X-Forwarded-Port = "80";
    }
}

注解

Remember to configure framework.trusted_proxies in the Symfony configuration so that Varnish is seen as a trusted proxy and the X-Forwarded-* headers are used.

Varnish automatically forwards the IP as X-Forwarded-For and leaves the X-Forwarded-Proto header in the request. If you do not configure Varnish as trusted proxy, Symfony will see all requests as coming through insecure HTTP connections from the Varnish host instead of the real client.

If the X-Forwarded-Port header is not set correctly, Symfony will append the port where the PHP application is running when generating absolute URLs, e.g. http://example.com:8080/my/path.

Cookies and Caching

By default, a sane caching proxy does not cache anything when a request is sent with cookies or a basic authentication header. This is because the content of the page is supposed to depend on the cookie value or authentication header.

If you know for sure that the backend never uses sessions or basic authentication, have varnish remove the corresponding header from requests to prevent clients from bypassing the cache. In practice, you will need sessions at least for some parts of the site, e.g. when using forms with CSRF Protection. In this situation, make sure to only start a session when actually needed and clear the session when it is no longer needed. Alternatively, you can look into Caching Pages that Contain CSRF Protected Forms.

Cookies created in Javascript and used only in the frontend, e.g. when using Google analytics are nonetheless sent to the server. These cookies are not relevant for the backend and should not affect the caching decision. Configure your Varnish cache to clean the cookies header. You want to keep the session cookie, if there is one, and get rid of all other cookies so that pages are cached if there is no active session. Unless you changed the default configuration of PHP, your session cookie has the name PHPSESSID:

sub vcl_recv {
    // Remove all cookies except the session ID.
    if (req.http.Cookie) {
        set req.http.Cookie = ";" + req.http.Cookie;
        set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID)=", "; \1=");
        set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");

        if (req.http.Cookie == "") {
            // If there are no more cookies, remove the header to get page cached.
            remove req.http.Cookie;
        }
    }
}

小技巧

If content is not different for every user, but depends on the roles of a user, a solution is to separate the cache per group. This pattern is implemented and explained by the FOSHttpCacheBundle under the name User Context.

Ensure Consistent Caching Behaviour

Varnish uses the cache headers sent by your application to determine how to cache content. However, versions prior to Varnish 4 did not respect Cache-Control: no-cache, no-store and private. To ensure consistent behavior, use the following configuration if you are still using Varnish 3:

  • Varnish 3
    sub vcl_fetch {
        /* By default, Varnish3 ignores Cache-Control: no-cache and private
           https://www.varnish-cache.org/docs/3.0/tutorial/increasing_your_hitrate.html#cache-control
         */
        if (beresp.http.Cache-Control ~ "private" ||
            beresp.http.Cache-Control ~ "no-cache" ||
            beresp.http.Cache-Control ~ "no-store"
        ) {
            return (hit_for_pass);
        }
    }
    

小技巧

You can see the default behavior of Varnish in the form of a VCL file: default.vcl for Varnish 3, builtin.vcl for Varnish 4.

Enable Edge Side Includes (ESI)

As explained in the Edge Side Includes section, Symfony detects whether it talks to a reverse proxy that understands ESI or not. When you use the Symfony reverse proxy, you don’t need to do anything. But to make Varnish instead of Symfony resolve the ESI tags, you need some configuration in Varnish. Symfony uses the Surrogate-Capability header from the Edge Architecture described by Akamai.

注解

Varnish only supports the src attribute for ESI tags (onerror and alt attributes are ignored).

First, configure Varnish so that it advertises its ESI support by adding a Surrogate-Capability header to requests forwarded to the backend application:

sub vcl_recv {
    // Add a Surrogate-Capability header to announce ESI support.
    set req.http.Surrogate-Capability = "abc=ESI/1.0";
}

注解

The abc part of the header isn’t important unless you have multiple “surrogates” that need to advertise their capabilities. See Surrogate-Capability Header for details.

Then, optimize Varnish so that it only parses the Response contents when there is at least one ESI tag by checking the Surrogate-Control header that Symfony adds automatically:

  • Varnish 4
    sub vcl_backend_response {
        // Check for ESI acknowledgement and remove Surrogate-Control header
        if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
            unset beresp.http.Surrogate-Control;
            set beresp.do_esi = true;
        }
    }
    
  • Varnish 3
    sub vcl_fetch {
        // Check for ESI acknowledgement and remove Surrogate-Control header
        if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
            unset beresp.http.Surrogate-Control;
            set beresp.do_esi = true;
        }
    }
    

小技巧

If you followed the advice about ensuring a consistent caching behavior, those vcl functions already exist. Just append the code to the end of the function, they won’t interfere with each other.

Cache Invalidation

If you want to cache content that changes frequently and still serve the most recent version to users, you need to invalidate that content. While cache invalidation allows you to purge content from your proxy before it has expired, it adds complexity to your caching setup.

小技巧

The open source FOSHttpCacheBundle takes the pain out of cache invalidation by helping you to organize your caching and invalidation setup.

The documentation of the FOSHttpCacheBundle explains how to configure Varnish and other reverse proxies for cache invalidation.

Caching Pages that Contain CSRF Protected Forms

CSRF tokens are meant to be different for every user. This is why you need to be cautious if you try to cache pages with forms including them.

For more information about how CSRF protection works in Symfony, please check CSRF Protection.

Why Caching Pages with a CSRF token is Problematic

Typically, each user is assigned a unique CSRF token, which is stored in the session for validation. This means that if you do cache a page with a form containing a CSRF token, you’ll cache the CSRF token of the first user only. When a user submits the form, the token won’t match the token stored in the session and all users (except for the first) will fail CSRF validation when submitting the form.

In fact, many reverse proxies (like Varnish) will refuse to cache a page with a CSRF token. This is because a cookie is sent in order to preserve the PHP session open and Varnish’s default behaviour is to not cache HTTP requests with cookies.

How to Cache Most of the Page and still be able to Use CSRF Protection

To cache a page that contains a CSRF token, you can use more advanced caching techniques like ESI fragments, where you cache the full page and embedding the form inside an ESI tag with no cache at all.

Another option would be to load the form via an uncached AJAX request, but cache the rest of the HTML response.

Or you can even load just the CSRF token with an AJAX request and replace the form field value with it.

Installing Composer

Composer is the package manager used by modern PHP applications and the recommended way to install Symfony2.

Install Composer on Linux and Mac OS X

To install Composer on Linux or Mac OS X, execute the following two commands:

$ curl -sS https://getcomposer.org/installer | php
$ sudo mv composer.phar /usr/local/bin/composer

注解

If you don’t have curl installed, you can also just download the installer file manually at http://getcomposer.org/installer and then run:

$ php installer
$ sudo mv composer.phar /usr/local/bin/composer
Install Composer on Windows

Download the installer from getcomposer.org/download, execute it and follow the instructions.

Learn more

You can read more about Composer in its documentation.

Configuration

How to Master and Create new Environments

Every application is the combination of code and a set of configuration that dictates how that code should function. The configuration may define the database being used, whether or not something should be cached, or how verbose logging should be. In Symfony, the idea of “environments” is the idea that the same codebase can be run using multiple different configurations. For example, the dev environment should use configuration that makes development easy and friendly, while the prod environment should use a set of configuration optimized for speed.

Different Environments, different Configuration Files

A typical Symfony application begins with three environments: dev, prod, and test. As discussed, each “environment” simply represents a way to execute the same codebase with different configuration. It should be no surprise then that each environment loads its own individual configuration file. If you’re using the YAML configuration format, the following files are used:

  • for the dev environment: app/config/config_dev.yml
  • for the prod environment: app/config/config_prod.yml
  • for the test environment: app/config/config_test.yml

This works via a simple standard that’s used by default inside the AppKernel class:

// app/AppKernel.php

// ...

class AppKernel extends Kernel
{
    // ...

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
    }
}

As you can see, when Symfony is loaded, it uses the given environment to determine which configuration file to load. This accomplishes the goal of multiple environments in an elegant, powerful and transparent way.

Of course, in reality, each environment differs only somewhat from others. Generally, all environments will share a large base of common configuration. Opening the “dev” configuration file, you can see how this is accomplished easily and transparently:

  • YAML
    imports:
        - { resource: config.yml }
    
    # ...
    
  • XML
    <imports>
        <import resource="config.xml" />
    </imports>
    
    <!-- ... -->
    
  • PHP
    $loader->import('config.php');
    
    // ...
    

To share common configuration, each environment’s configuration file simply first imports from a central configuration file (config.yml). The remainder of the file can then deviate from the default configuration by overriding individual parameters. For example, by default, the web_profiler toolbar is disabled. However, in the dev environment, the toolbar is activated by modifying the default value in the dev configuration file:

  • YAML
    # app/config/config_dev.yml
    imports:
        - { resource: config.yml }
    
    web_profiler:
        toolbar: true
        # ...
    
  • XML
    <!-- app/config/config_dev.xml -->
    <imports>
        <import resource="config.xml" />
    </imports>
    
    <webprofiler:config toolbar="true" />
    
  • PHP
    // app/config/config_dev.php
    $loader->import('config.php');
    
    $container->loadFromExtension('web_profiler', array(
        'toolbar' => true,
    
        // ...
    ));
    
Executing an Application in different Environments

To execute the application in each environment, load up the application using either the app.php (for the prod environment) or the app_dev.php (for the dev environment) front controller:

http://localhost/app.php      -> *prod* environment
http://localhost/app_dev.php  -> *dev* environment

注解

The given URLs assume that your web server is configured to use the web/ directory of the application as its root. Read more in Installing Symfony.

If you open up one of these files, you’ll quickly see that the environment used by each is explicitly set:

// web/app.php
// ...

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

// ...

As you can see, the prod key specifies that this application will run in the prod environment. A Symfony application can be executed in any environment by using this code and changing the environment string.

注解

The test environment is used when writing functional tests and is not accessible in the browser directly via a front controller. In other words, unlike the other environments, there is no app_test.php front controller file.

Selecting the Environment for Console Commands

By default, Symfony commands are executed in the dev environment and with the debug mode enabled. Use the --env and --no-debug options to modify this behavior:

# 'dev' environment and debug enabled
$ php app/console command_name

# 'prod' environment (debug is always disabled for 'prod')
$ php app/console command_name --env=prod

# 'test' environment and debug disabled
$ php app/console command_name --env=test --no-debug

In addition to the --env and --debug options, the behavior of Symfony commands can also be controlled with environment variables. The Symfony console application checks the existence and value of these environment variables before executing any command:

SYMFONY_ENV
Sets the execution environment of the command to the value of this variable (dev, prod, test, etc.);
SYMFONY_DEBUG
If 0, debug mode is disabled. Otherwise, debug mode is enabled.

These environment variables are very useful for production servers because they allow you to ensure that commands always run in the prod environment without having to add any command option.

Creating a new Environment

By default, a Symfony application has three environments that handle most cases. Of course, since an environment is nothing more than a string that corresponds to a set of configuration, creating a new environment is quite easy.

Suppose, for example, that before deployment, you need to benchmark your application. One way to benchmark the application is to use near-production settings, but with Symfony’s web_profiler enabled. This allows Symfony to record information about your application while benchmarking.

The best way to accomplish this is via a new environment called, for example, benchmark. Start by creating a new configuration file:

  • YAML
    # app/config/config_benchmark.yml
    imports:
        - { resource: config_prod.yml }
    
    framework:
        profiler: { only_exceptions: false }
    
  • XML
    <!-- app/config/config_benchmark.xml -->
    <imports>
        <import resource="config_prod.xml" />
    </imports>
    
    <framework:config>
        <framework:profiler only-exceptions="false" />
    </framework:config>
    
  • PHP
    // app/config/config_benchmark.php
    $loader->import('config_prod.php')
    
    $container->loadFromExtension('framework', array(
        'profiler' => array('only-exceptions' => false),
    ));
    

注解

Due to the way in which parameters are resolved, you cannot use them to build paths in imports dynamically. This means that something like the following doesn’t work:

  • YAML
    # app/config/config.yml
    imports:
        - { resource: "%kernel.root_dir%/parameters.yml" }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <imports>
            <import resource="%kernel.root_dir%/parameters.yml" />
        </imports>
    </container>
    
  • PHP
    // app/config/config.php
    $loader->import('%kernel.root_dir%/parameters.yml');
    

And with this simple addition, the application now supports a new environment called benchmark.

This new configuration file imports the configuration from the prod environment and modifies it. This guarantees that the new environment is identical to the prod environment, except for any changes explicitly made here.

Because you’ll want this environment to be accessible via a browser, you should also create a front controller for it. Copy the web/app.php file to web/app_benchmark.php and edit the environment to be benchmark:

// web/app_benchmark.php
// ...

// change just this line
$kernel = new AppKernel('benchmark', false);

// ...

The new environment is now accessible via:

http://localhost/app_benchmark.php

注解

Some environments, like the dev environment, are never meant to be accessed on any deployed server by the general public. This is because certain environments, for debugging purposes, may give too much information about the application or underlying infrastructure. To be sure these environments aren’t accessible, the front controller is usually protected from external IP addresses via the following code at the top of the controller:

if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'))) {
    die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
Environments and the Cache Directory

Symfony takes advantage of caching in many ways: the application configuration, routing configuration, Twig templates and more are cached to PHP objects stored in files on the filesystem.

By default, these cached files are largely stored in the app/cache directory. However, each environment caches its own set of files:

<your-project>/
├─ app/
│  ├─ cache/
│  │  ├─ dev/   # cache directory for the *dev* environment
│  │  └─ prod/  # cache directory for the *prod* environment
│  ├─ ...

Sometimes, when debugging, it may be helpful to inspect a cached file to understand how something is working. When doing so, remember to look in the directory of the environment you’re using (most commonly dev while developing and debugging). While it can vary, the app/cache/dev directory includes the following:

  • appDevDebugProjectContainer.php - the cached “service container” that represents the cached application configuration;
  • appDevUrlGenerator.php - the PHP class generated from the routing configuration and used when generating URLs;
  • appDevUrlMatcher.php - the PHP class used for route matching - look here to see the compiled regular expression logic used to match incoming URLs to different routes;
  • twig/ - this directory contains all the cached Twig templates.

注解

You can easily change the directory location and name. For more information read the article How to Override Symfony’s default Directory Structure.

How to Override Symfony’s default Directory Structure

Symfony automatically ships with a default directory structure. You can easily override this directory structure to create your own. The default directory structure is:

your-project/
├─ app/
│  ├─ cache/
│  ├─ config/
│  ├─ logs/
│  └─ ...
├─ src/
│  └─ ...
├─ vendor/
│  └─ ...
└─ web/
   ├─ app.php
   └─ ...
Override the cache Directory

You can override the cache directory by overriding the getCacheDir method in the AppKernel class of you application:

// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    // ...

    public function getCacheDir()
    {
        return $this->rootDir.'/'.$this->environment.'/cache';
    }
}

$this->rootDir is the absolute path to the app directory and $this->environment is the current environment (i.e. dev). In this case you have changed the location of the cache directory to app/{environment}/cache.

警告

You should keep the cache directory different for each environment, otherwise some unexpected behavior may happen. Each environment generates its own cached config files, and so each needs its own directory to store those cache files.

Override the logs Directory

Overriding the logs directory is the same as overriding the cache directory, the only difference is that you need to override the getLogDir method:

// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    // ...

    public function getLogDir()
    {
        return $this->rootDir.'/'.$this->environment.'/logs';
    }
}

Here you have changed the location of the directory to app/{environment}/logs.

Override the web Directory

If you need to rename or move your web directory, the only thing you need to guarantee is that the path to the app directory is still correct in your app.php and app_dev.php front controllers. If you simply renamed the directory, you’re fine. But if you moved it in some way, you may need to modify the paths inside these files:

require_once __DIR__.'/../Symfony/app/bootstrap.php.cache';
require_once __DIR__.'/../Symfony/app/AppKernel.php';

Since Symfony 2.1 (in which Composer is introduced), you also need to change the extra.symfony-web-dir option in the composer.json file:

{
    ...
    "extra": {
        ...
        "symfony-web-dir": "my_new_web_dir"
    }
}

小技巧

Some shared hosts have a public_html web directory root. Renaming your web directory from web to public_html is one way to make your Symfony project work on your shared host. Another way is to deploy your application to a directory outside of your web root, delete your public_html directory, and then replace it with a symbolic link to the web in your project.

注解

If you use the AsseticBundle you need to configure this, so it can use the correct web directory:

  • YAML
    # app/config/config.yml
    
    # ...
    assetic:
        # ...
        read_from: "%kernel.root_dir%/../../public_html"
    
  • XML
    <!-- app/config/config.xml -->
    
    <!-- ... -->
    <assetic:config read-from="%kernel.root_dir%/../../public_html" />
    
  • PHP
    // app/config/config.php
    
    // ...
    $container->loadFromExtension('assetic', array(
        // ...
        'read_from' => '%kernel.root_dir%/../../public_html',
    ));
    

Now you just need to clear the cache and dump the assets again and your application should work:

$ php app/console cache:clear --env=prod
$ php app/console assetic:dump --env=prod --no-debug
Override the vendor Directory

To override the vendor directory, you need to introduce changes in the following files:

  • app/autoload.php
  • composer.json

The change in the composer.json will look like this:

{
    ...
    "config": {
        "bin-dir": "bin",
        "vendor-dir": "/some/dir/vendor"
    },
    ...
}

In app/autoload.php, you need to modify the path leading to the vendor/autoload.php file:

// app/autoload.php
// ...
$loader = require '/some/dir/vendor/autoload.php';

小技巧

This modification can be of interest if you are working in a virtual environment and cannot use NFS - for example, if you’re running a Symfony app using Vagrant/VirtualBox in a guest operating system.

Using Parameters within a Dependency Injection Class

You have seen how to use configuration parameters within Symfony service containers. There are special cases such as when you want, for instance, to use the %kernel.debug% parameter to make the services in your bundle enter debug mode. For this case there is more work to do in order to make the system understand the parameter value. By default your parameter %kernel.debug% will be treated as a simple string. Consider this example with the AcmeDemoBundle:

// Inside Configuration class
$rootNode
    ->children()
        ->booleanNode('logging')->defaultValue('%kernel.debug%')->end()
        // ...
    ->end()
;

// Inside the Extension class
$config = $this->processConfiguration($configuration, $configs);
var_dump($config['logging']);

Now, examine the results to see this closely:

  • YAML
    my_bundle:
        logging: true
        # true, as expected
    
    my_bundle:
        logging: "%kernel.debug%"
        # true/false (depends on 2nd parameter of AppKernel),
        # as expected, because %kernel.debug% inside configuration
        # gets evaluated before being passed to the extension
    
    my_bundle: ~
    # passes the string "%kernel.debug%".
    # Which is always considered as true.
    # The Configurator does not know anything about
    # "%kernel.debug%" being a parameter.
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:my-bundle="http://example.org/schema/dic/my_bundle">
    
        <my-bundle:config logging="true" />
        <!-- true, as expected -->
    
         <my-bundle:config logging="%kernel.debug%" />
         <!-- true/false (depends on 2nd parameter of AppKernel),
              as expected, because %kernel.debug% inside configuration
              gets evaluated before being passed to the extension -->
    
        <my-bundle:config />
        <!-- passes the string "%kernel.debug%".
             Which is always considered as true.
             The Configurator does not know anything about
             "%kernel.debug%" being a parameter. -->
    </container>
    
  • PHP
    $container->loadFromExtension('my_bundle', array(
            'logging' => true,
            // true, as expected
        )
    );
    
    $container->loadFromExtension('my_bundle', array(
            'logging' => "%kernel.debug%",
            // true/false (depends on 2nd parameter of AppKernel),
            // as expected, because %kernel.debug% inside configuration
            // gets evaluated before being passed to the extension
        )
    );
    
    $container->loadFromExtension('my_bundle');
    // passes the string "%kernel.debug%".
    // Which is always considered as true.
    // The Configurator does not know anything about
    // "%kernel.debug%" being a parameter.
    

In order to support this use case, the Configuration class has to be injected with this parameter via the extension as follows:

namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    private $debug;

    public function  __construct($debug)
    {
        $this->debug = (Boolean) $debug;
    }

    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_demo');

        $rootNode
            ->children()
                // ...
                ->booleanNode('logging')->defaultValue($this->debug)->end()
                // ...
            ->end()
        ;

        return $treeBuilder;
    }
}

And set it in the constructor of Configuration via the Extension class:

namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;

class AcmeDemoExtension extends Extension
{
    // ...

    public function getConfiguration(array $config, ContainerBuilder $container)
    {
        return new Configuration($container->getParameter('kernel.debug'));
    }
}
Understanding how the Front Controller, Kernel and Environments Work together

The section How to Master and Create new Environments explained the basics on how Symfony uses environments to run your application with different configuration settings. This section will explain a bit more in-depth what happens when your application is bootstrapped. To hook into this process, you need to understand three parts that work together:

注解

Usually, you will not need to define your own front controller or AppKernel class as the Symfony Standard Edition provides sensible default implementations.

This documentation section is provided to explain what is going on behind the scenes.

The Front Controller

The front controller is a well-known design pattern; it is a section of code that all requests served by an application run through.

In the Symfony Standard Edition, this role is taken by the app.php and app_dev.php files in the web/ directory. These are the very first PHP scripts executed when a request is processed.

The main purpose of the front controller is to create an instance of the AppKernel (more on that in a second), make it handle the request and return the resulting response to the browser.

Because every request is routed through it, the front controller can be used to perform global initializations prior to setting up the kernel or to decorate the kernel with additional features. Examples include:

  • Configuring the autoloader or adding additional autoloading mechanisms;
  • Adding HTTP level caching by wrapping the kernel with an instance of AppCache;
  • Enabling (or skipping) the ClassCache
  • Enabling the Debug component.

The front controller can be chosen by requesting URLs like:

http://localhost/app_dev.php/some/path/...

As you can see, this URL contains the PHP script to be used as the front controller. You can use that to easily switch the front controller or use a custom one by placing it in the web/ directory (e.g. app_cache.php).

When using Apache and the RewriteRule shipped with the Standard Edition, you can omit the filename from the URL and the RewriteRule will use app.php as the default one.

注解

Pretty much every other web server should be able to achieve a behavior similar to that of the RewriteRule described above. Check your server documentation for details or see Configuring a Web Server.

注解

Make sure you appropriately secure your front controllers against unauthorized access. For example, you don’t want to make a debugging environment available to arbitrary users in your production environment.

Technically, the app/console script used when running Symfony on the command line is also a front controller, only that is not used for web, but for command line requests.

The Kernel Class

The Kernel is the core of Symfony. It is responsible for setting up all the bundles that make up your application and providing them with the application’s configuration. It then creates the service container before serving requests in its handle() method.

There are two methods declared in the KernelInterface that are left unimplemented in Kernel and thus serve as template methods:

To fill these (small) blanks, your application needs to subclass the Kernel and implement these methods. The resulting class is conventionally called the AppKernel.

Again, the Symfony Standard Edition provides an AppKernel in the app/ directory. This class uses the name of the environment - which is passed to the Kernel’s constructor method and is available via getEnvironment() - to decide which bundles to create. The logic for that is in registerBundles(), a method meant to be extended by you when you start adding bundles to your application.

You are, of course, free to create your own, alternative or additional AppKernel variants. All you need is to adapt your (or add a new) front controller to make use of the new kernel.

注解

The name and location of the AppKernel is not fixed. When putting multiple Kernels into a single application, it might therefore make sense to add additional sub-directories, for example app/admin/AdminKernel.php and app/api/ApiKernel.php. All that matters is that your front controller is able to create an instance of the appropriate kernel.

Having different AppKernels might be useful to enable different front controllers (on potentially different servers) to run parts of your application independently (for example, the admin UI, the frontend UI and database migrations).

注解

There’s a lot more the AppKernel can be used for, for example overriding the default directory structure. But odds are high that you don’t need to change things like this on the fly by having several AppKernel implementations.

The Environments

As just mentioned, the AppKernel has to implement another method - registerContainerConfiguration(). This method is responsible for loading the application’s configuration from the right environment.

Environments have been covered extensively in the previous chapter, and you probably remember that the Standard Edition comes with three of them - dev, prod and test.

More technically, these names are nothing more than strings passed from the front controller to the AppKernel‘s constructor. This name can then be used in the registerContainerConfiguration() method to decide which configuration files to load.

The Standard Edition’s AppKernel class implements this method by simply loading the app/config/config_*environment*.yml file. You are, of course, free to implement this method differently if you need a more sophisticated way of loading your configuration.

How to Set external Parameters in the Service Container

In the chapter How to Master and Create new Environments, you learned how to manage your application configuration. At times, it may benefit your application to store certain credentials outside of your project code. Database configuration is one such example. The flexibility of the Symfony service container allows you to easily do this.

Environment Variables

Symfony will grab any environment variable prefixed with SYMFONY__ and set it as a parameter in the service container. Some transformations are applied to the resulting parameter name:

  • SYMFONY__ prefix is removed;
  • Parameter name is lowercased;
  • Double underscores are replaced with a period, as a period is not a valid character in an environment variable name.

For example, if you’re using Apache, environment variables can be set using the following VirtualHost configuration:

<VirtualHost *:80>
    ServerName      Symfony
    DocumentRoot    "/path/to/symfony_2_app/web"
    DirectoryIndex  index.php index.html
    SetEnv          SYMFONY__DATABASE__USER user
    SetEnv          SYMFONY__DATABASE__PASSWORD secret

    <Directory "/path/to/symfony_2_app/web">
        AllowOverride All
        Allow from All
    </Directory>
</VirtualHost>

注解

The example above is for an Apache configuration, using the SetEnv directive. However, this will work for any web server which supports the setting of environment variables.

Also, in order for your console to work (which does not use Apache), you must export these as shell variables. On a Unix system, you can run the following:

$ export SYMFONY__DATABASE__USER=user
$ export SYMFONY__DATABASE__PASSWORD=secret

Now that you have declared an environment variable, it will be present in the PHP $_SERVER global variable. Symfony then automatically sets all $_SERVER variables prefixed with SYMFONY__ as parameters in the service container.

You can now reference these parameters wherever you need them.

  • YAML
    doctrine:
        dbal:
            driver    pdo_mysql
            dbname:   symfony_project
            user:     "%database.user%"
            password: "%database.password%"
    
  • XML
    <!-- xmlns:doctrine="http://symfony.com/schema/dic/doctrine" -->
    <!-- xsi:schemaLocation="http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> -->
    
    <doctrine:config>
        <doctrine:dbal
            driver="pdo_mysql"
            dbname="symfony_project"
            user="%database.user%"
            password="%database.password%"
        />
    </doctrine:config>
    
  • PHP
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
            'driver'   => 'pdo_mysql',
            'dbname'   => 'symfony_project',
            'user'     => '%database.user%',
            'password' => '%database.password%',
        )
    ));
    
Constants

The container also has support for setting PHP constants as parameters. See Constants as Parameters for more details.

Miscellaneous Configuration

The imports directive can be used to pull in parameters stored elsewhere. Importing a PHP file gives you the flexibility to add whatever is needed in the container. The following imports a file named parameters.php.

  • YAML
    # app/config/config.yml
    imports:
        - { resource: parameters.php }
    
  • XML
    <!-- app/config/config.xml -->
    <imports>
        <import resource="parameters.php" />
    </imports>
    
  • PHP
    // app/config/config.php
    $loader->import('parameters.php');
    

注解

A resource file can be one of many types. PHP, XML, YAML, INI, and closure resources are all supported by the imports directive.

In parameters.php, tell the service container the parameters that you wish to set. This is useful when important configuration is in a non-standard format. The example below includes a Drupal database configuration in the Symfony service container.

// app/config/parameters.php
include_once('/path/to/drupal/sites/default/settings.php');
$container->setParameter('drupal.database.url', $db_url);
How to Use PdoSessionHandler to Store Sessions in the Database

The default Symfony session storage writes the session information to file(s). Most medium to large websites use a database to store the session values instead of files, because databases are easier to use and scale in a multi-webserver environment.

Symfony has a built-in solution for database session storage called PdoSessionHandler. To use it, you just need to change some parameters in config.yml (or the configuration format of your choice):

2.1 新版功能: In Symfony 2.1 the class and namespace are slightly modified. You can now find the session storage classes in the Session\Storage namespace: Symfony\Component\HttpFoundation\Session\Storage. Also note that in Symfony 2.1 you should configure handler_id not storage_id like in Symfony 2.0. Below, you’ll notice that %session.storage.options% is not used anymore.

  • YAML
    # app/config/config.yml
    framework:
        session:
            # ...
            handler_id: session.handler.pdo
    
    parameters:
        pdo.db_options:
            db_table:    session
            db_id_col:   session_id
            db_data_col: session_value
            db_time_col: session_time
    
    services:
        pdo:
            class: PDO
            arguments:
                dsn:      "mysql:dbname=mydatabase"
                user:     myuser
                password: mypassword
            calls:
                - [setAttribute, [3, 2]] # \PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION
    
        session.handler.pdo:
            class:     Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
            arguments: ["@pdo", "%pdo.db_options%"]
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <framework:session handler-id="session.handler.pdo" cookie-lifetime="3600" auto-start="true"/>
    </framework:config>
    
    <parameters>
        <parameter key="pdo.db_options" type="collection">
            <parameter key="db_table">session</parameter>
            <parameter key="db_id_col">session_id</parameter>
            <parameter key="db_data_col">session_value</parameter>
            <parameter key="db_time_col">session_time</parameter>
        </parameter>
    </parameters>
    
    <services>
        <service id="pdo" class="PDO">
            <argument>mysql:dbname=mydatabase</argument>
            <argument>myuser</argument>
            <argument>mypassword</argument>
            <call method="setAttribute">
                <argument type="constant">PDO::ATTR_ERRMODE</argument>
                <argument type="constant">PDO::ERRMODE_EXCEPTION</argument>
            </call>
        </service>
    
        <service id="session.handler.pdo" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler">
            <argument type="service" id="pdo" />
            <argument>%pdo.db_options%</argument>
        </service>
    </services>
    
  • PHP
    // app/config/config.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->loadFromExtension('framework', array(
        ...,
        'session' => array(
            // ...,
            'handler_id' => 'session.handler.pdo',
        ),
    ));
    
    $container->setParameter('pdo.db_options', array(
        'db_table'      => 'session',
        'db_id_col'     => 'session_id',
        'db_data_col'   => 'session_value',
        'db_time_col'   => 'session_time',
    ));
    
    $pdoDefinition = new Definition('PDO', array(
        'mysql:dbname=mydatabase',
        'myuser',
        'mypassword',
    ));
    $pdoDefinition->addMethodCall('setAttribute', array(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION));
    $container->setDefinition('pdo', $pdoDefinition);
    
    $storageDefinition = new Definition('Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler', array(
        new Reference('pdo'),
        '%pdo.db_options%',
    ));
    $container->setDefinition('session.handler.pdo', $storageDefinition);
    
  • db_table: The name of the session table in your database
  • db_id_col: The name of the id column in your session table (VARCHAR(255) or larger)
  • db_data_col: The name of the value column in your session table (TEXT or CLOB)
  • db_time_col: The name of the time column in your session table (INTEGER)
Sharing your Database Connection Information

With the given configuration, the database connection settings are defined for the session storage connection only. This is OK when you use a separate database for the session data.

But if you’d like to store the session data in the same database as the rest of your project’s data, you can use the connection settings from the parameters.yml file by referencing the database-related parameters defined there:

  • YAML
    services:
        pdo:
            class: PDO
            arguments:
                - "mysql:host=%database_host%;port=%database_port%;dbname=%database_name%"
                - "%database_user%"
                - "%database_password%"
    
  • XML
    <service id="pdo" class="PDO">
        <argument>mysql:host=%database_host%;port=%database_port%;dbname=%database_name%</argument>
        <argument>%database_user%</argument>
        <argument>%database_password%</argument>
    </service>
    
  • PHP
    $pdoDefinition = new Definition('PDO', array(
        'mysql:host=%database_host%;port=%database_port%;dbname=%database_name%',
        '%database_user%',
        '%database_password%',
    ));
    
Example SQL Statements
MySQL

The SQL statement for creating the needed database table might look like the following (MySQL):

CREATE TABLE `session` (
    `session_id` varchar(255) NOT NULL,
    `session_value` text NOT NULL,
    `session_time` int(11) NOT NULL,
    PRIMARY KEY (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
PostgreSQL

For PostgreSQL, the statement should look like this:

CREATE TABLE session (
    session_id character varying(255) NOT NULL,
    session_value text NOT NULL,
    session_time integer NOT NULL,
    CONSTRAINT session_pkey PRIMARY KEY (session_id)
);
Microsoft SQL Server

For MSSQL, the statement might look like the following:

CREATE TABLE [dbo].[session](
    [session_id] [nvarchar](255) NOT NULL,
    [session_value] [ntext] NOT NULL,
    [session_time] [int] NOT NULL,
    PRIMARY KEY CLUSTERED(
        [session_id] ASC
    ) WITH (
        PAD_INDEX  = OFF,
        STATISTICS_NORECOMPUTE  = OFF,
        IGNORE_DUP_KEY = OFF,
        ALLOW_ROW_LOCKS  = ON,
        ALLOW_PAGE_LOCKS  = ON
    ) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
How to Use the Apache Router

Symfony, while fast out of the box, also provides various ways to increase that speed with a little bit of tweaking. One of these ways is by letting Apache handle routes directly, rather than using Symfony for this task.

Change Router Configuration Parameters

To dump Apache routes you must first tweak some configuration parameters to tell Symfony to use the ApacheUrlMatcher instead of the default one:

  • YAML
    # app/config/config_prod.yml
    parameters:
        router.options.matcher.cache_class: ~ # disable router cache
        router.options.matcher_class: Symfony\Component\Routing\Matcher\ApacheUrlMatcher
    
  • XML
    <!-- app/config/config_prod.xml -->
    <parameters>
        <parameter key="router.options.matcher.cache_class">null</parameter> <!-- disable router cache -->
        <parameter key="router.options.matcher_class">
            Symfony\Component\Routing\Matcher\ApacheUrlMatcher
        </parameter>
    </parameters>
    
  • PHP
    // app/config/config_prod.php
    $container->setParameter('router.options.matcher.cache_class', null); // disable router cache
    $container->setParameter(
        'router.options.matcher_class',
        'Symfony\Component\Routing\Matcher\ApacheUrlMatcher'
    );
    

小技巧

Note that ApacheUrlMatcher extends UrlMatcher so even if you don’t regenerate the mod_rewrite rules, everything will work (because at the end of ApacheUrlMatcher::match() a call to parent::match() is done).

Generating mod_rewrite Rules

To test that it’s working, create a very basic route for the AppBundle:

  • YAML
    # app/config/routing.yml
    hello:
        path: /hello/{name}
        defaults: { _controller: AppBundle:Demo:hello }
    
  • XML
    <!-- app/config/routing.xml -->
    <route id="hello" path="/hello/{name}">
        <default key="_controller">AppBundle:Demo:hello</default>
    </route>
    
  • PHP
    // app/config/routing.php
    $collection->add('hello', new Route('/hello/{name}', array(
        '_controller' => 'AppBundle:Demo:hello',
    )));
    

Now generate the mod_rewrite rules:

$ php app/console router:dump-apache -e=prod --no-debug

Which should roughly output the following:

# skip "real" requests
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .* - [QSA,L]

# hello
RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AppBundle\:Demo\:hello]

You can now rewrite web/.htaccess to use the new rules, so with this example it should look like this:

<IfModule mod_rewrite.c>
    RewriteEngine On

    # skip "real" requests
    RewriteCond %{REQUEST_FILENAME} -f
    RewriteRule .* - [QSA,L]

    # hello
    RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$
    RewriteRule .* app.php [QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AppBundle\:Demo\:hello]
</IfModule>

注解

The procedure above should be done each time you add/change a route if you want to take full advantage of this setup.

That’s it! You’re now all set to use Apache routes.

Additional Tweaks

To save a little bit of processing time, change occurrences of Request to ApacheRequest in web/app.php:

// web/app.php

require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
// require_once __DIR__.'/../app/AppCache.php';

use Symfony\Component\HttpFoundation\ApacheRequest;

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// $kernel = new AppCache($kernel);
$kernel->handle(ApacheRequest::createFromGlobals())->send();
Configuring a Web Server

The preferred way to develop your Symfony application is to use PHP’s internal web server. However, when using an older PHP version or when running the application in the production environment, you’ll need to use a fully-featured web server. This article describes several ways to use Symfony with Apache2 or Nginx.

When using Apache2, you can configure PHP as an Apache module or with FastCGI using PHP FPM. FastCGI also is the preferred way to use PHP with Nginx.

Apache2 with mod_php/PHP-CGI

For advanced Apache configuration options, see the official Apache documentation. The minimum basics to get your application running under Apache2 are:

<VirtualHost *:80>
    ServerName domain.tld
    ServerAlias www.domain.tld

    DocumentRoot /var/www/project/web
    <Directory /var/www/project/web>
        # enable the .htaccess rewrites
        AllowOverride All
        Order allow,deny
        Allow from All
    </Directory>

    ErrorLog /var/log/apache2/project_error.log
    CustomLog /var/log/apache2/project_access.log combined
</VirtualHost>

注解

If your system supports the APACHE_LOG_DIR variable, you may want to use ${APACHE_LOG_DIR}/ instead of /var/log/apache2/.

注解

For performance reasons, you will probably want to set AllowOverride None and implement the rewrite rules in the web/.htaccess into the VirtualHost config.

If you are using php-cgi, Apache does not pass HTTP basic username and password to PHP by default. To work around this limitation, you should use the following configuration snippet:

RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

警告

In Apache 2.4, Order allow,deny has been replaced by Require all granted, and hence you need to modify your Directory permission settings as follows:

<Directory /var/www/project/web>
    # enable the .htaccess rewrites
    AllowOverride All
    Require all granted
</Directory>
Apache2 with PHP-FPM

To make use of PHP5-FPM with Apache, you first have to ensure that you have the FastCGI process manager php-fpm binary and Apache’s FastCGI module installed (for example, on a Debian based system you have to install the libapache2-mod-fastcgi and php5-fpm packages).

PHP-FPM uses so-called pools to handle incoming FastCGI requests. You can configure an arbitrary number of pools in the FPM configuration. In a pool you configure either a TCP socket (IP and port) or a unix domain socket to listen on. Each pool can also be run under a different UID and GID:

; a pool called www
[www]
user = www-data
group = www-data

; use a unix domain socket
listen = /var/run/php5-fpm.sock

; or listen on a TCP socket
listen = 127.0.0.1:9000
Using mod_proxy_fcgi with Apache 2.4

If you are running Apache 2.4, you can easily use mod_proxy_fcgi to pass incoming requests to PHP-FPM. Configure PHP-FPM to listen on a TCP socket (mod_proxy currently does not support unix sockets), enable mod_proxy and mod_proxy_fcgi in your Apache configuration and use the SetHandler directive to pass requests for PHP files to PHP FPM:

<VirtualHost *:80>
    ServerName domain.tld
    ServerAlias www.domain.tld

    # Uncomment the following line to force Apache to pass the Authorization
    # header to PHP: required for "basic_auth" under PHP-FPM and FastCGI
    #
    # SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1

    # For Apache 2.4.9 or higher
    # Using SetHandler avoids issues with using ProxyPassMatch in combination
    # with mod_rewrite or mod_autoindex
    <FilesMatch \.php$>
        SetHandler proxy:fcgi://127.0.0.1:9000
    </FilesMatch>
    # If you use Apache version below 2.4.9 you must consider update or use this instead
    # ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/web/$1
    # If you run your Symfony application on a subpath of your document root, the
    # regular expression must be changed accordingly:
    # ProxyPassMatch ^/path-to-app/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/web/$1

    DocumentRoot /var/www/project/web
    <Directory /var/www/project/web>
        # enable the .htaccess rewrites
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog /var/log/apache2/project_error.log
    CustomLog /var/log/apache2/project_access.log combined
</VirtualHost>
PHP-FPM with Apache 2.2

On Apache 2.2 or lower, you cannot use mod_proxy_fcgi. You have to use the FastCgiExternalServer directive instead. Therefore, your Apache configuration should look something like this:

<VirtualHost *:80>
    ServerName domain.tld
    ServerAlias www.domain.tld

    AddHandler php5-fcgi .php
    Action php5-fcgi /php5-fcgi
    Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi
    FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -pass-header Authorization

    DocumentRoot /var/www/project/web
    <Directory /var/www/project/web>
        # enable the .htaccess rewrites
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>

    ErrorLog /var/log/apache2/project_error.log
    CustomLog /var/log/apache2/project_access.log combined
</VirtualHost>

If you prefer to use a unix socket, you have to use the -socket option instead:

FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -socket /var/run/php5-fpm.sock -pass-header Authorization
Nginx

For advanced Nginx configuration options, see the official Nginx documentation. The minimum basics to get your application running under Nginx are:

server {
    server_name domain.tld www.domain.tld;
    root /var/www/project/web;

    location / {
        # try to serve file directly, fallback to app.php
        try_files $uri /app.php$is_args$args;
    }
    # DEV
    # This rule should only be placed on your development environment
    # In production, don't include this and don't deploy app_dev.php or config.php
    location ~ ^/(app_dev|config)\.php(/|$) {
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS off;
    }
    # PROD
    location ~ ^/app\.php(/|$) {
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS off;
        # Prevents URIs that include the front controller. This will 404:
        # http://domain.tld/app.php/some-path
        # Remove the internal directive to allow URIs like this
        internal;
    }

    error_log /var/log/nginx/project_error.log;
    access_log /var/log/nginx/project_access.log;
}

注解

Depending on your PHP-FPM config, the fastcgi_pass can also be fastcgi_pass 127.0.0.1:9000.

小技巧

This executes only app.php, app_dev.php and config.php in the web directory. All other files will be served as text. You must also make sure that if you do deploy app_dev.php or config.php that these files are secured and not available to any outside user (the IP checking code at the top of each file does this by default).

If you have other PHP files in your web directory that need to be executed, be sure to include them in the location block above.

How to Organize Configuration Files

The default Symfony Standard Edition defines three execution environments called dev, prod and test. An environment simply represents a way to execute the same codebase with different configurations.

In order to select the configuration file to load for each environment, Symfony executes the registerContainerConfiguration() method of the AppKernel class:

// app/AppKernel.php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    // ...

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
    }
}

This method loads the app/config/config_dev.yml file for the dev environment and so on. In turn, this file loads the common configuration file located at app/config/config.yml. Therefore, the configuration files of the default Symfony Standard Edition follow this structure:

<your-project>/
├─ app/
│  └─ config/
│     ├─ config.yml
│     ├─ config_dev.yml
│     ├─ config_prod.yml
│     ├─ config_test.yml
│     ├─ parameters.yml
│     ├─ parameters.yml.dist
│     ├─ routing.yml
│     ├─ routing_dev.yml
│     └─ security.yml
├─ src/
├─ vendor/
└─ web/

This default structure was chosen for its simplicity — one file per environment. But as any other Symfony feature, you can customize it to better suit your needs. The following sections explain different ways to organize your configuration files. In order to simplify the examples, only the dev and prod environments are taken into account.

Different Directories per Environment

Instead of suffixing the files with _dev and _prod, this technique groups all the related configuration files under a directory with the same name as the environment:

<your-project>/
├─ app/
│  └─ config/
│     ├─ common/
│     │  ├─ config.yml
│     │  ├─ parameters.yml
│     │  ├─ routing.yml
│     │  └─ security.yml
│     ├─ dev/
│     │  ├─ config.yml
│     │  ├─ parameters.yml
│     │  ├─ routing.yml
│     │  └─ security.yml
│     └─ prod/
│        ├─ config.yml
│        ├─ parameters.yml
│        ├─ routing.yml
│        └─ security.yml
├─ src/
├─ vendor/
└─ web/

To make this work, change the code of the registerContainerConfiguration() method:

// app/AppKernel.php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    // ...

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load(__DIR__.'/config/'.$this->getEnvironment().'/config.yml');
    }
}

Then, make sure that each config.yml file loads the rest of the configuration files, including the common files. For instance, this would be the imports needed for the app/config/dev/config.yml file:

  • YAML
    # app/config/dev/config.yml
    imports:
        - { resource: '../common/config.yml' }
        - { resource: 'parameters.yml' }
        - { resource: 'security.yml' }
    
    # ...
    
  • XML
    <!-- app/config/dev/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <imports>
            <import resource="../common/config.xml" />
            <import resource="parameters.xml" />
            <import resource="security.xml" />
        </imports>
    
        <!-- ... -->
    </container>
    
  • PHP
    // app/config/dev/config.php
    $loader->import('../common/config.php');
    $loader->import('parameters.php');
    $loader->import('security.php');
    
    // ...
    

注解

Due to the way in which parameters are resolved, you cannot use them to build paths in imports dynamically. This means that something like the following doesn’t work:

  • YAML
    # app/config/config.yml
    imports:
        - { resource: "%kernel.root_dir%/parameters.yml" }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <imports>
            <import resource="%kernel.root_dir%/parameters.yml" />
        </imports>
    </container>
    
  • PHP
    // app/config/config.php
    $loader->import('%kernel.root_dir%/parameters.yml');
    
Semantic Configuration Files

A different organization strategy may be needed for complex applications with large configuration files. For instance, you could create one file per bundle and several files to define all application services:

<your-project>/
├─ app/
│  └─ config/
│     ├─ bundles/
│     │  ├─ bundle1.yml
│     │  ├─ bundle2.yml
│     │  ├─ ...
│     │  └─ bundleN.yml
│     ├─ environments/
│     │  ├─ common.yml
│     │  ├─ dev.yml
│     │  └─ prod.yml
│     ├─ routing/
│     │  ├─ common.yml
│     │  ├─ dev.yml
│     │  └─ prod.yml
│     └─ services/
│        ├─ frontend.yml
│        ├─ backend.yml
│        ├─ ...
│        └─ security.yml
├─ src/
├─ vendor/
└─ web/

Again, change the code of the registerContainerConfiguration() method to make Symfony aware of the new file organization:

// app/AppKernel.php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    // ...

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load(__DIR__.'/config/environments/'.$this->getEnvironment().'.yml');
    }
}

Following the same technique explained in the previous section, make sure to import the appropriate configuration files from each main file (common.yml, dev.yml and prod.yml).

Advanced Techniques

Symfony loads configuration files using the Config component, which provides some advanced features.

Mix and Match Configuration Formats

Configuration files can import files defined with any other built-in configuration format (.yml, .xml, .php, .ini):

  • YAML
    # app/config/config.yml
    imports:
        - { resource: 'parameters.yml' }
        - { resource: 'services.xml' }
        - { resource: 'security.yml' }
        - { resource: 'legacy.php' }
    
    # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <imports>
            <import resource="parameters.yml" />
            <import resource="services.xml" />
            <import resource="security.yml" />
            <import resource="legacy.php" />
        </imports>
    
        <!-- ... -->
    </container>
    
  • PHP
    // app/config/config.php
    $loader->import('parameters.yml');
    $loader->import('services.xml');
    $loader->import('security.yml');
    $loader->import('legacy.php');
    
    // ...
    

警告

The IniFileLoader parses the file contents using the parse_ini_file function. Therefore, you can only set parameters to string values. Use one of the other loaders if you want to use other data types (e.g. boolean, integer, etc.).

If you use any other configuration format, you have to define your own loader class extending it from FileLoader. When the configuration values are dynamic, you can use the PHP configuration file to execute your own logic. In addition, you can define your own services to load configurations from databases or web services.

Global Configuration Files

Some system administrators may prefer to store sensitive parameters in files outside the project directory. Imagine that the database credentials for your website are stored in the /etc/sites/mysite.com/parameters.yml file. Loading this file is as simple as indicating the full file path when importing it from any other configuration file:

  • YAML
    # app/config/config.yml
    imports:
        - { resource: 'parameters.yml' }
        - { resource: '/etc/sites/mysite.com/parameters.yml' }
    
    # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <imports>
            <import resource="parameters.yml" />
            <import resource="/etc/sites/mysite.com/parameters.yml" />
        </imports>
    
        <!-- ... -->
    </container>
    
  • PHP
    // app/config/config.php
    $loader->import('parameters.yml');
    $loader->import('/etc/sites/mysite.com/parameters.yml');
    
    // ...
    

Most of the time, local developers won’t have the same files that exist on the production servers. For that reason, the Config component provides the ignore_errors option to silently discard errors when the loaded file doesn’t exist:

  • YAML
    # app/config/config.yml
    imports:
        - { resource: 'parameters.yml' }
        - { resource: '/etc/sites/mysite.com/parameters.yml', ignore_errors: true }
    
    # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <imports>
            <import resource="parameters.yml" />
            <import resource="/etc/sites/mysite.com/parameters.yml" ignore-errors="true" />
        </imports>
    
        <!-- ... -->
    </container>
    
  • PHP
    // app/config/config.php
    $loader->import('parameters.yml');
    $loader->import('/etc/sites/mysite.com/parameters.yml', null, true);
    
    // ...
    

As you’ve seen, there are lots of ways to organize your configuration files. You can choose one of these or even create your own custom way of organizing the files. Don’t feel limited by the Standard Edition that comes with Symfony. For even more customization, see “How to Override Symfony’s default Directory Structure”.

Console

How to Create a Console Command

The Console page of the Components section (The Console Component) covers how to create a console command. This cookbook article covers the differences when creating console commands within the Symfony framework.

Automatically Registering Commands

To make the console commands available automatically with Symfony, create a Command directory inside your bundle and create a PHP file suffixed with Command.php for each command that you want to provide. For example, if you want to extend the AppBundle to greet you from the command line, create GreetCommand.php and add the following to it:

// src/AppBundle/Command/GreetCommand.php
namespace AppBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class GreetCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('demo:greet')
            ->setDescription('Greet someone')
            ->addArgument(
                'name',
                InputArgument::OPTIONAL,
                'Who do you want to greet?'
            )
            ->addOption(
                'yell',
                null,
                InputOption::VALUE_NONE,
                'If set, the task will yell in uppercase letters'
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        if ($name) {
            $text = 'Hello '.$name;
        } else {
            $text = 'Hello';
        }

        if ($input->getOption('yell')) {
            $text = strtoupper($text);
        }

        $output->writeln($text);
    }
}

This command will now automatically be available to run:

$ php app/console demo:greet Fabien
Getting Services from the Service Container

By using ContainerAwareCommand as the base class for the command (instead of the more basic Command), you have access to the service container. In other words, you have access to any configured service:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $name = $input->getArgument('name');
    $logger = $this->getContainer()->get('logger');

    $logger->info('Executing command for '.$name);
    // ...
}

However, due to the container scopes this code doesn’t work for some services. For instance, if you try to get the request service or any other service related to it, you’ll get the following error:

You cannot create a service ("request") of an inactive scope ("request").

Consider the following example that uses the translator service to translate some contents using a console command:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $name = $input->getArgument('name');
    $translator = $this->getContainer()->get('translator');
    if ($name) {
        $output->writeln(
            $translator->trans('Hello %name%!', array('%name%' => $name))
        );
    } else {
        $output->writeln($translator->trans('Hello!'));
    }
}

If you dig into the Translator component classes, you’ll see that the request service is required to get the locale into which the contents are translated:

// vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php
public function getLocale()
{
    if (null === $this->locale && $this->container->isScopeActive('request')
        && $this->container->has('request')) {
        $this->locale = $this->container->get('request')->getLocale();
    }

    return $this->locale;
}

Therefore, when using the translator service inside a command, you’ll get the previous “You cannot create a service of an inactive scope” error message. The solution in this case is as easy as setting the locale value explicitly before translating contents:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $name = $input->getArgument('name');
    $locale = $input->getArgument('locale');

    $translator = $this->getContainer()->get('translator');
    $translator->setLocale($locale);

    if ($name) {
        $output->writeln(
            $translator->trans('Hello %name%!', array('%name%' => $name))
        );
    } else {
        $output->writeln($translator->trans('Hello!'));
    }
}

However for other services the solution might be more complex. For more details, see How to Work with Scopes.

Testing Commands

When testing commands used as part of the full framework Symfony\Bundle\FrameworkBundle\Console\Application should be used instead of Symfony\Component\Console\Application:

use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use AppBundle\Command\GreetCommand;

class ListCommandTest extends \PHPUnit_Framework_TestCase
{
    public function testExecute()
    {
        // mock the Kernel or create one depending on your needs
        $application = new Application($kernel);
        $application->add(new GreetCommand());

        $command = $application->find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester->execute(
            array(
                'command' => $command->getName(),
                'name'    => 'Fabien',
                '--yell'  => true,
            )
        );

        $this->assertRegExp('/.../', $commandTester->getDisplay());

        // ...
    }
}

注解

In the specific case above, the name parameter and the --yell option are not mandatory for the command to work, but are shown so you can see how to customize them when calling the command.

To be able to use the fully set up service container for your console tests you can extend your test from WebTestCase:

use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use AppBundle\Command\GreetCommand;

class ListCommandTest extends WebTestCase
{
    public function testExecute()
    {
        $kernel = $this->createKernel();
        $kernel->boot();

        $application = new Application($kernel);
        $application->add(new GreetCommand());

        $command = $application->find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester->execute(
            array(
                'command' => $command->getName(),
                'name'    => 'Fabien',
                '--yell'  => true,
            )
        );

        $this->assertRegExp('/.../', $commandTester->getDisplay());

        // ...
    }
}
How to Use the Console

The Using Console Commands, Shortcuts and Built-in Commands page of the components documentation looks at the global console options. When you use the console as part of the full stack framework, some additional global options are available as well.

By default, console commands run in the dev environment and you may want to change this for some commands. For example, you may want to run some commands in the prod environment for performance reasons. Also, the result of some commands will be different depending on the environment. For example, the cache:clear command will clear and warm the cache for the specified environment only. To clear and warm the prod cache you need to run:

$ php app/console cache:clear --env=prod

or the equivalent:

$ php app/console cache:clear -e prod

In addition to changing the environment, you can also choose to disable debug mode. This can be useful where you want to run commands in the dev environment but avoid the performance hit of collecting debug data:

$ php app/console list --no-debug

There is an interactive shell which allows you to enter commands without having to specify php app/console each time, which is useful if you need to run several commands. To enter the shell run:

$ php app/console --shell
$ php app/console -s

You can now just run commands with the command name:

Symfony > list

When using the shell you can choose to run each command in a separate process:

$ php app/console --shell --process-isolation
$ php app/console -s --process-isolation

When you do this, the output will not be colorized and interactivity is not supported so you will need to pass all command params explicitly.

注解

Unless you are using isolated processes, clearing the cache in the shell will not have an effect on subsequent commands you run. This is because the original cached files are still being used.

How to Generate URLs and Send Emails from the Console

Unfortunately, the command line context does not know about your VirtualHost or domain name. This means that if you generate absolute URLs within a Console Command you’ll probably end up with something like http://localhost/foo/bar which is not very useful.

To fix this, you need to configure the “request context”, which is a fancy way of saying that you need to configure your environment so that it knows what URL it should use when generating URLs.

There are two ways of configuring the request context: at the application level and per Command.

Configuring the Request Context globally

2.2 新版功能: The base_url parameter was introduced in Symfony 2.2.

To configure the Request Context - which is used by the URL Generator - you can redefine the parameters it uses as default values to change the default host (localhost) and scheme (http). Starting with Symfony 2.2 you can also configure the base path if Symfony is not running in the root directory.

Note that this does not impact URLs generated via normal web requests, since those will override the defaults.

  • YAML
    # app/config/parameters.yml
    parameters:
        router.request_context.host: example.org
        router.request_context.scheme: https
        router.request_context.base_url: my/path
    
  • XML
    <!-- app/config/parameters.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
        <parameters>
            <parameter key="router.request_context.host">example.org</parameter>
            <parameter key="router.request_context.scheme">https</parameter>
            <parameter key="router.request_context.base_url">my/path</parameter>
        </parameters>
    </container>
    
  • PHP
    // app/config/config_test.php
    $container->setParameter('router.request_context.host', 'example.org');
    $container->setParameter('router.request_context.scheme', 'https');
    $container->setParameter('router.request_context.base_url', 'my/path');
    
Configuring the Request Context per Command

To change it only in one command you can simply fetch the Request Context from the router service and override its settings:

// src/AppBundle/Command/DemoCommand.php

// ...
class DemoCommand extends ContainerAwareCommand
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $context = $this->getContainer()->get('router')->getContext();
        $context->setHost('example.com');
        $context->setScheme('https');
        $context->setBaseUrl('my/path');

        // ... your code here
    }
}
Using Memory Spooling

2.3 新版功能: When using Symfony 2.3+ and SwiftmailerBundle 2.3.5+, the memory spool is now handled automatically in the CLI and the code below is not necessary anymore.

Sending emails in a console command works the same way as described in the How to Send an Email cookbook except if memory spooling is used.

When using memory spooling (see the How to Spool Emails cookbook for more information), you must be aware that because of how Symfony handles console commands, emails are not sent automatically. You must take care of flushing the queue yourself. Use the following code to send emails inside your console command:

$message = new \Swift_Message();

// ... prepare the message

$container = $this->getContainer();
$mailer = $container->get('mailer');

$mailer->send($message);

// now manually flush the queue
$spool = $mailer->getTransport()->getSpool();
$transport = $container->get('swiftmailer.transport.real');

$spool->flushQueue($transport);

Another option is to create an environment which is only used by console commands and uses a different spooling method.

注解

Taking care of the spooling is only needed when memory spooling is used. If you are using file spooling (or no spooling at all), there is no need to flush the queue manually within the command.

How to Enable Logging in Console Commands

The Console component doesn’t provide any logging capabilities out of the box. Normally, you run console commands manually and observe the output, which is why logging is not provided. However, there are cases when you might need logging. For example, if you are running console commands unattended, such as from cron jobs or deployment scripts, it may be easier to use Symfony’s logging capabilities instead of configuring other tools to gather console output and process it. This can be especially handful if you already have some existing setup for aggregating and analyzing Symfony logs.

There are basically two logging cases you would need:
  • Manually logging some information from your command;
  • Logging uncaught Exceptions.
Manually Logging from a Console Command

This one is really simple. When you create a console command within the full framework as described in “How to Create a Console Command”, your command extends ContainerAwareCommand. This means that you can simply access the standard logger service through the container and use it to do the logging:

// src/AppBundle/Command/GreetCommand.php
namespace AppBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Psr\Log\LoggerInterface;

class GreetCommand extends ContainerAwareCommand
{
    // ...

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        /** @var $logger LoggerInterface */
        $logger = $this->getContainer()->get('logger');

        $name = $input->getArgument('name');
        if ($name) {
            $text = 'Hello '.$name;
        } else {
            $text = 'Hello';
        }

        if ($input->getOption('yell')) {
            $text = strtoupper($text);
            $logger->warning('Yelled: '.$text);
        } else {
            $logger->info('Greeted: '.$text);
        }

        $output->writeln($text);
    }
}

Depending on the environment in which you run your command (and your logging setup), you should see the logged entries in app/logs/dev.log or app/logs/prod.log.

Enabling automatic Exceptions Logging

To get your console application to automatically log uncaught exceptions for all of your commands, you can use console events.

2.3 新版功能: Console events were introduced in Symfony 2.3.

First configure a listener for console exception events in the service container:

  • YAML
    # app/config/services.yml
    services:
        kernel.listener.command_dispatch:
            class: AppBundle\EventListener\ConsoleExceptionListener
            arguments:
                logger: "@logger"
            tags:
                - { name: kernel.event_listener, event: console.exception }
    
  • XML
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="kernel.listener.command_dispatch" class="AppBundle\EventListener\ConsoleExceptionListener">
                <argument type="service" id="logger"/>
                <tag name="kernel.event_listener" event="console.exception" />
            </service>
        </services>
    </container>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $definitionConsoleExceptionListener = new Definition(
        'AppBundle\EventListener\ConsoleExceptionListener',
        array(new Reference('logger'))
    );
    $definitionConsoleExceptionListener->addTag(
        'kernel.event_listener',
        array('event' => 'console.exception')
    );
    $container->setDefinition(
        'kernel.listener.command_dispatch',
        $definitionConsoleExceptionListener
    );
    

Then implement the actual listener:

// src/AppBundle/EventListener/ConsoleExceptionListener.php
namespace AppBundle\EventListener;

use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Psr\Log\LoggerInterface;

class ConsoleExceptionListener
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function onConsoleException(ConsoleExceptionEvent $event)
    {
        $command = $event->getCommand();
        $exception = $event->getException();

        $message = sprintf(
            '%s: %s (uncaught exception) at %s line %s while running console command `%s`',
            get_class($exception),
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine(),
            $command->getName()
        );

        $this->logger->error($message, array('exception' => $exception));
    }
}

In the code above, when any command throws an exception, the listener will receive an event. You can simply log it by passing the logger service via the service configuration. Your method receives a ConsoleExceptionEvent object, which has methods to get information about the event and the exception.

Logging non-0 Exit Statuses

The logging capabilities of the console can be further extended by logging non-0 exit statuses. This way you will know if a command had any errors, even if no exceptions were thrown.

First configure a listener for console terminate events in the service container:

  • YAML
    # app/config/services.yml
    services:
        kernel.listener.command_dispatch:
            class: AppBundle\EventListener\ErrorLoggerListener
            arguments:
                logger: "@logger"
            tags:
                - { name: kernel.event_listener, event: console.terminate }
    
  • XML
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="kernel.listener.command_dispatch" class="AppBundle\EventListener\ErrorLoggerListener">
                <argument type="service" id="logger"/>
                <tag name="kernel.event_listener" event="console.terminate" />
            </service>
        </services>
    </container>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $definitionErrorLoggerListener = new Definition(
        'AppBundle\EventListener\ErrorLoggerListener',
        array(new Reference('logger'))
    );
    $definitionErrorLoggerListener->addTag(
        'kernel.event_listener',
        array('event' => 'console.terminate')
    );
    $container->setDefinition(
        'kernel.listener.command_dispatch',
        $definitionErrorLoggerListener
    );
    

Then implement the actual listener:

// src/AppBundle/EventListener/ErrorLoggerListener.php
namespace AppBundle\EventListener;

use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Psr\Log\LoggerInterface;

class ErrorLoggerListener
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function onConsoleTerminate(ConsoleTerminateEvent $event)
    {
        $statusCode = $event->getExitCode();
        $command = $event->getCommand();

        if ($statusCode === 0) {
            return;
        }

        if ($statusCode > 255) {
            $statusCode = 255;
            $event->setExitCode($statusCode);
        }

        $this->logger->warning(sprintf(
            'Command `%s` exited with status code %d',
            $command->getName(),
            $statusCode
        ));
    }
}

Controller

How to Customize Error Pages

When an exception is thrown, the core HttpKernel class catches it and dispatches a kernel.exception event. This gives you the power to convert the exception into a Response in a few different ways.

The core TwigBundle sets up a listener for this event which will run a configurable (but otherwise arbitrary) controller to generate the response. The default controller used has a sensible way of picking one out of the available set of error templates.

Thus, error pages can be customized in different ways, depending on how much control you need:

  1. Use the default ExceptionController and create a few templates that allow you to customize how your different error pages look (easy);
  2. Replace the default exception controller with your own (intermediate).
  3. Use the kernel.exception event to come up with your own handling (advanced).
Using the Default ExceptionController

By default, the showAction() method of the ExceptionController will be called when an exception occurs.

This controller will either display an exception or error page, depending on the setting of the kernel.debug flag. While exception pages give you a lot of helpful information during development, error pages are meant to be shown to the user in production.

How the Template for the Error and Exception Pages Is Selected

The TwigBundle contains some default templates for error and exception pages in its Resources/views/Exception directory.

小技巧

In a standard Symfony installation, the TwigBundle can be found at vendor/symfony/symfony/src/Symfony/Bundle/TwigBundle. In addition to the standard HTML error page, it also provides a default error page for many of the most common response formats, including JSON (error.json.twig), XML (error.xml.twig) and even JavaScript (error.js.twig), to name a few.

Here is how the ExceptionController will pick one of the available templates based on the HTTP status code and request format:

  • For error pages, it first looks for a template for the given format and status code (like error404.json.twig);
  • If that does not exist or apply, it looks for a general template for the given format (like error.json.twig or exception.json.twig);
  • Finally, it ignores the format and falls back to the HTML template (like error.html.twig or exception.html.twig).

小技巧

If the exception being handled implements the HttpExceptionInterface, the getStatusCode() method will be called to obtain the HTTP status code to use. Otherwise, the status code will be “500”.

Overriding or Adding Templates

To override these templates, simply rely on the standard method for overriding templates that live inside a bundle. For more information, see Overriding Bundle Templates.

For example, to override the default error template, create a new template located at app/Resources/TwigBundle/views/Exception/error.html.twig:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>An Error Occurred: {{ status_text }}</title>
</head>
<body>
    <h1>Oops! An Error Occurred</h1>
    <h2>The server returned a "{{ status_code }} {{ status_text }}".</h2>
</body>
</html>

警告

You must not use is_granted in your error pages (or layout used by your error pages), because the router runs before the firewall. If the router throws an exception (for instance, when the route does not match), then using is_granted will throw a further exception. You can use is_granted safely by saying {% if app.user and is_granted('...') %}.

小技巧

If you’re not familiar with Twig, don’t worry. Twig is a simple, powerful and optional templating engine that integrates with Symfony. For more information about Twig see Creating and Using Templates.

This works not only to replace the default templates, but also to add new ones.

For instance, create an app/Resources/TwigBundle/views/Exception/error404.html.twig template to display a special page for 404 (page not found) errors. Refer to the previous section for the order in which the ExceptionController tries different template names.

小技巧

Often, the easiest way to customize an error page is to copy it from the TwigBundle into app/Resources/TwigBundle/views/Exception and then modify it.

注解

The debug-friendly exception pages shown to the developer can even be customized in the same way by creating templates such as exception.html.twig for the standard HTML exception page or exception.json.twig for the JSON exception page.

Replacing the Default ExceptionController

If you need a little more flexibility beyond just overriding the template, then you can change the controller that renders the error page. For example, you might need to pass some additional variables into your template.

警告

Make sure you don’t lose the exception pages that render the helpful error messages during development.

To do this, simply create a new controller and set the twig.exception_controller option to point to it.

  • YAML
    # app/config/config.yml
    twig:
        exception_controller:  AppBundle:Exception:showException
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:twig="http://symfony.com/schema/dic/twig"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/twig
            http://symfony.com/schema/dic/twig/twig-1.0.xsd">
    
        <twig:config>
            <twig:exception-controller>AppBundle:Exception:showException</twig:exception-controller>
        </twig:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'exception_controller' => 'AppBundle:Exception:showException',
        // ...
    ));
    

小技巧

You can also set up your controller as a service.

The default value of twig.controller.exception:showAction refers to the showAction method of the ExceptionController described previously, which is registered in the DIC as the twig.controller.exception service.

Your controller will be passed two parameters: exception, which is a FlattenException instance created from the exception being handled, and logger, an instance of DebugLoggerInterface (which may be null).

小技巧

The Request that will be dispatched to your controller is created in the ExceptionListener. This event listener is set up by the TwigBundle.

You can, of course, also extend the previously described ExceptionController. In that case, you might want to override one or both of the showAction and findTemplate methods. The latter one locates the template to be used.

警告

As of writing, the ExceptionController is not part of the Symfony API, so be aware that it might change in following releases.

Working with the kernel.exception Event

As mentioned in the beginning, the kernel.exception event is dispatched whenever the Symfony Kernel needs to handle an exception. For more information on that, see kernel.exception Event.

Working with this event is actually much more powerful than what has been explained before but also requires a thorough understanding of Symfony internals.

To give one example, assume your application throws specialized exceptions with a particular meaning to your domain.

In that case, all the default ExceptionListener and ExceptionController could do for you was trying to figure out the right HTTP status code and display your nice-looking error page.

Writing your own event listener for the kernel.exception event allows you to have a closer look at the exception and take different actions depending on it. Those actions might include logging the exception, redirecting the user to another page or rendering specialized error pages.

注解

If your listener calls setResponse() on the GetResponseForExceptionEvent, event propagation will be stopped and the response will be sent to the client.

This approach allows you to create centralized and layered error handling: Instead of catching (and handling) the same exceptions in various controllers again and again, you can have just one (or several) listeners deal with them.

小技巧

To see an example, have a look at the ExceptionListener in the Security Component.

It handles various security-related exceptions that are thrown in your application (like AccessDeniedException) and takes measures like redirecting the user to the login page, logging them out and other things.

Good luck!

How to Define Controllers as Services

In the book, you’ve learned how easily a controller can be used when it extends the base Controller class. While this works fine, controllers can also be specified as services.

注解

Specifying a controller as a service takes a little bit more work. The primary advantage is that the entire controller or any services passed to the controller can be modified via the service container configuration. This is especially useful when developing an open-source bundle or any bundle that will be used in many different projects.

A second advantage is that your controllers are more “sandboxed”. By looking at the constructor arguments, it’s easy to see what types of things this controller may or may not do. And because each dependency needs to be injected manually, it’s more obvious (i.e. if you have many constructor arguments) when your controller has become too big, and may need to be split into multiple controllers.

So, even if you don’t specify your controllers as services, you’ll likely see this done in some open-source Symfony bundles. It’s also important to understand the pros and cons of both approaches.

Defining the Controller as a Service

A controller can be defined as a service in the same way as any other class. For example, if you have the following simple controller:

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

Then you can define it as a service as follows:

  • YAML
    # app/config/services.yml
    services:
        app.hello_controller:
            class: AppBundle\Controller\HelloController
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="app.hello_controller" class="AppBundle\Controller\HelloController" />
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition('app.hello_controller', new Definition(
        'AppBundle\Controller\HelloController'
    ));
    
Referring to the Service

To refer to a controller that’s defined as a service, use the single colon (:) notation. For example, to forward to the indexAction() method of the service defined above with the id app.hello_controller:

$this->forward('app.hello_controller:indexAction', array('name' => $name));

注解

You cannot drop the Action part of the method name when using this syntax.

You can also route to the service by using the same notation when defining the route _controller value:

  • YAML
    # app/config/routing.yml
    hello:
        path:     /hello
        defaults: { _controller: app.hello_controller:indexAction }
    
  • XML
    <!-- app/config/routing.xml -->
    <route id="hello" path="/hello">
        <default key="_controller">app.hello_controller:indexAction</default>
    </route>
    
  • PHP
    // app/config/routing.php
    $collection->add('hello', new Route('/hello', array(
        '_controller' => 'app.hello_controller:indexAction',
    )));
    

小技巧

You can also use annotations to configure routing using a controller defined as a service. See the FrameworkExtraBundle documentation for details.

Alternatives to base Controller Methods

When using a controller defined as a service, it will most likely not extend the base Controller class. Instead of relying on its shortcut methods, you’ll interact directly with the services that you need. Fortunately, this is usually pretty easy and the base Controller class source code is a great source on how to perform many common tasks.

For example, if you want to render a template instead of creating the Response object directly, then your code would look like this if you were extending Symfony’s base controller:

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
    public function indexAction($name)
    {
        return $this->render(
            'AppBundle:Hello:index.html.twig',
            array('name' => $name)
        );
    }
}

If you look at the source code for the render function in Symfony’s base Controller class, you’ll see that this method actually uses the templating service:

public function render($view, array $parameters = array(), Response $response = null)
{
    return $this->container->get('templating')->renderResponse($view, $parameters, $response);
}

In a controller that’s defined as a service, you can instead inject the templating service and use it directly:

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    private $templating;

    public function __construct(EngineInterface $templating)
    {
        $this->templating = $templating;
    }

    public function indexAction($name)
    {
        return $this->templating->renderResponse(
            'AppBundle:Hello:index.html.twig',
            array('name' => $name)
        );
    }
}

The service definition also needs modifying to specify the constructor argument:

  • YAML
    # app/config/services.yml
    services:
        app.hello_controller:
            class:     AppBundle\Controller\HelloController
            arguments: ["@templating"]
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="app.hello_controller" class="AppBundle\Controller\HelloController">
            <argument type="service" id="templating"/>
        </service>
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('app.hello_controller', new Definition(
        'AppBundle\Controller\HelloController',
        array(new Reference('templating'))
    ));
    

Rather than fetching the templating service from the container, you can inject only the exact service(s) that you need directly into the controller.

注解

This does not mean that you cannot extend these controllers from your own base controller. The move away from the standard base controller is because its helper methods rely on having the container available which is not the case for controllers that are defined as services. It may be a good idea to extract common code into a service that’s injected rather than place that code into a base controller that you extend. Both approaches are valid, exactly how you want to organize your reusable code is up to you.

How to Optimize your Development Environment for Debugging

When you work on a Symfony project on your local machine, you should use the dev environment (app_dev.php front controller). This environment configuration is optimized for two main purposes:

  • Give the developer accurate feedback whenever something goes wrong (web debug toolbar, nice exception pages, profiler, ...);
  • Be as similar as possible as the production environment to avoid problems when deploying the project.
Disabling the Bootstrap File and Class Caching

And to make the production environment as fast as possible, Symfony creates big PHP files in your cache containing the aggregation of PHP classes your project needs for every request. However, this behavior can confuse your IDE or your debugger. This recipe shows you how you can tweak this caching mechanism to make it friendlier when you need to debug code that involves Symfony classes.

The app_dev.php front controller reads as follows by default:

// ...

$loader = require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';

$kernel = new AppKernel('dev', true);
$kernel->loadClassCache();
$request = Request::createFromGlobals();

To make your debugger happier, disable all PHP class caches by removing the call to loadClassCache() and by replacing the require statements like below:

// ...

// $loader = require_once __DIR__.'/../app/bootstrap.php.cache';
$loader = require_once __DIR__.'/../app/autoload.php';
require_once __DIR__.'/../app/AppKernel.php';

$kernel = new AppKernel('dev', true);
// $kernel->loadClassCache();
$request = Request::createFromGlobals();

小技巧

If you disable the PHP caches, don’t forget to revert after your debugging session.

Some IDEs do not like the fact that some classes are stored in different locations. To avoid problems, you can either tell your IDE to ignore the PHP cache files, or you can change the extension used by Symfony for these files:

$kernel->loadClassCache('classes', '.php.cache');

Deployment

How to Deploy a Symfony Application

注解

Deploying can be a complex and varied task depending on your setup and needs. This entry doesn’t try to explain everything, but rather offers the most common requirements and ideas for deployment.

Symfony Deployment Basics

The typical steps taken while deploying a Symfony application include:

  1. Upload your modified code to the live server;
  2. Update your vendor dependencies (typically done via Composer, and may be done before uploading);
  3. Running database migrations or similar tasks to update any changed data structures;
  4. Clearing (and perhaps more importantly, warming up) your cache.

A deployment may also include other things, such as:

  • Tagging a particular version of your code as a release in your source control repository;
  • Creating a temporary staging area to build your updated setup “offline”;
  • Running any tests available to ensure code and/or server stability;
  • Removal of any unnecessary files from web to keep your production environment clean;
  • Clearing of external cache systems (like Memcached or Redis).
How to Deploy a Symfony Application

There are several ways you can deploy a Symfony application.

Start with a few basic deployment strategies and build up from there.

Basic File Transfer

The most basic way of deploying an application is copying the files manually via ftp/scp (or similar method). This has its disadvantages as you lack control over the system as the upgrade progresses. This method also requires you to take some manual steps after transferring the files (see Common Post-Deployment Tasks)

Using Source Control

If you’re using source control (e.g. Git or SVN), you can simplify by having your live installation also be a copy of your repository. When you’re ready to upgrade it is as simple as fetching the latest updates from your source control system.

This makes updating your files easier, but you still need to worry about manually taking other steps (see Common Post-Deployment Tasks).

Using Build Scripts and other Tools

There are also high-quality tools to help ease the pain of deployment. There are even a few tools which have been specifically tailored to the requirements of Symfony, and which take special care to ensure that everything before, during, and after a deployment has gone correctly.

See The Tools for a list of tools that can help with deployment.

Common Post-Deployment Tasks

After deploying your actual source code, there are a number of common things you’ll need to do:

A) Check Requirements

Check if your server meets the requirements by running:

$ php app/check.php
B) Configure your app/config/parameters.yml File

This file should be customized on each system. The method you use to deploy your source code should not deploy this file. Instead, you should set it up manually (or via some build process) on your server(s).

C) Update your Vendors

Your vendors can be updated before transferring your source code (i.e. update the vendor/ directory, then transfer that with your source code) or afterwards on the server. Either way, just update your vendors as you normally do:

$ composer install --no-dev --optimize-autoloader

小技巧

The --optimize-autoloader flag makes Composer’s autoloader more performant by building a “class map”. The --no-dev flag ensures that development packages are not installed in the production environment.

警告

If you get a “class not found” error during this step, you may need to run export SYMFONY_ENV=prod before running this command so that the post-install-cmd scripts run in the prod environment.

D) Clear your Symfony Cache

Make sure you clear (and warm-up) your Symfony cache:

$ php app/console cache:clear --env=prod --no-debug
E) Dump your Assetic Assets

If you’re using Assetic, you’ll also want to dump your assets:

$ php app/console assetic:dump --env=prod --no-debug
F) Other Things!

There may be lots of other things that you need to do, depending on your setup:

  • Running any database migrations
  • Clearing your APC cache
  • Running assets:install (already taken care of in composer install)
  • Add/edit CRON jobs
  • Pushing assets to a CDN
  • ...
Application Lifecycle: Continuous Integration, QA, etc

While this entry covers the technical details of deploying, the full lifecycle of taking code from development up to production may have a lot more steps (think deploying to staging, QA, running tests, etc).

The use of staging, testing, QA, continuous integration, database migrations and the capability to roll back in case of failure are all strongly advised. There are simple and more complex tools and one can make the deployment as easy (or sophisticated) as your environment requires.

Don’t forget that deploying your application also involves updating any dependency (typically via Composer), migrating your database, clearing your cache and other potential things like pushing assets to a CDN (see Common Post-Deployment Tasks).

The Tools

Capifony:

This tool provides a specialized set of tools on top of Capistrano, tailored specifically to symfony and Symfony projects.

sf2debpkg:

This tool helps you build a native Debian package for your Symfony project.

Magallanes:

This Capistrano-like deployment tool is built in PHP, and may be easier for PHP developers to extend for their needs.

Bundles:

There are many bundles that add deployment features directly into your Symfony console.

Basic scripting:

You can of course use shell, Ant, or any other build tool to script the deploying of your project.

Platform as a Service Providers:

PaaS is a relatively new way to deploy your application. Typically a PaaS will use a single configuration file in your project’s root directory to determine how to build an environment on the fly that supports your software. One provider with confirmed Symfony support is PagodaBox.

小技巧

Looking for more? Talk to the community on the Symfony IRC channel #symfony (on freenode) for more information.

Deploying to Microsoft Azure Website Cloud

This step by step cookbook describes how to deploy a small Symfony web application to the Microsoft Azure Website cloud platform. It will explain how to setup a new Azure website including configuring the right PHP version and global environment variables. The document also shows how to you can leverage Git and Composer to deploy your Symfony application to the cloud.

Setting up the Azure Website

To setup a new Microsoft Azure Website, first signup with Azure or sign in with your credentials. Once you’re connected to your Azure Portal interface, scroll down to the bottom and select the New panel. On this panel, click Web Site and choose Custom Create:

Create a new custom Azure Website
Step 1: Create Web Site

Here, you will be prompted to fill in some basic information.

Setup the Azure Website

For the URL, enter the URL that you would like to use for your Symfony application, then pick Create new web hosting plan in the region you want. By default, a free 20 MB SQL database is selected in the database dropdown list. In this tutorial, the Symfony app will connect to a MySQL database. Pick the Create a new MySQL database option in the dropdown list. You can keep the DefaultConnection string name. Finally, check the box Publish from source control to enable a Git repository and go to the next step.

Step 2: New MySQL Database

On this step, you will be prompted to setup your MySQL database storage with a database name and a region. The MySQL database storage is provided by Microsoft in partnership with ClearDB. Choose the same region you selected for the hosting plan configuration in the previous step.

Setup the MySQL database

Agree to the terms and conditions and click on the right arrow to continue.

Step 3: Where Is your Source Code

Now, on the third step, select a Local Git repository item and click on the right arrow to configure your Azure Website credentials.

Setup a local Git repository
Step 4: New Username and Password

Great! You’re now on the final step. Create a username and a secure password: these will become essential identifiers to connect to the FTP server and also to push your application code to the Git repository.

Configure Azure Website credentials

Congratulations! Your Azure Website is now up and running. You can check it by browsing to the Website url you configured in the first step. You should see the following display in your web browser:

Azure Website is running

The Microsoft Azure portal also provides a complete control panel for the Azure Website.

Azure Website Control Panel

Your Azure Website is ready! But to run a Symfony site, you need to configure just a few additional things.

Configuring the Azure Website for Symfony

This section of the tutorial details how to configure the correct version of PHP to run Symfony. It also shows you how to enable some mandatory PHP extensions and how to properly configure PHP for a production environment.

Configuring the latest PHP Runtime

Even though Symfony only requires PHP 5.3.3 to run, it’s always recommended to use the most recent PHP version whenever possible. PHP 5.3 is no longer supported by the PHP core team, but you can update it easily in Azure.

To update your PHP version on Azure, go to the Configure tab of the control panel and select the version you want.

Enabling the most recent PHP runtime from Azure Website Control Panel

Click the Save button in the bottom bar to save your changes and restart the web server.

注解

Choosing a more recent PHP version can greatly improve runtime performance. PHP 5.5 ships with a new built-in PHP accelerator called OPCache that replaces APC. On an Azure Website, OPCache is already enabled and there is no need to install and setup APC.

The following screenshot shows the output of a phpinfo script run from an Azure Website to verify that PHP 5.5 is running with OPCache enabled.

OPCache Configuration
Tweaking php.ini Configuration Settings

Microsoft Azure allows you to override the php.ini global configuration settings by creating a custom .user.ini file under the project root directory (site/wwwroot).

; .user.ini
expose_php = Off
memory_limit = 256M
upload_max_filesize = 10M

None of these settings needs to be overridden. The default PHP configuration is already pretty good, so this is just an example to show how you can easily tweak PHP internal settings by uploading your custom .ini file.

You can either manually create this file on your Azure Website FTP server under the site/wwwroot directory or deploy it with Git. You can get your FTP server credentials from the Azure Website Control panel under the Dashboard tab on the right sidebar. If you want to use Git, simply put your .user.ini file at the root of your local repository and push your commits to your Azure Website repository.

注解

This cookbook has a section dedicated to explaining how to configure your Azure Website Git repository and how to push the commits to be deployed. See Deploying from Git. You can also learn more about configuring PHP internal settings on the official PHP MSDN documentation page.

Enabling the PHP intl Extension

This is the tricky part of the guide! At the time of writing this cookbook, Microsoft Azure Website provided the intl extension, but it’s not enabled by default. To enable the intl extension, there is no need to upload any DLL files as the php_intl.dll file already exists on Azure. In fact, this file just needs to be moved into the custom website extension directory.

注解

The Microsoft Azure team is currently working on enabling the intl PHP extension by default. In the near future, the following steps will no longer be necessary.

To get the php_intl.dll file under your site/wwwroot directory, simply access the online Kudu tool by browsing to the following url:

https://[your-website-name].scm.azurewebsites.net

Kudu is a set of tools to manage your application. It comes with a file explorer, a command line prompt, a log stream and a configuration settings summary page. Of course, this section can only be accessed if you’re logged in to your main Azure Website account.

The Kudu Panel

From the Kudu front page, click on the Debug Console navigation item in the main menu and choose CMD. This should open the Debug Console page that shows a file explorer and a console prompt below.

In the console prompt, type the following three commands to copy the original php_intl.dll extension file into a custom website ext/ directory. This new directory must be created under the main directory site/wwwroot.

$ cd site\wwwroot
$ mkdir ext
$ copy "D:\Program Files (x86)\PHP\v5.5\ext\php_intl.dll" ext

The whole process and output should look like this:

Executing commands in the online Kudu Console prompt

To complete the activation of the php_intl.dll extension, you must tell Azure Website to load it from the newly created ext directory. This can be done by registering a global PHP_EXTENSIONS environment variable from the Configure tab of the main Azure Website Control panel.

In the app settings section, register the PHP_EXTENSIONS environment variable with the value ext\php_intl.dll as shown in the screenshot below:

Registering custom PHP extensions

Hit “save” to confirm your changes and restart the web server. The PHP Intl extension should now be available in your web server environment. The following screenshot of a phpinfo page verifies the intl extension is properly enabled:

Intl extension is enabled

Great! The PHP environment setup is now complete. Next, you’ll learn how to configure the Git repository and push code to production. You’ll also learn how to install and configure the Symfony app after it’s deployed.

Deploying from Git

First, make sure Git is correctly installed on your local machine using the following command in your terminal:

$ git --version

注解

Get your Git from the git-scm.com website and follow the instructions to install and configure it on your local machine.

In the Azure Website Control panel, browse the Deployment tab to get the Git repository URL where you should push your code:

Git deployment panel

Now, you’ll want to connect your local Symfony application with this remote Git repository on Azure Website. If your Symfony application is not yet stored with Git, you must first create a Git repository in your Symfony application directory with the git init command and commit to it with the git commit command.

Also, make sure your Symfony repository has a .gitignore file at its root directory with at least the following contents:

/app/bootstrap.php.cache
/app/cache/*
/app/config/parameters.yml
/app/logs/*
!app/cache/.gitkeep
!app/logs/.gitkeep
/app/SymfonyRequirements.php
/build/
/vendor/
/bin/
/composer.phar
/web/app_dev.php
/web/bundles/
/web/config.php

The .gitignore file asks Git not to track any of the files and directories that match these patterns. This means these files won’t be deployed to the Azure Website.

Now, from the command line on your local machine, type the following at the root of your Symfony project:

$ git remote add azure https://<username>@<your-website-name>.scm.azurewebsites.net:443/<your-website-name>.git
$ git push azure master

Don’t forget to replace the values enclosed by < and > with your custom settings displayed in the Deployment tab of your Azure Website panel. The git remote command connects the Azure Website remote Git repository and assigns an alias to it with the name azure. The second git push command pushes all your commits to the remote master branch of your remote azure Git repository.

The deployment with Git should produce an output similar to the screenshot below:

Deploying files to the Git Azure Website repository

The code of the Symfony application has now been deployed to the Azure Website which you can browse from the file explorer of the Kudu application. You should see the app/, src/ and web/ directories under your site/wwwroot directory on the Azure Website filesystem.

Configure the Symfony Application

PHP has been configured and your code has been pushed with Git. The last step is to configure the application and install the third party dependencies it requires that aren’t tracked by Git. Switch back to the online Console of the Kudu application and execute the following commands in it:

$ cd site\wwwroot
$ curl -sS https://getcomposer.org/installer | php
$ php -d extension=php_intl.dll composer.phar install

The curl command retrieves and downloads the Composer command line tool and installs it at the root of the site/wwwroot directory. Then, running the Composer install command downloads and installs all necessary third-party libraries.

This may take a while depending on the number of third-party dependencies you’ve configured in your composer.json file.

注解

The -d switch allows you to quickly override/add any php.ini settings. In this command, we are forcing PHP to use the intl extension, because it is not enabled by default in Azure Website at the moment. Soon, this -d option will no longer be needed since Microsoft will enable the intl extension by default.

At the end of the composer install command, you will be prompted to fill in the values of some Symfony settings like database credentials, locale, mailer credentials, CSRF token protection, etc. These parameters come from the app/config/parameters.yml.dist file.

Configuring Symfony global parameters

The most important thing in this cookbook is to correctly setup your database settings. You can get your MySQL database settings on the right sidebar of the Azure Website Dashboard panel. Simply click on the View Connection Strings link to make them appear in a pop-in.

MySQL database settings

The displayed MySQL database settings should be something similar to the code below. Of course, each value depends on what you’ve already configured.

Database=mysymfonyMySQL;Data Source=eu-cdbr-azure-north-c.cloudapp.net;User Id=bff2481a5b6074;Password=bdf50b42

Switch back to the console and answer the prompted questions and provide the following answers. Don’t forget to adapt the values below with your real values from the MySQL connection string.

database_driver: pdo_mysql
database_host: u-cdbr-azure-north-c.cloudapp.net
database_port: null
database_name: mysymfonyMySQL
database_user: bff2481a5b6074
database_password: bdf50b42
// ...

Don’t forget to answer all the questions. It’s important to set a unique random string for the secret variable. For the mailer configuration, Azure Website doesn’t provide a built-in mailer service. You should consider configuring the host-name and credentials of some other third-party mailing service if your application needs to send emails.

Configuring Symfony

Your Symfony application is now configured and should be almost operational. The final step is to build the database schema. This can easily be done with the command line interface if you’re using Doctrine. In the online Console tool of the Kudu application, run the following command to mount the tables into your MySQL database.

$ php app/console doctrine:schema:update --force

This command builds the tables and indexes for your MySQL database. If your Symfony application is more complex than a basic Symfony Standard Edition, you may have additional commands to execute for setup (see How to Deploy a Symfony Application).

Make sure that your application is running by browsing the app.php front controller with your web browser and the following url:

http://<your-website-name>.azurewebsites.net/web/app.php

If Symfony is correctly installed, you should see the front page of your Symfony application showing.

Configure the Web Server

At this point, the Symfony application has been deployed and works perfectly on the Azure Website. However, the web folder is still part of the url, which you definitely don’t want. But don’t worry! You can easily configure the web server to point to the web folder and remove the web in the URL (and guarantee that nobody can access files outside of the web directory.)

To do this, create and deploy (see previous section about Git) the following web.config file. This file must be located at the root of your project next to the composer.json file. This file is the Microsoft IIS Server equivalent to the well-known .htaccess file from Apache. For a Symfony application, configure it with the following content:

<!-- web.config -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <clear />
        <rule name="BlockAccessToPublic" patternSyntax="Wildcard" stopProcessing="true">
          <match url="*" />
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
            <add input="{URL}" pattern="/web/*" />
          </conditions>
          <action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." statusDescription="You do not have permission to view this directory or page using the credentials that you supplied." />
        </rule>
        <rule name="RewriteAssetsToPublic" stopProcessing="true">
          <match url="^(.*)(\.css|\.js|\.jpg|\.png|\.gif)$" />
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
          </conditions>
          <action type="Rewrite" url="web/{R:0}" />
        </rule>
        <rule name="RewriteRequestsToPublic" stopProcessing="true">
          <match url="^(.*)$" />
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
          </conditions>
          <action type="Rewrite" url="web/app.php/{R:0}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

As you can see, the latest rule RewriteRequestsToPublic is responsible for rewriting any urls to the web/app.php front controller which allows you to skip the web/ folder in the URL. The first rule called BlockAccessToPublic matches all url patterns that contain the web/ folder and serves a 403 Forbidden HTTP response instead. This example is based on Benjamin Eberlei’s sample you can find on GitHub in the SymfonyAzureEdition bundle.

Deploy this file under the site/wwwroot directory of the Azure Website and browse to your application without the web/app.php segment in the URL.

Conclusion

Nice work! You’ve now deployed your Symfony application to the Microsoft Azure Website Cloud platform. You also saw that Symfony can be easily configured and executed on a Microsoft IIS web server. The process is simple and easy to implement. And as a bonus, Microsoft is continuing to reduce the number of steps needed so that deployment becomes even easier.

Deploying to Heroku Cloud

This step by step cookbook describes how to deploy a Symfony web application to the Heroku cloud platform. Its contents are based on the original article published by Heroku.

Setting up

To setup a new Heroku website, first signup with Heroku or sign in with your credentials. Then download and install the Heroku Toolbelt on your local computer.

You can also check out the getting Started with PHP on Heroku guide to gain more familiarity with the specifics of working with PHP applications on Heroku.

Preparing your Application

Deploying a Symfony application to Heroku doesn’t require any change in its code, but it requires some minor tweaks to its configuration.

By default, the Symfony app will log into your application’s app/log/ directory. This is not ideal as Heroku uses an ephemeral file system. On Heroku, the best way to handle logging is using Logplex. And the best way to send log data to Logplex is by writing to STDERR or STDOUT. Luckily, Symfony uses the excellent Monolog library for logging. So, a new log destination is just a change to a config file away.

Open the app/config/config_prod.yml file, locate the monolog/handlers/nested section (or create it if it doesn’t exist yet) and change the value of path from "%kernel.logs_dir%/%kernel.environment%.log" to "php://stderr":

# app/config/config_prod.yml
monolog:
    # ...
    handlers:
        # ...
        nested:
            # ...
            path: "php://stderr"

Once the application is deployed, run heroku logs --tail to keep the stream of logs from Heroku open in your terminal.

Creating a new Application on Heroku

To create a new Heroku application that you can push to, use the CLI create command:

$ heroku create

Creating mighty-hamlet-1981 in organization heroku... done, stack is cedar
http://mighty-hamlet-1981.herokuapp.com/ | git@heroku.com:mighty-hamlet-1981.git
Git remote heroku added

You are now ready to deploy the application as explained in the next section.

Deploying your Application on Heroku

To deploy your application to Heroku, you must first create a Procfile, which tells Heroku what command to use to launch the web server with the correct document root. After that, you will ensure that your Symfony application runs the prod environment, and then you’ll be ready to git push to Heroku for your first deploy!

Creating a Procfile

By default, Heroku will launch an Apache web server together with PHP to serve applications. However, two special circumstances apply to Symfony applications:

  1. The document root is in the web/ directory and not in the root directory of the application;
  2. The Composer bin-dir, where vendor binaries (and thus Heroku’s own boot scripts) are placed, is bin/ , and not the default vendor/bin.

注解

Vendor binaries are usually installed to vendor/bin by Composer, but sometimes (e.g. when running a Symfony Standard Edition project!), the location will be different. If in doubt, you can always run composer config bin-dir to figure out the right location.

Create a new file called Procfile (without any extension) at the root directory of the application and add just the following content:

web: bin/heroku-php-apache2 web/

If you prefer working on the command console, execute the following commands to create the Procfile file and to add it to the repository:

$ echo "web: bin/heroku-php-apache2 web/" > Procfile
$ git add .
$ git commit -m "Procfile for Apache and PHP"
[master 35075db] Procfile for Apache and PHP
 1 file changed, 1 insertion(+)
Setting the prod Environment

During a deploy, Heroku runs composer install --no-dev to install all of the dependencies your application requires. However, typical post-install-commands in composer.json, e.g. to install assets or clear (or pre-warm) caches, run using Symfony’s dev environment by default.

This is clearly not what you want - the app runs in “production” (even if you use it just for an experiment, or as a staging environment), and so any build steps should use the same prod environment as well.

Thankfully, the solution to this problem is very simple: Symfony will pick up an environment variable named SYMFONY_ENV and use that environment if nothing else is explicitly set. As Heroku exposes all config vars as environment variables, you can issue a single command to prepare your app for a deployment:

$ heroku config:set SYMFONY_ENV=prod
Pushing to Heroku

Next up, it’s finally time to deploy your application to Heroku. If you are doing this for the very first time, you may see a message such as the following:

The authenticity of host 'heroku.com (50.19.85.132)' can't be established.
RSA key fingerprint is 8b:48:5e:67:0e:c9:16:47:32:f2:87:0c:1f:c8:60:ad.
Are you sure you want to continue connecting (yes/no)?

In this case, you need to confirm by typing yes and hitting <Enter> key - ideally after you’ve verified that the RSA key fingerprint is correct.

Then, deploy your application executing this command:

$ git push heroku master

Initializing repository, done.
Counting objects: 130, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (107/107), done.
Writing objects: 100% (130/130), 70.88 KiB | 0 bytes/s, done.
Total 130 (delta 17), reused 0 (delta 0)

-----> PHP app detected

-----> Setting up runtime environment...
       - PHP 5.5.12
       - Apache 2.4.9
       - Nginx 1.4.6

-----> Installing PHP extensions:
       - opcache (automatic; bundled, using 'ext-opcache.ini')

-----> Installing dependencies...
       Composer version 64ac32fca9e64eb38e50abfadc6eb6f2d0470039 2014-05-24 20:57:50
       Loading composer repositories with package information
       Installing dependencies from lock file
         - ...

       Generating optimized autoload files
       Creating the "app/config/parameters.yml" file
       Clearing the cache for the dev environment with debug true
       Installing assets using the hard copy option
       Installing assets for Symfony\Bundle\FrameworkBundle into web/bundles/framework
       Installing assets for Acme\DemoBundle into web/bundles/acmedemo
       Installing assets for Sensio\Bundle\DistributionBundle into web/bundles/sensiodistribution

-----> Building runtime environment...

-----> Discovering process types
       Procfile declares types -> web

-----> Compressing... done, 61.5MB

-----> Launching... done, v3
       http://mighty-hamlet-1981.herokuapp.com/ deployed to Heroku

To git@heroku.com:mighty-hamlet-1981.git
 * [new branch]      master -> master

And that’s it! If you now open your browser, either by manually pointing it to the URL heroku create gave you, or by using the Heroku Toolbelt, the application will respond:

$ heroku open
Opening mighty-hamlet-1981... done

You should be seeing your Symfony application in your browser.

Deploying to Platform.sh

This step-by-step cookbook describes how to deploy a Symfony web application to Platform.sh. You can read more about using Symfony with Platform.sh on the official Platform.sh documentation.

Deploy an Existing Site

In this guide, it is assumed your codebase is already versioned with Git.

Get a Project on Platform.sh

You need to subscribe to a Platform.sh project. Choose the development plan and go through the checkout process. Once your project is ready, give it a name and choose: Import an existing site.

Prepare Your Application

To deploy your Symfony application on Platform.sh, you simply need to add a .platform.app.yaml at the root of your Git repository which will tell Platform.sh how to deploy your application (read more about Platform.sh configuration files).

# .platform.app.yaml

# This file describes an application. You can have multiple applications
# in the same project.

# The name of this app. Must be unique within a project.
name: myphpproject

# The toolstack used to build the application.
toolstack: "php:symfony"

# The relationships of the application with services or other applications.
# The left-hand side is the name of the relationship as it will be exposed
# to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand
# side is in the form `<service name>:<endpoint name>`.
relationships:
    database: "mysql:mysql"

# The configuration of app when it is exposed to the web.
web:
    # The public directory of the app, relative to its root.
    document_root: "/web"
    # The front-controller script to send non-static requests to.
    passthru: "/app.php"

# The size of the persistent disk of the application (in MB).
disk: 2048

# The mounts that will be performed when the package is deployed.
mounts:
    "/app/cache": "shared:files/cache"
    "/app/logs": "shared:files/logs"

# The hooks that will be performed when the package is deployed.
hooks:
    build: |
      rm web/app_dev.php
      app/console --env=prod assetic:dump --no-debug
    deploy: |
      app/console --env=prod cache:clear

For best practices, you should also add a .platform folder at the root of your Git repository which contains the following files:

# .platform/routes.yaml
"http://{default}/":
    type: upstream
    upstream: "php:php"
# .platform/services.yaml
mysql:
    type: mysql
    disk: 2048

An example of these configurations can be found on GitHub. The list of available services can be found on the Platform.sh documentation.

Configure Database Access

Platform.sh overrides your database specific configuration via importing the following file:

// app/config/parameters_platform.php
<?php
$relationships = getenv("PLATFORM_RELATIONSHIPS");
    if (!$relationships) {
        return;
}

$relationships = json_decode(base64_decode($relationships), true);

foreach ($relationships['database'] as $endpoint) {
    if (empty($endpoint['query']['is_master'])) {
      continue;
    }

    $container->setParameter('database_driver', 'pdo_' . $endpoint['scheme']);
    $container->setParameter('database_host', $endpoint['host']);
    $container->setParameter('database_port', $endpoint['port']);
    $container->setParameter('database_name', $endpoint['path']);
    $container->setParameter('database_user', $endpoint['username']);
    $container->setParameter('database_password', $endpoint['password']);
    $container->setParameter('database_path', '');
}

# Store session into /tmp.
ini_set('session.save_path', '/tmp/sessions');

Make sure this file is listed in your imports:

# app/config/config.yml
imports:
    - { resource: parameters_platform.php }
Deploy your Application

Now you need to add a remote to Platform.sh in your Git repository (copy the command that you see on the Platform.sh web UI):

$ git remote add platform [PROJECT-ID]@git.[CLUSTER].platform.sh:[PROJECT-ID].git
PROJECT-ID
Unique identifier of your project. Something like kjh43kbobssae
CLUSTER
Server location where your project is deployed. It can be eu or us

Commit the Platform.sh specific files created in the previous section:

$ git add .platform.app.yaml .platform/*
$ git add app/config/config.yml app/config/parameters_platform.php
$ git commit -m "Adding Platform.sh configuration files."

Push your code base to the newly added remote:

$ git push platform master

That’s it! Your application is being deployed on Platform.sh and you’ll soon be able to access it in your browser.

Every code change that you do from now on will be pushed to Git in order to redeploy your environment on Platform.sh.

More information about migrating your database and files can be found on the Platform.sh documentation.

Deploy a new Site

You can start a new Platform.sh project. Choose the development plan and go through the checkout process.

Once your project is ready, give it a name and choose: Create a new site. Choose the Symfony stack and a starting point such as Standard.

That’s it! Your Symfony application will be bootstrapped and deployed. You’ll soon be able to see it in your browser.

Doctrine

How to Handle File Uploads with Doctrine

Handling file uploads with Doctrine entities is no different than handling any other file upload. In other words, you’re free to move the file in your controller after handling a form submission. For examples of how to do this, see the file type reference page.

If you choose to, you can also integrate the file upload into your entity lifecycle (i.e. creation, update and removal). In this case, as your entity is created, updated, and removed from Doctrine, the file uploading and removal processing will take place automatically (without needing to do anything in your controller).

To make this work, you’ll need to take care of a number of details, which will be covered in this cookbook entry.

Basic Setup

First, create a simple Doctrine entity class to work with:

// src/AppBundle/Entity/Document.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 */
class Document
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     */
    public $name;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    public $path;

    public function getAbsolutePath()
    {
        return null === $this->path
            ? null
            : $this->getUploadRootDir().'/'.$this->path;
    }

    public function getWebPath()
    {
        return null === $this->path
            ? null
            : $this->getUploadDir().'/'.$this->path;
    }

    protected function getUploadRootDir()
    {
        // the absolute directory path where uploaded
        // documents should be saved
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }

    protected function getUploadDir()
    {
        // get rid of the __DIR__ so it doesn't screw up
        // when displaying uploaded doc/image in the view.
        return 'uploads/documents';
    }
}

The Document entity has a name and it is associated with a file. The path property stores the relative path to the file and is persisted to the database. The getAbsolutePath() is a convenience method that returns the absolute path to the file while the getWebPath() is a convenience method that returns the web path, which can be used in a template to link to the uploaded file.

小技巧

If you have not done so already, you should probably read the file type documentation first to understand how the basic upload process works.

注解

If you’re using annotations to specify your validation rules (as shown in this example), be sure that you’ve enabled validation by annotation (see validation configuration).

To handle the actual file upload in the form, use a “virtual” file field. For example, if you’re building your form directly in a controller, it might look like this:

public function uploadAction()
{
    // ...

    $form = $this->createFormBuilder($document)
        ->add('name')
        ->add('file')
        ->getForm();

    // ...
}

Next, create this property on your Document class and add some validation rules:

use Symfony\Component\HttpFoundation\File\UploadedFile;

// ...
class Document
{
    /**
     * @Assert\File(maxSize="6000000")
     */
    private $file;

    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this->file = $file;
    }

    /**
     * Get file.
     *
     * @return UploadedFile
     */
    public function getFile()
    {
        return $this->file;
    }
}
  • YAML
    # src/AppBundle/Resources/config/validation.yml
    AppBundle\Entity\Document:
        properties:
            file:
                - File:
                    maxSize: 6000000
    
  • Annotations
    // src/AppBundle/Entity/Document.php
    namespace AppBundle\Entity;
    
    // ...
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Document
    {
        /**
         * @Assert\File(maxSize="6000000")
         */
        private $file;
    
        // ...
    }
    
  • XML
    <!-- src/AppBundle/Resources/config/validation.xml -->
    <class name="AppBundle\Entity\Document">
        <property name="file">
            <constraint name="File">
                <option name="maxSize">6000000</option>
            </constraint>
        </property>
    </class>
    
  • PHP
    // src/AppBundle/Entity/Document.php
    namespace Acme\DemoBundle\Entity;
    
    // ...
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Document
    {
        // ...
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('file', new Assert\File(array(
                'maxSize' => 6000000,
            )));
        }
    }
    

注解

As you are using the File constraint, Symfony will automatically guess that the form field is a file upload input. That’s why you did not have to set it explicitly when creating the form above (->add('file')).

The following controller shows you how to handle the entire process:

// ...
use AppBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
// ...

/**
 * @Template()
 */
public function uploadAction(Request $request)
{
    $document = new Document();
    $form = $this->createFormBuilder($document)
        ->add('name')
        ->add('file')
        ->getForm();

    $form->handleRequest($request);

    if ($form->isValid()) {
        $em = $this->getDoctrine()->getManager();

        $em->persist($document);
        $em->flush();

        return $this->redirect($this->generateUrl(...));
    }

    return array('form' => $form->createView());
}

The previous controller will automatically persist the Document entity with the submitted name, but it will do nothing about the file and the path property will be blank.

An easy way to handle the file upload is to move it just before the entity is persisted and then set the path property accordingly. Start by calling a new upload() method on the Document class, which you’ll create in a moment to handle the file upload:

if ($form->isValid()) {
    $em = $this->getDoctrine()->getManager();

    $document->upload();

    $em->persist($document);
    $em->flush();

    return $this->redirect(...);
}

The upload() method will take advantage of the UploadedFile object, which is what’s returned after a file field is submitted:

public function upload()
{
    // the file property can be empty if the field is not required
    if (null === $this->getFile()) {
        return;
    }

    // use the original file name here but you should
    // sanitize it at least to avoid any security issues

    // move takes the target directory and then the
    // target filename to move to
    $this->getFile()->move(
        $this->getUploadRootDir(),
        $this->getFile()->getClientOriginalName()
    );

    // set the path property to the filename where you've saved the file
    $this->path = $this->getFile()->getClientOriginalName();

    // clean up the file property as you won't need it anymore
    $this->file = null;
}
Using Lifecycle Callbacks

警告

Using lifecycle callbacks is a limited technique that has some drawbacks. If you want to remove the hardcoded __DIR__ reference inside the Document::getUploadRootDir() method, the best way is to start using explicit doctrine listeners. There you will be able to inject kernel parameters such as kernel.root_dir to be able to build absolute paths.

Even if this implementation works, it suffers from a major flaw: What if there is a problem when the entity is persisted? The file would have already moved to its final location even though the entity’s path property didn’t persist correctly.

To avoid these issues, you should change the implementation so that the database operation and the moving of the file become atomic: if there is a problem persisting the entity or if the file cannot be moved, then nothing should happen.

To do this, you need to move the file right as Doctrine persists the entity to the database. This can be accomplished by hooking into an entity lifecycle callback:

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
}

Next, refactor the Document class to take advantage of these callbacks:

use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
    private $temp;

    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this->file = $file;
        // check if we have an old image path
        if (isset($this->path)) {
            // store the old name to delete after the update
            $this->temp = $this->path;
            $this->path = null;
        } else {
            $this->path = 'initial';
        }
    }

    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->getFile()) {
            // do whatever you want to generate a unique name
            $filename = sha1(uniqid(mt_rand(), true));
            $this->path = $filename.'.'.$this->getFile()->guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if (null === $this->getFile()) {
            return;
        }

        // if there is an error when moving the file, an exception will
        // be automatically thrown by move(). This will properly prevent
        // the entity from being persisted to the database on error
        $this->getFile()->move($this->getUploadRootDir(), $this->path);

        // check if we have an old image
        if (isset($this->temp)) {
            // delete the old image
            unlink($this->getUploadRootDir().'/'.$this->temp);
            // clear the temp image path
            $this->temp = null;
        }
        $this->file = null;
    }

    /**
     * @ORM\PostRemove()
     */
    public function removeUpload()
    {
        $file = $this->getAbsolutePath();
        if ($file) {
            unlink($file);
        }
    }
}

警告

If changes to your entity are handled by a Doctrine event listener or event subscriber, the preUpdate() callback must notify Doctrine about the changes being done. For full reference on preUpdate event restrictions, see preUpdate in the Doctrine Events documentation.

The class now does everything you need: it generates a unique filename before persisting, moves the file after persisting, and removes the file if the entity is ever deleted.

Now that the moving of the file is handled atomically by the entity, the call to $document->upload() should be removed from the controller:

if ($form->isValid()) {
    $em = $this->getDoctrine()->getManager();

    $em->persist($document);
    $em->flush();

    return $this->redirect(...);
}

注解

The @ORM\PrePersist() and @ORM\PostPersist() event callbacks are triggered before and after the entity is persisted to the database. On the other hand, the @ORM\PreUpdate() and @ORM\PostUpdate() event callbacks are called when the entity is updated.

警告

The PreUpdate and PostUpdate callbacks are only triggered if there is a change in one of the entity’s fields that are persisted. This means that, by default, if you modify only the $file property, these events will not be triggered, as the property itself is not directly persisted via Doctrine. One solution would be to use an updated field that’s persisted to Doctrine, and to modify it manually when changing the file.

Using the id as the Filename

If you want to use the id as the name of the file, the implementation is slightly different as you need to save the extension under the path property, instead of the actual filename:

use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
    private $temp;

    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this->file = $file;
        // check if we have an old image path
        if (is_file($this->getAbsolutePath())) {
            // store the old name to delete after the update
            $this->temp = $this->getAbsolutePath();
        } else {
            $this->path = 'initial';
        }
    }

    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->getFile()) {
            $this->path = $this->getFile()->guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if (null === $this->getFile()) {
            return;
        }

        // check if we have an old image
        if (isset($this->temp)) {
            // delete the old image
            unlink($this->temp);
            // clear the temp image path
            $this->temp = null;
        }

        // you must throw an exception here if the file cannot be moved
        // so that the entity is not persisted to the database
        // which the UploadedFile move() method does
        $this->getFile()->move(
            $this->getUploadRootDir(),
            $this->id.'.'.$this->getFile()->guessExtension()
        );

        $this->setFile(null);
    }

    /**
     * @ORM\PreRemove()
     */
    public function storeFilenameForRemove()
    {
        $this->temp = $this->getAbsolutePath();
    }

    /**
     * @ORM\PostRemove()
     */
    public function removeUpload()
    {
        if (isset($this->temp)) {
            unlink($this->temp);
        }
    }

    public function getAbsolutePath()
    {
        return null === $this->path
            ? null
            : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
    }
}

You’ll notice in this case that you need to do a little bit more work in order to remove the file. Before it’s removed, you must store the file path (since it depends on the id). Then, once the object has been fully removed from the database, you can safely delete the file (in PostRemove).

How to use Doctrine Extensions: Timestampable, Sluggable, Translatable, etc.

Doctrine2 is very flexible, and the community has already created a series of useful Doctrine extensions to help you with common entity-related tasks.

One library in particular - the DoctrineExtensions library - provides integration functionality for Sluggable, Translatable, Timestampable, Loggable, Tree and Sortable behaviors.

The usage for each of these extensions is explained in that repository.

However, to install/activate each extension you must register and activate an Event Listener. To do this, you have two options:

  1. Use the StofDoctrineExtensionsBundle, which integrates the above library.
  2. Implement this services directly by following the documentation for integration with Symfony: Install Gedmo Doctrine2 extensions in Symfony2
How to Register Event Listeners and Subscribers

Doctrine packages a rich event system that fires events when almost anything happens inside the system. For you, this means that you can create arbitrary services and tell Doctrine to notify those objects whenever a certain action (e.g. prePersist) happens within Doctrine. This could be useful, for example, to create an independent search index whenever an object in your database is saved.

Doctrine defines two types of objects that can listen to Doctrine events: listeners and subscribers. Both are very similar, but listeners are a bit more straightforward. For more, see The Event System on Doctrine’s website.

The Doctrine website also explains all existing events that can be listened to.

Configuring the Listener/Subscriber

To register a service to act as an event listener or subscriber you just have to tag it with the appropriate name. Depending on your use-case, you can hook a listener into every DBAL connection and ORM entity manager or just into one specific DBAL connection and all the entity managers that use this connection.

  • YAML
    doctrine:
        dbal:
            default_connection: default
            connections:
                default:
                    driver: pdo_sqlite
                    memory: true
    
    services:
        my.listener:
            class: Acme\SearchBundle\EventListener\SearchIndexer
            tags:
                - { name: doctrine.event_listener, event: postPersist }
        my.listener2:
            class: Acme\SearchBundle\EventListener\SearchIndexer2
            tags:
                - { name: doctrine.event_listener, event: postPersist, connection: default }
        my.subscriber:
            class: Acme\SearchBundle\EventListener\SearchIndexerSubscriber
            tags:
                - { name: doctrine.event_subscriber, connection: default }
    
  • XML
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
    
        <doctrine:config>
            <doctrine:dbal default-connection="default">
                <doctrine:connection driver="pdo_sqlite" memory="true" />
            </doctrine:dbal>
        </doctrine:config>
    
        <services>
            <service id="my.listener" class="Acme\SearchBundle\EventListener\SearchIndexer">
                <tag name="doctrine.event_listener" event="postPersist" />
            </service>
            <service id="my.listener2" class="Acme\SearchBundle\EventListener\SearchIndexer2">
                <tag name="doctrine.event_listener" event="postPersist" connection="default" />
            </service>
            <service id="my.subscriber" class="Acme\SearchBundle\EventListener\SearchIndexerSubscriber">
                <tag name="doctrine.event_subscriber" connection="default" />
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
            'default_connection' => 'default',
            'connections' => array(
                'default' => array(
                    'driver' => 'pdo_sqlite',
                    'memory' => true,
                ),
            ),
        ),
    ));
    
    $container
        ->setDefinition(
            'my.listener',
            new Definition('Acme\SearchBundle\EventListener\SearchIndexer')
        )
        ->addTag('doctrine.event_listener', array('event' => 'postPersist'))
    ;
    $container
        ->setDefinition(
            'my.listener2',
            new Definition('Acme\SearchBundle\EventListener\SearchIndexer2')
        )
        ->addTag('doctrine.event_listener', array('event' => 'postPersist', 'connection' => 'default'))
    ;
    $container
        ->setDefinition(
            'my.subscriber',
            new Definition('Acme\SearchBundle\EventListener\SearchIndexerSubscriber')
        )
        ->addTag('doctrine.event_subscriber', array('connection' => 'default'))
    ;
    
Creating the Listener Class

In the previous example, a service my.listener was configured as a Doctrine listener on the event postPersist. The class behind that service must have a postPersist method, which will be called when the event is dispatched:

// src/Acme/SearchBundle/EventListener/SearchIndexer.php
namespace Acme\SearchBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\StoreBundle\Entity\Product;

class SearchIndexer
{
    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $entityManager = $args->getEntityManager();

        // perhaps you only want to act on some "Product" entity
        if ($entity instanceof Product) {
            // ... do something with the Product
        }
    }
}

In each event, you have access to a LifecycleEventArgs object, which gives you access to both the entity object of the event and the entity manager itself.

One important thing to notice is that a listener will be listening for all entities in your application. So, if you’re interested in only handling a specific type of entity (e.g. a Product entity but not a BlogPost entity), you should check for the entity’s class type in your method (as shown above).

Creating the Subscriber Class

A Doctrine event subscriber must implement the Doctrine\Common\EventSubscriber interface and have an event method for each event it subscribes to:

// src/Acme/SearchBundle/EventListener/SearchIndexerSubscriber.php
namespace Acme\SearchBundle\EventListener;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
// for Doctrine 2.4: Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Acme\StoreBundle\Entity\Product;

class SearchIndexerSubscriber implements EventSubscriber
{
    public function getSubscribedEvents()
    {
        return array(
            'postPersist',
            'postUpdate',
        );
    }

    public function postUpdate(LifecycleEventArgs $args)
    {
        $this->index($args);
    }

    public function postPersist(LifecycleEventArgs $args)
    {
        $this->index($args);
    }

    public function index(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $entityManager = $args->getEntityManager();

        // perhaps you only want to act on some "Product" entity
        if ($entity instanceof Product) {
            // ... do something with the Product
        }
    }
}

小技巧

Doctrine event subscribers can not return a flexible array of methods to call for the events like the Symfony event subscriber can. Doctrine event subscribers must return a simple array of the event names they subscribe to. Doctrine will then expect methods on the subscriber with the same name as each subscribed event, just as when using an event listener.

For a full reference, see chapter The Event System in the Doctrine documentation.

How to Use Doctrine DBAL

注解

This article is about the Doctrine DBAL. Typically, you’ll work with the higher level Doctrine ORM layer, which simply uses the DBAL behind the scenes to actually communicate with the database. To read more about the Doctrine ORM, see “Databases and Doctrine”.

The Doctrine Database Abstraction Layer (DBAL) is an abstraction layer that sits on top of PDO and offers an intuitive and flexible API for communicating with the most popular relational databases. In other words, the DBAL library makes it easy to execute queries and perform other database actions.

小技巧

Read the official Doctrine DBAL Documentation to learn all the details and capabilities of Doctrine’s DBAL library.

To get started, configure the database connection parameters:

  • YAML
    # app/config/config.yml
    doctrine:
        dbal:
            driver:   pdo_mysql
            dbname:   Symfony
            user:     root
            password: null
            charset:  UTF8
    
  • XML
    <!-- app/config/config.xml -->
    <doctrine:config>
        <doctrine:dbal
            name="default"
            dbname="Symfony"
            user="root"
            password="null"
            driver="pdo_mysql"
        />
    </doctrine:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
            'driver'    => 'pdo_mysql',
            'dbname'    => 'Symfony',
            'user'      => 'root',
            'password'  => null,
        ),
    ));
    

For full DBAL configuration options, or to learn how to configure multiple connections, see Doctrine DBAL Configuration.

You can then access the Doctrine DBAL connection by accessing the database_connection service:

class UserController extends Controller
{
    public function indexAction()
    {
        $conn = $this->get('database_connection');
        $users = $conn->fetchAll('SELECT * FROM users');

        // ...
    }
}
Registering custom Mapping Types

You can register custom mapping types through Symfony’s configuration. They will be added to all configured connections. For more information on custom mapping types, read Doctrine’s Custom Mapping Types section of their documentation.

  • YAML
    # app/config/config.yml
    doctrine:
        dbal:
            types:
                custom_first:  AppBundle\Type\CustomFirst
                custom_second: AppBundle\Type\CustomSecond
    
  • XML
    <!-- app/config/config.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <doctrine:config>
            <doctrine:dbal>
                <doctrine:type name="custom_first" class="AppBundle\Type\CustomFirst" />
                <doctrine:type name="custom_second" class="AppBundle\Type\CustomSecond" />
            </doctrine:dbal>
        </doctrine:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
            'types' => array(
                'custom_first'  => 'AppBundle\Type\CustomFirst',
                'custom_second' => 'AppBundle\Type\CustomSecond',
            ),
        ),
    ));
    
Registering custom Mapping Types in the SchemaTool

The SchemaTool is used to inspect the database to compare the schema. To achieve this task, it needs to know which mapping type needs to be used for each database types. Registering new ones can be done through the configuration.

Now, map the ENUM type (not supported by DBAL by default) to the string mapping type:

  • YAML
    # app/config/config.yml
    doctrine:
        dbal:
           mapping_types:
              enum: string
    
  • XML
    <!-- app/config/config.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <doctrine:config>
            <doctrine:dbal>
                 <doctrine:mapping-type name="enum">string</doctrine:mapping-type>
            </doctrine:dbal>
        </doctrine:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
           'mapping_types' => array(
              'enum'  => 'string',
           ),
        ),
    ));
    
How to Generate Entities from an Existing Database

When starting work on a brand new project that uses a database, two different situations comes naturally. In most cases, the database model is designed and built from scratch. Sometimes, however, you’ll start with an existing and probably unchangeable database model. Fortunately, Doctrine comes with a bunch of tools to help generate model classes from your existing database.

注解

As the Doctrine tools documentation says, reverse engineering is a one-time process to get started on a project. Doctrine is able to convert approximately 70-80% of the necessary mapping information based on fields, indexes and foreign key constraints. Doctrine can’t discover inverse associations, inheritance types, entities with foreign keys as primary keys or semantical operations on associations such as cascade or lifecycle events. Some additional work on the generated entities will be necessary afterwards to design each to fit your domain model specificities.

This tutorial assumes you’re using a simple blog application with the following two tables: blog_post and blog_comment. A comment record is linked to a post record thanks to a foreign key constraint.

CREATE TABLE `blog_post` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
  `content` longtext COLLATE utf8_unicode_ci NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `blog_comment` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `post_id` bigint(20) NOT NULL,
  `author` varchar(20) COLLATE utf8_unicode_ci NOT NULL,
  `content` longtext COLLATE utf8_unicode_ci NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `blog_comment_post_id_idx` (`post_id`),
  CONSTRAINT `blog_post_id` FOREIGN KEY (`post_id`) REFERENCES `blog_post` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Before diving into the recipe, be sure your database connection parameters are correctly setup in the app/config/parameters.yml file (or wherever your database configuration is kept) and that you have initialized a bundle that will host your future entity class. In this tutorial it’s assumed that an AcmeBlogBundle exists and is located under the src/Acme/BlogBundle folder.

The first step towards building entity classes from an existing database is to ask Doctrine to introspect the database and generate the corresponding metadata files. Metadata files describe the entity class to generate based on table fields.

$ php app/console doctrine:mapping:import --force AcmeBlogBundle xml

This command line tool asks Doctrine to introspect the database and generate the XML metadata files under the src/Acme/BlogBundle/Resources/config/doctrine folder of your bundle. This generates two files: BlogPost.orm.xml and BlogComment.orm.xml.

小技巧

It’s also possible to generate the metadata files in YAML format by changing the last argument to yml.

The generated BlogPost.orm.xml metadata file looks as follows:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
  <entity name="Acme\BlogBundle\Entity\BlogPost" table="blog_post">
    <id name="id" type="bigint" column="id">
      <generator strategy="IDENTITY"/>
    </id>
    <field name="title" type="string" column="title" length="100" nullable="false"/>
    <field name="content" type="text" column="content" nullable="false"/>
    <field name="createdAt" type="datetime" column="created_at" nullable="false"/>
  </entity>
</doctrine-mapping>

Once the metadata files are generated, you can ask Doctrine to build related entity classes by executing the following two commands.

$ php app/console doctrine:mapping:convert annotation ./src
$ php app/console doctrine:generate:entities AcmeBlogBundle

The first command generates entity classes with annotation mappings. But if you want to use YAML or XML mapping instead of annotations, you should execute the second command only.

小技巧

If you want to use annotations, you can safely delete the XML (or YAML) files after running these two commands.

For example, the newly created BlogComment entity class looks as follow:

// src/Acme/BlogBundle/Entity/BlogComment.php
namespace Acme\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Acme\BlogBundle\Entity\BlogComment
 *
 * @ORM\Table(name="blog_comment")
 * @ORM\Entity
 */
class BlogComment
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="bigint")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string $author
     *
     * @ORM\Column(name="author", type="string", length=100, nullable=false)
     */
    private $author;

    /**
     * @var text $content
     *
     * @ORM\Column(name="content", type="text", nullable=false)
     */
    private $content;

    /**
     * @var datetime $createdAt
     *
     * @ORM\Column(name="created_at", type="datetime", nullable=false)
     */
    private $createdAt;

    /**
     * @var BlogPost
     *
     * @ORM\ManyToOne(targetEntity="BlogPost")
     * @ORM\JoinColumn(name="post_id", referencedColumnName="id")
     */
    private $post;
}

As you can see, Doctrine converts all table fields to pure private and annotated class properties. The most impressive thing is that it also discovered the relationship with the BlogPost entity class based on the foreign key constraint. Consequently, you can find a private $post property mapped with a BlogPost entity in the BlogComment entity class.

注解

If you want to have a one-to-many relationship, you will need to add it manually into the entity or to the generated XML or YAML files. Add a section on the specific entities for one-to-many defining the inversedBy and the mappedBy pieces.

The generated entities are now ready to be used. Have fun!

How to Work with multiple Entity Managers and Connections

You can use multiple Doctrine entity managers or connections in a Symfony application. This is necessary if you are using different databases or even vendors with entirely different sets of entities. In other words, one entity manager that connects to one database will handle some entities while another entity manager that connects to another database might handle the rest.

注解

Using multiple entity managers is pretty easy, but more advanced and not usually required. Be sure you actually need multiple entity managers before adding in this layer of complexity.

The following configuration code shows how you can configure two entity managers:

  • YAML
    doctrine:
        dbal:
            default_connection: default
            connections:
                default:
                    driver:   "%database_driver%"
                    host:     "%database_host%"
                    port:     "%database_port%"
                    dbname:   "%database_name%"
                    user:     "%database_user%"
                    password: "%database_password%"
                    charset:  UTF8
                customer:
                    driver:   "%database_driver2%"
                    host:     "%database_host2%"
                    port:     "%database_port2%"
                    dbname:   "%database_name2%"
                    user:     "%database_user2%"
                    password: "%database_password2%"
                    charset:  UTF8
    
        orm:
            default_entity_manager: default
            entity_managers:
                default:
                    connection: default
                    mappings:
                        AppBundle:  ~
                        AcmeStoreBundle: ~
                customer:
                    connection: customer
                    mappings:
                        AcmeCustomerBundle: ~
    
  • XML
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/doctrine"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <config>
            <dbal default-connection="default">
                <connection name="default"
                    driver="%database_driver%"
                    host="%database_host%"
                    port="%database_port%"
                    dbname="%database_name%"
                    user="%database_user%"
                    password="%database_password%"
                    charset="UTF8"
                />
    
                <connection name="customer"
                    driver="%database_driver2%"
                    host="%database_host2%"
                    port="%database_port2%"
                    dbname="%database_name2%"
                    user="%database_user2%"
                    password="%database_password2%"
                    charset="UTF8"
                />
            </dbal>
    
            <orm default-entity-manager="default">
                <entity-manager name="default" connection="default">
                    <mapping name="AppBundle" />
                    <mapping name="AcmeStoreBundle" />
                </entity-manager>
    
                <entity-manager name="customer" connection="customer">
                    <mapping name="AcmeCustomerBundle" />
                </entity-manager>
            </orm>
        </config>
    </srv:container>
    
  • PHP
    $container->loadFromExtension('doctrine', array(
        'dbal' => array(
            'default_connection' => 'default',
            'connections' => array(
                'default' => array(
                    'driver'   => '%database_driver%',
                    'host'     => '%database_host%',
                    'port'     => '%database_port%',
                    'dbname'   => '%database_name%',
                    'user'     => '%database_user%',
                    'password' => '%database_password%',
                    'charset'  => 'UTF8',
                ),
                'customer' => array(
                    'driver'   => '%database_driver2%',
                    'host'     => '%database_host2%',
                    'port'     => '%database_port2%',
                    'dbname'   => '%database_name2%',
                    'user'     => '%database_user2%',
                    'password' => '%database_password2%',
                    'charset'  => 'UTF8',
                ),
            ),
        ),
    
        'orm' => array(
            'default_entity_manager' => 'default',
            'entity_managers' => array(
                'default' => array(
                    'connection' => 'default',
                    'mappings'   => array(
                        'AppBundle'  => null,
                        'AcmeStoreBundle' => null,
                    ),
                ),
                'customer' => array(
                    'connection' => 'customer',
                    'mappings'   => array(
                        'AcmeCustomerBundle' => null,
                    ),
                ),
            ),
        ),
    ));
    

In this case, you’ve defined two entity managers and called them default and customer. The default entity manager manages entities in the AppBundle and AcmeStoreBundle, while the customer entity manager manages entities in the AcmeCustomerBundle. You’ve also defined two connections, one for each entity manager.

注解

When working with multiple connections and entity managers, you should be explicit about which configuration you want. If you do omit the name of the connection or entity manager, the default (i.e. default) is used.

When working with multiple connections to create your databases:

# Play only with "default" connection
$ php app/console doctrine:database:create

# Play only with "customer" connection
$ php app/console doctrine:database:create --connection=customer

When working with multiple entity managers to update your schema:

# Play only with "default" mappings
$ php app/console doctrine:schema:update --force

# Play only with "customer" mappings
$ php app/console doctrine:schema:update --force --em=customer

If you do omit the entity manager’s name when asking for it, the default entity manager (i.e. default) is returned:

class UserController extends Controller
{
    public function indexAction()
    {
        // All three return the "default" entity manager
        $em = $this->get('doctrine')->getManager();
        $em = $this->get('doctrine')->getManager('default');
        $em = $this->get('doctrine.orm.default_entity_manager');

        // Both of these return the "customer" entity manager
        $customerEm = $this->get('doctrine')->getManager('customer');
        $customerEm = $this->get('doctrine.orm.customer_entity_manager');
    }
}

You can now use Doctrine just as you did before - using the default entity manager to persist and fetch entities that it manages and the customer entity manager to persist and fetch its entities.

The same applies to repository calls:

class UserController extends Controller
{
    public function indexAction()
    {
        // Retrieves a repository managed by the "default" em
        $products = $this->get('doctrine')
            ->getRepository('AcmeStoreBundle:Product')
            ->findAll()
        ;

        // Explicit way to deal with the "default" em
        $products = $this->get('doctrine')
            ->getRepository('AcmeStoreBundle:Product', 'default')
            ->findAll()
        ;

        // Retrieves a repository managed by the "customer" em
        $customers = $this->get('doctrine')
            ->getRepository('AcmeCustomerBundle:Customer', 'customer')
            ->findAll()
        ;
    }
}
How to Register custom DQL Functions

Doctrine allows you to specify custom DQL functions. For more information on this topic, read Doctrine’s cookbook article “DQL User Defined Functions”.

In Symfony, you can register your custom DQL functions as follows:

  • YAML
    # app/config/config.yml
    doctrine:
        orm:
            # ...
            dql:
                string_functions:
                    test_string: AppBundle\DQL\StringFunction
                    second_string: AppBundle\DQL\SecondStringFunction
                numeric_functions:
                    test_numeric: AppBundle\DQL\NumericFunction
                datetime_functions:
                    test_datetime: AppBundle\DQL\DatetimeFunction
    
  • XML
    <!-- app/config/config.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <doctrine:config>
            <doctrine:orm>
                <!-- ... -->
                <doctrine:dql>
                    <doctrine:string-function name="test_string">AppBundle\DQL\StringFunction</doctrine:string-function>
                    <doctrine:string-function name="second_string">AppBundle\DQL\SecondStringFunction</doctrine:string-function>
                    <doctrine:numeric-function name="test_numeric">AppBundle\DQL\NumericFunction</doctrine:numeric-function>
                    <doctrine:datetime-function name="test_datetime">AppBundle\DQL\DatetimeFunction</doctrine:datetime-function>
                </doctrine:dql>
            </doctrine:orm>
        </doctrine:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('doctrine', array(
        'orm' => array(
            // ...
            'dql' => array(
                'string_functions' => array(
                    'test_string'   => 'AppBundle\DQL\StringFunction',
                    'second_string' => 'AppBundle\DQL\SecondStringFunction',
                ),
                'numeric_functions' => array(
                    'test_numeric' => 'AppBundle\DQL\NumericFunction',
                ),
                'datetime_functions' => array(
                    'test_datetime' => 'AppBundle\DQL\DatetimeFunction',
                ),
            ),
        ),
    ));
    
How to Define Relationships with Abstract Classes and Interfaces

One of the goals of bundles is to create discreet bundles of functionality that do not have many (if any) dependencies, allowing you to use that functionality in other applications without including unnecessary items.

Doctrine 2.2 includes a new utility called the ResolveTargetEntityListener, that functions by intercepting certain calls inside Doctrine and rewriting targetEntity parameters in your metadata mapping at runtime. It means that in your bundle you are able to use an interface or abstract class in your mappings and expect correct mapping to a concrete entity at runtime.

This functionality allows you to define relationships between different entities without making them hard dependencies.

Background

Suppose you have an InvoiceBundle which provides invoicing functionality and a CustomerBundle that contains customer management tools. You want to keep these separated, because they can be used in other systems without each other, but for your application you want to use them together.

In this case, you have an Invoice entity with a relationship to a non-existent object, an InvoiceSubjectInterface. The goal is to get the ResolveTargetEntityListener to replace any mention of the interface with a real object that implements that interface.

Set up

This article uses the following two basic entities (which are incomplete for brevity) to explain how to set up and use the ResolveTargetEntityListener.

A Customer entity:

// src/Acme/AppBundle/Entity/Customer.php

namespace Acme\AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Acme\CustomerBundle\Entity\Customer as BaseCustomer;
use Acme\InvoiceBundle\Model\InvoiceSubjectInterface;

/**
 * @ORM\Entity
 * @ORM\Table(name="customer")
 */
class Customer extends BaseCustomer implements InvoiceSubjectInterface
{
    // In this example, any methods defined in the InvoiceSubjectInterface
    // are already implemented in the BaseCustomer
}

An Invoice entity:

// src/Acme/InvoiceBundle/Entity/Invoice.php

namespace Acme\InvoiceBundle\Entity;

use Doctrine\ORM\Mapping AS ORM;
use Acme\InvoiceBundle\Model\InvoiceSubjectInterface;

/**
 * Represents an Invoice.
 *
 * @ORM\Entity
 * @ORM\Table(name="invoice")
 */
class Invoice
{
    /**
     * @ORM\ManyToOne(targetEntity="Acme\InvoiceBundle\Model\InvoiceSubjectInterface")
     * @var InvoiceSubjectInterface
     */
    protected $subject;
}

An InvoiceSubjectInterface:

// src/Acme/InvoiceBundle/Model/InvoiceSubjectInterface.php

namespace Acme\InvoiceBundle\Model;

/**
 * An interface that the invoice Subject object should implement.
 * In most circumstances, only a single object should implement
 * this interface as the ResolveTargetEntityListener can only
 * change the target to a single object.
 */
interface InvoiceSubjectInterface
{
    // List any additional methods that your InvoiceBundle
    // will need to access on the subject so that you can
    // be sure that you have access to those methods.

    /**
     * @return string
     */
    public function getName();
}

Next, you need to configure the listener, which tells the DoctrineBundle about the replacement:

  • YAML
    # app/config/config.yml
    doctrine:
        # ...
        orm:
            # ...
            resolve_target_entities:
                Acme\InvoiceBundle\Model\InvoiceSubjectInterface: Acme\AppBundle\Entity\Customer
    
  • XML
    <!-- app/config/config.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <doctrine:config>
            <doctrine:orm>
                <!-- ... -->
                <doctrine:resolve-target-entity interface="Acme\InvoiceBundle\Model\InvoiceSubjectInterface">Acme\AppBundle\Entity\Customer</doctrine:resolve-target-entity>
            </doctrine:orm>
        </doctrine:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('doctrine', array(
        'orm' => array(
            // ...
            'resolve_target_entities' => array(
                'Acme\InvoiceBundle\Model\InvoiceSubjectInterface' => 'Acme\AppBundle\Entity\Customer',
            ),
        ),
    ));
    
Final Thoughts

With the ResolveTargetEntityListener, you are able to decouple your bundles, keeping them usable by themselves, but still being able to define relationships between different objects. By using this method, your bundles will end up being easier to maintain independently.

How to Provide Model Classes for several Doctrine Implementations

When building a bundle that could be used not only with Doctrine ORM but also the CouchDB ODM, MongoDB ODM or PHPCR ODM, you should still only write one model class. The Doctrine bundles provide a compiler pass to register the mappings for your model classes.

注解

For non-reusable bundles, the easiest option is to put your model classes in the default locations: Entity for the Doctrine ORM or Document for one of the ODMs. For reusable bundles, rather than duplicate model classes just to get the auto mapping, use the compiler pass.

2.3 新版功能: The base mapping compiler pass was introduced in Symfony 2.3. The Doctrine bundles support it from DoctrineBundle >= 1.3.0, MongoDBBundle >= 3.0.0, PHPCRBundle >= 1.0.0-alpha2 and the (unversioned) CouchDBBundle supports the compiler pass since the CouchDB Mapping Compiler Pass pull request was merged.

If you want your bundle to support older versions of Symfony and Doctrine, you can provide a copy of the compiler pass in your bundle. See for example the FOSUserBundle mapping configuration addRegisterMappingsPass.

In your bundle class, write the following code to register the compiler pass. This one is written for the FOSUserBundle, so parts of it will need to be adapted for your case:

use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass;
use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\DoctrineMongoDBMappingsPass;
use Doctrine\Bundle\CouchDBBundle\DependencyInjection\Compiler\DoctrineCouchDBMappingsPass;
use Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\DoctrinePhpcrMappingsPass;

class FOSUserBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        // ...

        $modelDir = realpath(__DIR__.'/Resources/config/doctrine/model');
        $mappings = array(
            $modelDir => 'FOS\UserBundle\Model',
        );

        $ormCompilerClass = 'Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass';
        if (class_exists($ormCompilerClass)) {
            $container->addCompilerPass(
                DoctrineOrmMappingsPass::createXmlMappingDriver(
                    $mappings,
                    array('fos_user.model_manager_name'),
                    'fos_user.backend_type_orm'
            ));
        }

        $mongoCompilerClass = 'Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\DoctrineMongoDBMappingsPass';
        if (class_exists($mongoCompilerClass)) {
            $container->addCompilerPass(
                DoctrineMongoDBMappingsPass::createXmlMappingDriver(
                    $mappings,
                    array('fos_user.model_manager_name'),
                    'fos_user.backend_type_mongodb'
            ));
        }

        $couchCompilerClass = 'Doctrine\Bundle\CouchDBBundle\DependencyInjection\Compiler\DoctrineCouchDBMappingsPass';
        if (class_exists($couchCompilerClass)) {
            $container->addCompilerPass(
                DoctrineCouchDBMappingsPass::createXmlMappingDriver(
                    $mappings,
                    array('fos_user.model_manager_name'),
                    'fos_user.backend_type_couchdb'
            ));
        }

        $phpcrCompilerClass = 'Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\DoctrinePhpcrMappingsPass';
        if (class_exists($phpcrCompilerClass)) {
            $container->addCompilerPass(
                DoctrinePhpcrMappingsPass::createXmlMappingDriver(
                    $mappings,
                    array('fos_user.model_manager_name'),
                    'fos_user.backend_type_phpcr'
            ));
        }
    }
}

Note the class_exists check. This is crucial, as you do not want your bundle to have a hard dependency on all Doctrine bundles but let the user decide which to use.

The compiler pass provides factory methods for all drivers provided by Doctrine: Annotations, XML, Yaml, PHP and StaticPHP. The arguments are:

  • a map/hash of absolute directory path to namespace;
  • an array of container parameters that your bundle uses to specify the name of the Doctrine manager that it is using. In the above example, the FOSUserBundle stores the manager name that’s being used under the fos_user.model_manager_name parameter. The compiler pass will append the parameter Doctrine is using to specify the name of the default manager. The first parameter found is used and the mappings are registered with that manager;
  • an optional container parameter name that will be used by the compiler pass to determine if this Doctrine type is used at all. This is relevant if your user has more than one type of Doctrine bundle installed, but your bundle is only used with one type of Doctrine.

注解

The factory method is using the SymfonyFileLocator of Doctrine, meaning it will only see XML and YML mapping files if they do not contain the full namespace as the filename. This is by design: the SymfonyFileLocator simplifies things by assuming the files are just the “short” version of the class as their filename (e.g. BlogPost.orm.xml)

If you also need to map a base class, you can register a compiler pass with the DefaultFileLocator like this. This code is simply taken from the DoctrineOrmMappingsPass and adapted to use the DefaultFileLocator instead of the SymfonyFileLocator:

private function buildMappingCompilerPass()
{
    $arguments = array(array(realpath(__DIR__ . '/Resources/config/doctrine-base')), '.orm.xml');
    $locator = new Definition('Doctrine\Common\Persistence\Mapping\Driver\DefaultFileLocator', $arguments);
    $driver = new Definition('Doctrine\ORM\Mapping\Driver\XmlDriver', array($locator));

    return new DoctrineOrmMappingsPass(
        $driver,
        array('Full\Namespace'),
        array('your_bundle.manager_name'),
        'your_bundle.orm_enabled'
    );
}

Now place your mapping file into /Resources/config/doctrine-base with the fully qualified class name, separated by . instead of \, for example Other.Namespace.Model.Name.orm.xml. You may not mix the two as otherwise the SymfonyFileLocator will get confused.

Adjust accordingly for the other Doctrine implementations.

How to Implement a simple Registration Form

Some forms have extra fields whose values don’t need to be stored in the database. For example, you may want to create a registration form with some extra fields (like a “terms accepted” checkbox field) and embed the form that actually stores the account information.

The simple User Model

You have a simple User entity mapped to the database:

// src/Acme/AccountBundle/Entity/User.php
namespace Acme\AccountBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Entity
 * @UniqueEntity(fields="email", message="Email already taken")
 */
class User
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    protected $email;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank()
     * @Assert\Length(max = 4096)
     */
    protected $plainPassword;

    public function getId()
    {
        return $this->id;
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }

    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }
}

This User entity contains three fields and two of them (email and plainPassword) should display on the form. The email property must be unique in the database, this is enforced by adding this validation at the top of the class.

注解

If you want to integrate this User within the security system, you need to implement the UserInterface of the Security component.

Create a Form for the Model

Next, create the form for the User model:

// src/Acme/AccountBundle/Form/Type/UserType.php
namespace Acme\AccountBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('email', 'email');
        $builder->add('plainPassword', 'repeated', array(
           'first_name'  => 'password',
           'second_name' => 'confirm',
           'type'        => 'password',
        ));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\AccountBundle\Entity\User'
        ));
    }

    public function getName()
    {
        return 'user';
    }
}

There are just two fields: email and plainPassword (repeated to confirm the entered password). The data_class option tells the form the name of the underlying data class (i.e. your User entity).

小技巧

To explore more things about the Form component, read Forms.

Embedding the User Form into a Registration Form

The form that you’ll use for the registration page is not the same as the form used to simply modify the User (i.e. UserType). The registration form will contain further fields like “accept the terms”, whose value won’t be stored in the database.

Start by creating a simple class which represents the “registration”:

// src/Acme/AccountBundle/Form/Model/Registration.php
namespace Acme\AccountBundle\Form\Model;

use Symfony\Component\Validator\Constraints as Assert;

use Acme\AccountBundle\Entity\User;

class Registration
{
    /**
     * @Assert\Type(type="Acme\AccountBundle\Entity\User")
     * @Assert\Valid()
     */
    protected $user;

    /**
     * @Assert\NotBlank()
     * @Assert\True()
     */
    protected $termsAccepted;

    public function setUser(User $user)
    {
        $this->user = $user;
    }

    public function getUser()
    {
        return $this->user;
    }

    public function getTermsAccepted()
    {
        return $this->termsAccepted;
    }

    public function setTermsAccepted($termsAccepted)
    {
        $this->termsAccepted = (Boolean) $termsAccepted;
    }
}

Next, create the form for this Registration model:

// src/Acme/AccountBundle/Form/Type/RegistrationType.php
namespace Acme\AccountBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('user', new UserType());
        $builder->add(
            'terms',
            'checkbox',
            array('property_path' => 'termsAccepted')
        );
        $builder->add('Register', 'submit');
    }

    public function getName()
    {
        return 'registration';
    }
}

You don’t need to use a special method for embedding the UserType form. A form is a field, too - so you can add this like any other field, with the expectation that the Registration.user property will hold an instance of the User class.

Handling the Form Submission

Next, you need a controller to handle the form. Start by creating a simple controller for displaying the registration form:

// src/Acme/AccountBundle/Controller/AccountController.php
namespace Acme\AccountBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Acme\AccountBundle\Form\Type\RegistrationType;
use Acme\AccountBundle\Form\Model\Registration;

class AccountController extends Controller
{
    public function registerAction()
    {
        $registration = new Registration();
        $form = $this->createForm(new RegistrationType(), $registration, array(
            'action' => $this->generateUrl('account_create'),
        ));

        return $this->render(
            'AcmeAccountBundle:Account:register.html.twig',
            array('form' => $form->createView())
        );
    }
}

And its template:

{# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #}
{{ form(form) }}

Next, create the controller which handles the form submission. This performs the validation and saves the data into the database:

use Symfony\Component\HttpFoundation\Request;
// ...

public function createAction(Request $request)
{
    $em = $this->getDoctrine()->getManager();

    $form = $this->createForm(new RegistrationType(), new Registration());

    $form->handleRequest($request);

    if ($form->isValid()) {
        $registration = $form->getData();

        $em->persist($registration->getUser());
        $em->flush();

        return $this->redirect(...);
    }

    return $this->render(
        'AcmeAccountBundle:Account:register.html.twig',
        array('form' => $form->createView())
    );
}
Add new Routes

Next, update your routes. If you’re placing your routes inside your bundle (as shown here), don’t forget to make sure that the routing file is being imported.

  • YAML
    # src/Acme/AccountBundle/Resources/config/routing.yml
    account_register:
        path:     /register
        defaults: { _controller: AcmeAccountBundle:Account:register }
    
    account_create:
        path:     /register/create
        defaults: { _controller: AcmeAccountBundle:Account:create }
    
  • XML
    <!-- src/Acme/AccountBundle/Resources/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="account_register" path="/register">
            <default key="_controller">AcmeAccountBundle:Account:register</default>
        </route>
    
        <route id="account_create" path="/register/create">
            <default key="_controller">AcmeAccountBundle:Account:create</default>
        </route>
    </routes>
    
  • PHP
    // src/Acme/AccountBundle/Resources/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('account_register', new Route('/register', array(
        '_controller' => 'AcmeAccountBundle:Account:register',
    )));
    $collection->add('account_create', new Route('/register/create', array(
        '_controller' => 'AcmeAccountBundle:Account:create',
    )));
    
    return $collection;
    
Update your Database Schema

Of course, since you’ve added a User entity during this tutorial, make sure that your database schema has been updated properly:

$ php app/console doctrine:schema:update --force

That’s it! Your form now validates, and allows you to save the User object to the database. The extra terms checkbox on the Registration model class is used during validation, but not actually used afterwards when saving the User to the database.

Console Commands

The Doctrine2 ORM integration offers several console commands under the doctrine namespace. To view the command list you can use the list command:

$ php app/console list doctrine

A list of available commands will print out. You can find out more information about any of these commands (or any Symfony command) by running the help command. For example, to get details about the doctrine:database:create task, run:

$ php app/console help doctrine:database:create

Some notable or interesting tasks include:

  • doctrine:ensure-production-settings - checks to see if the current environment is configured efficiently for production. This should always be run in the prod environment:

    $ php app/console doctrine:ensure-production-settings --env=prod
    
  • doctrine:mapping:import - allows Doctrine to introspect an existing database and create mapping information. For more information, see How to Generate Entities from an Existing Database.

  • doctrine:mapping:info - tells you all of the entities that Doctrine is aware of and whether or not there are any basic errors with the mapping.

  • doctrine:query:dql and doctrine:query:sql - allow you to execute DQL or SQL queries directly from the command line.

Email

How to Send an Email

Sending emails is a classic task for any web application and one that has special complications and potential pitfalls. Instead of recreating the wheel, one solution to send emails is to use the SwiftmailerBundle, which leverages the power of the Swift Mailer library. This bundle comes with the Symfony Standard Edition.

Configuration

To use Swift Mailer, you’ll need to configure it for your mail server.

小技巧

Instead of setting up/using your own mail server, you may want to use a hosted mail provider such as Mandrill, SendGrid, Amazon SES or others. These give you an SMTP server, username and password (sometimes called keys) that can be used with the Swift Mailer configuration.

In a standard Symfony installation, some swiftmailer configuration is already included:

  • YAML
    # app/config/config.yml
    swiftmailer:
        transport: "%mailer_transport%"
        host:      "%mailer_host%"
        username:  "%mailer_user%"
        password:  "%mailer_password%"
    
  • XML
    <!-- app/config/config.xml -->
    
    <!--
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd
    -->
    
    <swiftmailer:config
        transport="%mailer_transport%"
        host="%mailer_host%"
        username="%mailer_user%"
        password="%mailer_password%" />
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('swiftmailer', array(
        'transport'  => "%mailer_transport%",
        'host'       => "%mailer_host%",
        'username'   => "%mailer_user%",
        'password'   => "%mailer_password%",
    ));
    

These values (e.g. %mailer_transport%), are reading from the parameters that are set in the parameters.yml file. You can modify the values in that file, or set the values directly here.

The following configuration attributes are available:

  • transport (smtp, mail, sendmail, or gmail)
  • username
  • password
  • host
  • port
  • encryption (tls, or ssl)
  • auth_mode (plain, login, or cram-md5)
  • spool
    • type (how to queue the messages, file or memory is supported, see How to Spool Emails)
    • path (where to store the messages)
  • delivery_address (an email address where to send ALL emails)
  • disable_delivery (set to true to disable delivery completely)
Sending Emails

The Swift Mailer library works by creating, configuring and then sending Swift_Message objects. The “mailer” is responsible for the actual delivery of the message and is accessible via the mailer service. Overall, sending an email is pretty straightforward:

public function indexAction($name)
{
    $mailer = $this->get('mailer');
    $message = $mailer->createMessage()
        ->setSubject('You have Completed Registration!')
        ->setFrom('send@example.com')
        ->setTo('recipient@example.com')
        ->setBody(
            $this->renderView(
                // app/Resources/views/Emails/registration.html.twig
                'Emails/registration.html.twig',
                array('name' => $name)
            ),
            'text/html'
        )
        /*
         * If you also want to include a plaintext version of the message
        ->addPart(
            $this->renderView(
                'Emails/registration.txt.twig',
                array('name' => $name)
            ),
            'text/plain'
        )
        */
    ;
    $mailer->send($message);

    return $this->render(...);
}

To keep things decoupled, the email body has been stored in a template and rendered with the renderView() method.

The $message object supports many more options, such as including attachments, adding HTML content, and much more. Fortunately, Swift Mailer covers the topic of Creating Messages in great detail in its documentation.

小技巧

Several other cookbook articles are available related to sending emails in Symfony:

How to Use Gmail to Send Emails

During development, instead of using a regular SMTP server to send emails, you might find using Gmail easier and more practical. The SwiftmailerBundle makes it really easy.

小技巧

Instead of using your regular Gmail account, it’s of course recommended that you create a special account.

In the development configuration file, change the transport setting to gmail and set the username and password to the Google credentials:

  • YAML
    # app/config/config_dev.yml
    swiftmailer:
        transport: gmail
        username:  your_gmail_username
        password:  your_gmail_password
    
  • XML
    <!-- app/config/config_dev.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/swiftmailer
            http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd">
    
        <!-- ... -->
        <swiftmailer:config
            transport="gmail"
            username="your_gmail_username"
            password="your_gmail_password"
        />
    </container>
    
  • PHP
    // app/config/config_dev.php
    $container->loadFromExtension('swiftmailer', array(
        'transport' => 'gmail',
        'username'  => 'your_gmail_username',
        'password'  => 'your_gmail_password',
    ));
    

You’re done!

小技巧

If you are using the Symfony Standard Edition, configure the parameters in parameters.yml:

# app/config/parameters.yml
parameters:
    # ...
    mailer_transport: gmail
    mailer_host:      ~
    mailer_user:      your_gmail_username
    mailer_password:  your_gmail_password

注解

The gmail transport is simply a shortcut that uses the smtp transport and sets encryption, auth_mode and host to work with Gmail.

How to Use the Cloud to Send Emails

Requirements for sending emails from a production system differ from your development setup as you don’t want to be limited in the number of emails, the sending rate or the sender address. Thus, using Gmail or similar services is not an option. If setting up and maintaining your own reliable mail server causes you a headache there’s a simple solution: Leverage the cloud to send your emails.

This cookbook shows how easy it is to integrate Amazon’s Simple Email Service (SES) into Symfony.

注解

You can use the same technique for other mail services, as most of the time there is nothing more to it than configuring an SMTP endpoint for Swift Mailer.

In the Symfony configuration, change the Swift Mailer settings transport, host, port and encryption according to the information provided in the SES console. Create your individual SMTP credentials in the SES console and complete the configuration with the provided username and password:

  • YAML
    # app/config/config.yml
    swiftmailer:
        transport:  smtp
        host:       email-smtp.us-east-1.amazonaws.com
        port:       465 # different ports are available, see SES console
        encryption: tls # TLS encryption is required
        username:   AWS_ACCESS_KEY  # to be created in the SES console
        password:   AWS_SECRET_KEY  # to be created in the SES console
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/swiftmailer
            http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd">
    
        <!-- ... -->
        <swiftmailer:config
            transport="smtp"
            host="email-smtp.us-east-1.amazonaws.com"
            port="465"
            encryption="tls"
            username="AWS_ACCESS_KEY"
            password="AWS_SECRET_KEY"
        />
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('swiftmailer', array(
        'transport'  => 'smtp',
        'host'       => 'email-smtp.us-east-1.amazonaws.com',
        'port'       => 465,
        'encryption' => 'tls',
        'username'   => 'AWS_ACCESS_KEY',
        'password'   => 'AWS_SECRET_KEY',
    ));
    

The port and encryption keys are not present in the Symfony Standard Edition configuration by default, but you can simply add them as needed.

And that’s it, you’re ready to start sending emails through the cloud!

小技巧

If you are using the Symfony Standard Edition, configure the parameters in parameters.yml and use them in your configuration files. This allows for different Swift Mailer configurations for each installation of your application. For instance, use Gmail during development and the cloud in production.

# app/config/parameters.yml
parameters:
    # ...
    mailer_transport:  smtp
    mailer_host:       email-smtp.us-east-1.amazonaws.com
    mailer_port:       465 # different ports are available, see SES console
    mailer_encryption: tls # TLS encryption is required
    mailer_user:       AWS_ACCESS_KEY # to be created in the SES console
    mailer_password:   AWS_SECRET_KEY # to be created in the SES console

注解

If you intend to use Amazon SES, please note the following:

  • You have to sign up to Amazon Web Services (AWS);
  • Every sender address used in the From or Return-Path (bounce address) header needs to be confirmed by the owner. You can also confirm an entire domain;
  • Initially you are in a restricted sandbox mode. You need to request production access before being allowed to send to arbitrary recipients;
  • SES may be subject to a charge.
How to Work with Emails during Development

When developing an application which sends email, you will often not want to actually send the email to the specified recipient during development. If you are using the SwiftmailerBundle with Symfony, you can easily achieve this through configuration settings without having to make any changes to your application’s code at all. There are two main choices when it comes to handling email during development: (a) disabling the sending of email altogether or (b) sending all email to a specific address.

Disabling Sending

You can disable sending email by setting the disable_delivery option to true. This is the default in the test environment in the Standard distribution. If you do this in the test specific config then email will not be sent when you run tests, but will continue to be sent in the prod and dev environments:

  • YAML
    # app/config/config_test.yml
    swiftmailer:
        disable_delivery:  true
    
  • XML
    <!-- app/config/config_test.xml -->
    
    <!--
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd
    -->
    
    <swiftmailer:config
        disable-delivery="true" />
    
  • PHP
    // app/config/config_test.php
    $container->loadFromExtension('swiftmailer', array(
        'disable_delivery'  => "true",
    ));
    

If you’d also like to disable deliver in the dev environment, simply add this same configuration to the config_dev.yml file.

Sending to a Specified Address

You can also choose to have all email sent to a specific address, instead of the address actually specified when sending the message. This can be done via the delivery_address option:

  • YAML
    # app/config/config_dev.yml
    swiftmailer:
        delivery_address: dev@example.com
    
  • XML
    <!-- app/config/config_dev.xml -->
    
    <!--
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd
    -->
    
    <swiftmailer:config delivery-address="dev@example.com" />
    
  • PHP
    // app/config/config_dev.php
    $container->loadFromExtension('swiftmailer', array(
        'delivery_address'  => "dev@example.com",
    ));
    

Now, suppose you’re sending an email to recipient@example.com.

public function indexAction($name)
{
    $message = \Swift_Message::newInstance()
        ->setSubject('Hello Email')
        ->setFrom('send@example.com')
        ->setTo('recipient@example.com')
        ->setBody(
            $this->renderView(
                'HelloBundle:Hello:email.txt.twig',
                array('name' => $name)
            )
        )
    ;
    $this->get('mailer')->send($message);

    return $this->render(...);
}

In the dev environment, the email will instead be sent to dev@example.com. Swift Mailer will add an extra header to the email, X-Swift-To, containing the replaced address, so you can still see who it would have been sent to.

注解

In addition to the to addresses, this will also stop the email being sent to any CC and BCC addresses set for it. Swift Mailer will add additional headers to the email with the overridden addresses in them. These are X-Swift-Cc and X-Swift-Bcc for the CC and BCC addresses respectively.

Viewing from the Web Debug Toolbar

You can view any email sent during a single response when you are in the dev environment using the Web Debug Toolbar. The email icon in the toolbar will show how many emails were sent. If you click it, a report will open showing the details of the sent emails.

If you’re sending an email and then immediately redirecting to another page, the web debug toolbar will not display an email icon or a report on the next page.

Instead, you can set the intercept_redirects option to true in the config_dev.yml file, which will cause the redirect to stop and allow you to open the report with details of the sent emails.

  • YAML
    # app/config/config_dev.yml
    web_profiler:
        intercept_redirects: true
    
  • XML
    <!-- app/config/config_dev.xml -->
    
    <!--
        xmlns:webprofiler="http://symfony.com/schema/dic/webprofiler"
        xsi:schemaLocation="http://symfony.com/schema/dic/webprofiler
        http://symfony.com/schema/dic/webprofiler/webprofiler-1.0.xsd">
    -->
    
    <webprofiler:config
        intercept-redirects="true"
    />
    
  • PHP
    // app/config/config_dev.php
    $container->loadFromExtension('web_profiler', array(
        'intercept_redirects' => 'true',
    ));
    

小技巧

Alternatively, you can open the profiler after the redirect and search by the submit URL used on the previous request (e.g. /contact/handle). The profiler’s search feature allows you to load the profiler information for any past requests.

How to Spool Emails

When you are using the SwiftmailerBundle to send an email from a Symfony application, it will default to sending the email immediately. You may, however, want to avoid the performance hit of the communication between Swift Mailer and the email transport, which could cause the user to wait for the next page to load while the email is sending. This can be avoided by choosing to “spool” the emails instead of sending them directly. This means that Swift Mailer does not attempt to send the email but instead saves the message to somewhere such as a file. Another process can then read from the spool and take care of sending the emails in the spool. Currently only spooling to file or memory is supported by Swift Mailer.

Spool Using Memory

When you use spooling to store the emails to memory, they will get sent right before the kernel terminates. This means the email only gets sent if the whole request got executed without any unhandled Exception or any errors. To configure swiftmailer with the memory option, use the following configuration:

  • YAML
    # app/config/config.yml
    swiftmailer:
        # ...
        spool: { type: memory }
    
  • XML
    <!-- app/config/config.xml -->
    
    <!--
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        http://symfony.com/schema/dic/swiftmailer
        http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd
    -->
    
    <swiftmailer:config>
         <swiftmailer:spool type="memory" />
    </swiftmailer:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('swiftmailer', array(
         // ...
        'spool' => array('type' => 'memory')
    ));
    
Spool Using a File

In order to use the spool with a file, use the following configuration:

  • YAML
    # app/config/config.yml
    swiftmailer:
        # ...
        spool:
            type: file
            path: /path/to/spool
    
  • XML
    <!-- app/config/config.xml -->
    
    <!--
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        http://symfony.com/schema/dic/swiftmailer
        http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd
    -->
    
    <swiftmailer:config>
         <swiftmailer:spool
             type="file"
             path="/path/to/spool" />
    </swiftmailer:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('swiftmailer', array(
         // ...
    
        'spool' => array(
            'type' => 'file',
            'path' => '/path/to/spool',
        ),
    ));
    

小技巧

If you want to store the spool somewhere with your project directory, remember that you can use the %kernel.root_dir% parameter to reference the project’s root:

path: "%kernel.root_dir%/spool"

Now, when your app sends an email, it will not actually be sent but instead added to the spool. Sending the messages from the spool is done separately. There is a console command to send the messages in the spool:

$ php app/console swiftmailer:spool:send --env=prod

It has an option to limit the number of messages to be sent:

$ php app/console swiftmailer:spool:send --message-limit=10 --env=prod

You can also set the time limit in seconds:

$ php app/console swiftmailer:spool:send --time-limit=10 --env=prod

Of course you will not want to run this manually in reality. Instead, the console command should be triggered by a cron job or scheduled task and run at a regular interval.

How to Test that an Email is Sent in a functional Test

Sending e-mails with Symfony is pretty straightforward thanks to the SwiftmailerBundle, which leverages the power of the Swift Mailer library.

To functionally test that an email was sent, and even assert the email subject, content or any other headers, you can use the Symfony Profiler.

Start with an easy controller action that sends an e-mail:

public function sendEmailAction($name)
{
    $message = \Swift_Message::newInstance()
        ->setSubject('Hello Email')
        ->setFrom('send@example.com')
        ->setTo('recipient@example.com')
        ->setBody('You should see me from the profiler!')
    ;

    $this->get('mailer')->send($message);

    return $this->render(...);
}

注解

Don’t forget to enable the profiler as explained in How to Use the Profiler in a Functional Test.

In your functional test, use the swiftmailer collector on the profiler to get information about the messages send on the previous request:

// src/AppBundle/Tests/Controller/MailControllerTest.php
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MailControllerTest extends WebTestCase
{
    public function testMailIsSentAndContentIsOk()
    {
        $client = static::createClient();

        // Enable the profiler for the next request (it does nothing if the profiler is not available)
        $client->enableProfiler();

        $crawler = $client->request('POST', '/path/to/above/action');

        $mailCollector = $client->getProfile()->getCollector('swiftmailer');

        // Check that an e-mail was sent
        $this->assertEquals(1, $mailCollector->getMessageCount());

        $collectedMessages = $mailCollector->getMessages();
        $message = $collectedMessages[0];

        // Asserting e-mail data
        $this->assertInstanceOf('Swift_Message', $message);
        $this->assertEquals('Hello Email', $message->getSubject());
        $this->assertEquals('send@example.com', key($message->getFrom()));
        $this->assertEquals('recipient@example.com', key($message->getTo()));
        $this->assertEquals(
            'You should see me from the profiler!',
            $message->getBody()
        );
    }
}

Event Dispatcher

How to Setup before and after Filters

It is quite common in web application development to need some logic to be executed just before or just after your controller actions acting as filters or hooks.

In symfony1, this was achieved with the preExecute and postExecute methods. Most major frameworks have similar methods but there is no such thing in Symfony. The good news is that there is a much better way to interfere with the Request -> Response process using the EventDispatcher component.

Token Validation Example

Imagine that you need to develop an API where some controllers are public but some others are restricted to one or some clients. For these private features, you might provide a token to your clients to identify themselves.

So, before executing your controller action, you need to check if the action is restricted or not. If it is restricted, you need to validate the provided token.

注解

Please note that for simplicity in this recipe, tokens will be defined in config and neither database setup nor authentication via the Security component will be used.

Before Filters with the kernel.controller Event

First, store some basic token configuration using config.yml and the parameters key:

  • YAML
    # app/config/config.yml
    parameters:
        tokens:
            client1: pass1
            client2: pass2
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="tokens" type="collection">
            <parameter key="client1">pass1</parameter>
            <parameter key="client2">pass2</parameter>
        </parameter>
    </parameters>
    
  • PHP
    // app/config/config.php
    $container->setParameter('tokens', array(
        'client1' => 'pass1',
        'client2' => 'pass2',
    ));
    
Tag Controllers to Be Checked

A kernel.controller listener gets notified on every request, right before the controller is executed. So, first, you need some way to identify if the controller that matches the request needs token validation.

A clean and easy way is to create an empty interface and make the controllers implement it:

namespace AppBundle\Controller;

interface TokenAuthenticatedController
{
    // ...
}

A controller that implements this interface simply looks like this:

namespace AppBundle\Controller;

use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class FooController extends Controller implements TokenAuthenticatedController
{
    // An action that needs authentication
    public function barAction()
    {
        // ...
    }
}
Creating an Event Listener

Next, you’ll need to create an event listener, which will hold the logic that you want executed before your controllers. If you’re not familiar with event listeners, you can learn more about them at How to Create an Event Listener:

// src/AppBundle/EventListener/TokenListener.php
namespace AppBundle\EventListener;

use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

class TokenListener
{
    private $tokens;

    public function __construct($tokens)
    {
        $this->tokens = $tokens;
    }

    public function onKernelController(FilterControllerEvent $event)
    {
        $controller = $event->getController();

        /*
         * $controller passed can be either a class or a Closure.
         * This is not usual in Symfony but it may happen.
         * If it is a class, it comes in array format
         */
        if (!is_array($controller)) {
            return;
        }

        if ($controller[0] instanceof TokenAuthenticatedController) {
            $token = $event->getRequest()->query->get('token');
            if (!in_array($token, $this->tokens)) {
                throw new AccessDeniedHttpException('This action needs a valid token!');
            }
        }
    }
}
Registering the Listener

Finally, register your listener as a service and tag it as an event listener. By listening on kernel.controller, you’re telling Symfony that you want your listener to be called just before any controller is executed.

  • YAML
    # app/config/services.yml
    services:
        app.tokens.action_listener:
            class: AppBundle\EventListener\TokenListener
            arguments: ["%tokens%"]
            tags:
                - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
    
  • XML
    <!-- app/config/services.xml -->
    <service id="app.tokens.action_listener" class="AppBundle\EventListener\TokenListener">
        <argument>%tokens%</argument>
        <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
    </service>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $listener = new Definition('AppBundle\EventListener\TokenListener', array('%tokens%'));
    $listener->addTag('kernel.event_listener', array(
        'event'  => 'kernel.controller',
        'method' => 'onKernelController'
    ));
    $container->setDefinition('app.tokens.action_listener', $listener);
    

With this configuration, your TokenListener onKernelController method will be executed on each request. If the controller that is about to be executed implements TokenAuthenticatedController, token authentication is applied. This lets you have a “before” filter on any controller that you want.

After Filters with the kernel.response Event

In addition to having a “hook” that’s executed before your controller, you can also add a hook that’s executed after your controller. For this example, imagine that you want to add a sha1 hash (with a salt using that token) to all responses that have passed this token authentication.

Another core Symfony event - called kernel.response - is notified on every request, but after the controller returns a Response object. Creating an “after” listener is as easy as creating a listener class and registering it as a service on this event.

For example, take the TokenListener from the previous example and first record the authentication token inside the request attributes. This will serve as a basic flag that this request underwent token authentication:

public function onKernelController(FilterControllerEvent $event)
{
    // ...

    if ($controller[0] instanceof TokenAuthenticatedController) {
        $token = $event->getRequest()->query->get('token');
        if (!in_array($token, $this->tokens)) {
            throw new AccessDeniedHttpException('This action needs a valid token!');
        }

        // mark the request as having passed token authentication
        $event->getRequest()->attributes->set('auth_token', $token);
    }
}

Now, add another method to this class - onKernelResponse - that looks for this flag on the request object and sets a custom header on the response if it’s found:

// add the new use statement at the top of your file
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

public function onKernelResponse(FilterResponseEvent $event)
{
    // check to see if onKernelController marked this as a token "auth'ed" request
    if (!$token = $event->getRequest()->attributes->get('auth_token')) {
        return;
    }

    $response = $event->getResponse();

    // create a hash and set it as a response header
    $hash = sha1($response->getContent().$token);
    $response->headers->set('X-CONTENT-HASH', $hash);
}

Finally, a second “tag” is needed in the service definition to notify Symfony that the onKernelResponse event should be notified for the kernel.response event:

  • YAML
    # app/config/services.yml
    services:
        app.tokens.action_listener:
            class: AppBundle\EventListener\TokenListener
            arguments: ["%tokens%"]
            tags:
                - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
                - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
    
  • XML
    <!-- app/config/services.xml -->
    <service id="app.tokens.action_listener" class="AppBundle\EventListener\TokenListener">
        <argument>%tokens%</argument>
        <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
        <tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" />
    </service>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $listener = new Definition('AppBundle\EventListener\TokenListener', array('%tokens%'));
    $listener->addTag('kernel.event_listener', array(
        'event'  => 'kernel.controller',
        'method' => 'onKernelController'
    ));
    $listener->addTag('kernel.event_listener', array(
        'event'  => 'kernel.response',
        'method' => 'onKernelResponse'
    ));
    $container->setDefinition('app.tokens.action_listener', $listener);
    

That’s it! The TokenListener is now notified before every controller is executed (onKernelController) and after every controller returns a response (onKernelResponse). By making specific controllers implement the TokenAuthenticatedController interface, your listener knows which controllers it should take action on. And by storing a value in the request’s “attributes” bag, the onKernelResponse method knows to add the extra header. Have fun!

How to Extend a Class without Using Inheritance

To allow multiple classes to add methods to another one, you can define the magic __call() method in the class you want to be extended like this:

class Foo
{
    // ...

    public function __call($method, $arguments)
    {
        // create an event named 'foo.method_is_not_found'
        $event = new HandleUndefinedMethodEvent($this, $method, $arguments);
        $this->dispatcher->dispatch('foo.method_is_not_found', $event);

        // no listener was able to process the event? The method does not exist
        if (!$event->isProcessed()) {
            throw new \Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
        }

        // return the listener returned value
        return $event->getReturnValue();
    }
}

This uses a special HandleUndefinedMethodEvent that should also be created. This is a generic class that could be reused each time you need to use this pattern of class extension:

use Symfony\Component\EventDispatcher\Event;

class HandleUndefinedMethodEvent extends Event
{
    protected $subject;
    protected $method;
    protected $arguments;
    protected $returnValue;
    protected $isProcessed = false;

    public function __construct($subject, $method, $arguments)
    {
        $this->subject = $subject;
        $this->method = $method;
        $this->arguments = $arguments;
    }

    public function getSubject()
    {
        return $this->subject;
    }

    public function getMethod()
    {
        return $this->method;
    }

    public function getArguments()
    {
        return $this->arguments;
    }

    /**
     * Sets the value to return and stops other listeners from being notified
     */
    public function setReturnValue($val)
    {
        $this->returnValue = $val;
        $this->isProcessed = true;
        $this->stopPropagation();
    }

    public function getReturnValue()
    {
        return $this->returnValue;
    }

    public function isProcessed()
    {
        return $this->isProcessed;
    }
}

Next, create a class that will listen to the foo.method_is_not_found event and add the method bar():

class Bar
{
    public function onFooMethodIsNotFound(HandleUndefinedMethodEvent $event)
    {
        // only respond to the calls to the 'bar' method
        if ('bar' != $event->getMethod()) {
            // allow another listener to take care of this unknown method
            return;
        }

        // the subject object (the foo instance)
        $foo = $event->getSubject();

        // the bar method arguments
        $arguments = $event->getArguments();

        // ... do something

        // set the return value
        $event->setReturnValue($someValue);
    }
}

Finally, add the new bar method to the Foo class by registering an instance of Bar with the foo.method_is_not_found event:

$bar = new Bar();
$dispatcher->addListener('foo.method_is_not_found', array($bar, 'onFooMethodIsNotFound'));
How to Customize a Method Behavior without Using Inheritance
Doing something before or after a Method Call

If you want to do something just before, or just after a method is called, you can dispatch an event respectively at the beginning or at the end of the method:

class Foo
{
    // ...

    public function send($foo, $bar)
    {
        // do something before the method
        $event = new FilterBeforeSendEvent($foo, $bar);
        $this->dispatcher->dispatch('foo.pre_send', $event);

        // get $foo and $bar from the event, they may have been modified
        $foo = $event->getFoo();
        $bar = $event->getBar();

        // the real method implementation is here
        $ret = ...;

        // do something after the method
        $event = new FilterSendReturnValue($ret);
        $this->dispatcher->dispatch('foo.post_send', $event);

        return $event->getReturnValue();
    }
}

In this example, two events are thrown: foo.pre_send, before the method is executed, and foo.post_send after the method is executed. Each uses a custom Event class to communicate information to the listeners of the two events. These event classes would need to be created by you and should allow, in this example, the variables $foo, $bar and $ret to be retrieved and set by the listeners.

For example, assuming the FilterSendReturnValue has a setReturnValue method, one listener might look like this:

public function onFooPostSend(FilterSendReturnValue $event)
{
    $ret = $event->getReturnValue();
    // modify the original ``$ret`` value

    $event->setReturnValue($ret);
}

Form

How to Customize Form Rendering

Symfony gives you a wide variety of ways to customize how a form is rendered. In this guide, you’ll learn how to customize every possible part of your form with as little effort as possible whether you use Twig or PHP as your templating engine.

Form Rendering Basics

Recall that the label, error and HTML widget of a form field can easily be rendered by using the form_row Twig function or the row PHP helper method:

  • Twig
    {{ form_row(form.age) }}
    
  • PHP
    <?php echo $view['form']->row($form['age']); ?>
    

You can also render each of the three parts of the field individually:

  • Twig
    <div>
        {{ form_label(form.age) }}
        {{ form_errors(form.age) }}
        {{ form_widget(form.age) }}
    </div>
    
  • PHP
    <div>
        <?php echo $view['form']->label($form['age']); ?>
        <?php echo $view['form']->errors($form['age']); ?>
        <?php echo $view['form']->widget($form['age']); ?>
    </div>
    

In both cases, the form label, errors and HTML widget are rendered by using a set of markup that ships standard with Symfony. For example, both of the above templates would render:

<div>
    <label for="form_age">Age</label>
    <ul>
        <li>This field is required</li>
    </ul>
    <input type="number" id="form_age" name="form[age]" />
</div>

To quickly prototype and test a form, you can render the entire form with just one line:

  • Twig
    {# renders all fields #}
    {{ form_widget(form) }}
    
    {# renders all fields *and* the form start and end tags #}
    {{ form(form) }}
    
  • PHP
    <!-- renders all fields -->
    <?php echo $view['form']->widget($form) ?>
    
    <!-- renders all fields *and* the form start and end tags -->
    <?php echo $view['form']->form($form) ?>
    

The remainder of this recipe will explain how every part of the form’s markup can be modified at several different levels. For more information about form rendering in general, see Rendering a Form in a Template.

What are Form Themes?

Symfony uses form fragments - a small piece of a template that renders just one part of a form - to render each part of a form - field labels, errors, input text fields, select tags, etc.

The fragments are defined as blocks in Twig and as template files in PHP.

A theme is nothing more than a set of fragments that you want to use when rendering a form. In other words, if you want to customize one portion of how a form is rendered, you’ll import a theme which contains a customization of the appropriate form fragments.

Symfony comes with a default theme (form_div_layout.html.twig in Twig and FrameworkBundle:Form in PHP) that defines each and every fragment needed to render every part of a form.

In the next section you will learn how to customize a theme by overriding some or all of its fragments.

For example, when the widget of an integer type field is rendered, an input number field is generated

  • Twig
    {{ form_widget(form.age) }}
    
  • PHP
    <?php echo $view['form']->widget($form['age']) ?>
    

renders:

<input type="number" id="form_age" name="form[age]" required="required" value="33" />

Internally, Symfony uses the integer_widget fragment to render the field. This is because the field type is integer and you’re rendering its widget (as opposed to its label or errors).

In Twig that would default to the block integer_widget from the form_div_layout.html.twig template.

In PHP it would rather be the integer_widget.html.php file located in the FrameworkBundle/Resources/views/Form folder.

The default implementation of the integer_widget fragment looks like this:

  • Twig
    {# form_div_layout.html.twig #}
    {% block integer_widget %}
        {% set type = type|default('number') %}
        {{ block('form_widget_simple') }}
    {% endblock integer_widget %}
    
  • PHP
    <!-- integer_widget.html.php -->
    <?php echo $view['form']->block($form, 'form_widget_simple', array('type' => isset($type) ? $type : "number")) ?>
    

As you can see, this fragment itself renders another fragment - form_widget_simple:

  • Twig
    {# form_div_layout.html.twig #}
    {% block form_widget_simple %}
        {% set type = type|default('text') %}
        <input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
    {% endblock form_widget_simple %}
    
  • PHP
    <!-- FrameworkBundle/Resources/views/Form/form_widget_simple.html.php -->
    <input
        type="<?php echo isset($type) ? $view->escape($type) : 'text' ?>"
        <?php if (!empty($value)): ?>value="<?php echo $view->escape($value) ?>"<?php endif ?>
        <?php echo $view['form']->block($form, 'widget_attributes') ?>
    />
    

The point is, the fragments dictate the HTML output of each part of a form. To customize the form output, you just need to identify and override the correct fragment. A set of these form fragment customizations is known as a form “theme”. When rendering a form, you can choose which form theme(s) you want to apply.

In Twig a theme is a single template file and the fragments are the blocks defined in this file.

In PHP a theme is a folder and the fragments are individual template files in this folder.

Form Theming

To see the power of form theming, suppose you want to wrap every input number field with a div tag. The key to doing this is to customize the integer_widget fragment.

Form Theming in Twig

When customizing the form field block in Twig, you have two options on where the customized form block can live:

Method Pros Cons
Inside the same template as the form Quick and easy Can’t be reused in other templates
Inside a separate template Can be reused by many templates Requires an extra template to be created

Both methods have the same effect but are better in different situations.

Method 1: Inside the same Template as the Form

The easiest way to customize the integer_widget block is to customize it directly in the template that’s actually rendering the form.

{% extends '::base.html.twig' %}

{% form_theme form _self %}

{% block integer_widget %}
    <div class="integer_widget">
        {% set type = type|default('number') %}
        {{ block('form_widget_simple') }}
    </div>
{% endblock %}

{% block content %}
    {# ... render the form #}

    {{ form_row(form.age) }}
{% endblock %}

By using the special {% form_theme form _self %} tag, Twig looks inside the same template for any overridden form blocks. Assuming the form.age field is an integer type field, when its widget is rendered, the customized integer_widget block will be used.

The disadvantage of this method is that the customized form block can’t be reused when rendering other forms in other templates. In other words, this method is most useful when making form customizations that are specific to a single form in your application. If you want to reuse a form customization across several (or all) forms in your application, read on to the next section.

Method 2: Inside a separate Template

You can also choose to put the customized integer_widget form block in a separate template entirely. The code and end-result are the same, but you can now re-use the form customization across many templates:

{# src/AppBundle/Resources/views/Form/fields.html.twig #}
{% block integer_widget %}
    <div class="integer_widget">
        {% set type = type|default('number') %}
        {{ block('form_widget_simple') }}
    </div>
{% endblock %}

Now that you’ve created the customized form block, you need to tell Symfony to use it. Inside the template where you’re actually rendering your form, tell Symfony to use the template via the form_theme tag:

{% form_theme form 'AppBundle:Form:fields.html.twig' %}

{{ form_widget(form.age) }}

When the form.age widget is rendered, Symfony will use the integer_widget block from the new template and the input tag will be wrapped in the div element specified in the customized block.

Multiple Templates

A form can also be customized by applying several templates. To do this, pass the name of all the templates as an array using the with keyword:

{% form_theme form with ['::common.html.twig', ':Form:fields.html.twig',
                         'AppBundle:Form:fields.html.twig'] %}

{# ... #}

The templates can be located at different bundles and they can even be stored at the global app/Resources/views/ directory.

Child Forms

You can also apply a form theme to a specific child of your form:

{% form_theme form.child 'AppBundle:Form:fields.html.twig' %}

This is useful when you want to have a custom theme for a nested form that’s different than the one of your main form. Just specify both your themes:

{% form_theme form 'AppBundle:Form:fields.html.twig' %}

{% form_theme form.child 'AppBundle:Form:fields_child.html.twig' %}
Form Theming in PHP

When using PHP as a templating engine, the only method to customize a fragment is to create a new template file - this is similar to the second method used by Twig.

The template file must be named after the fragment. You must create a integer_widget.html.php file in order to customize the integer_widget fragment.

<!-- src/AppBundle/Resources/views/Form/integer_widget.html.php -->
<div class="integer_widget">
    <?php echo $view['form']->block($form, 'form_widget_simple', array('type' => isset($type) ? $type : "number")) ?>
</div>

Now that you’ve created the customized form template, you need to tell Symfony to use it. Inside the template where you’re actually rendering your form, tell Symfony to use the theme via the setTheme helper method:

<?php $view['form']->setTheme($form, array('AppBundle:Form')); ?>

<?php $view['form']->widget($form['age']) ?>

When the form.age widget is rendered, Symfony will use the customized integer_widget.html.php template and the input tag will be wrapped in the div element.

If you want to apply a theme to a specific child form, pass it to the setTheme method:

<?php $view['form']->setTheme($form['child'], 'AppBundle:Form/Child'); ?>
Referencing base Form Blocks (Twig specific)

So far, to override a particular form block, the best method is to copy the default block from form_div_layout.html.twig, paste it into a different template, and then customize it. In many cases, you can avoid doing this by referencing the base block when customizing it.

This is easy to do, but varies slightly depending on if your form block customizations are in the same template as the form or a separate template.

Referencing Blocks from inside the same Template as the Form

Import the blocks by adding a use tag in the template where you’re rendering the form:

{% use 'form_div_layout.html.twig' with integer_widget as base_integer_widget %}

Now, when the blocks from form_div_layout.html.twig are imported, the integer_widget block is called base_integer_widget. This means that when you redefine the integer_widget block, you can reference the default markup via base_integer_widget:

{% block integer_widget %}
    <div class="integer_widget">
        {{ block('base_integer_widget') }}
    </div>
{% endblock %}
Referencing base Blocks from an external Template

If your form customizations live inside an external template, you can reference the base block by using the parent() Twig function:

{# src/AppBundle/Resources/views/Form/fields.html.twig #}
{% extends 'form_div_layout.html.twig' %}

{% block integer_widget %}
    <div class="integer_widget">
        {{ parent() }}
    </div>
{% endblock %}

注解

It is not possible to reference the base block when using PHP as the templating engine. You have to manually copy the content from the base block to your new template file.

Making Application-wide Customizations

If you’d like a certain form customization to be global to your application, you can accomplish this by making the form customizations in an external template and then importing it inside your application configuration.

Twig

By using the following configuration, any customized form blocks inside the AppBundle:Form:fields.html.twig template will be used globally when a form is rendered.

  • YAML
    # app/config/config.yml
    twig:
        form:
            resources:
                - 'AppBundle:Form:fields.html.twig'
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config>
        <twig:form>
            <resource>AppBundle:Form:fields.html.twig</resource>
        </twig:form>
        <!-- ... -->
    </twig:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'form' => array(
            'resources' => array(
                'AppBundle:Form:fields.html.twig',
            ),
        ),
    
        // ...
    ));
    

By default, Twig uses a div layout when rendering forms. Some people, however, may prefer to render forms in a table layout. Use the form_table_layout.html.twig resource to use such a layout:

  • YAML
    # app/config/config.yml
    twig:
        form:
            resources:
                - 'form_table_layout.html.twig'
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config>
        <twig:form>
            <resource>form_table_layout.html.twig</resource>
        </twig:form>
        <!-- ... -->
    </twig:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'form' => array(
            'resources' => array(
                'form_table_layout.html.twig',
            ),
        ),
    
        // ...
    ));
    

If you only want to make the change in one template, add the following line to your template file rather than adding the template as a resource:

{% form_theme form 'form_table_layout.html.twig' %}

Note that the form variable in the above code is the form view variable that you passed to your template.

PHP

By using the following configuration, any customized form fragments inside the src/AppBundle/Resources/views/Form folder will be used globally when a form is rendered.

  • YAML
    # app/config/config.yml
    framework:
        templating:
            form:
                resources:
                    - 'AppBundle:Form'
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <framework:templating>
            <framework:form>
                <resource>AppBundle:Form</resource>
            </framework:form>
        </framework:templating>
        <!-- ... -->
    </framework:config>
    
  • PHP
    // app/config/config.php
    // PHP
    $container->loadFromExtension('framework', array(
        'templating' => array(
            'form' => array(
                'resources' => array(
                    'AppBundle:Form',
                ),
            ),
         ),
    
         // ...
    ));
    

By default, the PHP engine uses a div layout when rendering forms. Some people, however, may prefer to render forms in a table layout. Use the FrameworkBundle:FormTable resource to use such a layout:

  • YAML
    # app/config/config.yml
    framework:
        templating:
            form:
                resources:
                    - 'FrameworkBundle:FormTable'
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <framework:templating>
            <framework:form>
                <resource>FrameworkBundle:FormTable</resource>
            </framework:form>
        </framework:templating>
        <!-- ... -->
    </framework:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'templating' => array(
            'form' => array(
                'resources' => array(
                    'FrameworkBundle:FormTable',
                ),
            ),
        ),
    
         // ...
    ));
    

If you only want to make the change in one template, add the following line to your template file rather than adding the template as a resource:

<?php $view['form']->setTheme($form, array('FrameworkBundle:FormTable')); ?>

Note that the $form variable in the above code is the form view variable that you passed to your template.

How to Customize an individual Field

So far, you’ve seen the different ways you can customize the widget output of all text field types. You can also customize individual fields. For example, suppose you have two text fields in a product form - name and description - but you only want to customize one of the fields. This can be accomplished by customizing a fragment whose name is a combination of the field’s id attribute and which part of the field is being customized. For example, to customize the name field only:

  • Twig
    {% form_theme form _self %}
    
    {% block _product_name_widget %}
        <div class="text_widget">
            {{ block('form_widget_simple') }}
        </div>
    {% endblock %}
    
    {{ form_widget(form.name) }}
    
  • PHP
    <!-- Main template -->
    <?php echo $view['form']->setTheme($form, array('AppBundle:Form')); ?>
    
    <?php echo $view['form']->widget($form['name']); ?>
    
    <!-- src/AppBundle/Resources/views/Form/_product_name_widget.html.php -->
    <div class="text_widget">
          echo $view['form']->block('form_widget_simple') ?>
    </div>
    

Here, the _product_name_widget fragment defines the template to use for the field whose id is product_name (and name is product[name]).

小技巧

The product portion of the field is the form name, which may be set manually or generated automatically based on your form type name (e.g. ProductType equates to product). If you’re not sure what your form name is, just view the source of your generated form.

If you want to change the product or name portion of the block name _product_name_widget you can set the block_name option in your form type:

use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('name', 'text', array(
        'block_name' => 'custom_name',
    ));
}

Then the block name will be _product_custom_name_widget.

You can also override the markup for an entire field row using the same method:

  • Twig
    {% form_theme form _self %}
    
    {% block _product_name_row %}
        <div class="name_row">
            {{ form_label(form) }}
            {{ form_errors(form) }}
            {{ form_widget(form) }}
        </div>
    {% endblock %}
    
    {{ form_row(form.name) }}
    
  • PHP
    <!-- Main template -->
    <?php echo $view['form']->setTheme($form, array('AppBundle:Form')); ?>
    
    <?php echo $view['form']->row($form['name']); ?>
    
    <!-- src/AppBundle/Resources/views/Form/_product_name_row.html.php -->
    <div class="name_row">
        <?php echo $view['form']->label($form) ?>
        <?php echo $view['form']->errors($form) ?>
        <?php echo $view['form']->widget($form) ?>
    </div>
    
Other common Customizations

So far, this recipe has shown you several different ways to customize a single piece of how a form is rendered. The key is to customize a specific fragment that corresponds to the portion of the form you want to control (see naming form blocks).

In the next sections, you’ll see how you can make several common form customizations. To apply these customizations, use one of the methods described in the Form Theming section.

Customizing Error Output

注解

The Form component only handles how the validation errors are rendered, and not the actual validation error messages. The error messages themselves are determined by the validation constraints you apply to your objects. For more information, see the chapter on validation.

There are many different ways to customize how errors are rendered when a form is submitted with errors. The error messages for a field are rendered when you use the form_errors helper:

  • Twig
    {{ form_errors(form.age) }}
    
  • PHP
    <?php echo $view['form']->errors($form['age']); ?>
    

By default, the errors are rendered inside an unordered list:

<ul>
    <li>This field is required</li>
</ul>

To override how errors are rendered for all fields, simply copy, paste and customize the form_errors fragment.

  • Twig
    {# form_errors.html.twig #}
    {% block form_errors %}
        {% spaceless %}
            {% if errors|length > 0 %}
            <ul>
                {% for error in errors %}
                    <li>{{ error.message }}</li>
                {% endfor %}
            </ul>
            {% endif %}
        {% endspaceless %}
    {% endblock form_errors %}
    
  • PHP
    <!-- form_errors.html.php -->
    <?php if ($errors): ?>
        <ul>
            <?php foreach ($errors as $error): ?>
                <li><?php echo $error->getMessage() ?></li>
            <?php endforeach ?>
        </ul>
    <?php endif ?>
    

小技巧

See Form Theming for how to apply this customization.

You can also customize the error output for just one specific field type. To customize only the markup used for these errors, follow the same directions as above but put the contents in a relative _errors block (or file in case of PHP templates). For example: text_errors (or text_errors.html.php).

小技巧

See Form Fragment Naming to find out which specific block or file you have to customize.

Certain errors that are more global to your form (i.e. not specific to just one field) are rendered separately, usually at the top of your form:

  • Twig
    {{ form_errors(form) }}
    
  • PHP
    <?php echo $view['form']->render($form); ?>
    

To customize only the markup used for these errors, follow the same directions as above, but now check if the compound variable is set to true. If it is true, it means that what’s being currently rendered is a collection of fields (e.g. a whole form), and not just an individual field.

  • Twig
    {# form_errors.html.twig #}
    {% block form_errors %}
        {% spaceless %}
            {% if errors|length > 0 %}
                {% if compound %}
                    <ul>
                        {% for error in errors %}
                            <li>{{ error.message }}</li>
                        {% endfor %}
                    </ul>
                {% else %}
                    {# ... display the errors for a single field #}
                {% endif %}
            {% endif %}
        {% endspaceless %}
    {% endblock form_errors %}
    
  • PHP
    <!-- form_errors.html.php -->
    <?php if ($errors): ?>
        <?php if ($compound): ?>
            <ul>
                <?php foreach ($errors as $error): ?>
                    <li><?php echo $error->getMessage() ?></li>
                <?php endforeach ?>
            </ul>
        <?php else: ?>
            <!-- ... render the errors for a single field -->
        <?php endif ?>
    <?php endif ?>
    
Customizing the “Form Row”

When you can manage it, the easiest way to render a form field is via the form_row function, which renders the label, errors and HTML widget of a field. To customize the markup used for rendering all form field rows, override the form_row fragment. For example, suppose you want to add a class to the div element around each row:

  • Twig
    {# form_row.html.twig #}
    {% block form_row %}
        <div class="form_row">
            {{ form_label(form) }}
            {{ form_errors(form) }}
            {{ form_widget(form) }}
        </div>
    {% endblock form_row %}
    
  • PHP
    <!-- form_row.html.php -->
    <div class="form_row">
        <?php echo $view['form']->label($form) ?>
        <?php echo $view['form']->errors($form) ?>
        <?php echo $view['form']->widget($form) ?>
    </div>
    

小技巧

See Form Theming for how to apply this customization.

Adding a “Required” Asterisk to Field Labels

If you want to denote all of your required fields with a required asterisk (*), you can do this by customizing the form_label fragment.

In Twig, if you’re making the form customization inside the same template as your form, modify the use tag and add the following:

{% use 'form_div_layout.html.twig' with form_label as base_form_label %}

{% block form_label %}
    {{ block('base_form_label') }}

    {% if required %}
        <span class="required" title="This field is required">*</span>
    {% endif %}
{% endblock %}

In Twig, if you’re making the form customization inside a separate template, use the following:

{% extends 'form_div_layout.html.twig' %}

{% block form_label %}
    {{ parent() }}

    {% if required %}
        <span class="required" title="This field is required">*</span>
    {% endif %}
{% endblock %}

When using PHP as a templating engine you have to copy the content from the original template:

<!-- form_label.html.php -->

<!-- original content -->
<?php if ($required) { $label_attr['class'] = trim((isset($label_attr['class']) ? $label_attr['class'] : '').' required'); } ?>
<?php if (!$compound) { $label_attr['for'] = $id; } ?>
<?php if (!$label) { $label = $view['form']->humanize($name); } ?>
<label <?php foreach ($label_attr as $k => $v) { printf('%s="%s" ', $view->escape($k), $view->escape($v)); } ?>><?php echo $view->escape($view['translator']->trans($label, array(), $translation_domain)) ?></label>

<!-- customization -->
<?php if ($required) : ?>
    <span class="required" title="This field is required">*</span>
<?php endif ?>

小技巧

See Form Theming for how to apply this customization.

Adding “help” Messages

You can also customize your form widgets to have an optional “help” message.

In Twig, if you’re making the form customization inside the same template as your form, modify the use tag and add the following:

{% use 'form_div_layout.html.twig' with form_widget_simple as base_form_widget_simple %}

{% block form_widget_simple %}
    {{ block('base_form_widget_simple') }}

    {% if help is defined %}
        <span class="help">{{ help }}</span>
    {% endif %}
{% endblock %}

In Twig, if you’re making the form customization inside a separate template, use the following:

{% extends 'form_div_layout.html.twig' %}

{% block form_widget_simple %}
    {{ parent() }}

    {% if help is defined %}
        <span class="help">{{ help }}</span>
    {% endif %}
{% endblock %}

When using PHP as a templating engine you have to copy the content from the original template:

<!-- form_widget_simple.html.php -->

<!-- Original content -->
<input
    type="<?php echo isset($type) ? $view->escape($type) : 'text' ?>"
    <?php if (!empty($value)): ?>value="<?php echo $view->escape($value) ?>"<?php endif ?>
    <?php echo $view['form']->block($form, 'widget_attributes') ?>
/>

<!-- Customization -->
<?php if (isset($help)) : ?>
    <span class="help"><?php echo $view->escape($help) ?></span>
<?php endif ?>

To render a help message below a field, pass in a help variable:

  • Twig
    {{ form_widget(form.title, {'help': 'foobar'}) }}
    
  • PHP
    <?php echo $view['form']->widget($form['title'], array('help' => 'foobar')) ?>
    

小技巧

See Form Theming for how to apply this customization.

Using Form Variables

Most of the functions available for rendering different parts of a form (e.g. the form widget, form label, form errors, etc.) also allow you to make certain customizations directly. Look at the following example:

  • Twig
    {# render a widget, but add a "foo" class to it #}
    {{ form_widget(form.name, { 'attr': {'class': 'foo'} }) }}
    
  • PHP
    <!-- render a widget, but add a "foo" class to it -->
    <?php echo $view['form']->widget($form['name'], array(
        'attr' => array(
            'class' => 'foo',
        ),
    )) ?>
    

The array passed as the second argument contains form “variables”. For more details about this concept in Twig, see More about Form Variables.

How to Use Data Transformers

You’ll often find the need to transform the data the user entered in a form into something else for use in your program. You could easily do this manually in your controller, but what if you want to use this specific form in different places?

Say you have a one-to-one relation of Task to Issue, e.g. a Task optionally has an issue linked to it. Adding a listbox with all possible issues can eventually lead to a really long listbox in which it is impossible to find something. You might want to add a textbox instead, where the user can simply enter the issue number.

You could try to do this in your controller, but it’s not the best solution. It would be better if this issue were automatically converted to an Issue object. This is where Data Transformers come into play.

Creating the Transformer

First, create an IssueToNumberTransformer class - this class will be responsible for converting to and from the issue number and the Issue object:

// src/Acme/TaskBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace Acme\TaskBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\TaskBundle\Entity\Issue;

class IssueToNumberTransformer implements DataTransformerInterface
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    /**
     * Transforms an object (issue) to a string (number).
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($issue)
    {
        if (null === $issue) {
            return "";
        }

        return $issue->getNumber();
    }

    /**
     * Transforms a string (number) to an object (issue).
     *
     * @param  string $number
     *
     * @return Issue|null
     *
     * @throws TransformationFailedException if object (issue) is not found.
     */
    public function reverseTransform($number)
    {
        if (!$number) {
            return null;
        }

        $issue = $this->om
            ->getRepository('AcmeTaskBundle:Issue')
            ->findOneBy(array('number' => $number))
        ;

        if (null === $issue) {
            throw new TransformationFailedException(sprintf(
                'An issue with number "%s" does not exist!',
                $number
            ));
        }

        return $issue;
    }
}

小技巧

If you want a new issue to be created when an unknown number is entered, you can instantiate it rather than throwing the TransformationFailedException.

注解

When null is passed to the transform() method, your transformer should return an equivalent value of the type it is transforming to (e.g. an empty string, 0 for integers or 0.0 for floats).

Using the Transformer

Now that you have the transformer built, you just need to add it to your issue field in some form.

You can also use transformers without creating a new custom form type by calling addModelTransformer (or addViewTransformer - see Model and View Transformers) on any field builder:

use Symfony\Component\Form\FormBuilderInterface;
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // ...

        // this assumes that the entity manager was passed in as an option
        $entityManager = $options['em'];
        $transformer = new IssueToNumberTransformer($entityManager);

        // add a normal text field, but add your transformer to it
        $builder->add(
            $builder->create('issue', 'text')
                ->addModelTransformer($transformer)
        );
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver
            ->setDefaults(array(
                'data_class' => 'Acme\TaskBundle\Entity\Task',
            ))
            ->setRequired(array(
                'em',
            ))
            ->setAllowedTypes(array(
                'em' => 'Doctrine\Common\Persistence\ObjectManager',
            ));

        // ...
    }

    // ...
}

This example requires that you pass in the entity manager as an option when creating your form. Later, you’ll learn how you could create a custom issue field type to avoid needing to do this in your controller:

$taskForm = $this->createForm(new TaskType(), $task, array(
    'em' => $this->getDoctrine()->getManager(),
));

Cool, you’re done! Your user will be able to enter an issue number into the text field and it will be transformed back into an Issue object. This means that, after a successful submission, the Form framework will pass a real Issue object to Task::setIssue() instead of the issue number.

If the issue isn’t found, a form error will be created for that field and its error message can be controlled with the invalid_message field option.

警告

Notice that adding a transformer requires using a slightly more complicated syntax when adding the field. The following is wrong, as the transformer would be applied to the entire form, instead of just this field:

// THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM
// see above example for correct code
$builder->add('issue', 'text')
    ->addModelTransformer($transformer);
Model and View Transformers

In the above example, the transformer was used as a “model” transformer. In fact, there are two different types of transformers and three different types of underlying data.

_images/DataTransformersTypes.png

In any form, the three different types of data are:

  1. Model data - This is the data in the format used in your application (e.g. an Issue object). If you call Form::getData or Form::setData, you’re dealing with the “model” data.
  2. Norm Data - This is a normalized version of your data, and is commonly the same as your “model” data (though not in our example). It’s not commonly used directly.
  3. View Data - This is the format that’s used to fill in the form fields themselves. It’s also the format in which the user will submit the data. When you call Form::submit($data), the $data is in the “view” data format.

The two different types of transformers help convert to and from each of these types of data:

Model transformers:
  • transform: “model data” => “norm data”
  • reverseTransform: “norm data” => “model data”
View transformers:
  • transform: “norm data” => “view data”
  • reverseTransform: “view data” => “norm data”

Which transformer you need depends on your situation.

To use the view transformer, call addViewTransformer.

So why Use the Model Transformer?

In this example, the field is a text field, and a text field is always expected to be a simple, scalar format in the “norm” and “view” formats. For this reason, the most appropriate transformer was the “model” transformer (which converts to/from the norm format - string issue number - to the model format - Issue object).

The difference between the transformers is subtle and you should always think about what the “norm” data for a field should really be. For example, the “norm” data for a text field is a string, but is a DateTime object for a date field.

Using Transformers in a custom Field Type

In the above example, you applied the transformer to a normal text field. This was easy, but has two downsides:

1) You need to always remember to apply the transformer whenever you’re adding a field for issue numbers.

2) You need to worry about passing in the em option whenever you’re creating a form that uses the transformer.

Because of these, you may choose to create a custom field type. First, create the custom field type class:

// src/Acme/TaskBundle/Form/Type/IssueSelectorType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class IssueSelectorType extends AbstractType
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new IssueToNumberTransformer($this->om);
        $builder->addModelTransformer($transformer);
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'invalid_message' => 'The selected issue does not exist',
        ));
    }

    public function getParent()
    {
        return 'text';
    }

    public function getName()
    {
        return 'issue_selector';
    }
}

Next, register your type as a service and tag it with form.type so that it’s recognized as a custom field type:

  • YAML
    services:
        acme_demo.type.issue_selector:
            class: Acme\TaskBundle\Form\Type\IssueSelectorType
            arguments: ["@doctrine.orm.entity_manager"]
            tags:
                - { name: form.type, alias: issue_selector }
    
  • XML
    <service id="acme_demo.type.issue_selector" class="Acme\TaskBundle\Form\Type\IssueSelectorType">
        <argument type="service" id="doctrine.orm.entity_manager"/>
        <tag name="form.type" alias="issue_selector" />
    </service>
    
  • PHP
    $container
        ->setDefinition('acme_demo.type.issue_selector', array(
            new Reference('doctrine.orm.entity_manager'),
        ))
        ->addTag('form.type', array(
            'alias' => 'issue_selector',
        ))
    ;
    

Now, whenever you need to use your special issue_selector field type, it’s quite easy:

// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('issue', 'issue_selector');
    }

    public function getName()
    {
        return 'task';
    }
}
How to Dynamically Modify Forms Using Form Events

Often times, a form can’t be created statically. In this entry, you’ll learn how to customize your form based on three common use-cases:

  1. Customizing your Form Based on the Underlying Data

    Example: you have a “Product” form and need to modify/add/remove a field

    based on the data on the underlying Product being edited.

  2. How to dynamically Generate Forms Based on user Data

    Example: you create a “Friend Message” form and need to build a drop-down that contains only users that are friends with the current authenticated user.

  3. Dynamic Generation for Submitted Forms

    Example: on a registration form, you have a “country” field and a “state” field which should populate dynamically based on the value in the “country” field.

If you wish to learn more about the basics behind form events, you can take a look at the Form Events documentation.

Customizing your Form Based on the Underlying Data

Before jumping right into dynamic form generation, hold on and recall what a bare form class looks like:

// src/AppBundle/Form/Type/ProductType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
        $builder->add('price');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Product'
        ));
    }

    public function getName()
    {
        return 'product';
    }
}

注解

If this particular section of code isn’t already familiar to you, you probably need to take a step back and first review the Forms chapter before proceeding.

Assume for a moment that this form utilizes an imaginary “Product” class that has only two properties (“name” and “price”). The form generated from this class will look the exact same regardless if a new Product is being created or if an existing product is being edited (e.g. a product fetched from the database).

Suppose now, that you don’t want the user to be able to change the name value once the object has been created. To do this, you can rely on Symfony’s EventDispatcher system to analyze the data on the object and modify the form based on the Product object’s data. In this entry, you’ll learn how to add this level of flexibility to your forms.

Adding an Event Listener to a Form Class

So, instead of directly adding that name widget, the responsibility of creating that particular field is delegated to an event listener:

// src/AppBundle/Form/Type/ProductType.php
namespace AppBundle\Form\Type;

// ...
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('price');

        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            // ... adding the name field if needed
        });
    }

    // ...
}

The goal is to create a name field only if the underlying Product object is new (e.g. hasn’t been persisted to the database). Based on that, the event listener might look like the following:

// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...
    $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
        $product = $event->getData();
        $form = $event->getForm();

        // check if the Product object is "new"
        // If no data is passed to the form, the data is "null".
        // This should be considered a new "Product"
        if (!$product || null === $product->getId()) {
            $form->add('name', 'text');
        }
    });
}

2.2 新版功能: The ability to pass a string into FormInterface::add was introduced in Symfony 2.2.

注解

The FormEvents::PRE_SET_DATA line actually resolves to the string form.pre_set_data. FormEvents serves an organizational purpose. It is a centralized location in which you can find all of the various form events available. You can view the full list of form events via the FormEvents class.

Adding an Event Subscriber to a Form Class

For better reusability or if there is some heavy logic in your event listener, you can also move the logic for creating the name field to an event subscriber:

// src/AppBundle/Form/Type/ProductType.php
namespace AppBundle\Form\Type;

// ...
use AppBundle\Form\EventListener\AddNameFieldSubscriber;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('price');

        $builder->addEventSubscriber(new AddNameFieldSubscriber());
    }

    // ...
}

Now the logic for creating the name field resides in it own subscriber class:

// src/AppBundle/Form/EventListener/AddNameFieldSubscriber.php
namespace AppBundle\Form\EventListener;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class AddNameFieldSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        // Tells the dispatcher that you want to listen on the form.pre_set_data
        // event and that the preSetData method should be called.
        return array(FormEvents::PRE_SET_DATA => 'preSetData');
    }

    public function preSetData(FormEvent $event)
    {
        $product = $event->getData();
        $form = $event->getForm();

        if (!$product || null === $product->getId()) {
            $form->add('name', 'text');
        }
    }
}
How to dynamically Generate Forms Based on user Data

Sometimes you want a form to be generated dynamically based not only on data from the form but also on something else - like some data from the current user. Suppose you have a social website where a user can only message people marked as friends on the website. In this case, a “choice list” of whom to message should only contain users that are the current user’s friends.

Creating the Form Type

Using an event listener, your form might look like this:

// src/AppBundle/Form/Type/FriendMessageFormType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class FriendMessageFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('subject', 'text')
            ->add('body', 'textarea')
        ;
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            // ... add a choice list of friends of the current application user
        });
    }

    public function getName()
    {
        return 'friend_message';
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
    }
}

The problem is now to get the current user and create a choice field that contains only this user’s friends.

Luckily it is pretty easy to inject a service inside of the form. This can be done in the constructor:

private $securityContext;

public function __construct(SecurityContext $securityContext)
{
    $this->securityContext = $securityContext;
}

注解

You might wonder, now that you have access to the User (through the security context), why not just use it directly in buildForm and omit the event listener? This is because doing so in the buildForm method would result in the whole form type being modified and not just this one form instance. This may not usually be a problem, but technically a single form type could be used on a single request to create many forms or fields.

Customizing the Form Type

Now that you have all the basics in place you can take advantage of the SecurityContext and fill in the listener logic:

// src/AppBundle/FormType/FriendMessageFormType.php

use Symfony\Component\Security\Core\SecurityContext;
use Doctrine\ORM\EntityRepository;
// ...

class FriendMessageFormType extends AbstractType
{
    private $securityContext;

    public function __construct(SecurityContext $securityContext)
    {
        $this->securityContext = $securityContext;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('subject', 'text')
            ->add('body', 'textarea')
        ;

        // grab the user, do a quick sanity check that one exists
        $user = $this->securityContext->getToken()->getUser();
        if (!$user) {
            throw new \LogicException(
                'The FriendMessageFormType cannot be used without an authenticated user!'
            );
        }

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($user) {
                $form = $event->getForm();

                $formOptions = array(
                    'class' => 'AppBundle\Entity\User',
                    'property' => 'fullName',
                    'query_builder' => function (EntityRepository $er) use ($user) {
                        // build a custom query
                        // return $er->createQueryBuilder('u')->addOrderBy('fullName', 'DESC');

                        // or call a method on your repository that returns the query builder
                        // the $er is an instance of your UserRepository
                        // return $er->createOrderByFullNameQueryBuilder();
                    },
                );

                // create the field, this is similar the $builder->add()
                // field name, field type, data, options
                $form->add('friend', 'entity', $formOptions);
            }
        );
    }

    // ...
}

注解

The multiple and expanded form options will default to false because the type of the friend field is entity.

Using the Form

Our form is now ready to use and there are two possible ways to use it inside of a controller:

  1. create it manually and remember to pass the security context to it;

or

  1. define it as a service.
a) Creating the Form manually

This is very simple, and is probably the better approach unless you’re using your new form type in many places or embedding it into other forms:

class FriendMessageController extends Controller
{
    public function newAction(Request $request)
    {
        $securityContext = $this->container->get('security.context');
        $form = $this->createForm(
            new FriendMessageFormType($securityContext)
        );

        // ...
    }
}
b) Defining the Form as a Service

To define your form as a service, just create a normal service and then tag it with form.type.

  • YAML
    # app/config/config.yml
    services:
        app.form.friend_message:
            class: AppBundle\Form\Type\FriendMessageFormType
            arguments: ["@security.context"]
            tags:
                - { name: form.type, alias: friend_message }
    
  • XML
    <!-- app/config/config.xml -->
    <services>
        <service id="app.form.friend_message" class="AppBundle\Form\Type\FriendMessageFormType">
            <argument type="service" id="security.context" />
            <tag name="form.type" alias="friend_message" />
        </service>
    </services>
    
  • PHP
    // app/config/config.php
    $definition = new Definition('AppBundle\Form\Type\FriendMessageFormType');
    $definition->addTag('form.type', array('alias' => 'friend_message'));
    $container->setDefinition(
        'app.form.friend_message',
        $definition,
        array('security.context')
    );
    

If you wish to create it from within a controller or any other service that has access to the form factory, you then use:

use Symfony\Component\DependencyInjection\ContainerAware;

class FriendMessageController extends ContainerAware
{
    public function newAction(Request $request)
    {
        $form = $this->get('form.factory')->create('friend_message');

        // ...
    }
}

If you extend the Symfony\Bundle\FrameworkBundle\Controller\Controller class, you can simply call:

$form = $this->createForm('friend_message');

You can also easily embed the form type into another form:

// inside some other "form type" class
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('message', 'friend_message');
}
Dynamic Generation for Submitted Forms

Another case that can appear is that you want to customize the form specific to the data that was submitted by the user. For example, imagine you have a registration form for sports gatherings. Some events will allow you to specify your preferred position on the field. This would be a choice field for example. However the possible choices will depend on each sport. Football will have attack, defense, goalkeeper etc... Baseball will have a pitcher but will not have a goalkeeper. You will need the correct options in order for validation to pass.

The meetup is passed as an entity field to the form. So we can access each sport like this:

// src/AppBundle/Form/Type/SportMeetupType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
// ...

class SportMeetupType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('sport', 'entity', array(
                'class'       => 'AppBundle:Sport',
                'empty_value' => '',
            ))
        ;

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) {
                $form = $event->getForm();

                // this would be your entity, i.e. SportMeetup
                $data = $event->getData();

                $sport = $data->getSport();
                $positions = null === $sport ? array() : $sport->getAvailablePositions();

                $form->add('position', 'entity', array(
                    'class'       => 'AppBundle:Position',
                    'empty_value' => '',
                    'choices'     => $positions,
                ));
            }
        );
    }

    // ...
}

When you’re building this form to display to the user for the first time, then this example works perfectly.

However, things get more difficult when you handle the form submission. This is because the PRE_SET_DATA event tells us the data that you’re starting with (e.g. an empty SportMeetup object), not the submitted data.

On a form, we can usually listen to the following events:

  • PRE_SET_DATA
  • POST_SET_DATA
  • PRE_SUBMIT
  • SUBMIT
  • POST_SUBMIT

2.3 新版功能: The events PRE_SUBMIT, SUBMIT and POST_SUBMIT were introduced in Symfony 2.3. Before, they were named PRE_BIND, BIND and POST_BIND.

2.2.6 新版功能: The behavior of the POST_SUBMIT event changed slightly in 2.2.6, which the below example uses.

The key is to add a POST_SUBMIT listener to the field that your new field depends on. If you add a POST_SUBMIT listener to a form child (e.g. sport), and add new children to the parent form, the Form component will detect the new field automatically and map it to the submitted client data.

The type would now look like:

// src/AppBundle/Form/Type/SportMeetupType.php
namespace AppBundle\Form\Type;

// ...
use Symfony\Component\Form\FormInterface;
use AppBundle\Entity\Sport;

class SportMeetupType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('sport', 'entity', array(
                'class'       => 'AppBundle:Sport',
                'empty_value' => '',
            ));
        ;

        $formModifier = function (FormInterface $form, Sport $sport = null) {
            $positions = null === $sport ? array() : $sport->getAvailablePositions();

            $form->add('position', 'entity', array(
                'class'       => 'AppBundle:Position',
                'empty_value' => '',
                'choices'     => $positions,
            ));
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifier) {
                // this would be your entity, i.e. SportMeetup
                $data = $event->getData();

                $formModifier($event->getForm(), $data->getSport());
            }
        );

        $builder->get('sport')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifier) {
                // It's important here to fetch $event->getForm()->getData(), as
                // $event->getData() will get you the client data (that is, the ID)
                $sport = $event->getForm()->getData();

                // since we've added the listener to the child, we'll have to pass on
                // the parent to the callback functions!
                $formModifier($event->getForm()->getParent(), $sport);
            }
        );
    }

    // ...
}

You can see that you need to listen on these two events and have different callbacks only because in two different scenarios, the data that you can use is available in different events. Other than that, the listeners always perform exactly the same things on a given form.

One piece that is still missing is the client-side updating of your form after the sport is selected. This should be handled by making an AJAX call back to your application. Assume that you have a sport meetup creation controller:

// src/AppBundle/Controller/MeetupController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\SportMeetup;
use AppBundle\Form\Type\SportMeetupType;
// ...

class MeetupController extends Controller
{
    public function createAction(Request $request)
    {
        $meetup = new SportMeetup();
        $form = $this->createForm(new SportMeetupType(), $meetup);
        $form->handleRequest($request);
        if ($form->isValid()) {
            // ... save the meetup, redirect etc.
        }

        return $this->render(
            'AppBundle:Meetup:create.html.twig',
            array('form' => $form->createView())
        );
    }

    // ...
}

The associated template uses some JavaScript to update the position form field according to the current selection in the sport field:

  • Twig
    {# src/AppBundle/Resources/views/Meetup/create.html.twig #}
    {{ form_start(form) }}
        {{ form_row(form.sport) }}    {# <select id="meetup_sport" ... #}
        {{ form_row(form.position) }} {# <select id="meetup_position" ... #}
        {# ... #}
    {{ form_end(form) }}
    
    <script>
    var $sport = $('#meetup_sport');
    // When sport gets selected ...
    $sport.change(function() {
      // ... retrieve the corresponding form.
      var $form = $(this).closest('form');
      // Simulate form data, but only include the selected sport value.
      var data = {};
      data[$sport.attr('name')] = $sport.val();
      // Submit data via AJAX to the form's action path.
      $.ajax({
        url : $form.attr('action'),
        type: $form.attr('method'),
        data : data,
        success: function(html) {
          // Replace current position field ...
          $('#meetup_position').replaceWith(
            // ... with the returned one from the AJAX response.
            $(html).find('#meetup_position')
          );
          // Position field now displays the appropriate positions.
        }
      });
    });
    </script>
    
  • PHP
    <!-- src/AppBundle/Resources/views/Meetup/create.html.php -->
    <?php echo $view['form']->start($form) ?>
        <?php echo $view['form']->row($form['sport']) ?>    <!-- <select id="meetup_sport" ... -->
        <?php echo $view['form']->row($form['position']) ?> <!-- <select id="meetup_position" ... -->
        <!-- ... -->
    <?php echo $view['form']->end($form) ?>
    
    <script>
    var $sport = $('#meetup_sport');
    // When sport gets selected ...
    $sport.change(function() {
      // ... retrieve the corresponding form.
      var $form = $(this).closest('form');
      // Simulate form data, but only include the selected sport value.
      var data = {};
      data[$sport.attr('name')] = $sport.val();
      // Submit data via AJAX to the form's action path.
      $.ajax({
        url : $form.attr('action'),
        type: $form.attr('method'),
        data : data,
        success: function(html) {
          // Replace current position field ...
          $('#meetup_position').replaceWith(
            // ... with the returned one from the AJAX response.
            $(html).find('#meetup_position')
          );
          // Position field now displays the appropriate positions.
        }
      });
    });
    </script>
    

The major benefit of submitting the whole form to just extract the updated position field is that no additional server-side code is needed; all the code from above to generate the submitted form can be reused.

Suppressing Form Validation

To suppress form validation you can use the POST_SUBMIT event and prevent the ValidationListener from being called.

The reason for needing to do this is that even if you set group_validation to false there are still some integrity checks executed. For example an uploaded file will still be checked to see if it is too large and the form will still check to see if non-existing fields were submitted. To disable all of this, use a listener:

use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
        $event->stopPropagation();
    }, 900); // Always set a higher priority than ValidationListener

    // ...
}

警告

By doing this, you may accidentally disable something more than just form validation, since the POST_SUBMIT event may have other listeners.

How to Embed a Collection of Forms

In this entry, you’ll learn how to create a form that embeds a collection of many other forms. This could be useful, for example, if you had a Task class and you wanted to edit/create/remove many Tag objects related to that Task, right inside the same form.

注解

In this entry, it’s loosely assumed that you’re using Doctrine as your database store. But if you’re not using Doctrine (e.g. Propel or just a database connection), it’s all very similar. There are only a few parts of this tutorial that really care about “persistence”.

If you are using Doctrine, you’ll need to add the Doctrine metadata, including the ManyToMany association mapping definition on the Task’s tags property.

First, suppose that each Task belongs to multiple Tag objects. Start by creating a simple Task class:

// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;

class Task
{
    protected $description;

    protected $tags;

    public function __construct()
    {
        $this->tags = new ArrayCollection();
    }

    public function getDescription()
    {
        return $this->description;
    }

    public function setDescription($description)
    {
        $this->description = $description;
    }

    public function getTags()
    {
        return $this->tags;
    }
}

注解

The ArrayCollection is specific to Doctrine and is basically the same as using an array (but it must be an ArrayCollection if you’re using Doctrine).

Now, create a Tag class. As you saw above, a Task can have many Tag objects:

// src/Acme/TaskBundle/Entity/Tag.php
namespace Acme\TaskBundle\Entity;

class Tag
{
    public $name;
}

小技巧

The name property is public here, but it can just as easily be protected or private (but then it would need getName and setName methods).

Then, create a form class so that a Tag object can be modified by the user:

// src/Acme/TaskBundle/Form/Type/TagType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class TagType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\TaskBundle\Entity\Tag',
        ));
    }

    public function getName()
    {
        return 'tag';
    }
}

With this, you have enough to render a tag form by itself. But since the end goal is to allow the tags of a Task to be modified right inside the task form itself, create a form for the Task class.

Notice that you embed a collection of TagType forms using the collection field type:

// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('description');

        $builder->add('tags', 'collection', array('type' => new TagType()));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\TaskBundle\Entity\Task',
        ));
    }

    public function getName()
    {
        return 'task';
    }
}

In your controller, you’ll now initialize a new instance of TaskType:

// src/Acme/TaskBundle/Controller/TaskController.php
namespace Acme\TaskBundle\Controller;

use Acme\TaskBundle\Entity\Task;
use Acme\TaskBundle\Entity\Tag;
use Acme\TaskBundle\Form\Type\TaskType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class TaskController extends Controller
{
    public function newAction(Request $request)
    {
        $task = new Task();

        // dummy code - this is here just so that the Task has some tags
        // otherwise, this isn't an interesting example
        $tag1 = new Tag();
        $tag1->name = 'tag1';
        $task->getTags()->add($tag1);
        $tag2 = new Tag();
        $tag2->name = 'tag2';
        $task->getTags()->add($tag2);
        // end dummy code

        $form = $this->createForm(new TaskType(), $task);

        $form->handleRequest($request);

        if ($form->isValid()) {
            // ... maybe do some form processing, like saving the Task and Tag objects
        }

        return $this->render('AcmeTaskBundle:Task:new.html.twig', array(
            'form' => $form->createView(),
        ));
    }
}

The corresponding template is now able to render both the description field for the task form as well as all the TagType forms for any tags that are already related to this Task. In the above controller, I added some dummy code so that you can see this in action (since a Task has zero tags when first created).

  • Twig
    {# src/Acme/TaskBundle/Resources/views/Task/new.html.twig #}
    
    {# ... #}
    
    {{ form_start(form) }}
        {# render the task's only field: description #}
        {{ form_row(form.description) }}
    
        <h3>Tags</h3>
        <ul class="tags">
            {# iterate over each existing tag and render its only field: name #}
            {% for tag in form.tags %}
                <li>{{ form_row(tag.name) }}</li>
            {% endfor %}
        </ul>
    {{ form_end(form) }}
    
    {# ... #}
    
  • PHP
    <!-- src/Acme/TaskBundle/Resources/views/Task/new.html.php -->
    
    <!-- ... -->
    
    <?php echo $view['form']->start($form) ?>
        <!-- render the task's only field: description -->
        <?php echo $view['form']->row($form['description']) ?>
    
        <h3>Tags</h3>
        <ul class="tags">
            <?php foreach($form['tags'] as $tag): ?>
                <li><?php echo $view['form']->row($tag['name']) ?></li>
            <?php endforeach ?>
        </ul>
    <?php echo $view['form']->end($form) ?>
    
    <!-- ... -->
    

When the user submits the form, the submitted data for the tags field are used to construct an ArrayCollection of Tag objects, which is then set on the tag field of the Task instance.

The tags collection is accessible naturally via $task->getTags() and can be persisted to the database or used however you need.

So far, this works great, but this doesn’t allow you to dynamically add new tags or delete existing tags. So, while editing existing tags will work great, your user can’t actually add any new tags yet.

警告

In this entry, you embed only one collection, but you are not limited to this. You can also embed nested collection as many level down as you like. But if you use Xdebug in your development setup, you may receive a Maximum function nesting level of '100' reached, aborting! error. This is due to the xdebug.max_nesting_level PHP setting, which defaults to 100.

This directive limits recursion to 100 calls which may not be enough for rendering the form in the template if you render the whole form at once (e.g form_widget(form)). To fix this you can set this directive to a higher value (either via a php.ini file or via ini_set, for example in app/autoload.php) or render each form field by hand using form_row.

Allowing “new” Tags with the “Prototype”

Allowing the user to dynamically add new tags means that you’ll need to use some JavaScript. Previously you added two tags to your form in the controller. Now let the user add as many tag forms as they need directly in the browser. This will be done through a bit of JavaScript.

The first thing you need to do is to let the form collection know that it will receive an unknown number of tags. So far you’ve added two tags and the form type expects to receive exactly two, otherwise an error will be thrown: This form should not contain extra fields. To make this flexible, add the allow_add option to your collection field:

// src/Acme/TaskBundle/Form/Type/TaskType.php

// ...
use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('description');

    $builder->add('tags', 'collection', array(
        'type'         => new TagType(),
        'allow_add'    => true,
    ));
}

In addition to telling the field to accept any number of submitted objects, the allow_add also makes a “prototype” variable available to you. This “prototype” is a little “template” that contains all the HTML to be able to render any new “tag” forms. To render it, make the following change to your template:

  • Twig
    <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}">
        ...
    </ul>
    
  • PHP
    <ul class="tags" data-prototype="<?php
        echo $view->escape($view['form']->row($form['tags']->vars['prototype']))
    ?>">
        ...
    </ul>
    

注解

If you render your whole “tags” sub-form at once (e.g. form_row(form.tags)), then the prototype is automatically available on the outer div as the data-prototype attribute, similar to what you see above.

小技巧

The form.tags.vars.prototype is a form element that looks and feels just like the individual form_widget(tag) elements inside your for loop. This means that you can call form_widget, form_row or form_label on it. You could even choose to render only one of its fields (e.g. the name field):

{{ form_widget(form.tags.vars.prototype.name)|e }}

On the rendered page, the result will look something like this:

<ul class="tags" data-prototype="&lt;div&gt;&lt;label class=&quot; required&quot;&gt;__name__&lt;/label&gt;&lt;div id=&quot;task_tags___name__&quot;&gt;&lt;div&gt;&lt;label for=&quot;task_tags___name___name&quot; class=&quot; required&quot;&gt;Name&lt;/label&gt;&lt;input type=&quot;text&quot; id=&quot;task_tags___name___name&quot; name=&quot;task[tags][__name__][name]&quot; required=&quot;required&quot; maxlength=&quot;255&quot; /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;">

The goal of this section will be to use JavaScript to read this attribute and dynamically add new tag forms when the user clicks a “Add a tag” link. To make things simple, this example uses jQuery and assumes you have it included somewhere on your page.

Add a script tag somewhere on your page so you can start writing some JavaScript.

First, add a link to the bottom of the “tags” list via JavaScript. Second, bind to the “click” event of that link so you can add a new tag form (addTagForm will be show next):

var $collectionHolder;

// setup an "add a tag" link
var $addTagLink = $('<a href="#" class="add_tag_link">Add a tag</a>');
var $newLinkLi = $('<li></li>').append($addTagLink);

jQuery(document).ready(function() {
    // Get the ul that holds the collection of tags
    $collectionHolder = $('ul.tags');

    // add the "add a tag" anchor and li to the tags ul
    $collectionHolder.append($newLinkLi);

    // count the current form inputs we have (e.g. 2), use that as the new
    // index when inserting a new item (e.g. 2)
    $collectionHolder.data('index', $collectionHolder.find(':input').length);

    $addTagLink.on('click', function(e) {
        // prevent the link from creating a "#" on the URL
        e.preventDefault();

        // add a new tag form (see next code block)
        addTagForm($collectionHolder, $newLinkLi);
    });
});

The addTagForm function’s job will be to use the data-prototype attribute to dynamically add a new form when this link is clicked. The data-prototype HTML contains the tag text input element with a name of task[tags][__name__][name] and id of task_tags___name___name. The __name__ is a little “placeholder”, which you’ll replace with a unique, incrementing number (e.g. task[tags][3][name]).

The actual code needed to make this all work can vary quite a bit, but here’s one example:

function addTagForm($collectionHolder, $newLinkLi) {
    // Get the data-prototype explained earlier
    var prototype = $collectionHolder.data('prototype');

    // get the new index
    var index = $collectionHolder.data('index');

    // Replace '__name__' in the prototype's HTML to
    // instead be a number based on how many items we have
    var newForm = prototype.replace(/__name__/g, index);

    // increase the index with one for the next item
    $collectionHolder.data('index', index + 1);

    // Display the form in the page in an li, before the "Add a tag" link li
    var $newFormLi = $('<li></li>').append(newForm);
    $newLinkLi.before($newFormLi);
}

注解

It is better to separate your JavaScript in real JavaScript files than to write it inside the HTML as is done here.

Now, each time a user clicks the Add a tag link, a new sub form will appear on the page. When the form is submitted, any new tag forms will be converted into new Tag objects and added to the tags property of the Task object.

参见

You can find a working example in this JSFiddle.

To make handling these new tags easier, add an “adder” and a “remover” method for the tags in the Task class:

// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;

// ...
class Task
{
    // ...

    public function addTag(Tag $tag)
    {
        $this->tags->add($tag);
    }

    public function removeTag(Tag $tag)
    {
        // ...
    }
}

Next, add a by_reference option to the tags field and set it to false:

// src/Acme/TaskBundle/Form/Type/TaskType.php

// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('tags', 'collection', array(
        // ...
        'by_reference' => false,
    ));
}

With these two changes, when the form is submitted, each new Tag object is added to the Task class by calling the addTag method. Before this change, they were added internally by the form by calling $task->getTags()->add($tag). That was just fine, but forcing the use of the “adder” method makes handling these new Tag objects easier (especially if you’re using Doctrine, which we talk about next!).

警告

You have to create both addTag and removeTag methods, otherwise the form will still use setTag even if by_reference is false. You’ll learn more about the removeTag method later in this article.

Allowing Tags to be Removed

The next step is to allow the deletion of a particular item in the collection. The solution is similar to allowing tags to be added.

Start by adding the allow_delete option in the form Type:

// src/Acme/TaskBundle/Form/Type/TaskType.php

// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('tags', 'collection', array(
        // ...
        'allow_delete' => true,
    ));
}

Now, you need to put some code into the removeTag method of Task:

// src/Acme/TaskBundle/Entity/Task.php

// ...
class Task
{
    // ...

    public function removeTag(Tag $tag)
    {
        $this->tags->removeElement($tag);
    }
}
Template Modifications

The allow_delete option has one consequence: if an item of a collection isn’t sent on submission, the related data is removed from the collection on the server. The solution is thus to remove the form element from the DOM.

First, add a “delete this tag” link to each tag form:

jQuery(document).ready(function() {
    // Get the ul that holds the collection of tags
    $collectionHolder = $('ul.tags');

    // add a delete link to all of the existing tag form li elements
    $collectionHolder.find('li').each(function() {
        addTagFormDeleteLink($(this));
    });

    // ... the rest of the block from above
});

function addTagForm() {
    // ...

    // add a delete link to the new form
    addTagFormDeleteLink($newFormLi);
}

The addTagFormDeleteLink function will look something like this:

function addTagFormDeleteLink($tagFormLi) {
    var $removeFormA = $('<a href="#">delete this tag</a>');
    $tagFormLi.append($removeFormA);

    $removeFormA.on('click', function(e) {
        // prevent the link from creating a "#" on the URL
        e.preventDefault();

        // remove the li for the tag form
        $tagFormLi.remove();
    });
}

When a tag form is removed from the DOM and submitted, the removed Tag object will not be included in the collection passed to setTags. Depending on your persistence layer, this may or may not be enough to actually remove the relationship between the removed Tag and Task object.

How to Create a Custom Form Field Type

Symfony comes with a bunch of core field types available for building forms. However there are situations where you may want to create a custom form field type for a specific purpose. This recipe assumes you need a field definition that holds a person’s gender, based on the existing choice field. This section explains how the field is defined, how you can customize its layout and finally, how you can register it for use in your application.

Defining the Field Type

In order to create the custom field type, first you have to create the class representing the field. In this situation the class holding the field type will be called GenderType and the file will be stored in the default location for form fields, which is <BundleName>\Form\Type. Make sure the field extends AbstractType:

// src/AppBundle/Form/Type/GenderType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class GenderType extends AbstractType
{
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'choices' => array(
                'm' => 'Male',
                'f' => 'Female',
            )
        ));
    }

    public function getParent()
    {
        return 'choice';
    }

    public function getName()
    {
        return 'gender';
    }
}

小技巧

The location of this file is not important - the Form\Type directory is just a convention.

Here, the return value of the getParent function indicates that you’re extending the choice field type. This means that, by default, you inherit all of the logic and rendering of that field type. To see some of the logic, check out the ChoiceType class. There are three methods that are particularly important:

buildForm()
Each field type has a buildForm method, which is where you configure and build any field(s). Notice that this is the same method you use to setup your forms, and it works the same here.
buildView()
This method is used to set any extra variables you’ll need when rendering your field in a template. For example, in ChoiceType, a multiple variable is set and used in the template to set (or not set) the multiple attribute on the select field. See Creating a Template for the Field for more details.
setDefaultOptions()
This defines options for your form type that can be used in buildForm() and buildView(). There are a lot of options common to all fields (see form Field Type), but you can create any others that you need here.

小技巧

If you’re creating a field that consists of many fields, then be sure to set your “parent” type as form or something that extends form. Also, if you need to modify the “view” of any of your child types from your parent type, use the finishView() method.

The getName() method returns an identifier which should be unique in your application. This is used in various places, such as when customizing how your form type will be rendered.

The goal of this field was to extend the choice type to enable selection of a gender. This is achieved by fixing the choices to a list of possible genders.

Creating a Template for the Field

Each field type is rendered by a template fragment, which is determined in part by the value of your getName() method. For more information, see What are Form Themes?.

In this case, since the parent field is choice, you don’t need to do any work as the custom field type will automatically be rendered like a choice type. But for the sake of this example, suppose that when your field is “expanded” (i.e. radio buttons or checkboxes, instead of a select field), you want to always render it in a ul element. In your form theme template (see above link for details), create a gender_widget block to handle this:

  • Twig
    {# src/AppBundle/Resources/views/Form/fields.html.twig #}
    {% block gender_widget %}
        {% spaceless %}
            {% if expanded %}
                <ul {{ block('widget_container_attributes') }}>
                {% for child in form %}
                    <li>
                        {{ form_widget(child) }}
                        {{ form_label(child) }}
                    </li>
                {% endfor %}
                </ul>
            {% else %}
                {# just let the choice widget render the select tag #}
                {{ block('choice_widget') }}
            {% endif %}
        {% endspaceless %}
    {% endblock %}
    
  • PHP
    <!-- src/AppBundle/Resources/views/Form/gender_widget.html.php -->
    <?php if ($expanded) : ?>
        <ul <?php $view['form']->block($form, 'widget_container_attributes') ?>>
        <?php foreach ($form as $child) : ?>
            <li>
                <?php echo $view['form']->widget($child) ?>
                <?php echo $view['form']->label($child) ?>
            </li>
        <?php endforeach ?>
        </ul>
    <?php else : ?>
        <!-- just let the choice widget render the select tag -->
        <?php echo $view['form']->renderBlock('choice_widget') ?>
    <?php endif ?>
    

注解

Make sure the correct widget prefix is used. In this example the name should be gender_widget, according to the value returned by getName. Further, the main config file should point to the custom form template so that it’s used when rendering all forms.

When using Twig this is:

  • YAML
    # app/config/config.yml
    twig:
        form:
            resources:
                - 'AppBundle:Form:fields.html.twig'
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config>
        <twig:form>
            <twig:resource>AppBundle:Form:fields.html.twig</twig:resource>
        </twig:form>
    </twig:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'form' => array(
            'resources' => array(
                'AppBundle:Form:fields.html.twig',
            ),
        ),
    ));
    

For the PHP templating engine, your configuration should look like this:

  • YAML
    # app/config/config.yml
    framework:
        templating:
            form:
                resources:
                    - 'AppBundle:Form'
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:templating>
                <framework:form>
                    <framework:resource>AppBundle:Form</twig:resource>
                </framework:form>
            </framework:templating>
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'templating' => array(
            'form' => array(
                'resources' => array(
                    'AppBundle:Form',
                ),
            ),
        ),
    ));
    
Using the Field Type

You can now use your custom field type immediately, simply by creating a new instance of the type in one of your forms:

// src/AppBundle/Form/Type/AuthorType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class AuthorType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('gender_code', new GenderType(), array(
            'empty_value' => 'Choose a gender',
        ));
    }
}

But this only works because the GenderType() is very simple. What if the gender codes were stored in configuration or in a database? The next section explains how more complex field types solve this problem.

Creating your Field Type as a Service

So far, this entry has assumed that you have a very simple custom field type. But if you need access to configuration, a database connection, or some other service, then you’ll want to register your custom type as a service. For example, suppose that you’re storing the gender parameters in configuration:

  • YAML
    # app/config/config.yml
    parameters:
        genders:
            m: Male
            f: Female
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="genders" type="collection">
            <parameter key="m">Male</parameter>
            <parameter key="f">Female</parameter>
        </parameter>
    </parameters>
    
  • PHP
    // app/config/config.php
    $container->setParameter('genders.m', 'Male');
    $container->setParameter('genders.f', 'Female');
    

To use the parameter, define your custom field type as a service, injecting the genders parameter value as the first argument to its to-be-created __construct function:

  • YAML
    # src/AppBundle/Resources/config/services.yml
    services:
        acme_demo.form.type.gender:
            class: AppBundle\Form\Type\GenderType
            arguments:
                - "%genders%"
            tags:
                - { name: form.type, alias: gender }
    
  • XML
    <!-- src/AppBundle/Resources/config/services.xml -->
    <service id="acme_demo.form.type.gender" class="AppBundle\Form\Type\GenderType">
        <argument>%genders%</argument>
        <tag name="form.type" alias="gender" />
    </service>
    
  • PHP
    // src/AppBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container
        ->setDefinition('acme_demo.form.type.gender', new Definition(
            'AppBundle\Form\Type\GenderType',
            array('%genders%')
        ))
        ->addTag('form.type', array(
            'alias' => 'gender',
        ))
    ;
    

小技巧

Make sure the services file is being imported. See Importing Configuration with imports for details.

Be sure that the alias attribute of the tag corresponds with the value returned by the getName method defined earlier. You’ll see the importance of this in a moment when you use the custom field type. But first, add a __construct method to GenderType, which receives the gender configuration:

// src/AppBundle/Form/Type/GenderType.php
namespace AppBundle\Form\Type;

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...

// ...
class GenderType extends AbstractType
{
    private $genderChoices;

    public function __construct(array $genderChoices)
    {
        $this->genderChoices = $genderChoices;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'choices' => $this->genderChoices,
        ));
    }

    // ...
}

Great! The GenderType is now fueled by the configuration parameters and registered as a service. Additionally, because you used the form.type alias in its configuration, using the field is now much easier:

// src/AppBundle/Form/Type/AuthorType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\FormBuilderInterface;

// ...

class AuthorType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('gender_code', 'gender', array(
            'empty_value' => 'Choose a gender',
        ));
    }
}

Notice that instead of instantiating a new instance, you can just refer to it by the alias used in your service configuration, gender. Have fun!

How to Create a Form Type Extension

Custom form field types are great when you need field types with a specific purpose, such as a gender selector, or a VAT number input.

But sometimes, you don’t really need to add new field types - you want to add features on top of existing types. This is where form type extensions come in.

Form type extensions have 2 main use-cases:

  1. You want to add a generic feature to several types (such as adding a “help” text to every field type);
  2. You want to add a specific feature to a single type (such as adding a “download” feature to the “file” field type).

In both those cases, it might be possible to achieve your goal with custom form rendering, or custom form field types. But using form type extensions can be cleaner (by limiting the amount of business logic in templates) and more flexible (you can add several type extensions to a single form type).

Form type extensions can achieve most of what custom field types can do, but instead of being field types of their own, they plug into existing types.

Imagine that you manage a Media entity, and that each media is associated to a file. Your Media form uses a file type, but when editing the entity, you would like to see its image automatically rendered next to the file input.

You could of course do this by customizing how this field is rendered in a template. But field type extensions allow you to do this in a nice DRY fashion.

Defining the Form Type Extension

Your first task will be to create the form type extension class (called ImageTypeExtension in this article). By standard, form extensions usually live in the Form\Extension directory of one of your bundles.

When creating a form type extension, you can either implement the FormTypeExtensionInterface interface or extend the AbstractTypeExtension class. In most cases, it’s easier to extend the abstract class:

// src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php
namespace Acme\DemoBundle\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;

class ImageTypeExtension extends AbstractTypeExtension
{
    /**
     * Returns the name of the type being extended.
     *
     * @return string The name of the type being extended
     */
    public function getExtendedType()
    {
        return 'file';
    }
}

The only method you must implement is the getExtendedType function. It is used to indicate the name of the form type that will be extended by your extension.

小技巧

The value you return in the getExtendedType method corresponds to the value returned by the getName method in the form type class you wish to extend.

In addition to the getExtendedType function, you will probably want to override one of the following methods:

  • buildForm()
  • buildView()
  • setDefaultOptions()
  • finishView()

For more information on what those methods do, you can refer to the Creating Custom Field Types cookbook article.

Registering your Form Type Extension as a Service

The next step is to make Symfony aware of your extension. All you need to do is to declare it as a service by using the form.type_extension tag:

  • YAML
    services:
        acme_demo_bundle.image_type_extension:
            class: Acme\DemoBundle\Form\Extension\ImageTypeExtension
            tags:
                - { name: form.type_extension, alias: file }
    
  • XML
    <service id="acme_demo_bundle.image_type_extension"
        class="Acme\DemoBundle\Form\Extension\ImageTypeExtension"
    >
        <tag name="form.type_extension" alias="file" />
    </service>
    
  • PHP
    $container
        ->register(
            'acme_demo_bundle.image_type_extension',
            'Acme\DemoBundle\Form\Extension\ImageTypeExtension'
        )
        ->addTag('form.type_extension', array('alias' => 'file'));
    

The alias key of the tag is the type of field that this extension should be applied to. In your case, as you want to extend the file field type, you will use file as an alias.

Adding the extension Business Logic

The goal of your extension is to display nice images next to file inputs (when the underlying model contains images). For that purpose, suppose that you use an approach similar to the one described in How to handle File Uploads with Doctrine: you have a Media model with a file property (corresponding to the file field in the form) and a path property (corresponding to the image path in the database):

// src/Acme/DemoBundle/Entity/Media.php
namespace Acme\DemoBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Media
{
    // ...

    /**
     * @var string The path - typically stored in the database
     */
    private $path;

    /**
     * @var \Symfony\Component\HttpFoundation\File\UploadedFile
     * @Assert\File(maxSize="2M")
     */
    public $file;

    // ...

    /**
     * Get the image URL
     *
     * @return null|string
     */
    public function getWebPath()
    {
        // ... $webPath being the full image URL, to be used in templates

        return $webPath;
    }
}

Your form type extension class will need to do two things in order to extend the file form type:

  1. Override the setDefaultOptions method in order to add an image_path option;
  2. Override the buildForm and buildView methods in order to pass the image URL to the view.

The logic is the following: when adding a form field of type file, you will be able to specify a new option: image_path. This option will tell the file field how to get the actual image URL in order to display it in the view:

// src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php
namespace Acme\DemoBundle\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class ImageTypeExtension extends AbstractTypeExtension
{
    /**
     * Returns the name of the type being extended.
     *
     * @return string The name of the type being extended
     */
    public function getExtendedType()
    {
        return 'file';
    }

    /**
     * Add the image_path option
     *
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setOptional(array('image_path'));
    }

    /**
     * Pass the image URL to the view
     *
     * @param FormView $view
     * @param FormInterface $form
     * @param array $options
     */
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        if (array_key_exists('image_path', $options)) {
            $parentData = $form->getParent()->getData();

            if (null !== $parentData) {
                $accessor = PropertyAccess::createPropertyAccessor();
                $imageUrl = $accessor->getValue($parentData, $options['image_path']);
            } else {
                 $imageUrl = null;
            }

            // set an "image_url" variable that will be available when rendering this field
            $view->vars['image_url'] = $imageUrl;
        }
    }

}
Override the File Widget Template Fragment

Each field type is rendered by a template fragment. Those template fragments can be overridden in order to customize form rendering. For more information, you can refer to the What are Form Themes? article.

In your extension class, you have added a new variable (image_url), but you still need to take advantage of this new variable in your templates. Specifically, you need to override the file_widget block:

  • Twig
    {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
    {% extends 'form_div_layout.html.twig' %}
    
    {% block file_widget %}
        {% spaceless %}
    
        {{ block('form_widget') }}
        {% if image_url is not null %}
            <img src="{{ asset(image_url) }}"/>
        {% endif %}
    
        {% endspaceless %}
    {% endblock %}
    
  • PHP
    <!-- src/Acme/DemoBundle/Resources/views/Form/file_widget.html.php -->
    <?php echo $view['form']->widget($form) ?>
    <?php if (null !== $image_url): ?>
        <img src="<?php echo $view['assets']->getUrl($image_url) ?>"/>
    <?php endif ?>
    

注解

You will need to change your config file or explicitly specify how you want your form to be themed in order for Symfony to use your overridden block. See What are Form Themes? for more information.

Using the Form Type Extension

From now on, when adding a field of type file in your form, you can specify an image_path option that will be used to display an image next to the file field. For example:

// src/Acme/DemoBundle/Form/Type/MediaType.php
namespace Acme\DemoBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class MediaType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', 'text')
            ->add('file', 'file', array('image_path' => 'webPath'));
    }

    public function getName()
    {
        return 'media';
    }
}

When displaying the form, if the underlying model has already been associated with an image, you will see it displayed next to the file input.

How to Reduce Code Duplication with “inherit_data”

2.3 新版功能: This inherit_data option was introduced in Symfony 2.3. Before, it was known as virtual.

The inherit_data form field option can be very useful when you have some duplicated fields in different entities. For example, imagine you have two entities, a Company and a Customer:

// src/AppBundle/Entity/Company.php
namespace AppBundle\Entity;

class Company
{
    private $name;
    private $website;

    private $address;
    private $zipcode;
    private $city;
    private $country;
}
// src/AppBundle/Entity/Customer.php
namespace AppBundle\Entity;

class Customer
{
    private $firstName;
    private $lastName;

    private $address;
    private $zipcode;
    private $city;
    private $country;
}

As you can see, each entity shares a few of the same fields: address, zipcode, city, country.

Start with building two forms for these entities, CompanyType and CustomerType:

// src/AppBundle/Form/Type/CompanyType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class CompanyType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', 'text')
            ->add('website', 'text');
    }
}
// src/AppBundle/Form/Type/CustomerType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;

class CustomerType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstName', 'text')
            ->add('lastName', 'text');
    }
}

Instead of including the duplicated fields address, zipcode, city and country in both of these forms, create a third form called LocationType for that:

// src/AppBundle/Form/Type/LocationType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class LocationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('address', 'textarea')
            ->add('zipcode', 'text')
            ->add('city', 'text')
            ->add('country', 'text');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'inherit_data' => true
        ));
    }

    public function getName()
    {
        return 'location';
    }
}

The location form has an interesting option set, namely inherit_data. This option lets the form inherit its data from its parent form. If embedded in the company form, the fields of the location form will access the properties of the Company instance. If embedded in the customer form, the fields will access the properties of the Customer instance instead. Easy, eh?

注解

Instead of setting the inherit_data option inside LocationType, you can also (just like with any option) pass it in the third argument of $builder->add().

Finally, make this work by adding the location form to your two original forms:

// src/AppBundle/Form/Type/CompanyType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('foo', new LocationType(), array(
        'data_class' => 'AppBundle\Entity\Company'
    ));
}
// src/AppBundle/Form/Type/CustomerType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('bar', new LocationType(), array(
        'data_class' => 'AppBundle\Entity\Customer'
    ));
}

That’s it! You have extracted duplicated field definitions to a separate location form that you can reuse wherever you need it.

警告

Forms with the inherit_data option set cannot have *_SET_DATA event listeners.

How to Unit Test your Forms

The Form component consists of 3 core objects: a form type (implementing FormTypeInterface), the Form and the FormView.

The only class that is usually manipulated by programmers is the form type class which serves as a form blueprint. It is used to generate the Form and the FormView. You could test it directly by mocking its interactions with the factory but it would be complex. It is better to pass it to FormFactory like it is done in a real application. It is simple to bootstrap and you can trust the Symfony components enough to use them as a testing base.

There is already a class that you can benefit from for simple FormTypes testing: TypeTestCase. It is used to test the core types and you can use it to test your types too.

2.3 新版功能: The TypeTestCase has moved to the Symfony\Component\Form\Test namespace in 2.3. Previously, the class was located in Symfony\Component\Form\Tests\Extension\Core\Type.

注解

Depending on the way you installed your Symfony or Symfony Form component the tests may not be downloaded. Use the --prefer-source option with Composer if this is the case.

The Basics

The simplest TypeTestCase implementation looks like the following:

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTest.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;

class TestedTypeTest extends TypeTestCase
{
    public function testSubmitValidData()
    {
        $formData = array(
            'test' => 'test',
            'test2' => 'test2',
        );

        $type = new TestedType();
        $form = $this->factory->create($type);

        $object = new TestObject();
        $object->fromArray($formData);

        // submit the data to the form directly
        $form->submit($formData);

        $this->assertTrue($form->isSynchronized());
        $this->assertEquals($object, $form->getData());

        $view = $form->createView();
        $children = $view->children;

        foreach (array_keys($formData) as $key) {
            $this->assertArrayHasKey($key, $children);
        }
    }
}

So, what does it test? Here comes a detailed explanation.

First you verify if the FormType compiles. This includes basic class inheritance, the buildForm function and options resolution. This should be the first test you write:

$type = new TestedType();
$form = $this->factory->create($type);

This test checks that none of your data transformers used by the form failed. The isSynchronized() method is only set to false if a data transformer throws an exception:

$form->submit($formData);
$this->assertTrue($form->isSynchronized());

注解

Don’t test the validation: it is applied by a listener that is not active in the test case and it relies on validation configuration. Instead, unit test your custom constraints directly.

Next, verify the submission and mapping of the form. The test below checks if all the fields are correctly specified:

$this->assertEquals($object, $form->getData());

Finally, check the creation of the FormView. You should check if all widgets you want to display are available in the children property:

$view = $form->createView();
$children = $view->children;

foreach (array_keys($formData) as $key) {
    $this->assertArrayHasKey($key, $children);
}
Adding a Type your Form Depends on

Your form may depend on other types that are defined as services. It might look like this:

// src/Acme/TestBundle/Form/Type/TestedType.php

// ... the buildForm method
$builder->add('acme_test_child_type');

To create your form correctly, you need to make the type available to the form factory in your test. The easiest way is to register it manually before creating the parent form using the PreloadedExtension class:

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Form\PreloadedExtension;

class TestedTypeTest extends TypeTestCase
{
    protected function getExtensions()
    {
        $childType = new TestChildType();
        return array(new PreloadedExtension(array(
            $childType->getName() => $childType,
        ), array()));
    }

    public function testSubmitValidData()
    {
        $type = new TestedType();
        $form = $this->factory->create($type);

        // ... your test
    }
}

警告

Make sure the child type you add is well tested. Otherwise you may be getting errors that are not related to the form you are currently testing but to its children.

Adding custom Extensions

It often happens that you use some options that are added by form extensions. One of the cases may be the ValidatorExtension with its invalid_message option. The TypeTestCase loads only the core form extension so an “Invalid option” exception will be raised if you try to use it for testing a class that depends on other extensions. You need add those extensions to the factory object:

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension;
use Symfony\Component\Validator\ConstraintViolationList;

class TestedTypeTest extends TypeTestCase
{
    protected function setUp()
    {
        parent::setUp();

        $validator = $this->getMock('\Symfony\Component\Validator\ValidatorInterface');
        $validator->method('validate')->will($this->returnValue(new ConstraintViolationList()));

        $this->factory = Forms::createFormFactoryBuilder()
            ->addExtensions($this->getExtensions())
            ->addTypeExtension(
                new FormTypeValidatorExtension(
                    $validator
                )
            )
            ->addTypeGuesser(
                $this->getMockBuilder(
                    'Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser'
                )
                    ->disableOriginalConstructor()
                    ->getMock()
            )
            ->getFormFactory();

        $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
        $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory);
    }

    // ... your tests
}
Testing against different Sets of Data

If you are not familiar yet with PHPUnit’s data providers, this might be a good opportunity to use them:

// src/Acme/TestBundle/Tests/Form/Type/TestedTypeTests.php
namespace Acme\TestBundle\Tests\Form\Type;

use Acme\TestBundle\Form\Type\TestedType;
use Acme\TestBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;

class TestedTypeTest extends TypeTestCase
{

    /**
     * @dataProvider getValidTestData
     */
    public function testForm($data)
    {
        // ... your test
    }

    public function getValidTestData()
    {
        return array(
            array(
                'data' => array(
                    'test' => 'test',
                    'test2' => 'test2',
                ),
            ),
            array(
                'data' => array(),
            ),
            array(
                'data' => array(
                    'test' => null,
                    'test2' => null,
                ),
            ),
        );
    }
}

The code above will run your test three times with 3 different sets of data. This allows for decoupling the test fixtures from the tests and easily testing against multiple sets of data.

You can also pass another argument, such as a boolean if the form has to be synchronized with the given set of data or not etc.

How to Configure empty Data for a Form Class

The empty_data option allows you to specify an empty data set for your form class. This empty data set would be used if you submit your form, but haven’t called setData() on your form or passed in data when you created your form. For example:

public function indexAction()
{
    $blog = ...;

    // $blog is passed in as the data, so the empty_data
    // option is not needed
    $form = $this->createForm(new BlogType(), $blog);

    // no data is passed in, so empty_data is
    // used to get the "starting data"
    $form = $this->createForm(new BlogType());
}

By default, empty_data is set to null. Or, if you have specified a data_class option for your form class, it will default to a new instance of that class. That instance will be created by calling the constructor with no arguments.

If you want to override this default behavior, there are two ways to do this.

Option 1: Instantiate a new Class

One reason you might use this option is if you want to use a constructor that takes arguments. Remember, the default data_class option calls that constructor with no arguments:

// src/AppBundle/Form/Type/BlogType.php

// ...
use Symfony\Component\Form\AbstractType;
use AppBundle\Entity\Blog;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class BlogType extends AbstractType
{
    private $someDependency;

    public function __construct($someDependency)
    {
        $this->someDependency = $someDependency;
    }
    // ...

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'empty_data' => new Blog($this->someDependency),
        ));
    }
}

You can instantiate your class however you want. In this example, we pass some dependency into the BlogType when we instantiate it, then use that to instantiate the Blog class. The point is, you can set empty_data to the exact “new” object that you want to use.

Option 2: Provide a Closure

Using a closure is the preferred method, since it will only create the object if it is needed.

The closure must accept a FormInterface instance as the first argument:

use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormInterface;
// ...

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'empty_data' => function (FormInterface $form) {
            return new Blog($form->get('title')->getData());
        },
    ));
}
How to Use the submit() Function to Handle Form Submissions

2.3 新版功能: The handleRequest() method was introduced in Symfony 2.3.

With the handleRequest() method, it is really easy to handle form submissions:

use Symfony\Component\HttpFoundation\Request;
// ...

public function newAction(Request $request)
{
    $form = $this->createFormBuilder()
        // ...
        ->getForm();

    $form->handleRequest($request);

    if ($form->isValid()) {
        // perform some action...

        return $this->redirect($this->generateUrl('task_success'));
    }

    return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
        'form' => $form->createView(),
    ));
}

小技巧

To see more about this method, read Handling Form Submissions.

Calling Form::submit() manually

2.3 新版功能: Before Symfony 2.3, the submit() method was known as bind().

In some cases, you want better control over when exactly your form is submitted and what data is passed to it. Instead of using the handleRequest() method, pass the submitted data directly to submit():

use Symfony\Component\HttpFoundation\Request;
// ...

public function newAction(Request $request)
{
    $form = $this->createFormBuilder()
        // ...
        ->getForm();

    if ($request->isMethod('POST')) {
        $form->submit($request->request->get($form->getName()));

        if ($form->isValid()) {
            // perform some action...

            return $this->redirect($this->generateUrl('task_success'));
        }
    }

    return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
        'form' => $form->createView(),
    ));
}

小技巧

Forms consisting of nested fields expect an array in submit(). You can also submit individual fields by calling submit() directly on the field:

$form->get('firstName')->submit('Fabien');
Passing a Request to Form::submit() (Deprecated)

2.3 新版功能: Before Symfony 2.3, the submit method was known as bind.

Before Symfony 2.3, the submit() method accepted a Request object as a convenient shortcut to the previous example:

use Symfony\Component\HttpFoundation\Request;
// ...

public function newAction(Request $request)
{
    $form = $this->createFormBuilder()
        // ...
        ->getForm();

    if ($request->isMethod('POST')) {
        $form->submit($request);

        if ($form->isValid()) {
            // perform some action...

            return $this->redirect($this->generateUrl('task_success'));
        }
    }

    return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
        'form' => $form->createView(),
    ));
}

Passing the Request directly to submit() still works, but is deprecated and will be removed in Symfony 3.0. You should use the method handleRequest() instead.

How to Use the virtual Form Field Option

As of Symfony 2.3, the virtual option is renamed to inherit_data. You can read everything about the new option in “How to Reduce Code Duplication with “inherit_data””.

Logging

How to Use Monolog to Write Logs

Monolog is a logging library for PHP 5.3 used by Symfony. It is inspired by the Python LogBook library.

Usage

To log a message simply get the logger service from the container in your controller:

public function indexAction()
{
    $logger = $this->get('logger');
    $logger->info('I just got the logger');
    $logger->error('An error occurred');

    // ...
}

The logger service has different methods for different logging levels. See LoggerInterface for details on which methods are available.

Handlers and Channels: Writing Logs to different Locations

In Monolog each logger defines a logging channel, which organizes your log messages into different “categories”. Then, each channel has a stack of handlers to write the logs (the handlers can be shared).

小技巧

When injecting the logger in a service you can use a custom channel control which “channel” the logger will log to.

The basic handler is the StreamHandler which writes logs in a stream (by default in the app/logs/prod.log in the prod environment and app/logs/dev.log in the dev environment).

Monolog comes also with a powerful built-in handler for the logging in prod environment: FingersCrossedHandler. It allows you to store the messages in a buffer and to log them only if a message reaches the action level (error in the configuration provided in the Standard Edition) by forwarding the messages to another handler.

Using several Handlers

The logger uses a stack of handlers which are called successively. This allows you to log the messages in several ways easily.

  • YAML
    # app/config/config.yml
    monolog:
        handlers:
            applog:
                type: stream
                path: /var/log/symfony.log
                level: error
            main:
                type: fingers_crossed
                action_level: warning
                handler: file
            file:
                type: stream
                level: debug
            syslog:
                type: syslog
                level: error
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd">
    
        <monolog:config>
            <monolog:handler
                name="applog"
                type="stream"
                path="/var/log/symfony.log"
                level="error"
            />
            <monolog:handler
                name="main"
                type="fingers_crossed"
                action-level="warning"
                handler="file"
            />
            <monolog:handler
                name="file"
                type="stream"
                level="debug"
            />
            <monolog:handler
                name="syslog"
                type="syslog"
                level="error"
            />
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('monolog', array(
        'handlers' => array(
            'applog' => array(
                'type'  => 'stream',
                'path'  => '/var/log/symfony.log',
                'level' => 'error',
            ),
            'main' => array(
                'type'         => 'fingers_crossed',
                'action_level' => 'warning',
                'handler'      => 'file',
            ),
            'file' => array(
                'type'  => 'stream',
                'level' => 'debug',
            ),
            'syslog' => array(
                'type'  => 'syslog',
                'level' => 'error',
            ),
        ),
    ));
    

The above configuration defines a stack of handlers which will be called in the order they are defined.

小技巧

The handler named “file” will not be included in the stack itself as it is used as a nested handler of the fingers_crossed handler.

注解

If you want to change the config of MonologBundle in another config file you need to redefine the whole stack. It cannot be merged because the order matters and a merge does not allow to control the order.

Changing the Formatter

The handler uses a Formatter to format the record before logging it. All Monolog handlers use an instance of Monolog\Formatter\LineFormatter by default but you can replace it easily. Your formatter must implement Monolog\Formatter\FormatterInterface.

  • YAML
    # app/config/config.yml
    services:
        my_formatter:
            class: Monolog\Formatter\JsonFormatter
    monolog:
        handlers:
            file:
                type: stream
                level: debug
                formatter: my_formatter
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd">
    
        <services>
            <service id="my_formatter" class="Monolog\Formatter\JsonFormatter" />
        </services>
    
        <monolog:config>
            <monolog:handler
                name="file"
                type="stream"
                level="debug"
                formatter="my_formatter"
            />
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container
        ->register('my_formatter', 'Monolog\Formatter\JsonFormatter');
    
    $container->loadFromExtension('monolog', array(
        'handlers' => array(
            'file' => array(
                'type'      => 'stream',
                'level'     => 'debug',
                'formatter' => 'my_formatter',
            ),
        ),
    ));
    
Adding some extra Data in the Log Messages

Monolog allows you to process the record before logging it to add some extra data. A processor can be applied for the whole handler stack or only for a specific handler.

A processor is simply a callable receiving the record as its first argument.

Processors are configured using the monolog.processor DIC tag. See the reference about it.

Adding a Session/Request Token

Sometimes it is hard to tell which entries in the log belong to which session and/or request. The following example will add a unique token for each request using a processor.

namespace Acme\MyBundle;

use Symfony\Component\HttpFoundation\Session\Session;

class SessionRequestProcessor
{
    private $session;
    private $token;

    public function __construct(Session $session)
    {
        $this->session = $session;
    }

    public function processRecord(array $record)
    {
        if (null === $this->token) {
            try {
                $this->token = substr($this->session->getId(), 0, 8);
            } catch (\RuntimeException $e) {
                $this->token = '????????';
            }
            $this->token .= '-' . substr(uniqid(), -8);
        }
        $record['extra']['token'] = $this->token;

        return $record;
    }
}
  • YAML
    # app/config/config.yml
    services:
        monolog.formatter.session_request:
            class: Monolog\Formatter\LineFormatter
            arguments:
                - "[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n"
    
        monolog.processor.session_request:
            class: Acme\MyBundle\SessionRequestProcessor
            arguments:  ["@session"]
            tags:
                - { name: monolog.processor, method: processRecord }
    
    monolog:
        handlers:
            main:
                type: stream
                path: "%kernel.logs_dir%/%kernel.environment%.log"
                level: debug
                formatter: monolog.formatter.session_request
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd">
    
        <services>
            <service id="monolog.formatter.session_request"
                class="Monolog\Formatter\LineFormatter">
    
                <argument>[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%&#xA;</argument>
            </service>
    
            <service id="monolog.processor.session_request"
                class="Acme\MyBundle\SessionRequestProcessor">
    
                <argument type="service" id="session" />
                <tag name="monolog.processor" method="processRecord" />
            </service>
        </services>
    
        <monolog:config>
            <monolog:handler
                name="main"
                type="stream"
                path="%kernel.logs_dir%/%kernel.environment%.log"
                level="debug"
                formatter="monolog.formatter.session_request"
            />
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container
        ->register(
            'monolog.formatter.session_request',
            'Monolog\Formatter\LineFormatter'
        )
        ->addArgument('[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n');
    
    $container
        ->register(
            'monolog.processor.session_request',
            'Acme\MyBundle\SessionRequestProcessor'
        )
        ->addArgument(new Reference('session'))
        ->addTag('monolog.processor', array('method' => 'processRecord'));
    
    $container->loadFromExtension('monolog', array(
        'handlers' => array(
            'main' => array(
                'type'      => 'stream',
                'path'      => '%kernel.logs_dir%/%kernel.environment%.log',
                'level'     => 'debug',
                'formatter' => 'monolog.formatter.session_request',
            ),
        ),
    ));
    

注解

If you use several handlers, you can also register a processor at the handler level or at the channel level instead of registering it globally (see the following sections).

Registering Processors per Handler

You can register a processor per handler using the handler option of the monolog.processor tag:

  • YAML
    # app/config/config.yml
    services:
        monolog.processor.session_request:
            class: Acme\MyBundle\SessionRequestProcessor
            arguments:  ["@session"]
            tags:
                - { name: monolog.processor, method: processRecord, handler: main }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd">
    
        <services>
            <service id="monolog.processor.session_request"
                class="Acme\MyBundle\SessionRequestProcessor">
    
                <argument type="service" id="session" />
                <tag name="monolog.processor" method="processRecord" handler="main" />
            </service>
        </services>
    </container>
    
  • PHP
    // app/config/config.php
    $container
        ->register(
            'monolog.processor.session_request',
            'Acme\MyBundle\SessionRequestProcessor'
        )
        ->addArgument(new Reference('session'))
        ->addTag('monolog.processor', array('method' => 'processRecord', 'handler' => 'main'));
    
Registering Processors per Channel

You can register a processor per channel using the channel option of the monolog.processor tag:

  • YAML
    # app/config/config.yml
    services:
        monolog.processor.session_request:
            class: Acme\MyBundle\SessionRequestProcessor
            arguments:  ["@session"]
            tags:
                - { name: monolog.processor, method: processRecord, channel: main }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd">
    
        <services>
            <service id="monolog.processor.session_request"
                class="Acme\MyBundle\SessionRequestProcessor">
    
                <argument type="service" id="session" />
                <tag name="monolog.processor" method="processRecord" channel="main" />
            </service>
        </services>
    </container>
    
  • PHP
    // app/config/config.php
    $container
        ->register(
            'monolog.processor.session_request',
            'Acme\MyBundle\SessionRequestProcessor'
        )
        ->addArgument(new Reference('session'))
        ->addTag('monolog.processor', array('method' => 'processRecord', 'channel' => 'main'));
    
How to Configure Monolog to Email Errors

Monolog can be configured to send an email when an error occurs with an application. The configuration for this requires a few nested handlers in order to avoid receiving too many emails. This configuration looks complicated at first but each handler is fairly straight forward when it is broken down.

  • YAML
    # app/config/config_prod.yml
    monolog:
        handlers:
            mail:
                type:         fingers_crossed
                # 500 errors are logged at the critical level
                action_level: critical
                # to also log 400 level errors (but not 404's):
                # action_level: error
                # excluded_404s:
                #     - ^/
                handler:      buffered
            buffered:
                type:    buffer
                handler: swift
            swift:
                type:       swift_mailer
                from_email: error@example.com
                to_email:   error@example.com
                # or list of recipients
                # to_email:   [dev1@example.com, dev2@example.com, ...]
                subject:    An Error Occurred!
                level:      debug
    
  • XML
    <!-- app/config/config_prod.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/monolog http://symfony.com/schema/dic/monolog/monolog-1.0.xsd">
    
        <monolog:config>
            <monolog:handler
                name="mail"
                type="fingers_crossed"
                action-level="critical"
                handler="buffered"
                <!--
                To also log 400 level errors (but not 404's):
                action-level="error"
                And add this child inside this monolog:handler
                <monolog:excluded-404>^/</monolog:excluded-404>
                -->
            />
            <monolog:handler
                name="buffered"
                type="buffer"
                handler="swift"
            />
            <monolog:handler
                name="swift"
                type="swift_mailer"
                from-email="error@example.com"
                subject="An Error Occurred!"
                level="debug">
    
                <monolog:to-email>error@example.com</monolog:to-email>
    
                <!-- or multiple to-email elements -->
                <!--
                <monolog:to-email>dev1@example.com</monolog:to-email>
                <monolog:to-email>dev2@example.com</monolog:to-email>
                ...
                -->
            </monolog:handler>
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config_prod.php
    $container->loadFromExtension('monolog', array(
        'handlers' => array(
            'mail' => array(
                'type'         => 'fingers_crossed',
                'action_level' => 'critical',
                // to also log 400 level errors (but not 404's):
                // 'action_level' => 'error',
                // 'excluded_404s' => array(
                //     '^/',
                // ),
                'handler'      => 'buffered',
            ),
            'buffered' => array(
                'type'    => 'buffer',
                'handler' => 'swift',
            ),
            'swift' => array(
                'type'       => 'swift_mailer',
                'from_email' => 'error@example.com',
                'to_email'   => 'error@example.com',
                // or a list of recipients
                // 'to_email'   => array('dev1@example.com', 'dev2@example.com', ...),
                'subject'    => 'An Error Occurred!',
                'level'      => 'debug',
            ),
        ),
    ));
    

The mail handler is a fingers_crossed handler which means that it is only triggered when the action level, in this case critical is reached. It then logs everything including messages below the action level. The critical level is only triggered for 5xx HTTP code errors. The handler setting means that the output is then passed onto the buffered handler.

小技巧

If you want both 400 level and 500 level errors to trigger an email, set the action_level to error instead of critical. See the code above for an example.

The buffered handler simply keeps all the messages for a request and then passes them onto the nested handler in one go. If you do not use this handler then each message will be emailed separately. This is then passed to the swift handler. This is the handler that actually deals with emailing you the error. The settings for this are straightforward, the to and from addresses and the subject.

You can combine these handlers with other handlers so that the errors still get logged on the server as well as the emails being sent:

警告

The default spool setting for swiftmailer is set to memory, which means that emails are sent at the very end of the request. However, this does not work with buffered logs at the moment. In order to enable emailing logs per the example below, you must comment out the spool: { type: memory } line in the config.yml file.

  • YAML
    # app/config/config_prod.yml
    monolog:
        handlers:
            main:
                type:         fingers_crossed
                action_level: critical
                handler:      grouped
            grouped:
                type:    group
                members: [streamed, buffered]
            streamed:
                type:  stream
                path:  "%kernel.logs_dir%/%kernel.environment%.log"
                level: debug
            buffered:
                type:    buffer
                handler: swift
            swift:
                type:       swift_mailer
                from_email: error@example.com
                to_email:   error@example.com
                subject:    An Error Occurred!
                level:      debug
    
  • XML
    <!-- app/config/config_prod.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/monolog http://symfony.com/schema/dic/monolog/monolog-1.0.xsd">
    
        <monolog:config>
            <monolog:handler
                name="main"
                type="fingers_crossed"
                action_level="critical"
                handler="grouped"
            />
            <monolog:handler
                name="grouped"
                type="group"
            >
                <member type="stream"/>
                <member type="buffered"/>
            </monolog:handler>
            <monolog:handler
                name="stream"
                path="%kernel.logs_dir%/%kernel.environment%.log"
                level="debug"
            />
            <monolog:handler
                name="buffered"
                type="buffer"
                handler="swift"
            />
            <monolog:handler
                name="swift"
                from-email="error@example.com"
                to-email="error@example.com"
                subject="An Error Occurred!"
                level="debug"
            />
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config_prod.php
    $container->loadFromExtension('monolog', array(
        'handlers' => array(
            'main' => array(
                'type'         => 'fingers_crossed',
                'action_level' => 'critical',
                'handler'      => 'grouped',
            ),
            'grouped' => array(
                'type'    => 'group',
                'members' => array('streamed', 'buffered'),
            ),
            'streamed'  => array(
                'type'  => 'stream',
                'path'  => '%kernel.logs_dir%/%kernel.environment%.log',
                'level' => 'debug',
            ),
            'buffered'    => array(
                'type'    => 'buffer',
                'handler' => 'swift',
            ),
            'swift' => array(
                'type'       => 'swift_mailer',
                'from_email' => 'error@example.com',
                'to_email'   => 'error@example.com',
                'subject'    => 'An Error Occurred!',
                'level'      => 'debug',
            ),
        ),
    ));
    

This uses the group handler to send the messages to the two group members, the buffered and the stream handlers. The messages will now be both written to the log file and emailed.

How to Configure Monolog to Exclude 404 Errors from the Log

2.3 新版功能: This feature was introduced to the MonologBundle in version 2.4. This version is compatible with Symfony 2.3, but only MonologBundle 2.3 is installed by default. To use this feature, upgrade your bundle manually.

Sometimes your logs become flooded with unwanted 404 HTTP errors, for example, when an attacker scans your app for some well-known application paths (e.g. /phpmyadmin). When using a fingers_crossed handler, you can exclude logging these 404 errors based on a regular expression in the MonologBundle configuration:

  • YAML
    # app/config/config.yml
    monolog:
        handlers:
            main:
                # ...
                type: fingers_crossed
                handler: ...
                excluded_404s:
                    - ^/phpmyadmin
    
  • XML
    <!-- app/config/config.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd"
    >
        <monolog:config>
            <monolog:handler type="fingers_crossed" name="main" handler="...">
                <!-- ... -->
                <monolog:excluded-404>^/phpmyadmin</monolog:excluded-404>
            </monolog:handler>
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('monolog', array(
        'handlers' => array(
            'main' => array(
                // ...
                'type'          => 'fingers_crossed',
                'handler'       => ...,
                'excluded_404s' => array(
                    '^/phpmyadmin',
                ),
            ),
        ),
    ));
    
How to Log Messages to different Files

The Symfony Standard Edition contains a bunch of channels for logging: doctrine, event, security and request. Each channel corresponds to a logger service (monolog.logger.XXX) in the container and is injected to the concerned service. The purpose of channels is to be able to organize different types of log messages.

By default, Symfony logs every message into a single file (regardless of the channel).

Switching a Channel to a different Handler

Now, suppose you want to log the doctrine channel to a different file.

To do so, just create a new handler and configure it like this:

  • YAML
    # app/config/config.yml
    monolog:
        handlers:
            main:
                type:     stream
                path:     /var/log/symfony.log
                channels: ["!doctrine"]
            doctrine:
                type:     stream
                path:     /var/log/doctrine.log
                channels: [doctrine]
    
  • XML
    <!-- app/config/config.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd"
    >
        <monolog:config>
            <monolog:handler name="main" type="stream" path="/var/log/symfony.log">
                <monolog:channels>
                    <monolog:channel>!doctrine</monolog:channel>
                </monolog:channels>
            </monolog:handler>
    
            <monolog:handler name="doctrine" type="stream" path="/var/log/doctrine.log">
                <monolog:channels>
                    <monolog:channel>doctrine</monolog:channel>
                </monolog:channels>
            </monolog:handler>
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('monolog', array(
        'handlers' => array(
            'main'     => array(
                'type'     => 'stream',
                'path'     => '/var/log/symfony.log',
                'channels' => array(
                    '!doctrine',
                ),
            ),
            'doctrine' => array(
                'type'     => 'stream',
                'path'     => '/var/log/doctrine.log',
                'channels' => array(
                    'doctrine',
                ),
            ),
        ),
    ));
    
YAML Specification

You can specify the configuration by many forms:

channels: ~    # Include all the channels

channels: foo  # Include only channel "foo"
channels: "!foo" # Include all channels, except "foo"

channels: [foo, bar]   # Include only channels "foo" and "bar"
channels: ["!foo", "!bar"] # Include all channels, except "foo" and "bar"
Creating your own Channel

You can change the channel monolog logs to one service at a time. This is done either via the configuration below or by tagging your service with monolog.logger and specifying which channel the service should log to. With the tag, the logger that is injected into that service is preconfigured to use the channel you’ve specified.

Configure Additional Channels without Tagged Services

2.3 新版功能: This feature was introduced to the MonologBundle in version 2.4. This version is compatible with Symfony 2.3, but only MonologBundle 2.3 is installed by default. To use this feature, upgrade your bundle manually.

With MonologBundle 2.4 you can configure additional channels without the need to tag your services:

  • YAML
    # app/config/config.yml
    monolog:
        channels: ["foo", "bar"]
    
  • XML
    <!-- app/config/config.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd"
    >
        <monolog:config>
            <monolog:channel>foo</monolog:channel>
            <monolog:channel>bar</monolog:channel>
        </monolog:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('monolog', array(
        'channels' => array(
            'foo',
            'bar',
        ),
    ));
    

With this, you can now send log messages to the foo channel by using the automatically registered logger service monolog.logger.foo.

Learn more from the Cookbook

Profiler

How to Create a custom Data Collector

The Symfony Profiler delegates data collecting to data collectors. Symfony comes bundled with a few of them, but you can easily create your own.

Creating a custom Data Collector

Creating a custom data collector is as simple as implementing the DataCollectorInterface:

interface DataCollectorInterface
{
    /**
     * Collects data for the given Request and Response.
     *
     * @param Request    $request   A Request instance
     * @param Response   $response  A Response instance
     * @param \Exception $exception An Exception instance
     */
    function collect(Request $request, Response $response, \Exception $exception = null);

    /**
     * Returns the name of the collector.
     *
     * @return string The collector name
     */
    function getName();
}

The getName() method must return a unique name. This is used to access the information later on (see How to Use the Profiler in a Functional Test for instance).

The collect() method is responsible for storing the data it wants to give access to in local properties.

警告

As the profiler serializes data collector instances, you should not store objects that cannot be serialized (like PDO objects), or you need to provide your own serialize() method.

Most of the time, it is convenient to extend DataCollector and populate the $this->data property (it takes care of serializing the $this->data property):

class MemoryDataCollector extends DataCollector
{
    public function collect(Request $request, Response $response, \Exception $exception = null)
    {
        $this->data = array(
            'memory' => memory_get_peak_usage(true),
        );
    }

    public function getMemory()
    {
        return $this->data['memory'];
    }

    public function getName()
    {
        return 'memory';
    }
}
Enabling custom Data Collectors

To enable a data collector, add it as a regular service in one of your configuration, and tag it with data_collector:

  • YAML
    services:
        data_collector.your_collector_name:
            class: Fully\Qualified\Collector\Class\Name
            tags:
                - { name: data_collector }
    
  • XML
    <service id="data_collector.your_collector_name" class="Fully\Qualified\Collector\Class\Name">
        <tag name="data_collector" />
    </service>
    
  • PHP
    $container
        ->register('data_collector.your_collector_name', 'Fully\Qualified\Collector\Class\Name')
        ->addTag('data_collector')
    ;
    
Adding Web Profiler Templates

When you want to display the data collected by your data collector in the web debug toolbar or the web profiler, create a Twig template following this skeleton:

{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}

{% block toolbar %}
    {# the web debug toolbar content #}
{% endblock %}

{% block head %}
    {# if the web profiler panel needs some specific JS or CSS files #}
{% endblock %}

{% block menu %}
    {# the menu content #}
{% endblock %}

{% block panel %}
    {# the panel content #}
{% endblock %}

Each block is optional. The toolbar block is used for the web debug toolbar and menu and panel are used to add a panel to the web profiler.

All blocks have access to the collector object.

小技巧

Built-in templates use a base64 encoded image for the toolbar:

<img src="data:image/png;base64,..." />

You can easily calculate the base64 value for an image with this little script:

#!/usr/bin/env php
<?php
echo base64_encode(file_get_contents($_SERVER['argv'][1]));

To enable the template, add a template attribute to the data_collector tag in your configuration. For example, assuming your template is in some AcmeDebugBundle:

  • YAML
    services:
        data_collector.your_collector_name:
            class: Acme\DebugBundle\Collector\Class\Name
            tags:
                - { name: data_collector, template: "AcmeDebugBundle:Collector:templatename", id: "your_collector_name" }
    
  • XML
    <service id="data_collector.your_collector_name" class="Acme\DebugBundle\Collector\Class\Name">
        <tag name="data_collector" template="AcmeDebugBundle:Collector:templatename" id="your_collector_name" />
    </service>
    
  • PHP
    $container
        ->register('data_collector.your_collector_name', 'Acme\DebugBundle\Collector\Class\Name')
        ->addTag('data_collector', array(
            'template' => 'AcmeDebugBundle:Collector:templatename',
            'id'       => 'your_collector_name',
        ))
    ;
    
How to Use Matchers to Enable the Profiler Conditionally

By default, the profiler is only activated in the development environment. But it’s imaginable that a developer may want to see the profiler even in production. Another situation may be that you want to show the profiler only when an admin has logged in. You can enable the profiler in these situations by using matchers.

Using the built-in Matcher

Symfony provides a built-in matcher which can match paths and IPs. For example, if you want to only show the profiler when accessing the page with the 168.0.0.1 IP, then you can use this configuration:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        profiler:
            matcher:
                ip: 168.0.0.1
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <framework:profiler
            ip="168.0.0.1"
        />
    </framework:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'profiler' => array(
            'ip' => '168.0.0.1',
        ),
    ));
    

You can also set a path option to define the path on which the profiler should be enabled. For instance, setting it to ^/admin/ will enable the profiler only for the /admin/ URLs.

Creating a custom Matcher

You can also create a custom matcher. This is a service that checks whether the profiler should be enabled or not. To create that service, create a class which implements RequestMatcherInterface. This interface requires one method: matches(). This method returns false to disable the profiler and true to enable the profiler.

To enable the profiler when a ROLE_SUPER_ADMIN is logged in, you can use something like:

// src/AppBundle/Profiler/SuperAdminMatcher.php
namespace AppBundle\Profiler;

use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;

class SuperAdminMatcher implements RequestMatcherInterface
{
    protected $securityContext;

    public function __construct(SecurityContext $securityContext)
    {
        $this->securityContext = $securityContext;
    }

    public function matches(Request $request)
    {
        return $this->securityContext->isGranted('ROLE_SUPER_ADMIN');
    }
}

Then, you need to configure the service:

  • YAML
    # app/config/services.yml
    services:
        app.profiler.matcher.super_admin:
            class: AppBundle\Profiler\SuperAdminMatcher
            arguments: ["@security.context"]
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="app.profiler.matcher.super_admin"
            class="AppBundle\Profiler\SuperAdminMatcher">
            <argument type="service" id="security.context" />
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('app.profiler.matcher.super_admin', new Definition(
        'AppBundle\Profiler\SuperAdminMatcher',
        array(new Reference('security.context'))
    );
    

Now the service is registered, the only thing left to do is configure the profiler to use this service as the matcher:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        profiler:
            matcher:
                service: app.profiler.matcher.super_admin
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <!-- ... -->
        <framework:profiler
            service="app.profiler.matcher.super_admin"
        />
    </framework:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'profiler' => array(
            'service' => 'app.profiler.matcher.super_admin',
        ),
    ));
    
Switching the Profiler Storage

By default the profile stores the collected data in files in the cache directory. You can control the storage being used through the dsn, username, password and lifetime options. For example, the following configuration uses MySQL as the storage for the profiler with a lifetime of one hour:

  • YAML
    # app/config/config.yml
    framework:
        profiler:
            dsn:      "mysql:host=localhost;dbname=%database_name%"
            username: "%database_user%"
            password: "%database_password%"
            lifetime: 3600
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    >
        <framework:config>
            <framework:profiler
                dsn="mysql:host=localhost;dbname=%database_name%"
                username="%database_user%"
                password="%database_password%"
                lifetime="3600"
            />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    
    // ...
    $container->loadFromExtension('framework', array(
        'profiler' => array(
            'dsn'      => 'mysql:host=localhost;dbname=%database_name%',
            'username' => '%database_user',
            'password' => '%database_password%',
            'lifetime' => 3600,
        ),
    ));
    

The HttpKernel component currently supports the following profiler storage implementations:

Request

How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy

When you deploy your application, you may be behind a load balancer (e.g. an AWS Elastic Load Balancer) or a reverse proxy (e.g. Varnish for caching).

For the most part, this doesn’t cause any problems with Symfony. But, when a request passes through a proxy, certain request information is sent using special X-Forwarded-* headers. For example, instead of reading the REMOTE_ADDR header (which will now be the IP address of your reverse proxy), the user’s true IP will be stored in an X-Forwarded-For header.

If you don’t configure Symfony to look for these headers, you’ll get incorrect information about the client’s IP address, whether or not the client is connecting via HTTPS, the client’s port and the hostname being requested.

Solution: trusted_proxies

This is no problem, but you do need to tell Symfony that this is happening and which reverse proxy IP addresses will be doing this type of thing:

  • YAML
    # app/config/config.yml
    # ...
    framework:
        trusted_proxies:  [192.0.0.1, 10.0.0.0/8]
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config trusted-proxies="192.0.0.1, 10.0.0.0/8">
            <!-- ... -->
        </framework>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'trusted_proxies' => array('192.0.0.1', '10.0.0.0/8'),
    ));
    

In this example, you’re saying that your reverse proxy (or proxies) has the IP address 192.0.0.1 or matches the range of IP addresses that use the CIDR notation 10.0.0.0/8. For more details, see the framework.trusted_proxies option.

That’s it! Symfony will now look for the correct X-Forwarded-* headers to get information like the client’s IP address, host, port and whether or not the request is using HTTPS.

But what if the IP of my Reverse Proxy Changes Constantly!

Some reverse proxies (like Amazon’s Elastic Load Balancers) don’t have a static IP address or even a range that you can target with the CIDR notation. In this case, you’ll need to - very carefully - trust all proxies.

  1. Configure your web server(s) to not respond to traffic from any clients other than your load balancers. For AWS, this can be done with security groups.

  2. Once you’ve guaranteed that traffic will only come from your trusted reverse proxies, configure Symfony to always trust incoming request. This is done inside of your front controller:

    // web/app.php
    
    // ...
    Request::setTrustedProxies(array($request->server->get('REMOTE_ADDR')));
    
    $response = $kernel->handle($request);
    // ...
    

That’s it! It’s critical that you prevent traffic from all non-trusted sources. If you allow outside traffic, they could “spoof” their true IP address and other information.

My Reverse Proxy Uses Non-Standard (not X-Forwarded) Headers

Most reverse proxies store information on specific X-Forwarded-* headers. But if your reverse proxy uses non-standard header names, you can configure these (see “Trusting Proxies”). The code for doing this will need to live in your front controller (e.g. web/app.php).

How to Register a new Request Format and Mime Type

Every Request has a “format” (e.g. html, json), which is used to determine what type of content to return in the Response. In fact, the request format, accessible via getRequestFormat(), is used to set the MIME type of the Content-Type header on the Response object. Internally, Symfony contains a map of the most common formats (e.g. html, json) and their associated MIME types (e.g. text/html, application/json). Of course, additional format-MIME type entries can easily be added. This document will show how you can add the jsonp format and corresponding MIME type.

Create a kernel.request Listener

The key to defining a new MIME type is to create a class that will “listen” to the kernel.request event dispatched by the Symfony kernel. The kernel.request event is dispatched early in Symfony’s request handling process and allows you to modify the request object.

Create the following class, replacing the path with a path to a bundle in your project:

// src/AppBundle/EventListener/RequestListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class RequestListener
{
    public function onKernelRequest(GetResponseEvent $event)
    {
        $event->getRequest()->setFormat('jsonp', 'application/javascript');
    }
}
Registering your Listener

As with any other listener, you need to add it in one of your configuration files and register it as a listener by adding the kernel.event_listener tag:

  • YAML
    # app/config/services.yml
    services:
        app.listener.request:
            class: AppBundle\EventListener\RequestListener
            tags:
                - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
    
  • XML
    <!-- app/config/services.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
        <services>
            <service id="app.listener.request"
                class="AppBundle\EventListener\RequestListener">
                <tag name="kernel.event_listener"
                    event="kernel.request"
                    method="onKernelRequest"
                />
            </service>
        </services>
    </container>
    
  • PHP
    # app/config/services.php
    $definition = new Definition('AppBundle\EventListener\RequestListener');
    $definition->addTag('kernel.event_listener', array(
        'event'  => 'kernel.request',
        'method' => 'onKernelRequest',
    ));
    $container->setDefinition('app.listener.request', $definition);
    

At this point, the app.listener.request service has been configured and will be notified when the Symfony kernel dispatches the kernel.request event.

小技巧

You can also register the listener in a configuration extension class (see Importing Configuration via Container Extensions for more information).

Routing

How to Force Routes to always Use HTTPS or HTTP

Sometimes, you want to secure some routes and be sure that they are always accessed via the HTTPS protocol. The Routing component allows you to enforce the URI scheme via schemes:

  • YAML
    secure:
        path:     /secure
        defaults: { _controller: AppBundle:Main:secure }
        schemes:  [https]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="secure" path="/secure" schemes="https">
            <default key="_controller">AppBundle:Main:secure</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('secure', new Route('/secure', array(
        '_controller' => 'AppBundle:Main:secure',
    ), array(), array(), '', array('https')));
    
    return $collection;
    

The above configuration forces the secure route to always use HTTPS.

When generating the secure URL, and if the current scheme is HTTP, Symfony will automatically generate an absolute URL with HTTPS as the scheme:

{# If the current scheme is HTTPS #}
{{ path('secure') }}
{# generates /secure #}

{# If the current scheme is HTTP #}
{{ path('secure') }}
{# generates https://example.com/secure #}

The requirement is also enforced for incoming requests. If you try to access the /secure path with HTTP, you will automatically be redirected to the same URL, but with the HTTPS scheme.

The above example uses https for the scheme, but you can also force a URL to always use http.

注解

The Security component provides another way to enforce HTTP or HTTPS via the requires_channel setting. This alternative method is better suited to secure an “area” of your website (all URLs under /admin) or when you want to secure URLs defined in a third party bundle (see How to Force HTTPS or HTTP for different URLs for more details).

How to Allow a “/” Character in a Route Parameter

Sometimes, you need to compose URLs with parameters that can contain a slash /. For example, take the classic /hello/{username} route. By default, /hello/Fabien will match this route but not /hello/Fabien/Kris. This is because Symfony uses this character as separator between route parts.

This guide covers how you can modify a route so that /hello/Fabien/Kris matches the /hello/{username} route, where {username} equals Fabien/Kris.

Configure the Route

By default, the Symfony Routing component requires that the parameters match the following regex path: [^/]+. This means that all characters are allowed except /.

You must explicitly allow / to be part of your parameter by specifying a more permissive regex path.

  • Annotations
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    class DemoController
    {
        /**
         * @Route("/hello/{name}", name="_hello", requirements={"name"=".+"})
         */
        public function helloAction($name)
        {
            // ...
        }
    }
    
  • YAML
    _hello:
        path:     /hello/{username}
        defaults: { _controller: AppBundle:Demo:hello }
        requirements:
            username: .+
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="_hello" path="/hello/{username}">
            <default key="_controller">AppBundle:Demo:hello</default>
            <requirement key="username">.+</requirement>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('_hello', new Route('/hello/{username}', array(
        '_controller' => 'AppBundle:Demo:hello',
    ), array(
        'username' => '.+',
    )));
    
    return $collection;
    

That’s it! Now, the {username} parameter can contain the / character.

How to Configure a Redirect without a custom Controller

Sometimes, a URL needs to redirect to another URL. You can do that by creating a new controller action whose only task is to redirect, but using the RedirectController of the FrameworkBundle is even easier.

You can redirect to a specific path (e.g. /about) or to a specific route using its name (e.g. homepage).

Redirecting Using a Path

Assume there is no default controller for the / path of your application and you want to redirect these requests to /app. You will need to use the urlRedirect() action to redirect to this new url:

  • YAML
    # app/config/routing.yml
    
    # load some routes - one should ultimately have the path "/app"
    AppBundle:
        resource: "@AppBundle/Controller/"
        type:     annotation
        prefix:   /app
    
    # redirecting the root
    root:
        path: /
        defaults:
            _controller: FrameworkBundle:Redirect:urlRedirect
            path: /app
            permanent: true
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <!-- load some routes - one should ultimately have the path "/app" -->
        <import resource="@AppBundle/Controller/"
            type="annotation"
            prefix="/app"
        />
    
        <!-- redirecting the root -->
        <route id="root" path="/">
            <default key="_controller">FrameworkBundle:Redirect:urlRedirect</default>
            <default key="path">/app</default>
            <default key="permanent">true</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    
    // load some routes - one should ultimately have the path "/app"
    $appRoutes = $loader->import("@AppBundle/Controller/", "annotation");
    $appRoutes->setPrefix('/app');
    
    $collection->addCollection($appRoutes);
    
    // redirecting the root
    $collection->add('root', new Route('/', array(
        '_controller' => 'FrameworkBundle:Redirect:urlRedirect',
        'path'        => '/app',
        'permanent'   => true,
    )));
    
    return $collection;
    

In this example, you configured a route for the / path and let the RedirectController redirect it to /app. The permanent switch tells the action to issue a 301 HTTP status code instead of the default 302 HTTP status code.

Redirecting Using a Route

Assume you are migrating your website from WordPress to Symfony, you want to redirect /wp-admin to the route sonata_admin_dashboard. You don’t know the path, only the route name. This can be achieved using the redirect() action:

  • YAML
    # app/config/routing.yml
    
    # ...
    
    # redirecting the admin home
    root:
        path: /wp-admin
        defaults:
            _controller: FrameworkBundle:Redirect:redirect
            route: sonata_admin_dashboard
            permanent: true
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <!-- ... -->
    
        <!-- redirecting the admin home -->
        <route id="root" path="/wp-admin">
            <default key="_controller">FrameworkBundle:Redirect:redirect</default>
            <default key="route">sonata_admin_dashboard</default>
            <default key="permanent">true</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    // ...
    
    // redirecting the root
    $collection->add('root', new Route('/wp-admin', array(
        '_controller' => 'FrameworkBundle:Redirect:redirect',
        'route'       => 'sonata_admin_dashboard',
        'permanent'   => true,
    )));
    
    return $collection;
    

警告

Because you are redirecting to a route instead of a path, the required option is called route in the redirect action, instead of path in the urlRedirect action.

How to Use HTTP Methods beyond GET and POST in Routes

The HTTP method of a request is one of the requirements that can be checked when seeing if it matches a route. This is introduced in the routing chapter of the book “Routing” with examples using GET and POST. You can also use other HTTP verbs in this way. For example, if you have a blog post entry then you could use the same URL path to show it, make changes to it and delete it by matching on GET, PUT and DELETE.

  • YAML
    blog_show:
        path:     /blog/{slug}
        defaults: { _controller: AppBundle:Blog:show }
        methods:  [GET]
    
    blog_update:
        path:     /blog/{slug}
        defaults: { _controller: AppBundle:Blog:update }
        methods:  [PUT]
    
    blog_delete:
        path:     /blog/{slug}
        defaults: { _controller: AppBundle:Blog:delete }
        methods:  [DELETE]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog_show" path="/blog/{slug}" methods="GET">
            <default key="_controller">AppBundle:Blog:show</default>
        </route>
    
        <route id="blog_update" path="/blog/{slug}" methods="PUT">
            <default key="_controller">AppBundle:Blog:update</default>
        </route>
    
        <route id="blog_delete" path="/blog/{slug}" methods="DELETE">
            <default key="_controller">AppBundle:Blog:delete</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog_show', new Route('/blog/{slug}', array(
        '_controller' => 'AppBundle:Blog:show',
    ), array(), array(), '', array(), array('GET')));
    
    $collection->add('blog_update', new Route('/blog/{slug}', array(
        '_controller' => 'AppBundle:Blog:update',
    ), array(), array(), '', array(), array('PUT')));
    
    $collection->add('blog_delete', new Route('/blog/{slug}', array(
        '_controller' => 'AppBundle:Blog:delete',
    ), array(), array(), '', array('DELETE')));
    
    return $collection;
    
Faking the Method with _method

注解

The _method functionality shown here is disabled by default in Symfony 2.2 and enabled by default in Symfony 2.3. To control it in Symfony 2.2, you must call Request::enableHttpMethodParameterOverride before you handle the request (e.g. in your front controller). In Symfony 2.3, use the http_method_override option.

Unfortunately, life isn’t quite this simple, since most browsers do not support sending PUT and DELETE requests. Fortunately, Symfony provides you with a simple way of working around this limitation. By including a _method parameter in the query string or parameters of an HTTP request, Symfony will use this as the method when matching routes. Forms automatically include a hidden field for this parameter if their submission method is not GET or POST. See the related chapter in the forms documentation for more information.

How to Use Service Container Parameters in your Routes

Sometimes you may find it useful to make some parts of your routes globally configurable. For instance, if you build an internationalized site, you’ll probably start with one or two locales. Surely you’ll add a requirement to your routes to prevent a user from matching a locale other than the locales you support.

You could hardcode your _locale requirement in all your routes, but a better solution is to use a configurable service container parameter right inside your routing configuration:

  • YAML
    # app/config/routing.yml
    contact:
        path:     /{_locale}/contact
        defaults: { _controller: AppBundle:Main:contact }
        requirements:
            _locale: "%app.locales%"
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="contact" path="/{_locale}/contact">
            <default key="_controller">AppBundle:Main:contact</default>
            <requirement key="_locale">%app.locales%</requirement>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('contact', new Route('/{_locale}/contact', array(
        '_controller' => 'AppBundle:Main:contact',
    ), array(
        '_locale' => '%app.locales%',
    )));
    
    return $collection;
    

You can now control and set the app.locales parameter somewhere in your container:

  • YAML
    # app/config/config.yml
    parameters:
        app.locales: en|es
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="app.locales">en|es</parameter>
    </parameters>
    
  • PHP
    // app/config/config.php
    $container->setParameter('app.locales', 'en|es');
    

You can also use a parameter to define your route path (or part of your path):

  • YAML
    # app/config/routing.yml
    some_route:
        path:     /%app.route_prefix%/contact
        defaults: { _controller: AppBundle:Main:contact }
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="some_route" path="/%app.route_prefix%/contact">
            <default key="_controller">AppBundle:Main:contact</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('some_route', new Route('/%app.route_prefix%/contact', array(
        '_controller' => 'AppBundle:Main:contact',
    )));
    
    return $collection;
    

注解

Just like in normal service container configuration files, if you actually need a % in your route, you can escape the percent sign by doubling it, e.g. /score-50%%, which would resolve to /score-50%.

However, as the % characters included in any URL are automatically encoded, the resulting URL of this example would be /score-50%25 (%25 is the result of encoding the % character).

参见

For parameter handling within a Dependency Injection class see Using Parameters within a Dependency Injection Class.

How to Create a custom Route Loader

A custom route loader allows you to add routes to an application without including them, for example, in a YAML file. This comes in handy when you have a bundle but don’t want to manually add the routes for the bundle to app/config/routing.yml. This may be especially important when you want to make the bundle reusable, or when you have open-sourced it as this would slow down the installation process and make it error-prone.

Alternatively, you could also use a custom route loader when you want your routes to be automatically generated or located based on some convention or pattern. One example is the FOSRestBundle where routing is generated based off the names of the action methods in a controller.

注解

There are many bundles out there that use their own route loaders to accomplish cases like those described above, for instance FOSRestBundle, JMSI18nRoutingBundle, KnpRadBundle and SonataAdminBundle.

Loading Routes

The routes in a Symfony application are loaded by the DelegatingLoader. This loader uses several other loaders (delegates) to load resources of different types, for instance YAML files or @Route and @Method annotations in controller files. The specialized loaders implement LoaderInterface and therefore have two important methods: supports() and load().

Take these lines from the routing.yml in the AcmeDemoBundle of the Standard Edition:

# src/Acme/DemoBundle/Resources/config/routing.yml
_demo:
    resource: "@AcmeDemoBundle/Controller/DemoController.php"
    type:     annotation
    prefix:   /demo

When the main loader parses this, it tries all the delegate loaders and calls their supports() method with the given resource (@AcmeDemoBundle/Controller/DemoController.php) and type (annotation) as arguments. When one of the loader returns true, its load() method will be called, which should return a RouteCollection containing Route objects.

Creating a custom Loader

To load routes from some custom source (i.e. from something other than annotations, YAML or XML files), you need to create a custom route loader. This loader should implement LoaderInterface.

The sample loader below supports loading routing resources with a type of extra. The type extra isn’t important - you can just invent any resource type you want. The resource name itself is not actually used in the example:

namespace Acme\DemoBundle\Routing;

use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolverInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

class ExtraLoader implements LoaderInterface
{
    private $loaded = false;

    public function load($resource, $type = null)
    {
        if (true === $this->loaded) {
            throw new \RuntimeException('Do not add the "extra" loader twice');
        }

        $routes = new RouteCollection();

        // prepare a new route
        $path = '/extra/{parameter}';
        $defaults = array(
            '_controller' => 'AcmeDemoBundle:Demo:extra',
        );
        $requirements = array(
            'parameter' => '\d+',
        );
        $route = new Route($path, $defaults, $requirements);

        // add the new route to the route collection:
        $routeName = 'extraRoute';
        $routes->add($routeName, $route);

        $this->loaded = true;

        return $routes;
    }

    public function supports($resource, $type = null)
    {
        return 'extra' === $type;
    }

    public function getResolver()
    {
        // needed, but can be blank, unless you want to load other resources
        // and if you do, using the Loader base class is easier (see below)
    }

    public function setResolver(LoaderResolverInterface $resolver)
    {
        // same as above
    }
}

注解

Make sure the controller you specify really exists.

Now define a service for the ExtraLoader:

  • YAML
    services:
        acme_demo.routing_loader:
            class: Acme\DemoBundle\Routing\ExtraLoader
            tags:
                - { name: routing.loader }
    
  • XML
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="acme_demo.routing_loader" class="Acme\DemoBundle\Routing\ExtraLoader">
                <tag name="routing.loader" />
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container
        ->setDefinition(
            'acme_demo.routing_loader',
            new Definition('Acme\DemoBundle\Routing\ExtraLoader')
        )
        ->addTag('routing.loader')
    ;
    

Notice the tag routing.loader. All services with this tag will be marked as potential route loaders and added as specialized routers to the DelegatingLoader.

Using the custom Loader

If you did nothing else, your custom routing loader would not be called. Instead, you only need to add a few extra lines to the routing configuration:

  • YAML
    # app/config/routing.yml
    AcmeDemoBundle_Extra:
        resource: .
        type: extra
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <import resource="." type="extra" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection($loader->import('.', 'extra'));
    
    return $collection;
    

The important part here is the type key. Its value should be “extra”. This is the type which the ExtraLoader supports and this will make sure its load() method gets called. The resource key is insignificant for the ExtraLoader, so it is set to ”.”.

注解

The routes defined using custom route loaders will be automatically cached by the framework. So whenever you change something in the loader class itself, don’t forget to clear the cache.

More advanced Loaders

In most cases it’s better not to implement LoaderInterface yourself, but extend from Loader. This class knows how to use a LoaderResolver to load secondary routing resources.

Of course you still need to implement supports() and load(). Whenever you want to load another resource - for instance a YAML routing configuration file - you can call the import() method:

namespace Acme\DemoBundle\Routing;

use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\RouteCollection;

class AdvancedLoader extends Loader
{
    public function load($resource, $type = null)
    {
        $collection = new RouteCollection();

        $resource = '@AcmeDemoBundle/Resources/config/import_routing.yml';
        $type = 'yaml';

        $importedRoutes = $this->import($resource, $type);

        $collection->addCollection($importedRoutes);

        return $collection;
    }

    public function supports($resource, $type = null)
    {
        return $type === 'advanced_extra';
    }
}

注解

The resource name and type of the imported routing configuration can be anything that would normally be supported by the routing configuration loader (YAML, XML, PHP, annotation, etc.).

Redirect URLs with a Trailing Slash

The goal of this cookbook is to demonstrate how to redirect URLs with a trailing slash to the same URL without a trailing slash (for example /en/blog/ to /en/blog).

Create a controller that will match any URL with a trailing slash, remove the trailing slash (keeping query parameters if any) and redirect to the new URL with a 301 response status code:

// src/AppBundle/Controller/RedirectingController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class RedirectingController extends Controller
{
    public function removeTrailingSlashAction(Request $request)
    {
        $pathInfo = $request->getPathInfo();
        $requestUri = $request->getRequestUri();

        $url = str_replace($pathInfo, rtrim($pathInfo, ' /'), $requestUri);

        return $this->redirect($url, 301);
    }
}

After that, create a route to this controller that’s matched whenever a URL with a trailing slash is requested. Be sure to put this route last in your system, as explained below:

  • YAML
    remove_trailing_slash:
        path: /{url}
        defaults: { _controller: AppBundle:Redirecting:removeTrailingSlash }
        requirements:
            url: .*/$
        methods: [GET]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing">
        <route id="remove_trailing_slash" path="/{url}" methods="GET">
            <default key="_controller">AppBundle:Redirecting:removeTrailingSlash</default>
            <requirement key="url">.*/$</requirement>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add(
        'remove_trailing_slash',
        new Route(
            '/{url}',
            array(
                '_controller' => 'AppBundle:Redirecting:removeTrailingSlash',
            ),
            array(
                'url' => '.*/$',
            ),
            array(),
            '',
            array(),
            array('GET')
        )
    );
    

注解

Redirecting a POST request does not work well in old browsers. A 302 on a POST request would send a GET request after the redirection for legacy reasons. For that reason, the route here only matches GET requests.

警告

Make sure to include this route in your routing configuration at the very end of your route listing. Otherwise, you risk redirecting real routes (including Symfony core routes) that actually do have a trailing slash in their path.

How to Pass Extra Information from a Route to a Controller

Parameters inside the defaults collection don’t necessarily have to match a placeholder in the route path. In fact, you can use the defaults array to specify extra parameters that will then be accessible as arguments to your controller:

  • YAML
    # app/config/routing.yml
    blog:
        path:      /blog/{page}
        defaults:
            _controller: AppBundle:Blog:index
            page:        1
            title:       "Hello world!"
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="blog" path="/blog/{page}">
            <default key="_controller">AppBundle:Blog:index</default>
            <default key="page">1</default>
            <default key="title">Hello world!</default>
        </route>
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('blog', new Route('/blog/{page}', array(
        '_controller' => 'AppBundle:Blog:index',
        'page'        => 1,
        'title'       => 'Hello world!',
    )));
    
    return $collection;
    

Now, you can access this extra parameter in your controller:

public function indexAction($page, $title)
{
    // ...
}

As you can see, the $title variable was never defined inside the route path, but you can still access its value from inside your controller.

Security

How to Build a Traditional Login Form

小技巧

If you need a login form and are storing users in some sort of a database, then you should consider using FOSUserBundle, which helps you build your User object and gives you many routes and controllers for common tasks like login, registration and forgot password.

In this entry, you’ll build a traditional login form. Of course, when the user logs in, you can load your users from anywhere - like the database. See B) Configuring how Users are Loaded for details.

This chapter assumes that you’ve followed the beginning of the security chapter and have http_basic authentication working properly.

First, enable form login under your firewall:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            default:
                anonymous: ~
                http_basic: ~
                form_login:
                    login_path: /login
                    check_path: /login_check
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="main">
                <anonymous />
                <form-login login-path="/login" check-path="/login_check" />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array(
                'anonymous'  => array(),
                'form_login' => array(
                    'login_path' => '/login',
                    'check_path' => '/login_check',
                ),
            ),
        ),
    ));
    

小技巧

The login_path and check_path can also be route names (but cannot have mandatory wildcards - e.g. /login/{foo} where foo has no default value).

Now, when the security system initiates the authentication process, it will redirect the user to the login form /login. Implementing this login form visually is your job. First, create a new SecurityController inside a bundle with an empty loginAction:

// src/AppBundle/Controller/SecurityController.php
namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class SecurityController extends Controller
{
    public function loginAction(Request $request)
    {
        // todo...
    }
}

Next, create two routes: one for each of the paths your configured earlier under your form_login configuration (/login and /login_check):

  • Annotations
    // src/AppBundle/Controller/SecurityController.php
    // ...
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    class SecurityController extends Controller
    {
        /**
         * @Route("/login", name="login_route")
         */
        public function loginAction(Request $request)
        {
            // todo ...
        }
    
        /**
         * @Route("/login_check", name="login_check")
         */
        public function loginCheckAction()
        {
        }
    }
    
  • YAML
    # app/config/routing.yml
    login_route:
        path:     /login
        defaults: { _controller: AppBundle:Security:login }
    login_check:
        path: /login_check
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="login_route" path="/login">
            <default key="_controller">AppBundle:Security:login</default>
        </route>
    
        <route id="login_check" path="/login_check" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('login_route', new Route('/login', array(
        '_controller' => 'AppBundle:Security:login',
    )));
    $collection->add('login_check', new Route('/login_check', array()));
    
    return $collection;
    

Great! Next, add the logic to loginAction that will display the login form:

// src/AppBundle/Controller/SecurityController.php
// ...

// ADD THIS use STATEMENT above your class
use Symfony\Component\Security\Core\SecurityContextInterface;

public function loginAction(Request $request)
{
    $session = $request->getSession();

    // get the login error if there is one
    if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
        $error = $request->attributes->get(
            SecurityContextInterface::AUTHENTICATION_ERROR
        );
    } elseif (null !== $session && $session->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
        $error = $session->get(SecurityContextInterface::AUTHENTICATION_ERROR);
        $session->remove(SecurityContextInterface::AUTHENTICATION_ERROR);
    } else {
        $error = null;
    }

    // last username entered by the user
    $lastUsername = (null === $session) ? '' : $session->get(SecurityContextInterface::LAST_USERNAME);

    return $this->render(
        'security/login.html.twig',
        array(
            // last username entered by the user
            'last_username' => $lastUsername,
            'error'         => $error,
        )
    );
}

Don’t let this controller confuse you. As you’ll see in a moment, when the user submits the form, the security system automatically handles the form submission for you. If the user had submitted an invalid username or password, this controller reads the form submission error from the security system so that it can be displayed back to the user.

In other words, your job is to display the login form and any login errors that may have occurred, but the security system itself takes care of checking the submitted username and password and authenticating the user.

Finally, create the template:

  • Twig
    {# app/Resources/views/security/login.html.twig #}
    {# ... you will probably extends your base template, like base.html.twig #}
    
    {% if error %}
        <div>{{ error.messageKey|trans(error.messageData) }}</div>
    {% endif %}
    
    <form action="{{ path('login_check') }}" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        {#
            If you want to control the URL the user
            is redirected to on success (more details below)
            <input type="hidden" name="_target_path" value="/account" />
        #}
    
        <button type="submit">login</button>
    </form>
    
  • PHP
    <!-- src/Acme/SecurityBundle/Resources/views/Security/login.html.php -->
    <?php if ($error): ?>
        <div><?php echo $error->getMessage() ?></div>
    <?php endif ?>
    
    <form action="<?php echo $view['router']->generate('login_check') ?>" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="<?php echo $last_username ?>" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        <!--
            If you want to control the URL the user
            is redirected to on success (more details below)
            <input type="hidden" name="_target_path" value="/account" />
        -->
    
        <button type="submit">login</button>
    </form>
    

小技巧

The error variable passed into the template is an instance of AuthenticationException. It may contain more information - or even sensitive information - about the authentication failure, so use it wisely!

The form can look like anything, but has a few requirements:

  • The form must POST to /login_check, since that’s what you configured under the form_login key in security.yml.
  • The username must have the name _username and the password must have the name _password.

小技巧

Actually, all of this can be configured under the form_login key. See Form Login Configuration for more details.

警告

This login form is currently not protected against CSRF attacks. Read Using CSRF Protection in the Login Form on how to protect your login form.

And that’s it! When you submit the form, the security system will automatically check the user’s credentials and either authenticate the user or send the user back to the login form where the error can be displayed.

To review the whole process:

  1. The user tries to access a resource that is protected;
  2. The firewall initiates the authentication process by redirecting the user to the login form (/login);
  3. The /login page renders login form via the route and controller created in this example;
  4. The user submits the login form to /login_check;
  5. The security system intercepts the request, checks the user’s submitted credentials, authenticates the user if they are correct, and sends the user back to the login form if they are not.
Redirecting after Success

If the submitted credentials are correct, the user will be redirected to the original page that was requested (e.g. /admin/foo). If the user originally went straight to the login page, they’ll be redirected to the homepage. This can all be customized, allowing you to, for example, redirect the user to a specific URL.

For more details on this and how to customize the form login process in general, see How to Customize your Form Login.

Avoid common Pitfalls

When setting up your login form, watch out for a few common pitfalls.

1. Create the correct routes

First, be sure that you’ve defined the /login and /login_check routes correctly and that they correspond to the login_path and check_path config values. A misconfiguration here can mean that you’re redirected to a 404 page instead of the login page, or that submitting the login form does nothing (you just see the login form over and over again).

2. Be sure the login page isn’t secure (redirect loop!)

Also, be sure that the login page is accessible by anonymous users. For example, the following configuration - which requires the ROLE_ADMIN role for all URLs (including the /login URL), will cause a redirect loop:

  • YAML
    # app/config/security.yml
    
    # ...
    access_control:
        - { path: ^/, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    
    <!-- ... -->
    <access-control>
        <rule path="^/" role="ROLE_ADMIN" />
    </access-control>
    
  • PHP
    // app/config/security.php
    
    // ...
    'access_control' => array(
        array('path' => '^/', 'role' => 'ROLE_ADMIN'),
    ),
    

Adding an access control that matches /login/* and requires no authentication fixes the problem:

  • YAML
    # app/config/security.yml
    
    # ...
    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    
    <!-- ... -->
    <access-control>
        <rule path="^/login" role="IS_AUTHENTICATED_ANONYMOUSLY" />
        <rule path="^/" role="ROLE_ADMIN" />
    </access-control>
    
  • PHP
    // app/config/security.php
    
    // ...
    'access_control' => array(
        array('path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'),
        array('path' => '^/', 'role' => 'ROLE_ADMIN'),
    ),
    

Also, if your firewall does not allow for anonymous users (no anonymous key), you’ll need to create a special firewall that allows anonymous users for the login page:

  • YAML
    # app/config/security.yml
    
    # ...
    firewalls:
        # order matters! This must be before the ^/ firewall
        login_firewall:
            pattern:   ^/login$
            anonymous: ~
        secured_area:
            pattern:    ^/
            form_login: ~
    
  • XML
    <!-- app/config/security.xml -->
    
    <!-- ... -->
    <firewall name="login_firewall" pattern="^/login$">
        <anonymous />
    </firewall>
    <firewall name="secured_area" pattern="^/">
        <form-login />
    </firewall>
    
  • PHP
    // app/config/security.php
    
    // ...
    'firewalls' => array(
        'login_firewall' => array(
            'pattern'   => '^/login$',
            'anonymous' => array(),
        ),
        'secured_area' => array(
            'pattern'    => '^/',
            'form_login' => array(),
        ),
    ),
    

3. Be sure /login_check is behind a firewall

Next, make sure that your check_path URL (e.g. /login_check) is behind the firewall you’re using for your form login (in this example, the single firewall matches all URLs, including /login_check). If /login_check doesn’t match any firewall, you’ll receive a Unable to find the controller for path "/login_check" exception.

4. Multiple firewalls don’t share security context

If you’re using multiple firewalls and you authenticate against one firewall, you will not be authenticated against any other firewalls automatically. Different firewalls are like different security systems. To do this you have to explicitly specify the same Firewall Context for different firewalls. But usually for most applications, having one main firewall is enough.

5. Routing error pages are not covered by firewalls

As routing is done before security, 404 error pages are not covered by any firewall. This means you can’t check for security or even access the user object on these pages. See How to Customize Error Pages for more details.

How to Load Security Users from the Database (the Entity Provider)

The security layer is one of the smartest tools of Symfony. It handles two things: the authentication and the authorization processes. Although it may seem difficult to understand how it works internally, the security system is very flexible and allows you to integrate your application with any authentication backend, like Active Directory, an OAuth server or a database.

Introduction

This article focuses on how to authenticate users against a database table managed by a Doctrine entity class. The content of this cookbook entry is split in three parts. The first part is about designing a Doctrine User entity class and making it usable in the security layer of Symfony. The second part describes how to easily authenticate a user with the Doctrine EntityUserProvider object bundled with the framework and some configuration. Finally, the tutorial will demonstrate how to create a custom EntityUserProvider object to retrieve users from a database with custom conditions.

The Data Model

For the purpose of this cookbook, the AcmeUserBundle bundle contains a User entity class with the following fields: id, username, password, email and isActive. The isActive field tells whether or not the user account is active.

To make it shorter, the getter and setter methods for each have been removed to focus on the most important methods that come from the UserInterface.

小技巧

You can generate the missing getter and setters by running:

$ php app/console doctrine:generate:entities Acme/UserBundle/Entity/User
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Acme\UserBundle\Entity\User
 *
 * @ORM\Table(name="acme_users")
 * @ORM\Entity(repositoryClass="Acme\UserBundle\Entity\UserRepository")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=25, unique=true)
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=64)
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=60, unique=true)
     */
    private $email;

    /**
     * @ORM\Column(name="is_active", type="boolean")
     */
    private $isActive;

    public function __construct()
    {
        $this->isActive = true;
        // may not be needed, see section on salt below
        // $this->salt = md5(uniqid(null, true));
    }

    /**
     * @inheritDoc
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * @inheritDoc
     */
    public function getSalt()
    {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return null;
    }

    /**
     * @inheritDoc
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * @inheritDoc
     */
    public function getRoles()
    {
        return array('ROLE_USER');
    }

    /**
     * @inheritDoc
     */
    public function eraseCredentials()
    {
    }

    /**
     * @see \Serializable::serialize()
     */
    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
            // $this->salt,
        ));
    }

    /**
     * @see \Serializable::unserialize()
     */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
            // $this->salt
        ) = unserialize($serialized);
    }
}

注解

If you choose to implement EquatableInterface, you determine yourself which properties need to be compared to distinguish your user objects.

小技巧

Generate the database table for your User entity by running:

$ php app/console doctrine:schema:update --force

In order to use an instance of the AcmeUserBundle:User class in the Symfony security layer, the entity class must implement the UserInterface. This interface forces the class to implement the five following methods:

For more details on each of these, see UserInterface.

Below is an export of the User table from MySQL with user admin and password admin (which has been encoded). For details on how to create user records and encode their password, see C) Encoding the User’s Password.

$ mysql> SELECT * FROM acme_users;
+----+----------+------------------------------------------+--------------------+-----------+
| id | username | password                                 | email              | is_active |
+----+----------+------------------------------------------+--------------------+-----------+
|  1 | admin    | d033e22ae348aeb5660fc2140aec35850c4da997 | admin@example.com  |         1 |
+----+----------+------------------------------------------+--------------------+-----------+

The next part will focus on how to authenticate one of these users thanks to the Doctrine entity user provider and a couple of lines of configuration.

Authenticating Someone against a Database

Authenticating a Doctrine user against the database with the Symfony security layer is a piece of cake. Everything resides in the configuration of the SecurityBundle stored in the app/config/security.yml file.

Below is an example of configuration where the user will enter their username and password via HTTP basic authentication. That information will then be checked against your User entity records in the database:

  • YAML
    # app/config/security.yml
    security:
        encoders:
            Acme\UserBundle\Entity\User:
                algorithm: bcrypt
    
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]
    
        providers:
            administrators:
                entity: { class: AcmeUserBundle:User, property: username }
    
        firewalls:
            admin_area:
                pattern:    ^/admin
                http_basic: ~
    
        access_control:
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <encoder class="Acme\UserBundle\Entity\User"
            algorithm="bcrypt"
        />
    
        <role id="ROLE_ADMIN">ROLE_USER</role>
        <role id="ROLE_SUPER_ADMIN">ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
    
        <provider name="administrators">
            <entity class="AcmeUserBundle:User" property="username" />
        </provider>
    
        <firewall name="admin_area" pattern="^/admin">
            <http-basic />
        </firewall>
    
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'encoders' => array(
            'Acme\UserBundle\Entity\User' => array(
                'algorithm' => 'bcrypt',
            ),
        ),
        'role_hierarchy' => array(
            'ROLE_ADMIN'       => 'ROLE_USER',
            'ROLE_SUPER_ADMIN' => array('ROLE_USER', 'ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'),
        ),
        'providers' => array(
            'administrator' => array(
                'entity' => array(
                    'class'    => 'AcmeUserBundle:User',
                    'property' => 'username',
                ),
            ),
        ),
        'firewalls' => array(
            'admin_area' => array(
                'pattern' => '^/admin',
                'http_basic' => null,
            ),
        ),
        'access_control' => array(
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

The encoders section associates the bcrypt password encoder to the entity class. This means that Symfony will expect the password that’s stored in the database to be encoded using this encoder. For details on how to create a new User object with a properly encoded password, see the C) Encoding the User’s Password section of the security chapter.

警告

If you’re using PHP 5.4 or lower, you’ll need to install the ircmaxell/password-compat library via Composer in order to be able to use the bcrypt encoder:

{
    "require": {
        ...
        "ircmaxell/password-compat": "~1.0.3"
    }
}

The providers section defines an administrators user provider. A user provider is a “source” of where users are loaded during authentication. In this case, the entity keyword means that Symfony will use the Doctrine entity user provider to load User entity objects from the database by using the username unique field. In other words, this tells Symfony how to fetch the user from the database before checking the password validity.

注解

By default, the entity provider uses the default entity manager to fetch user information from the database. If you use multiple entity managers, you can specify which manager to use with the manager_name option:

  • YAML
    # app/config/config.yml
    security:
        # ...
    
        providers:
            administrators:
                entity:
                    class: AcmeUserBundle:User
                    property: username
                    manager_name: customer
    
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
        <config>
            <!-- ... -->
    
            <provider name="administrators">
                <entity class="AcmeUserBundle:User"
                    property="username"
                    manager-name="customer" />
            </provider>
    
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('security', array(
        // ...
        'providers' => array(
            'administrator' => array(
                'entity' => array(
                    'class' => 'AcmeUserBundle:User',
                    'property' => 'username',
                    'manager_name' => 'customer',
                ),
            ),
        ),
        // ...
    ));
    
Forbid inactive Users

If a User’s isActive property is set to false (i.e. is_active is 0 in the database), the user will still be able to login access the site normally. To prevent “inactive” users from logging in, you’ll need to do a little more work.

The easiest way to exclude inactive users is to implement the AdvancedUserInterface interface that takes care of checking the user’s account status. The AdvancedUserInterface extends the UserInterface interface, so you just need to switch to the new interface in the AcmeUserBundle:User entity class to benefit from simple and advanced authentication behaviors.

The AdvancedUserInterface interface adds four extra methods to validate the account status:

For this example, the first three methods will return true whereas the isEnabled() method will return the boolean value in the isActive field.

// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;

class User implements AdvancedUserInterface, \Serializable
{
    // ...

    public function isAccountNonExpired()
    {
        return true;
    }

    public function isAccountNonLocked()
    {
        return true;
    }

    public function isCredentialsNonExpired()
    {
        return true;
    }

    public function isEnabled()
    {
        return $this->isActive;
    }
}

Now, if you try to authenticate as a user who’s is_active database field is set to 0, you won’t be allowed.

注解

When using the AdvancedUserInterface, you should also add any of the properties used by these methods (like isActive()) to the serialize() method. If you don’t do this, your user may not be deserialized correctly from the session on each request.

The next session will focus on how to write a custom entity provider to authenticate a user with their username or email address.

Authenticating Someone with a Custom Entity Provider

The next step is to allow a user to authenticate with their username or email address as they are both unique in the database. Unfortunately, the native entity provider is only able to handle a single property to fetch the user from the database.

To accomplish this, create a custom entity provider that looks for a user whose username or email field matches the submitted login username. The good news is that a Doctrine repository object can act as an entity user provider if it implements the UserProviderInterface. This interface comes with three methods to implement: loadUserByUsername($username), refreshUser(UserInterface $user), and supportsClass($class). For more details, see UserProviderInterface.

The code below shows the implementation of the UserProviderInterface in the UserRepository class:

// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\UserBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;

class UserRepository extends EntityRepository implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        $q = $this
            ->createQueryBuilder('u')
            ->where('u.username = :username OR u.email = :email')
            ->setParameter('username', $username)
            ->setParameter('email', $username)
            ->getQuery();

        try {
            // The Query::getSingleResult() method throws an exception
            // if there is no record matching the criteria.
            $user = $q->getSingleResult();
        } catch (NoResultException $e) {
            $message = sprintf(
                'Unable to find an active admin AcmeUserBundle:User object identified by "%s".',
                $username
            );
            throw new UsernameNotFoundException($message, 0, $e);
        }

        return $user;
    }

    public function refreshUser(UserInterface $user)
    {
        $class = get_class($user);
        if (!$this->supportsClass($class)) {
            throw new UnsupportedUserException(
                sprintf(
                    'Instances of "%s" are not supported.',
                    $class
                )
            );
        }

        return $this->find($user->getId());
    }

    public function supportsClass($class)
    {
        return $this->getEntityName() === $class
            || is_subclass_of($class, $this->getEntityName());
    }
}

To finish the implementation, the configuration of the security layer must be changed to tell Symfony to use the new custom entity provider instead of the generic Doctrine entity provider. It’s trivial to achieve by removing the property field in the security.providers.administrators.entity section of the security.yml file.

  • YAML
    # app/config/security.yml
    security:
        # ...
        providers:
            administrators:
                entity: { class: AcmeUserBundle:User }
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
    
        <provider name="administrator">
            <entity class="AcmeUserBundle:User" />
        </provider>
    
        <!-- ... -->
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        ...,
        'providers' => array(
            'administrator' => array(
                'entity' => array(
                    'class' => 'AcmeUserBundle:User',
                ),
            ),
        ),
        ...,
    ));
    

By doing this, the security layer will use an instance of UserRepository and call its loadUserByUsername() method to fetch a user from the database whether they filled in their username or email address.

Managing Roles in the Database

The end of this tutorial focuses on how to store and retrieve a list of roles from the database. As mentioned previously, when your user is loaded, its getRoles() method returns the array of security roles that should be assigned to the user. You can load this data from anywhere - a hardcoded list used for all users (e.g. array('ROLE_USER')), a Doctrine array property called roles, or via a Doctrine relationship, as you’ll learn about in this section.

警告

In a typical setup, you should always return at least 1 role from the getRoles() method. By convention, a role called ROLE_USER is usually returned. If you fail to return any roles, it may appear as if your user isn’t authenticated at all.

警告

In order to work with the security configuration examples on this page all roles must be prefixed with ROLE_ (see the section about roles in the book). For example, your roles will be ROLE_ADMIN or ROLE_USER instead of ADMIN or USER.

In this example, the AcmeUserBundle:User entity class defines a many-to-many relationship with a AcmeUserBundle:Role entity class. A user can be related to several roles and a role can be composed of one or more users. The previous getRoles() method now returns the list of related roles. Notice that __construct() and getRoles() methods have changed:

// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
// ...

class User implements AdvancedUserInterface, \Serializable
{
    // ...

    /**
     * @ORM\ManyToMany(targetEntity="Role", inversedBy="users")
     *
     */
    private $roles;

    public function __construct()
    {
        $this->roles = new ArrayCollection();
    }

    public function getRoles()
    {
        return $this->roles->toArray();
    }

    // ...

}

The AcmeUserBundle:Role entity class defines three fields (id, name and role). The unique role field contains the role name (e.g. ROLE_ADMIN) used by the Symfony security layer to secure parts of the application:

// src/Acme/Bundle/UserBundle/Entity/Role.php
namespace Acme\UserBundle\Entity;

use Symfony\Component\Security\Core\Role\RoleInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="acme_role")
 * @ORM\Entity()
 */
class Role implements RoleInterface
{
    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(name="name", type="string", length=30)
     */
    private $name;

    /**
     * @ORM\Column(name="role", type="string", length=20, unique=true)
     */
    private $role;

    /**
     * @ORM\ManyToMany(targetEntity="User", mappedBy="roles")
     */
    private $users;

    public function __construct()
    {
        $this->users = new ArrayCollection();
    }

    /**
     * @see RoleInterface
     */
    public function getRole()
    {
        return $this->role;
    }

    // ... getters and setters for each property
}

For brevity, the getter and setter methods are hidden, but you can generate them:

$ php app/console doctrine:generate:entities Acme/UserBundle/Entity/User

Don’t forget also to update your database schema:

$ php app/console doctrine:schema:update --force

This will create the acme_role table and a user_role that stores the many-to-many relationship between acme_user and acme_role. If you had one user linked to one role, your database might look something like this:

$ mysql> SELECT * FROM acme_role;
+----+-------+------------+
| id | name  | role       |
+----+-------+------------+
|  1 | admin | ROLE_ADMIN |
+----+-------+------------+

$ mysql> SELECT * FROM user_role;
+---------+---------+
| user_id | role_id |
+---------+---------+
|       1 |       1 |
+---------+---------+

And that’s it! When the user logs in, Symfony security system will call the User::getRoles method. This will return an array of Role objects that Symfony will use to determine if the user should have access to certain parts of the system.

Improving Performance with a Join

To improve performance and avoid lazy loading of roles when retrieving a user from the custom entity provider, you can use a Doctrine join to the roles relationship in the UserRepository::loadUserByUsername() method. This will fetch the user and their associated roles with a single query:

// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\UserBundle\Entity;

// ...

class UserRepository extends EntityRepository implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        $q = $this
            ->createQueryBuilder('u')
            ->select('u, r')
            ->leftJoin('u.roles', 'r')
            ->where('u.username = :username OR u.email = :email')
            ->setParameter('username', $username)
            ->setParameter('email', $username)
            ->getQuery();

        // ...
    }

    // ...
}

The QueryBuilder::leftJoin() method joins and fetches related roles from the AcmeUserBundle:User model class when a user is retrieved by their email address or username.

Understanding serialize and how a User is Saved in the Session

If you’re curious about the importance of the serialize() method inside the User class or how the User object is serialized or deserialized, then this section is for you. If not, feel free to skip this.

Once the user is logged in, the entire User object is serialized into the session. On the next request, the User object is deserialized. Then, value of the id property is used to re-query for a fresh User object from the database. Finally, the fresh User object is compared in some way to the deserialized User object to make sure that they represent the same user. For example, if the username on the 2 User objects doesn’t match for some reason, then the user will be logged out for security reasons.

Even though this all happens automatically, there are a few important side-effects.

First, the Serializable interface and its serialize and unserialize methods have been added to allow the User class to be serialized to the session. This may or may not be needed depending on your setup, but it’s probably a good idea. In theory, only the id needs to be serialized, because the refreshUser() method refreshes the user on each request by using the id (as explained above). However in practice, this means that the User object is reloaded from the database on each request using the id from the serialized object. This makes sure all of the User’s data is fresh.

Symfony also uses the username, salt, and password to verify that the User has not changed between requests. Failing to serialize these may cause you to be logged out on each request. If your User implements the EquatableInterface, then instead of these properties being checked, your isEqualTo method is simply called, and you can check whatever properties you want. Unless you understand this, you probably won’t need to implement this interface or worry about it.

2.1 新版功能: In Symfony 2.1, the equals method was removed from UserInterface and the EquatableInterface was introduced in its place.

How to Add “Remember Me” Login Functionality

Once a user is authenticated, their credentials are typically stored in the session. This means that when the session ends they will be logged out and have to provide their login details again next time they wish to access the application. You can allow users to choose to stay logged in for longer than the session lasts using a cookie with the remember_me firewall option. The firewall needs to have a secret key configured, which is used to encrypt the cookie’s content. It also has several options with default values which are shown here:

  • YAML
    # app/config/security.yml
    firewalls:
        main:
            remember_me:
                key:      "%secret%"
                lifetime: 31536000 # 365 days in seconds
                path:     /
                domain:   ~ # Defaults to the current domain from $_SERVER
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <remember-me
                key      = "%secret%"
                lifetime = "31536000" <!-- 365 days in seconds -->
                path     = "/"
                domain   = "" <!-- Defaults to the current domain from $_SERVER -->
            />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array(
                'remember_me' => array(
                    'key'      => '%secret%',
                    'lifetime' => 31536000, // 365 days in seconds
                    'path'     => '/',
                    'domain'   => '', // Defaults to the current domain from $_SERVER
                ),
            ),
        ),
    ));
    

It’s a good idea to provide the user with the option to use or not use the remember me functionality, as it will not always be appropriate. The usual way of doing this is to add a checkbox to the login form. By giving the checkbox the name _remember_me, the cookie will automatically be set when the checkbox is checked and the user successfully logs in. So, your specific login form might ultimately look like this:

  • Twig
    {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
    {% if error %}
        <div>{{ error.message }}</div>
    {% endif %}
    
    <form action="{{ path('login_check') }}" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        <input type="checkbox" id="remember_me" name="_remember_me" checked />
        <label for="remember_me">Keep me logged in</label>
    
        <input type="submit" name="login" />
    </form>
    
  • PHP
    <!-- src/Acme/SecurityBundle/Resources/views/Security/login.html.php -->
    <?php if ($error): ?>
        <div><?php echo $error->getMessage() ?></div>
    <?php endif ?>
    
    <form action="<?php echo $view['router']->generate('login_check') ?>" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username"
               name="_username" value="<?php echo $last_username ?>" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        <input type="checkbox" id="remember_me" name="_remember_me" checked />
        <label for="remember_me">Keep me logged in</label>
    
        <input type="submit" name="login" />
    </form>
    

The user will then automatically be logged in on subsequent visits while the cookie remains valid.

Forcing the User to Re-authenticate before Accessing certain Resources

When the user returns to your site, they are authenticated automatically based on the information stored in the remember me cookie. This allows the user to access protected resources as if the user had actually authenticated upon visiting the site.

In some cases, however, you may want to force the user to actually re-authenticate before accessing certain resources. For example, you might allow “remember me” users to see basic account information, but then require them to actually re-authenticate before modifying that information.

The Security component provides an easy way to do this. In addition to roles explicitly assigned to them, users are automatically given one of the following roles depending on how they are authenticated:

  • IS_AUTHENTICATED_ANONYMOUSLY - automatically assigned to a user who is in a firewall protected part of the site but who has not actually logged in. This is only possible if anonymous access has been allowed.
  • IS_AUTHENTICATED_REMEMBERED - automatically assigned to a user who was authenticated via a remember me cookie.
  • IS_AUTHENTICATED_FULLY - automatically assigned to a user that has provided their login details during the current session.

You can use these to control access beyond the explicitly assigned roles.

注解

If you have the IS_AUTHENTICATED_REMEMBERED role, then you also have the IS_AUTHENTICATED_ANONYMOUSLY role. If you have the IS_AUTHENTICATED_FULLY role, then you also have the other two roles. In other words, these roles represent three levels of increasing “strength” of authentication.

You can use these additional roles for finer grained control over access to parts of a site. For example, you may want your user to be able to view their account at /account when authenticated by cookie but to have to provide their login details to be able to edit the account details. You can do this by securing specific controller actions using these roles. The edit action in the controller could be secured using the service context.

In the following example, the action is only allowed if the user has the IS_AUTHENTICATED_FULLY role.

// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException

public function editAction()
{
    if (false === $this->get('security.context')->isGranted(
        'IS_AUTHENTICATED_FULLY'
       )) {
        throw new AccessDeniedException();
    }

    // ...
}

You can also choose to install and use the optional JMSSecurityExtraBundle, which can secure your controller using annotations:

use JMS\SecurityExtraBundle\Annotation\Secure;

/**
 * @Secure(roles="IS_AUTHENTICATED_FULLY")
 */
public function editAction($name)
{
    // ...
}

小技巧

If you also had an access control in your security configuration that required the user to have a ROLE_USER role in order to access any of the account area, then you’d have the following situation:

  • If a non-authenticated (or anonymously authenticated user) tries to access the account area, the user will be asked to authenticate.
  • Once the user has entered their username and password, assuming the user receives the ROLE_USER role per your configuration, the user will have the IS_AUTHENTICATED_FULLY role and be able to access any page in the account section, including the editAction controller.
  • If the user’s session ends, when the user returns to the site, they will be able to access every account page - except for the edit page - without being forced to re-authenticate. However, when they try to access the editAction controller, they will be forced to re-authenticate, since they are not, yet, fully authenticated.

For more information on securing services or methods in this way, see How to Secure any Service or Method in your Application.

How to Impersonate a User

Sometimes, it’s useful to be able to switch from one user to another without having to log out and log in again (for instance when you are debugging or trying to understand a bug a user sees that you can’t reproduce). This can be easily done by activating the switch_user firewall listener:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                # ...
                switch_user: true
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
        <config>
            <firewall>
                <!-- ... -->
                <switch-user />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main'=> array(
                // ...
                'switch_user' => true
            ),
        ),
    ));
    

To switch to another user, just add a query string with the _switch_user parameter and the username as the value to the current URL:

http://example.com/somewhere?_switch_user=thomas

To switch back to the original user, use the special _exit username:

http://example.com/somewhere?_switch_user=_exit

During impersonation, the user is provided with a special role called ROLE_PREVIOUS_ADMIN. In a template, for instance, this role can be used to show a link to exit impersonation:

  • Twig
    {% if is_granted('ROLE_PREVIOUS_ADMIN') %}
        <a href="{{ path('homepage', {'_switch_user': '_exit'}) }}">Exit impersonation</a>
    {% endif %}
    
  • PHP
    <?php if ($view['security']->isGranted('ROLE_PREVIOUS_ADMIN')): ?>
        <a
            href="<?php echo $view['router']->generate('homepage', array(
                '_switch_user' => '_exit',
            ) ?>"
        >
            Exit impersonation
        </a>
    <?php endif ?>
    

Of course, this feature needs to be made available to a small group of users. By default, access is restricted to users having the ROLE_ALLOWED_TO_SWITCH role. The name of this role can be modified via the role setting. For extra security, you can also change the query parameter name via the parameter setting:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                # ...
                switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
        <config>
            <firewall>
                <!-- ... -->
                <switch-user role="ROLE_ADMIN" parameter="_want_to_be_this_user" />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main'=> array(
                // ...
                'switch_user' => array(
                    'role' => 'ROLE_ADMIN',
                    'parameter' => '_want_to_be_this_user',
                ),
            ),
        ),
    ));
    
How to Implement your own Voter to Blacklist IP Addresses

The Symfony Security component provides several layers to authorize users. One of the layers is called a “voter”. A voter is a dedicated class that checks if the user has the rights to connect to the application or access a specific resource/URL. For instance, Symfony provides a layer that checks if the user is fully authorized or if it has some expected roles.

It is sometimes useful to create a custom voter to handle a specific case not handled by the framework. In this section, you’ll learn how to create a voter that will allow you to blacklist users by their IP.

The Voter Interface

A custom voter must implement VoterInterface, which requires the following three methods:

interface VoterInterface
{
    public function supportsAttribute($attribute);
    public function supportsClass($class);
    public function vote(TokenInterface $token, $object, array $attributes);
}

The supportsAttribute() method is used to check if the voter supports the given user attribute (i.e: a role like ROLE_USER, an ACL EDIT, etc.).

The supportsClass() method is used to check if the voter supports the class of the object whose access is being checked.

The vote() method must implement the business logic that verifies whether or not the user has access. This method must return one of the following values:

  • VoterInterface::ACCESS_GRANTED: The authorization will be granted by this voter;
  • VoterInterface::ACCESS_ABSTAIN: The voter cannot decide if authorization should be granted;
  • VoterInterface::ACCESS_DENIED: The authorization will be denied by this voter.

In this example, you’ll check if the user’s IP address matches against a list of blacklisted addresses and “something” will be the application. If the user’s IP is blacklisted, you’ll return VoterInterface::ACCESS_DENIED, otherwise you’ll return VoterInterface::ACCESS_ABSTAIN as this voter’s purpose is only to deny access, not to grant access.

Creating a custom Voter

To blacklist a user based on its IP, you can use the request service and compare the IP address against a set of blacklisted IP addresses:

// src/AppBundle/Security/Authorization/Voter/ClientIpVoter.php
namespace AppBundle\Security\Authorization\Voter;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class ClientIpVoter implements VoterInterface
{
    private $container;

    private $blacklistedIp;

    public function __construct(ContainerInterface $container, array $blacklistedIp = array())
    {
        $this->container     = $container;
        $this->blacklistedIp = $blacklistedIp;
    }

    public function supportsAttribute($attribute)
    {
        // you won't check against a user attribute, so return true
        return true;
    }

    public function supportsClass($class)
    {
        // your voter supports all type of token classes, so return true
        return true;
    }

    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $request = $this->container->get('request');
        if (in_array($request->getClientIp(), $this->blacklistedIp)) {
            return VoterInterface::ACCESS_DENIED;
        }

        return VoterInterface::ACCESS_ABSTAIN;
    }
}

That’s it! The voter is done. The next step is to inject the voter into the security layer. This can be done easily through the service container.

小技巧

Your implementation of the methods supportsAttribute() and supportsClass() are not being called internally by the framework. Once you have registered your voter the vote() method will always be called, regardless of whether or not these two methods return true. Therefore you need to call those methods in your implementation of the vote() method and return ACCESS_ABSTAIN if your voter does not support the class or attribute.

Declaring the Voter as a Service

To inject the voter into the security layer, you must declare it as a service, and tag it as a security.voter:

  • YAML
    # src/Acme/AcmeBundle/Resources/config/services.yml
    services:
        security.access.blacklist_voter:
            class:     AppBundle\Security\Authorization\Voter\ClientIpVoter
            arguments: ["@service_container", [123.123.123.123, 171.171.171.171]]
            public:    false
            tags:
                - { name: security.voter }
    
  • XML
    <!-- src/Acme/AcmeBundle/Resources/config/services.xml -->
    <service id="security.access.blacklist_voter"
             class="AppBundle\Security\Authorization\Voter\ClientIpVoter" public="false">
        <argument type="service" id="service_container" strict="false" />
        <argument type="collection">
            <argument>123.123.123.123</argument>
            <argument>171.171.171.171</argument>
        </argument>
        <tag name="security.voter" />
    </service>
    
  • PHP
    // src/Acme/AcmeBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $definition = new Definition(
        'AppBundle\Security\Authorization\Voter\ClientIpVoter',
        array(
            new Reference('service_container'),
            array('123.123.123.123', '171.171.171.171'),
        ),
    );
    $definition->addTag('security.voter');
    $definition->setPublic(false);
    
    $container->setDefinition('security.access.blacklist_voter', $definition);
    

小技巧

Be sure to import this configuration file from your main application configuration file (e.g. app/config/config.yml). For more information see Importing Configuration with imports. To read more about defining services in general, see the Service Container chapter.

Changing the Access Decision Strategy

In order for the new voter to take effect, you need to change the default access decision strategy, which, by default, grants access if any voter grants access.

In this case, choose the unanimous strategy. Unlike the affirmative strategy (the default), with the unanimous strategy, if only one voter denies access (e.g. the ClientIpVoter), access is not granted to the end user.

To do that, override the default access_decision_manager section of your application configuration file with the following code.

  • YAML
    # app/config/security.yml
    security:
        access_decision_manager:
            # strategy can be: affirmative, unanimous or consensus
            strategy: unanimous
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <!-- strategy can be: affirmative, unanimous or consensus -->
        <access-decision-manager strategy="unanimous">
    </config>
    
  • PHP
    // app/config/security.xml
    $container->loadFromExtension('security', array(
        // strategy can be: affirmative, unanimous or consensus
        'access_decision_manager' => array(
            'strategy' => 'unanimous',
        ),
    ));
    

That’s it! Now, when deciding whether or not a user should have access, the new voter will deny access to any user in the list of blacklisted IPs.

参见

For a more advanced usage see Access Decision Manager.

How to Use Voters to Check User Permissions

In Symfony, you can check the permission to access data by using the ACL module, which is a bit overwhelming for many applications. A much easier solution is to work with custom voters, which are like simple conditional statements.

参见

Voters can also be used in other ways, like, for example, blacklisting IP addresses from the entire application: How to Implement your own Voter to Blacklist IP Addresses.

小技巧

Take a look at the authorization chapter for an even deeper understanding on voters.

How Symfony Uses Voters

In order to use voters, you have to understand how Symfony works with them. All voters are called each time you use the isGranted() method on Symfony’s security context (i.e. the security.context service). Each one decides if the current user should have access to some resource.

Ultimately, Symfony uses one of three different approaches on what to do with the feedback from all voters: affirmative, consensus and unanimous.

For more information take a look at the section about access decision managers.

The Voter Interface

A custom voter must implement VoterInterface, which has this structure:

interface VoterInterface
{
    public function supportsAttribute($attribute);
    public function supportsClass($class);
    public function vote(TokenInterface $token, $object, array $attributes);
}

The supportsAttribute() method is used to check if the voter supports the given user attribute (i.e: a role like ROLE_USER, an ACL EDIT, etc.).

The supportsClass() method is used to check if the voter supports the class of the object whose access is being checked.

The vote() method must implement the business logic that verifies whether or not the user has access. This method must return one of the following values:

  • VoterInterface::ACCESS_GRANTED: The authorization will be granted by this voter;
  • VoterInterface::ACCESS_ABSTAIN: The voter cannot decide if authorization should be granted;
  • VoterInterface::ACCESS_DENIED: The authorization will be denied by this voter.

In this example, the voter will check if the user has access to a specific object according to your custom conditions (e.g. they must be the owner of the object). If the condition fails, you’ll return VoterInterface::ACCESS_DENIED, otherwise you’ll return VoterInterface::ACCESS_GRANTED. In case the responsibility for this decision does not belong to this voter, it will return VoterInterface::ACCESS_ABSTAIN.

Creating the custom Voter

The goal is to create a voter that checks if a user has access to view or edit a particular object. Here’s an example implementation:

// src/AppBundle/Security/Authorization/Voter/PostVoter.php
namespace AppBundle\Security\Authorization\Voter;

use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class PostVoter implements VoterInterface
{
    const VIEW = 'view';
    const EDIT = 'edit';

    public function supportsAttribute($attribute)
    {
        return in_array($attribute, array(
            self::VIEW,
            self::EDIT,
        ));
    }

    public function supportsClass($class)
    {
        $supportedClass = 'AppBundle\Entity\Post';

        return $supportedClass === $class || is_subclass_of($class, $supportedClass);
    }

    /**
     * @var \AppBundle\Entity\Post $post
     */
    public function vote(TokenInterface $token, $post, array $attributes)
    {
        // check if class of this object is supported by this voter
        if (!$this->supportsClass(get_class($post))) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        // check if the voter is used correct, only allow one attribute
        // this isn't a requirement, it's just one easy way for you to
        // design your voter
        if (1 !== count($attributes)) {
            throw new \InvalidArgumentException(
                'Only one attribute is allowed for VIEW or EDIT'
            );
        }

        // set the attribute to check against
        $attribute = $attributes[0];

        // check if the given attribute is covered by this voter
        if (!$this->supportsAttribute($attribute)) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        // get current logged in user
        $user = $token->getUser();

        // make sure there is a user object (i.e. that the user is logged in)
        if (!$user instanceof UserInterface) {
            return VoterInterface::ACCESS_DENIED;
        }

        switch($attribute) {
            case self::VIEW:
                // the data object could have for example a method isPrivate()
                // which checks the Boolean attribute $private
                if (!$post->isPrivate()) {
                    return VoterInterface::ACCESS_GRANTED;
                }
                break;

            case self::EDIT:
                // we assume that our data object has a method getOwner() to
                // get the current owner user entity for this data object
                if ($user->getId() === $post->getOwner()->getId()) {
                    return VoterInterface::ACCESS_GRANTED;
                }
                break;
        }

        return VoterInterface::ACCESS_DENIED;
    }
}

That’s it! The voter is done. The next step is to inject the voter into the security layer.

Declaring the Voter as a Service

To inject the voter into the security layer, you must declare it as a service and tag it with security.voter:

  • YAML
    # src/AppBundle/Resources/config/services.yml
    services:
        security.access.post_voter:
            class:      AppBundle\Security\Authorization\Voter\PostVoter
            public:     false
            tags:
               - { name: security.voter }
    
  • XML
    <!-- src/AppBundle/Resources/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
        <services>
            <service id="security.access.post_document_voter"
                class="AppBundle\Security\Authorization\Voter\PostVoter"
                public="false">
                <tag name="security.voter" />
            </service>
        </services>
    </container>
    
  • PHP
    // src/AppBundle/Resources/config/services.php
    $container
        ->register(
                'security.access.post_document_voter',
                'AppBundle\Security\Authorization\Voter\PostVoter'
        )
        ->addTag('security.voter')
    ;
    
How to Use the Voter in a Controller

The registered voter will then always be asked as soon as the method isGranted() from the security context is called.

// src/AppBundle/Controller/PostController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class PostController extends Controller
{
    public function showAction($id)
    {
        // get a Post instance
        $post = ...;

        // keep in mind, this will call all registered security voters
        if (false === $this->get('security.context')->isGranted('view', $post)) {
            throw new AccessDeniedException('Unauthorised access!');
        }

        return new Response('<h1>'.$post->getName().'</h1>');
    }
}

It’s that easy!

How to Use Access Control Lists (ACLs)

In complex applications, you will often face the problem that access decisions cannot only be based on the person (Token) who is requesting access, but also involve a domain object that access is being requested for. This is where the ACL system comes in.

Imagine you are designing a blog system where your users can comment on your posts. Now, you want a user to be able to edit their own comments, but not those of other users; besides, you yourself want to be able to edit all comments. In this scenario, Comment would be the domain object that you want to restrict access to. You could take several approaches to accomplish this using Symfony, two basic approaches are (non-exhaustive):

  • Enforce security in your business methods: Basically, that means keeping a reference inside each Comment to all users who have access, and then compare these users to the provided Token.
  • Enforce security with roles: In this approach, you would add a role for each Comment object, i.e. ROLE_COMMENT_1, ROLE_COMMENT_2, etc.

Both approaches are perfectly valid. However, they couple your authorization logic to your business code which makes it less reusable elsewhere, and also increases the difficulty of unit testing. Besides, you could run into performance issues if many users would have access to a single domain object.

Fortunately, there is a better way, which you will find out about now.

Bootstrapping

Now, before you can finally get into action, you need to do some bootstrapping. First, you need to configure the connection the ACL system is supposed to use:

  • YAML
    # app/config/security.yml
    security:
        acl:
            connection: default
    
  • XML
    <!-- app/config/security.xml -->
    <acl>
        <connection>default</connection>
    </acl>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', 'acl', array(
        'connection' => 'default',
    ));
    

注解

The ACL system requires a connection from either Doctrine DBAL (usable by default) or Doctrine MongoDB (usable with MongoDBAclBundle). However, that does not mean that you have to use Doctrine ORM or ODM for mapping your domain objects. You can use whatever mapper you like for your objects, be it Doctrine ORM, MongoDB ODM, Propel, raw SQL, etc. The choice is yours.

After the connection is configured, you have to import the database structure. Fortunately, there is a task for this. Simply run the following command:

$ php app/console init:acl
Getting Started

Coming back to the small example from the beginning, you can now implement ACL for it.

Once the ACL is created, you can grant access to objects by creating an Access Control Entry (ACE) to solidify the relationship between the entity and your user.

Creating an ACL and Adding an ACE
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;

class BlogController extends Controller
{
    // ...

    public function addCommentAction(Post $post)
    {
        $comment = new Comment();

        // ... setup $form, and submit data

        if ($form->isValid()) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($comment);
            $entityManager->flush();

            // creating the ACL
            $aclProvider = $this->get('security.acl.provider');
            $objectIdentity = ObjectIdentity::fromDomainObject($comment);
            $acl = $aclProvider->createAcl($objectIdentity);

            // retrieving the security identity of the currently logged-in user
            $securityContext = $this->get('security.context');
            $user = $securityContext->getToken()->getUser();
            $securityIdentity = UserSecurityIdentity::fromAccount($user);

            // grant owner access
            $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
            $aclProvider->updateAcl($acl);
        }
    }
}

There are a couple of important implementation decisions in this code snippet. For now, I only want to highlight two:

First, you may have noticed that ->createAcl() does not accept domain objects directly, but only implementations of the ObjectIdentityInterface. This additional step of indirection allows you to work with ACLs even when you have no actual domain object instance at hand. This will be extremely helpful if you want to check permissions for a large number of objects without actually hydrating these objects.

The other interesting part is the ->insertObjectAce() call. In the example, you are granting the user who is currently logged in owner access to the Comment. The MaskBuilder::MASK_OWNER is a pre-defined integer bitmask; don’t worry the mask builder will abstract away most of the technical details, but using this technique you can store many different permissions in one database row which gives a considerable boost in performance.

小技巧

The order in which ACEs are checked is significant. As a general rule, you should place more specific entries at the beginning.

Checking Access
// src/AppBundle/Controller/BlogController.php

// ...

class BlogController
{
    // ...

    public function editCommentAction(Comment $comment)
    {
        $securityContext = $this->get('security.context');

        // check for edit access
        if (false === $securityContext->isGranted('EDIT', $comment)) {
            throw new AccessDeniedException();
        }

        // ... retrieve actual comment object, and do your editing here
    }
}

In this example, you check whether the user has the EDIT permission. Internally, Symfony maps the permission to several integer bitmasks, and checks whether the user has any of them.

注解

You can define up to 32 base permissions (depending on your OS PHP might vary between 30 to 32). In addition, you can also define cumulative permissions.

Cumulative Permissions

In the first example above, you only granted the user the OWNER base permission. While this effectively also allows the user to perform any operation such as view, edit, etc. on the domain object, there are cases where you may want to grant these permissions explicitly.

The MaskBuilder can be used for creating bit masks easily by combining several base permissions:

$builder = new MaskBuilder();
$builder
    ->add('view')
    ->add('edit')
    ->add('delete')
    ->add('undelete')
;
$mask = $builder->get(); // int(29)

This integer bitmask can then be used to grant a user the base permissions you added above:

$identity = new UserSecurityIdentity('johannes', 'Acme\UserBundle\Entity\User');
$acl->insertObjectAce($identity, $mask);

The user is now allowed to view, edit, delete, and un-delete objects.

How to Use advanced ACL Concepts

The aim of this chapter is to give a more in-depth view of the ACL system, and also explain some of the design decisions behind it.

Design Concepts

Symfony’s object instance security capabilities are based on the concept of an Access Control List. Every domain object instance has its own ACL. The ACL instance holds a detailed list of Access Control Entries (ACEs) which are used to make access decisions. Symfony’s ACL system focuses on two main objectives:

  • providing a way to efficiently retrieve a large amount of ACLs/ACEs for your domain objects, and to modify them;
  • providing a way to easily make decisions of whether a person is allowed to perform an action on a domain object or not.

As indicated by the first point, one of the main capabilities of Symfony’s ACL system is a high-performance way of retrieving ACLs/ACEs. This is extremely important since each ACL might have several ACEs, and inherit from another ACL in a tree-like fashion. Therefore, no ORM is leveraged, instead the default implementation interacts with your connection directly using Doctrine’s DBAL.

Object Identities

The ACL system is completely decoupled from your domain objects. They don’t even have to be stored in the same database, or on the same server. In order to achieve this decoupling, in the ACL system your objects are represented through object identity objects. Every time you want to retrieve the ACL for a domain object, the ACL system will first create an object identity from your domain object, and then pass this object identity to the ACL provider for further processing.

Security Identities

This is analog to the object identity, but represents a user, or a role in your application. Each role, or user has its own security identity.

Database Table Structure

The default implementation uses five database tables as listed below. The tables are ordered from least rows to most rows in a typical application:

  • acl_security_identities: This table records all security identities (SID) which hold ACEs. The default implementation ships with two security identities: RoleSecurityIdentity and UserSecurityIdentity.
  • acl_classes: This table maps class names to a unique ID which can be referenced from other tables.
  • acl_object_identities: Each row in this table represents a single domain object instance.
  • acl_object_identity_ancestors: This table allows all the ancestors of an ACL to be determined in a very efficient way.
  • acl_entries: This table contains all ACEs. This is typically the table with the most rows. It can contain tens of millions without significantly impacting performance.
Scope of Access Control Entries

Access control entries can have different scopes in which they apply. In Symfony, there are basically two different scopes:

  • Class-Scope: These entries apply to all objects with the same class.
  • Object-Scope: This was the scope solely used in the previous chapter, and it only applies to one specific object.

Sometimes, you will find the need to apply an ACE only to a specific field of the object. Suppose you want the ID only to be viewable by an administrator, but not by your customer service. To solve this common problem, two more sub-scopes have been added:

  • Class-Field-Scope: These entries apply to all objects with the same class, but only to a specific field of the objects.
  • Object-Field-Scope: These entries apply to a specific object, and only to a specific field of that object.
Pre-Authorization Decisions

For pre-authorization decisions, that is decisions made before any secure method (or secure action) is invoked, the proven AccessDecisionManager service is used. The AccessDecisionManager is also used for reaching authorization decisions based on roles. Just like roles, the ACL system adds several new attributes which may be used to check for different permissions.

Built-in Permission Map
Attribute Intended Meaning Integer Bitmasks
VIEW Whether someone is allowed to view the domain object. VIEW, EDIT, OPERATOR, MASTER, or OWNER
EDIT Whether someone is allowed to make changes to the domain object. EDIT, OPERATOR, MASTER, or OWNER
CREATE Whether someone is allowed to create the domain object. CREATE, OPERATOR, MASTER, or OWNER
DELETE Whether someone is allowed to delete the domain object. DELETE, OPERATOR, MASTER, or OWNER
UNDELETE Whether someone is allowed to restore a previously deleted domain object. UNDELETE, OPERATOR, MASTER, or OWNER
OPERATOR Whether someone is allowed to perform all of the above actions. OPERATOR, MASTER, or OWNER
MASTER Whether someone is allowed to perform all of the above actions, and in addition is allowed to grant any of the above permissions to others. MASTER, or OWNER
OWNER Whether someone owns the domain object. An owner can perform any of the above actions and grant master and owner permissions. OWNER
Permission Attributes vs. Permission Bitmasks

Attributes are used by the AccessDecisionManager, just like roles. Often, these attributes represent in fact an aggregate of integer bitmasks. Integer bitmasks on the other hand, are used by the ACL system internally to efficiently store your users’ permissions in the database, and perform access checks using extremely fast bitmask operations.

Extensibility

The above permission map is by no means static, and theoretically could be completely replaced at will. However, it should cover most problems you encounter, and for interoperability with other bundles, you are encouraged to stick to the meaning envisaged for them.

Post Authorization Decisions

Post authorization decisions are made after a secure method has been invoked, and typically involve the domain object which is returned by such a method. After invocation providers also allow to modify, or filter the domain object before it is returned.

Due to current limitations of the PHP language, there are no post-authorization capabilities build into the core Security component. However, there is an experimental JMSSecurityExtraBundle which adds these capabilities. See its documentation for further information on how this is accomplished.

Process for Reaching Authorization Decisions

The ACL class provides two methods for determining whether a security identity has the required bitmasks, isGranted and isFieldGranted. When the ACL receives an authorization request through one of these methods, it delegates this request to an implementation of PermissionGrantingStrategy. This allows you to replace the way access decisions are reached without actually modifying the ACL class itself.

The PermissionGrantingStrategy first checks all your object-scope ACEs. If none is applicable, the class-scope ACEs will be checked. If none is applicable, then the process will be repeated with the ACEs of the parent ACL. If no parent ACL exists, an exception will be thrown.

How to Force HTTPS or HTTP for different URLs

You can force areas of your site to use the HTTPS protocol in the security config. This is done through the access_control rules using the requires_channel option. For example, if you want to force all URLs starting with /secure to use HTTPS then you could use the following configuration:

  • YAML
    access_control:
        - { path: ^/secure, roles: ROLE_ADMIN, requires_channel: https }
    
  • XML
    <access-control>
        <rule path="^/secure" role="ROLE_ADMIN" requires_channel="https" />
    </access-control>
    
  • PHP
    'access_control' => array(
        array(
            'path'             => '^/secure',
            'role'             => 'ROLE_ADMIN',
            'requires_channel' => 'https',
        ),
    ),
    

The login form itself needs to allow anonymous access, otherwise users will be unable to authenticate. To force it to use HTTPS you can still use access_control rules by using the IS_AUTHENTICATED_ANONYMOUSLY role:

  • YAML
    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
    
  • XML
    <access-control>
        <rule path="^/login"
              role="IS_AUTHENTICATED_ANONYMOUSLY"
              requires_channel="https" />
    </access-control>
    
  • PHP
    'access_control' => array(
        array(
            'path'             => '^/login',
            'role'             => 'IS_AUTHENTICATED_ANONYMOUSLY',
            'requires_channel' => 'https',
        ),
    ),
    

It is also possible to specify using HTTPS in the routing configuration, see How to Force Routes to always Use HTTPS or HTTP for more details.

How to Customize your Form Login

Using a form login for authentication is a common, and flexible, method for handling authentication in Symfony. Pretty much every aspect of the form login can be customized. The full, default configuration is shown in the next section.

Form Login Configuration Reference

To see the full form login configuration reference, see SecurityBundle Configuration (“security”). Some of the more interesting options are explained below.

Redirecting after Success

You can change where the login form redirects after a successful login using the various config options. By default the form will redirect to the URL the user requested (i.e. the URL which triggered the login form being shown). For example, if the user requested http://www.example.com/admin/post/18/edit, then after they successfully log in, they will eventually be sent back to http://www.example.com/admin/post/18/edit. This is done by storing the requested URL in the session. If no URL is present in the session (perhaps the user went directly to the login page), then the user is redirected to the default page, which is / (i.e. the homepage) by default. You can change this behavior in several ways.

注解

As mentioned, by default the user is redirected back to the page originally requested. Sometimes, this can cause problems, like if a background Ajax request “appears” to be the last visited URL, causing the user to be redirected there. For information on controlling this behavior, see How to Change the default Target Path Behavior.

Changing the default Page

First, the default page can be set (i.e. the page the user is redirected to if no previous page was stored in the session). To set it to the default_security_target route use the following config:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                form_login:
                    # ...
                    default_target_path: default_security_target
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <form-login
                default_target_path="default_security_target"
            />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array(
                // ...
    
                'form_login' => array(
                    // ...
                    'default_target_path' => 'default_security_target',
                ),
            ),
        ),
    ));
    

Now, when no URL is set in the session, users will be sent to the default_security_target route.

Always Redirect to the default Page

You can make it so that users are always redirected to the default page regardless of what URL they had requested previously by setting the always_use_default_target_path option to true:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                form_login:
                    # ...
                    always_use_default_target_path: true
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <form-login
                always_use_default_target_path="true"
            />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array(
                // ...
    
                'form_login' => array(
                    // ...
                    'always_use_default_target_path' => true,
                ),
            ),
        ),
    ));
    
Using the Referring URL

In case no previous URL was stored in the session, you may wish to try using the HTTP_REFERER instead, as this will often be the same. You can do this by setting use_referer to true (it defaults to false):

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                form_login:
                    # ...
                    use_referer:        true
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <form-login
                use_referer="true"
            />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array(
                // ...
    
                'form_login' => array(
                    // ...
                    'use_referer' => true,
                ),
            ),
        ),
    ));
    
Control the Redirect URL from inside the Form

You can also override where the user is redirected to via the form itself by including a hidden field with the name _target_path. For example, to redirect to the URL defined by some account route, use the following:

  • Twig
    {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
    {% if error %}
        <div>{{ error.message }}</div>
    {% endif %}
    
    <form action="{{ path('login_check') }}" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        <input type="hidden" name="_target_path" value="account" />
    
        <input type="submit" name="login" />
    </form>
    
  • PHP
    <!-- src/Acme/SecurityBundle/Resources/views/Security/login.html.php -->
    <?php if ($error): ?>
        <div><?php echo $error->getMessage() ?></div>
    <?php endif ?>
    
    <form action="<?php echo $view['router']->generate('login_check') ?>" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="<?php echo $last_username ?>" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        <input type="hidden" name="_target_path" value="account" />
    
        <input type="submit" name="login" />
    </form>
    

Now, the user will be redirected to the value of the hidden form field. The value attribute can be a relative path, absolute URL, or a route name. You can even change the name of the hidden form field by changing the target_path_parameter option to another value.

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                form_login:
                    target_path_parameter: redirect_url
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <form-login
                target_path_parameter="redirect_url"
            />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array(
                'form_login' => array(
                    'target_path_parameter' => redirect_url,
                ),
            ),
        ),
    ));
    
Redirecting on Login Failure

In addition to redirecting the user after a successful login, you can also set the URL that the user should be redirected to after a failed login (e.g. an invalid username or password was submitted). By default, the user is redirected back to the login form itself. You can set this to a different route (e.g. login_failure) with the following config:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                form_login:
                    # ...
                    failure_path: login_failure
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <form-login
                failure_path="login_failure"
            />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array(
                // ...
    
                'form_login' => array(
                    // ...
                    'failure_path' => 'login_failure',
                ),
            ),
        ),
    ));
    
How to Secure any Service or Method in your Application

In the security chapter, you can see how to secure a controller by requesting the security.context service from the Service Container and checking the current user’s role:

// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function helloAction($name)
{
    if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }

    // ...
}

You can also secure any service in a similar way by injecting the security.context service into it. For a general introduction to injecting dependencies into services see the Service Container chapter of the book. For example, suppose you have a NewsletterManager class that sends out emails and you want to restrict its use to only users who have some ROLE_NEWSLETTER_ADMIN role. Before you add security, the class looks something like this:

// src/AppBundle/Newsletter/NewsletterManager.php
namespace AppBundle\Newsletter;

class NewsletterManager
{

    public function sendNewsletter()
    {
        // ... where you actually do the work
    }

    // ...
}

Your goal is to check the user’s role when the sendNewsletter() method is called. The first step towards this is to inject the security.context service into the object. Since it won’t make sense not to perform the security check, this is an ideal candidate for constructor injection, which guarantees that the security context object will be available inside the NewsletterManager class:

namespace AppBundle\Newsletter;

use Symfony\Component\Security\Core\SecurityContextInterface;

class NewsletterManager
{
    protected $securityContext;

    public function __construct(SecurityContextInterface $securityContext)
    {
        $this->securityContext = $securityContext;
    }

    // ...
}

Then in your service configuration, you can inject the service:

  • YAML
    # app/config/services.yml
    services:
        newsletter_manager:
            class:     AppBundle\Newsletter\NewsletterManager
            arguments: ["@security.context"]
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <argument type="service" id="security.context"/>
        </service>
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('newsletter_manager', new Definition(
        'AppBundle\Newsletter\NewsletterManager',
        array(new Reference('security.context'))
    ));
    

The injected service can then be used to perform the security check when the sendNewsletter() method is called:

namespace AppBundle\Newsletter;

use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\SecurityContextInterface;
// ...

class NewsletterManager
{
    protected $securityContext;

    public function __construct(SecurityContextInterface $securityContext)
    {
        $this->securityContext = $securityContext;
    }

    public function sendNewsletter()
    {
        if (false === $this->securityContext->isGranted('ROLE_NEWSLETTER_ADMIN')) {
            throw new AccessDeniedException();
        }

        // ...
    }

    // ...
}

If the current user does not have the ROLE_NEWSLETTER_ADMIN, they will be prompted to log in.

Securing Methods Using Annotations

You can also secure method calls in any service with annotations by using the optional JMSSecurityExtraBundle bundle. This bundle is not included in the Symfony Standard Distribution, but you can choose to install it.

To enable the annotations functionality, tag the service you want to secure with the security.secure_service tag (you can also automatically enable this functionality for all services, see the sidebar below):

  • YAML
    # app/services.yml
    
    # ...
    services:
        newsletter_manager:
            # ...
            tags:
                -  { name: security.secure_service }
    
  • XML
    <!-- app/services.xml -->
    <!-- ... -->
    
    <services>
        <service id="newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <!-- ... -->
            <tag name="security.secure_service" />
        </service>
    </services>
    
  • PHP
    // app/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $definition = new Definition(
        'AppBundle\Newsletter\NewsletterManager',
        array(new Reference('security.context'))
    ));
    $definition->addTag('security.secure_service');
    $container->setDefinition('newsletter_manager', $definition);
    

You can then achieve the same results as above using an annotation:

namespace AppBundle\Newsletter;

use JMS\SecurityExtraBundle\Annotation\Secure;
// ...

class NewsletterManager
{

    /**
     * @Secure(roles="ROLE_NEWSLETTER_ADMIN")
     */
    public function sendNewsletter()
    {
        // ...
    }

    // ...
}

注解

The annotations work because a proxy class is created for your class which performs the security checks. This means that, whilst you can use annotations on public and protected methods, you cannot use them with private methods or methods marked final.

The JMSSecurityExtraBundle also allows you to secure the parameters and return values of methods. For more information, see the JMSSecurityExtraBundle documentation.

How to Create a custom User Provider

Part of Symfony’s standard authentication process depends on “user providers”. When a user submits a username and password, the authentication layer asks the configured user provider to return a user object for a given username. Symfony then checks whether the password of this user is correct and generates a security token so the user stays authenticated during the current session. Out of the box, Symfony has an “in_memory” and an “entity” user provider. In this entry you’ll see how you can create your own user provider, which could be useful if your users are accessed via a custom database, a file, or - as shown in this example - a web service.

Create a User Class

First, regardless of where your user data is coming from, you’ll need to create a User class that represents that data. The User can look however you want and contain any data. The only requirement is that the class implements UserInterface. The methods in this interface should therefore be defined in the custom user class: getRoles(), getPassword(), getSalt(), getUsername(), eraseCredentials(). It may also be useful to implement the EquatableInterface interface, which defines a method to check if the user is equal to the current user. This interface requires an isEqualTo() method.

This is how your WebserviceUser class looks in action:

// src/Acme/WebserviceUserBundle/Security/User/WebserviceUser.php
namespace Acme\WebserviceUserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

class WebserviceUser implements UserInterface, EquatableInterface
{
    private $username;
    private $password;
    private $salt;
    private $roles;

    public function __construct($username, $password, $salt, array $roles)
    {
        $this->username = $username;
        $this->password = $password;
        $this->salt = $salt;
        $this->roles = $roles;
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getSalt()
    {
        return $this->salt;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function eraseCredentials()
    {
    }

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            return false;
        }

        if ($this->password !== $user->getPassword()) {
            return false;
        }

        if ($this->salt !== $user->getSalt()) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }
}

If you have more information about your users - like a “first name” - then you can add a firstName field to hold that data.

Create a User Provider

Now that you have a User class, you’ll create a user provider, which will grab user information from some web service, create a WebserviceUser object, and populate it with data.

The user provider is just a plain PHP class that has to implement the UserProviderInterface, which requires three methods to be defined: loadUserByUsername($username), refreshUser(UserInterface $user), and supportsClass($class). For more details, see UserProviderInterface.

Here’s an example of how this might look:

// src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php
namespace Acme\WebserviceUserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

class WebserviceUserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        // make a call to your webservice here
        $userData = ...
        // pretend it returns an array on success, false if there is no user

        if ($userData) {
            $password = '...';

            // ...

            return new WebserviceUser($username, $password, $salt, $roles);
        }

        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'Acme\WebserviceUserBundle\Security\User\WebserviceUser';
    }
}
Create a Service for the User Provider

Now you make the user provider available as a service:

  • YAML
    # src/Acme/WebserviceUserBundle/Resources/config/services.yml
    services:
        webservice_user_provider:
            class: Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider
    
  • XML
    <!-- src/Acme/WebserviceUserBundle/Resources/config/services.xml -->
    <services>
        <service id="webservice_user_provider" class="Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider" />
    </services>
    
  • PHP
    // src/Acme/WebserviceUserBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition(
        'webservice_user_provider',
        new Definition('Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider')
    );
    

小技巧

The real implementation of the user provider will probably have some dependencies or configuration options or other services. Add these as arguments in the service definition.

注解

Make sure the services file is being imported. See Importing Configuration with imports for details.

Modify security.yml

Everything comes together in your security configuration. Add the user provider to the list of providers in the “security” section. Choose a name for the user provider (e.g. “webservice”) and mention the id of the service you just defined.

  • YAML
    # app/config/security.yml
    security:
        providers:
            webservice:
                id: webservice_user_provider
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <provider name="webservice" id="webservice_user_provider" />
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'webservice' => array(
                'id' => 'webservice_user_provider',
            ),
        ),
    ));
    

Symfony also needs to know how to encode passwords that are supplied by website users, e.g. by filling in a login form. You can do this by adding a line to the “encoders” section in your security configuration:

  • YAML
    # app/config/security.yml
    security:
        encoders:
            Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <encoder class="Acme\WebserviceUserBundle\Security\User\WebserviceUser">sha512</encoder>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'encoders' => array(
            'Acme\WebserviceUserBundle\Security\User\WebserviceUser' => 'sha512',
        ),
    ));
    

The value here should correspond with however the passwords were originally encoded when creating your users (however those users were created). When a user submits their password, the salt value is appended to the password and then encoded using this algorithm before being compared to the hashed password returned by your getPassword() method. Additionally, depending on your options, the password may be encoded multiple times and encoded to base64.

How to Create a custom Authentication Provider

If you have read the chapter on Security, you understand the distinction Symfony makes between authentication and authorization in the implementation of security. This chapter discusses the core classes involved in the authentication process, and how to implement a custom authentication provider. Because authentication and authorization are separate concepts, this extension will be user-provider agnostic, and will function with your application’s user providers, may they be based in memory, a database, or wherever else you choose to store them.

Meet WSSE

The following chapter demonstrates how to create a custom authentication provider for WSSE authentication. The security protocol for WSSE provides several security benefits:

  1. Username / Password encryption
  2. Safe guarding against replay attacks
  3. No web server configuration required

WSSE is very useful for the securing of web services, may they be SOAP or REST.

There is plenty of great documentation on WSSE, but this article will focus not on the security protocol, but rather the manner in which a custom protocol can be added to your Symfony application. The basis of WSSE is that a request header is checked for encrypted credentials, verified using a timestamp and nonce, and authenticated for the requested user using a password digest.

注解

WSSE also supports application key validation, which is useful for web services, but is outside the scope of this chapter.

The Token

The role of the token in the Symfony security context is an important one. A token represents the user authentication data present in the request. Once a request is authenticated, the token retains the user’s data, and delivers this data across the security context. First, you’ll create your token class. This will allow the passing of all relevant information to your authentication provider.

// src/AppBundle/Security/Authentication/Token/WsseUserToken.php
namespace AppBundle\Security\Authentication\Token;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

class WsseUserToken extends AbstractToken
{
    public $created;
    public $digest;
    public $nonce;

    public function __construct(array $roles = array())
    {
        parent::__construct($roles);

        // If the user has roles, consider it authenticated
        $this->setAuthenticated(count($roles) > 0);
    }

    public function getCredentials()
    {
        return '';
    }
}

注解

The WsseUserToken class extends the Security component’s AbstractToken class, which provides basic token functionality. Implement the TokenInterface on any class to use as a token.

The Listener

Next, you need a listener to listen on the security context. The listener is responsible for fielding requests to the firewall and calling the authentication provider. A listener must be an instance of ListenerInterface. A security listener should handle the GetResponseEvent event, and set an authenticated token in the security context if successful.

// src/AppBundle/Security/Firewall/WsseListener.php
namespace AppBundle\Security\Firewall;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use AppBundle\Security\Authentication\Token\WsseUserToken;

class WsseListener implements ListenerInterface
{
    protected $securityContext;
    protected $authenticationManager;

    public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
    {
        $this->securityContext = $securityContext;
        $this->authenticationManager = $authenticationManager;
    }

    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
        if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) {
            return;
        }

        $token = new WsseUserToken();
        $token->setUser($matches[1]);

        $token->digest   = $matches[2];
        $token->nonce    = $matches[3];
        $token->created  = $matches[4];

        try {
            $authToken = $this->authenticationManager->authenticate($token);
            $this->securityContext->setToken($authToken);

            return;
        } catch (AuthenticationException $failed) {
            // ... you might log something here

            // To deny the authentication clear the token. This will redirect to the login page.
            // Make sure to only clear your token, not those of other authentication listeners.
            // $token = $this->securityContext->getToken();
            // if ($token instanceof WsseUserToken && $this->providerKey === $token->getProviderKey()) {
            //     $this->securityContext->setToken(null);
            // }
            // return;
        }

        // By default deny authorization
        $response = new Response();
        $response->setStatusCode(403);
        $event->setResponse($response);
    }
}

This listener checks the request for the expected X-WSSE header, matches the value returned for the expected WSSE information, creates a token using that information, and passes the token on to the authentication manager. If the proper information is not provided, or the authentication manager throws an AuthenticationException, a 403 Response is returned.

注解

A class not used above, the AbstractAuthenticationListener class, is a very useful base class which provides commonly needed functionality for security extensions. This includes maintaining the token in the session, providing success / failure handlers, login form URLs, and more. As WSSE does not require maintaining authentication sessions or login forms, it won’t be used for this example.

注解

Returning prematurely from the listener is relevant only if you want to chain authentication providers (for example to allow anonymous users). If you want to forbid access to anonymous users and have a nice 403 error, you should set the status code of the response before returning.

The Authentication Provider

The authentication provider will do the verification of the WsseUserToken. Namely, the provider will verify the Created header value is valid within five minutes, the Nonce header value is unique within five minutes, and the PasswordDigest header value matches with the user’s password.

// src/AppBundle/Security/Authentication/Provider/WsseProvider.php
namespace AppBundle\Security\Authentication\Provider;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use AppBundle\Security\Authentication\Token\WsseUserToken;

class WsseProvider implements AuthenticationProviderInterface
{
    private $userProvider;
    private $cacheDir;

    public function __construct(UserProviderInterface $userProvider, $cacheDir)
    {
        $this->userProvider = $userProvider;
        $this->cacheDir     = $cacheDir;
    }

    public function authenticate(TokenInterface $token)
    {
        $user = $this->userProvider->loadUserByUsername($token->getUsername());

        if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
            $authenticatedToken = new WsseUserToken($user->getRoles());
            $authenticatedToken->setUser($user);

            return $authenticatedToken;
        }

        throw new AuthenticationException('The WSSE authentication failed.');
    }

    /**
     * This function is specific to Wsse authentication and is only used to help this example
     *
     * For more information specific to the logic here, see
     * https://github.com/symfony/symfony-docs/pull/3134#issuecomment-27699129
     */
    protected function validateDigest($digest, $nonce, $created, $secret)
    {
        // Check created time is not in the future
        if (strtotime($created) > time()) {
            return false;
        }

        // Expire timestamp after 5 minutes
        if (time() - strtotime($created) > 300) {
            return false;
        }

        // Validate that the nonce is *not* used in the last 5 minutes
        // if it has, this could be a replay attack
        if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) {
            throw new NonceExpiredException('Previously used nonce detected');
        }
        // If cache directory does not exist we create it
        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0777, true);
        }
        file_put_contents($this->cacheDir.'/'.$nonce, time());

        // Validate Secret
        $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));

        return $digest === $expected;
    }

    public function supports(TokenInterface $token)
    {
        return $token instanceof WsseUserToken;
    }
}

注解

The AuthenticationProviderInterface requires an authenticate method on the user token, and a supports method, which tells the authentication manager whether or not to use this provider for the given token. In the case of multiple providers, the authentication manager will then move to the next provider in the list.

The Factory

You have created a custom token, custom listener, and custom provider. Now you need to tie them all together. How do you make a unique provider available for every firewall? The answer is by using a factory. A factory is where you hook into the Security component, telling it the name of your provider and any configuration options available for it. First, you must create a class which implements SecurityFactoryInterface.

// src/AppBundle/DependencyInjection/Security/Factory/WsseFactory.php
namespace AppBundle\DependencyInjection\Security\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;

class WsseFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.wsse.'.$id;
        $container
            ->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProvider))
        ;

        $listenerId = 'security.authentication.listener.wsse.'.$id;
        $listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener'));

        return array($providerId, $listenerId, $defaultEntryPoint);
    }

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'wsse';
    }

    public function addConfiguration(NodeDefinition $node)
    {
    }
}

The SecurityFactoryInterface requires the following methods:

create
Method which adds the listener and authentication provider to the DI container for the appropriate security context.
getPosition
Method which must be of type pre_auth, form, http, and remember_me and defines the position at which the provider is called.
getKey
Method which defines the configuration key used to reference the provider in the firewall configuration.
addConfiguration
Method which is used to define the configuration options underneath the configuration key in your security configuration. Setting configuration options are explained later in this chapter.

注解

A class not used in this example, AbstractFactory, is a very useful base class which provides commonly needed functionality for security factories. It may be useful when defining an authentication provider of a different type.

Now that you have created a factory class, the wsse key can be used as a firewall in your security configuration.

注解

You may be wondering “why do you need a special factory class to add listeners and providers to the dependency injection container?”. This is a very good question. The reason is you can use your firewall multiple times, to secure multiple parts of your application. Because of this, each time your firewall is used, a new service is created in the DI container. The factory is what creates these new services.

Configuration

It’s time to see your authentication provider in action. You will need to do a few things in order to make this work. The first thing is to add the services above to the DI container. Your factory class above makes reference to service ids that do not exist yet: wsse.security.authentication.provider and wsse.security.authentication.listener. It’s time to define those services.

  • YAML
    # src/AppBundle/Resources/config/services.yml
    services:
        wsse.security.authentication.provider:
            class: AppBundle\Security\Authentication\Provider\WsseProvider
            arguments: ["", "%kernel.cache_dir%/security/nonces"]
    
        wsse.security.authentication.listener:
            class: AppBundle\Security\Firewall\WsseListener
            arguments: ["@security.context", "@security.authentication.manager"]
    
  • XML
    <!-- src/AppBundle/Resources/config/services.xml -->
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="wsse.security.authentication.provider"
                class="AppBundle\Security\Authentication\Provider\WsseProvider" public="false">
                <argument /> <!-- User Provider -->
                <argument>%kernel.cache_dir%/security/nonces</argument>
            </service>
    
            <service id="wsse.security.authentication.listener"
                class="AppBundle\Security\Firewall\WsseListener" public="false">
                <argument type="service" id="security.context"/>
                <argument type="service" id="security.authentication.manager" />
            </service>
        </services>
    </container>
    
  • PHP
    // src/AppBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('wsse.security.authentication.provider',
        new Definition(
            'AppBundle\Security\Authentication\Provider\WsseProvider', array(
                '',
                '%kernel.cache_dir%/security/nonces',
            )
        )
    );
    
    $container->setDefinition('wsse.security.authentication.listener',
        new Definition(
            'AppBundle\Security\Firewall\WsseListener', array(
                new Reference('security.context'),
                new Reference('security.authentication.manager'),
            )
        )
    );
    

Now that your services are defined, tell your security context about your factory in your bundle class:

// src/AppBundle/AppBundle.php
namespace AppBundle;

use AppBundle\DependencyInjection\Security\Factory\WsseFactory;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AppBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $extension = $container->getExtension('security');
        $extension->addSecurityListenerFactory(new WsseFactory());
    }
}

You are finished! You can now define parts of your app as under WSSE protection.

  • YAML
    security:
        firewalls:
            wsse_secured:
                pattern:   /api/.*
                stateless: true
                wsse:      true
    
  • XML
    <config>
        <firewall name="wsse_secured" pattern="/api/.*">
            <stateless />
            <wsse />
        </firewall>
    </config>
    
  • PHP
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'wsse_secured' => array(
                'pattern' => '/api/.*',
                'stateless'    => true,
                'wsse'    => true,
            ),
        ),
    ));
    

Congratulations! You have written your very own custom security authentication provider!

A little Extra

How about making your WSSE authentication provider a bit more exciting? The possibilities are endless. Why don’t you start by adding some sparkle to that shine?

Configuration

You can add custom options under the wsse key in your security configuration. For instance, the time allowed before expiring the Created header item, by default, is 5 minutes. Make this configurable, so different firewalls can have different timeout lengths.

You will first need to edit WsseFactory and define the new option in the addConfiguration method.

class WsseFactory implements SecurityFactoryInterface
{
    // ...

    public function addConfiguration(NodeDefinition $node)
    {
      $node
        ->children()
        ->scalarNode('lifetime')->defaultValue(300)
        ->end();
    }
}

Now, in the create method of the factory, the $config argument will contain a lifetime key, set to 5 minutes (300 seconds) unless otherwise set in the configuration. Pass this argument to your authentication provider in order to put it to use.

class WsseFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.wsse.'.$id;
        $container
            ->setDefinition($providerId,
              new DefinitionDecorator('wsse.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProvider))
            ->replaceArgument(2, $config['lifetime']);
        // ...
    }

    // ...
}

注解

You’ll also need to add a third argument to the wsse.security.authentication.provider service configuration, which can be blank, but will be filled in with the lifetime in the factory. The WsseProvider class will also now need to accept a third constructor argument - the lifetime - which it should use instead of the hard-coded 300 seconds. These two steps are not shown here.

The lifetime of each WSSE request is now configurable, and can be set to any desirable value per firewall.

  • YAML
    security:
        firewalls:
            wsse_secured:
                pattern:   /api/.*
                stateless: true
                wsse:      { lifetime: 30 }
    
  • XML
    <config>
        <firewall name="wsse_secured"
            pattern="/api/.*"
        >
            <stateless />
            <wsse lifetime="30" />
        </firewall>
    </config>
    
  • PHP
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'wsse_secured' => array(
                'pattern' => '/api/.*',
                'stateless' => true,
                'wsse'    => array(
                    'lifetime' => 30,
                ),
            ),
        ),
    ));
    

The rest is up to you! Any relevant configuration items can be defined in the factory and consumed or passed to the other classes in the container.

Using pre Authenticated Security Firewalls

A lot of authentication modules are already provided by some web servers, including Apache. These modules generally set some environment variables that can be used to determine which user is accessing your application. Out of the box, Symfony supports most authentication mechanisms. These requests are called pre authenticated requests because the user is already authenticated when reaching your application.

X.509 Client Certificate Authentication

When using client certificates, your webserver is doing all the authentication process itself. With Apache, for example, you would use the SSLVerifyClient Require directive.

Enable the x509 authentication for a particular firewall in the security configuration:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                pattern: ^/
                x509:
                    provider: your_user_provider
    
  • XML
    <?xml version="1.0" ?>
    <!-- app/config/security.xml -->
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:srv="http://symfony.com/schema/dic/services">
    
        <config>
            <firewall name="secured_area" pattern="^/">
                <x509 provider="your_user_provider"/>
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                'pattern' => '^/'
                'x509'    => array(
                    'provider' => 'your_user_provider',
                ),
            ),
        ),
    ));
    

By default, the firewall provides the SSL_CLIENT_S_DN_Email variable to the user provider, and sets the SSL_CLIENT_S_DN as credentials in the PreAuthenticatedToken. You can override these by setting the user and the credentials keys in the x509 firewall configuration respectively.

注解

An authentication provider will only inform the user provider of the username that made the request. You will need to create (or use) a “user provider” that is referenced by the provider configuration parameter (your_user_provider in the configuration example). This provider will turn the username into a User object of your choice. For more information on creating or configuring a user provider, see:

How to Change the default Target Path Behavior

By default, the Security component retains the information of the last request URI in a session variable named _security.main.target_path (with main being the name of the firewall, defined in security.yml). Upon a successful login, the user is redirected to this path, as to help them continue from the last known page they visited.

In some situations, this is not ideal. For example, when the last request URI was an XMLHttpRequest which returned a non-HTML or partial HTML response, the user is redirected back to a page which the browser cannot render.

To get around this behavior, you would simply need to extend the ExceptionListener class and override the default method named setTargetPath().

First, override the security.exception_listener.class parameter in your configuration file. This can be done from your main configuration file (in app/config) or from a configuration file being imported from a bundle:

  • YAML
    # app/config/services.yml
    parameters:
        # ...
        security.exception_listener.class: AppBundle\Security\Firewall\ExceptionListener
    
  • XML
    <!-- app/config/services.xml -->
    <parameters>
        <!-- ... -->
        <parameter key="security.exception_listener.class">AppBundle\Security\Firewall\ExceptionListener</parameter>
    </parameters>
    
  • PHP
    // app/config/services.php
    // ...
    $container->setParameter('security.exception_listener.class', 'AppBundle\Security\Firewall\ExceptionListener');
    

Next, create your own ExceptionListener:

// src/AppBundle/Security/Firewall/ExceptionListener.php
namespace AppBundle\Security\Firewall;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\ExceptionListener as BaseExceptionListener;

class ExceptionListener extends BaseExceptionListener
{
    protected function setTargetPath(Request $request)
    {
        // Do not save target path for XHR requests
        // You can add any more logic here you want
        // Note that non-GET requests are already ignored
        if ($request->isXmlHttpRequest()) {
            return;
        }

        parent::setTargetPath($request);
    }
}

Add as much or as little logic here as required for your scenario!

Using CSRF Protection in the Login Form

When using a login form, you should make sure that you are protected against CSRF (Cross-site request forgery). The Security component already has built-in support for CSRF. In this article you’ll learn how you can use it in your login form.

注解

Login CSRF attacks are a bit less well-known. See Forging Login Requests if you’re curious about more details.

Configuring CSRF Protection

First, configure the Security component so it can use CSRF protection. The Security component needs a CSRF token provider. You can set this to use the default provider available in the Form component:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                form_login:
                    # ...
                    csrf_provider: form.csrf_provider
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="secured_area">
                <!-- ... -->
    
                <form-login csrf-provider="form.csrf_provider" />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'form_login' => array(
                    // ...
                    'csrf_provider' => 'form.csrf_provider',
                )
            )
        )
    ));
    

The Security component can be configured further, but this is all information it needs to be able to use CSRF in the login form.

Rendering the CSRF field

Now that Security component will check for the CSRF token, you have to add a hidden field to the login form containing the CSRF token. By default, this field is named _csrf_token. That hidden field must contain the CSRF token, which can be generated by using the csrf_token function. That function requires a token ID, which must be set to authenticate when using the login form:

  • Twig
    {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
    
    {# ... #}
    <form action="{{ path('login_check') }}" method="post">
        {# ... the login fields #}
    
        <input type="hidden" name="_csrf_token"
            value="{{ csrf_token('authenticate') }}"
        >
    
        <button type="submit">login</button>
    </form>
    
  • PHP
    <!-- src/Acme/SecurityBundle/Resources/views/Security/login.html.php -->
    
    <!-- ... -->
    <form action="<?php echo $view['router']->generate('login_check') ?>" method="post">
        <!-- ... the login fields -->
    
        <input type="hidden" name="_csrf_token"
            value="<?php echo $view['form']->csrfToken('authenticate') ?>"
        >
    
        <button type="submit">login</button>
    </form>
    

After this, you have protected your login form against CSRF attacks.

小技巧

You can change the name of the field by setting csrf_parameter and change the token ID by setting intention in your configuration:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                form_login:
                    # ...
                    csrf_parameter: _csrf_security_token
                    intention: a_private_string
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="secured_area">
                <!-- ... -->
    
                <form-login csrf-parameter="_csrf_security_token"
                    intention="a_private_string" />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'form_login' => array(
                    // ...
                    'csrf_parameter' => '_csrf_security_token',
                    'intention'      => 'a_private_string',
                )
            )
        )
    ));
    
How Does the Security access_control Work?

For each incoming request, Symfony checks each access_control entry to find one that matches the current request. As soon as it finds a matching access_control entry, it stops - only the first matching access_control is used to enforce access.

Each access_control has several options that configure two different things:

  1. should the incoming request match this access control entry
  2. once it matches, should some sort of access restriction be enforced:
1. Matching Options

Symfony creates an instance of RequestMatcher for each access_control entry, which determines whether or not a given access control should be used on this request. The following access_control options are used for matching:

  • path
  • ip or ips
  • host
  • methods

Take the following access_control entries as an example:

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
            - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ }
            - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
            - { path: ^/admin, roles: ROLE_USER }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
            <access-control>
                <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
                <rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$" />
                <rule path="^/admin" role="ROLE_USER_METHOD" method="POST, PUT" />
                <rule path="^/admin" role="ROLE_USER" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'access_control' => array(
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER_IP',
                'ip' => '127.0.0.1',
            ),
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER_HOST',
                'host' => 'symfony\.com$',
            ),
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER_METHOD',
                'method' => 'POST, PUT',
            ),
            array(
                'path' => '^/admin',
                'role' => 'ROLE_USER',
            ),
        ),
    ));
    

For each incoming request, Symfony will decide which access_control to use based on the URI, the client’s IP address, the incoming host name, and the request method. Remember, the first rule that matches is used, and if ip, host or method are not specified for an entry, that access_control will match any ip, host or method:

URI IP HOST METHOD access_control Why?
/admin/user 127.0.0.1 example.com GET rule #1 (ROLE_USER_IP) The URI matches path and the IP matches ip.
/admin/user 127.0.0.1 symfony.com GET rule #1 (ROLE_USER_IP) The path and ip still match. This would also match the ROLE_USER_HOST entry, but only the first access_control match is used.
/admin/user 168.0.0.1 symfony.com GET rule #2 (ROLE_USER_HOST) The ip doesn’t match the first rule, so the second rule (which matches) is used.
/admin/user 168.0.0.1 symfony.com POST rule #2 (ROLE_USER_HOST) The second rule still matches. This would also match the third rule (ROLE_USER_METHOD), but only the first matched access_control is used.
/admin/user 168.0.0.1 example.com POST rule #3 (ROLE_USER_METHOD) The ip and host don’t match the first two entries, but the third - ROLE_USER_METHOD - matches and is used.
/admin/user 168.0.0.1 example.com GET rule #4 (ROLE_USER) The ip, host and method prevent the first three entries from matching. But since the URI matches the path pattern of the ROLE_USER entry, it is used.
/foo 127.0.0.1 symfony.com POST matches no entries This doesn’t match any access_control rules, since its URI doesn’t match any of the path values.
2. Access Enforcement

Once Symfony has decided which access_control entry matches (if any), it then enforces access restrictions based on the roles and requires_channel options:

  • role If the user does not have the given role(s), then access is denied (internally, an AccessDeniedException is thrown);
  • requires_channel If the incoming request’s channel (e.g. http) does not match this value (e.g. https), the user will be redirected (e.g. redirected from http to https, or vice versa).

小技巧

If access is denied, the system will try to authenticate the user if not already (e.g. redirect the user to the login page). If the user is already logged in, the 403 “access denied” error page will be shown. See How to Customize Error Pages for more information.

Matching access_control By IP

Certain situations may arise when you need to have an access_control entry that only matches requests coming from some IP address or range. For example, this could be used to deny access to a URL pattern to all requests except those from a trusted, internal server.

警告

As you’ll read in the explanation below the example, the ips option does not restrict to a specific IP address. Instead, using the ips key means that the access_control entry will only match this IP address, and users accessing it from a different IP address will continue down the access_control list.

Here is an example of how you configure some example /internal* URL pattern so that it is only accessible by requests from the local server itself:

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            #
            - { path: ^/internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] }
            - { path: ^/internal, roles: ROLE_NO_ACCESS }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
            <access-control>
                <rule path="^/esi" role="IS_AUTHENTICATED_ANONYMOUSLY"
                    ips="127.0.0.1, ::1" />
                <rule path="^/esi" role="ROLE_NO_ACCESS" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'access_control' => array(
            array(
                'path' => '^/esi',
                'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
                'ips' => '127.0.0.1, ::1'
            ),
            array(
                'path' => '^/esi',
                'role' => 'ROLE_NO_ACCESS'
            ),
        ),
    ));
    

Here is how it works when the path is /internal/something coming from the external IP address 10.0.0.1:

  • The first access control rule is ignored as the path matches but the IP address does not match either of the IPs listed;
  • The second access control rule is enabled (the only restriction being the path) and so it matches. If you make sure that no users ever have ROLE_NO_ACCESS, then access is denied (ROLE_NO_ACCESS can be anything that does not match an existing role, it just serves as a trick to always deny access).

But if the same request comes from 127.0.0.1 or ::1 (the IPv6 loopback address):

  • Now, the first access control rule is enabled as both the path and the ip match: access is allowed as the user always has the IS_AUTHENTICATED_ANONYMOUSLY role.
  • The second access rule is not examined as the first rule matched.
Forcing a Channel (http, https)

You can also require a user to access a URL via SSL; just use the requires_channel argument in any access_control entries. If this access_control is matched and the request is using the http channel, the user will be redirected to https:

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <access-control>
            <rule path="^/cart/checkout"
                role="IS_AUTHENTICATED_ANONYMOUSLY"
                requires-channel="https" />
        </access-control>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'access_control' => array(
            array(
                'path' => '^/cart/checkout',
                'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
                'requires_channel' => 'https',
            ),
        ),
    ));
    
How to Use multiple User Providers

Each authentication mechanism (e.g. HTTP Authentication, form login, etc) uses exactly one user provider, and will use the first declared user provider by default. But what if you want to specify a few users via configuration and the rest of your users in the database? This is possible by creating a new provider that chains the two together:

  • YAML
    # app/config/security.yml
    security:
        providers:
            chain_provider:
                chain:
                    providers: [in_memory, user_db]
            in_memory:
                memory:
                    users:
                        foo: { password: test }
            user_db:
                entity: { class: Acme\UserBundle\Entity\User, property: username }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="chain_provider">
                <chain>
                    <provider>in_memory</provider>
                    <provider>user_db</provider>
                </chain>
            </provider>
            <provider name="in_memory">
                <memory>
                    <user name="foo" password="test" />
                </memory>
            </provider>
            <provider name="user_db">
                <entity class="Acme\UserBundle\Entity\User" property="username" />
            </provider>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'chain_provider' => array(
                'chain' => array(
                    'providers' => array('in_memory', 'user_db'),
                ),
            ),
            'in_memory' => array(
                'memory' => array(
                   'users' => array(
                       'foo' => array('password' => 'test'),
                   ),
                ),
            ),
            'user_db' => array(
                'entity' => array(
                    'class' => 'Acme\UserBundle\Entity\User',
                    'property' => 'username',
                ),
            ),
        ),
    ));
    

Now, all authentication mechanisms will use the chain_provider, since it’s the first specified. The chain_provider will, in turn, try to load the user from both the in_memory and user_db providers.

You can also configure the firewall or individual authentication mechanisms to use a specific provider. Again, unless a provider is specified explicitly, the first provider is always used:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                pattern: ^/
                provider: user_db
                http_basic:
                    realm: "Secured Demo Area"
                    provider: in_memory
                form_login: ~
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="secured_area" pattern="^/" provider="user_db">
                <!-- ... -->
                <http-basic realm="Secured Demo Area" provider="in_memory" />
                <form-login />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'pattern' => '^/',
                'provider' => 'user_db',
                'http_basic' => array(
                    // ...
                    'provider' => 'in_memory',
                ),
                'form_login' => array(),
            ),
        ),
    ));
    

In this example, if a user tries to log in via HTTP authentication, the authentication system will use the in_memory user provider. But if the user tries to log in via the form login, the user_db provider will be used (since it’s the default for the firewall as a whole).

For more information about user provider and firewall configuration, see the SecurityBundle Configuration (“security”).

How to Use the Serializer

Serializing and deserializing to and from objects and different formats (e.g. JSON or XML) is a very complex topic. Symfony comes with a Serializer Component, which gives you some tools that you can leverage for your solution.

In fact, before you start, get familiar with the serializer, normalizers and encoders by reading the Serializer Component. You should also check out the JMSSerializerBundle, which expands on the functionality offered by Symfony’s core serializer.

Activating the Serializer

2.3 新版功能: The Serializer has always existed in Symfony, but prior to Symfony 2.3, you needed to build the serializer service yourself.

The serializer service is not available by default. To turn it on, activate it in your configuration:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        serializer:
            enabled: true
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <!-- ... -->
        <framework:serializer enabled="true" />
    </framework:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'serializer' => array(
            'enabled' => true
        ),
    ));
    
Adding Normalizers and Encoders

Once enabled, the serializer service will be available in the container and will be loaded with two encoders (JsonEncoder and XmlEncoder) but no normalizers, meaning you’ll need to load your own.

You can load normalizers and/or encoders by tagging them as serializer.normalizer and serializer.encoder. It’s also possible to set the priority of the tag in order to decide the matching order.

Here is an example on how to load the GetSetMethodNormalizer:

  • YAML
    # app/config/config.yml
    services:
       get_set_method_normalizer:
          class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
          tags:
             - { name: serializer.normalizer }
    
  • XML
    <!-- app/config/config.xml -->
    <services>
        <service id="get_set_method_normalizer" class="Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer">
            <tag name="serializer.normalizer" />
        </service>
    </services>
    
  • PHP
    // app/config/config.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $definition = new Definition(
        'Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer'
    ));
    $definition->addTag('serializer.normalizer');
    $container->setDefinition('get_set_method_normalizer', $definition);
    

注解

The GetSetMethodNormalizer is broken by design. As soon as you have a circular object graph, an infinite loop is created when calling the getters. You’re encouraged to add your own normalizers that fit your use-case.

Service Container

How to Create an Event Listener

Symfony has various events and hooks that can be used to trigger custom behavior in your application. Those events are thrown by the HttpKernel component and can be viewed in the KernelEvents class.

To hook into an event and add your own custom logic, you have to create a service that will act as an event listener on that event. In this entry, you will create a service that will act as an Exception Listener, allowing you to modify how exceptions are shown by your application. The KernelEvents::EXCEPTION event is just one of the core kernel events:

// src/AppBundle/EventListener/AcmeExceptionListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

class AcmeExceptionListener
{
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        // You get the exception object from the received event
        $exception = $event->getException();
        $message = sprintf(
            'My Error says: %s with code: %s',
            $exception->getMessage(),
            $exception->getCode()
        );

        // Customize your response object to display the exception details
        $response = new Response();
        $response->setContent($message);

        // HttpExceptionInterface is a special type of exception that
        // holds status code and header details
        if ($exception instanceof HttpExceptionInterface) {
            $response->setStatusCode($exception->getStatusCode());
            $response->headers->replace($exception->getHeaders());
        } else {
            $response->setStatusCode(500);
        }

        // Send the modified response object to the event
        $event->setResponse($response);
    }
}

小技巧

Each event receives a slightly different type of $event object. For the kernel.exception event, it is GetResponseForExceptionEvent. To see what type of object each event listener receives, see KernelEvents.

注解

When setting a response for the kernel.request, kernel.view or kernel.exception events, the propagation is stopped, so the lower priority listeners on that event don’t get called.

Now that the class is created, you just need to register it as a service and notify Symfony that it is a “listener” on the kernel.exception event by using a special “tag”:

  • YAML
    # app/config/services.yml
    services:
        kernel.listener.your_listener_name:
            class: AppBundle\EventListener\AcmeExceptionListener
            tags:
                - { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
    
  • XML
    <!-- app/config/services.xml -->
    <service id="kernel.listener.your_listener_name" class="AppBundle\EventListener\AcmeExceptionListener">
        <tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" />
    </service>
    
  • PHP
    // app/config/services.php
    $container
        ->register('kernel.listener.your_listener_name', 'AppBundle\EventListener\AcmeExceptionListener')
        ->addTag('kernel.event_listener', array('event' => 'kernel.exception', 'method' => 'onKernelException'))
    ;
    

注解

There is an additional tag option priority that is optional and defaults to 0. This value can be from -255 to 255, and the listeners will be executed in the order of their priority (highest to lowest). This is useful when you need to guarantee that one listener is executed before another.

Request Events, Checking Types

A single page can make several requests (one master request, and then multiple sub-requests), which is why when working with the KernelEvents::REQUEST event, you might need to check the type of the request. This can be easily done as follow:

// src/AppBundle/EventListener/AcmeRequestListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernel;

class AcmeRequestListener
{
    public function onKernelRequest(GetResponseEvent $event)
    {
        if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) {
            // don't do anything if it's not the master request
            return;
        }

        // ...
    }
}

小技巧

Two types of request are available in the HttpKernelInterface interface: HttpKernelInterface::MASTER_REQUEST and HttpKernelInterface::SUB_REQUEST.

How to Work with Scopes

This entry is all about scopes, a somewhat advanced topic related to the Service Container. If you’ve ever gotten an error mentioning “scopes” when creating services, or need to create a service that depends on the request service, then this entry is for you.

Understanding Scopes

The scope of a service controls how long an instance of a service is used by the container. The DependencyInjection component provides two generic scopes:

  • container (the default one): The same instance is used each time you request it from this container.
  • prototype: A new instance is created each time you request the service.

The ContainerAwareHttpKernel also defines a third scope: request. This scope is tied to the request, meaning a new instance is created for each subrequest and is unavailable outside the request (for instance in the CLI).

Scopes add a constraint on the dependencies of a service: a service cannot depend on services from a narrower scope. For example, if you create a generic my_foo service, but try to inject the request service, you will receive a ScopeWideningInjectionException when compiling the container. Read the sidebar below for more details.

注解

A service can of course depend on a service from a wider scope without any issue.

Using a Service from a narrower Scope

If your service has a dependency on a scoped service (like the request), you have three ways to deal with it:

  • Use setter injection if the dependency is “synchronized”; this is the recommended way and the best solution for the request instance as it is synchronized with the request scope (see Using a Synchronized Service);
  • Put your service in the same scope as the dependency (or a narrower one). If you depend on the request service, this means putting your new service in the request scope (see Changing the Scope of your Service);
  • Pass the entire container to your service and retrieve your dependency from the container each time you need it to be sure you have the right instance – your service can live in the default container scope (see Passing the Container as a Dependency of your Service).

Each scenario is detailed in the following sections.

Using a Synchronized Service

2.3 新版功能: Synchronized services were introduced in Symfony 2.3.

Injecting the container or setting your service to a narrower scope have drawbacks. For synchronized services (like the request), using setter injection is the best option as it has no drawbacks and everything works without any special code in your service or in your definition:

// src/AppBundle/Mail/Mailer.php
namespace AppBundle\Mail;

use Symfony\Component\HttpFoundation\Request;

class Mailer
{
    protected $request;

    public function setRequest(Request $request = null)
    {
        $this->request = $request;
    }

    public function sendEmail()
    {
        if (null === $this->request) {
            // throw an error?
        }

        // ... do something using the request here
    }
}

Whenever the request scope is entered or left, the service container will automatically call the setRequest() method with the current request instance.

You might have noticed that the setRequest() method accepts null as a valid value for the request argument. That’s because when leaving the request scope, the request instance can be null (for the master request for instance). Of course, you should take care of this possibility in your code. This should also be taken into account when declaring your service:

  • YAML
    # app/config/services.yml
    services:
        greeting_card_manager:
            class: AppBundle\Mail\GreetingCardManager
            calls:
                - [setRequest, ["@?request="]]
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="greeting_card_manager"
            class="AppBundle\Mail\GreetingCardManager"
        >
            <call method="setRequest">
                <argument type="service" id="request" on-invalid="null" strict="false" />
            </call>
        </service>
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    $definition = $container->setDefinition(
        'greeting_card_manager',
        new Definition('AppBundle\Mail\GreetingCardManager')
    )
    ->addMethodCall('setRequest', array(
        new Reference('request', ContainerInterface::NULL_ON_INVALID_REFERENCE, false)
    ));
    

小技巧

You can declare your own synchronized services very easily; here is the declaration of the request service for reference:

  • YAML
    services:
        request:
            scope: request
            synthetic: true
            synchronized: true
    
  • XML
    <services>
        <service id="request" scope="request" synthetic="true" synchronized="true" />
    </services>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    $definition = $container->setDefinition('request')
        ->setScope('request')
        ->setSynthetic(true)
        ->setSynchronized(true);
    

警告

The service using the synchronized service will need to be public in order to have its setter called when the scope changes.

Changing the Scope of your Service

Changing the scope of a service should be done in its definition:

  • YAML
    # app/config/services.yml
    services:
        greeting_card_manager:
            class: AppBundle\Mail\GreetingCardManager
            scope: request
            arguments: ["@request"]
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="greeting_card_manager"
                class="AppBundle\Mail\GreetingCardManager"
                scope="request">
            <argument type="service" id="request" />
        </service>
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $definition = $container->setDefinition(
        'greeting_card_manager',
        new Definition(
            'AppBundle\Mail\GreetingCardManager',
            array(new Reference('request'),
        ))
    )->setScope('request');
    
Passing the Container as a Dependency of your Service

Setting the scope to a narrower one is not always possible (for instance, a twig extension must be in the container scope as the Twig environment needs it as a dependency). In these cases, you can pass the entire container into your service:

// src/AppBundle/Mail/Mailer.php
namespace AppBundle\Mail;

use Symfony\Component\DependencyInjection\ContainerInterface;

class Mailer
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function sendEmail()
    {
        $request = $this->container->get('request');
        // ... do something using the request here
    }
}

警告

Take care not to store the request in a property of the object for a future call of the service as it would cause the same issue described in the first section (except that Symfony cannot detect that you are wrong).

The service config for this class would look something like this:

  • YAML
    # app/config/services.yml
    services:
        my_mailer:
            class:     AppBundle\Mail\Mailer
            arguments: ["@service_container"]
            # scope: container can be omitted as it is the default
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="my_mailer" class="AppBundle\Mail\Mailer">
             <argument type="service" id="service_container" />
        </service>
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('my_mailer', new Definition(
        'AppBundle\Mail\Mailer',
        array(new Reference('service_container'))
    ));
    

注解

Injecting the whole container into a service is generally not a good idea (only inject what you need).

小技巧

If you define a controller as a service then you can get the Request object without injecting the container by having it passed in as an argument of your action method. See 将 Request 作为控制器参数 for details.

How to Work with Compiler Passes in Bundles

Compiler passes give you an opportunity to manipulate other service definitions that have been registered with the service container. You can read about how to create them in the components section “Compiling the Container”. To register a compiler pass from a bundle you need to add it to the build method of the bundle definition class:

// src/Acme/MailerBundle/AcmeMailerBundle.php
namespace Acme\MailerBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;

use Acme\MailerBundle\DependencyInjection\Compiler\CustomCompilerPass;

class AcmeMailerBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new CustomCompilerPass());
    }
}

One of the most common use-cases of compiler passes is to work with tagged services (read more about tags in the components section “Working with Tagged Services”). If you are using custom tags in a bundle then by convention, tag names consist of the name of the bundle (lowercase, underscores as separators), followed by a dot, and finally the “real” name. For example, if you want to introduce some sort of “transport” tag in your AcmeMailerBundle, you should call it acme_mailer.transport.

Sessions

Session Proxy Examples

The session proxy mechanism has a variety of uses and this example demonstrates two common uses. Rather than injecting the session handler as normal, a handler is injected into the proxy and registered with the session storage driver:

use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;

$proxy = new YourProxy(new PdoSessionHandler());
$session = new Session(new NativeSessionStorage(array(), $proxy));

Below, you’ll learn two real examples that can be used for YourProxy: encryption of session data and readonly guest sessions.

Encryption of Session Data

If you wanted to encrypt the session data, you could use the proxy to encrypt and decrypt the session as required:

use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;

class EncryptedSessionProxy extends SessionHandlerProxy
{
    private $key;

    public function __construct(\SessionHandlerInterface $handler, $key)
    {
        $this->key = $key;

        parent::__construct($handler);
    }

    public function read($id)
    {
        $data = parent::read($id);

        return mcrypt_decrypt(\MCRYPT_3DES, $this->key, $data);
    }

    public function write($id, $data)
    {
        $data = mcrypt_encrypt(\MCRYPT_3DES, $this->key, $data);

        return parent::write($id, $data);
    }
}
Readonly Guest Sessions

There are some applications where a session is required for guest users, but where there is no particular need to persist the session. In this case you can intercept the session before it is written:

use Foo\User;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;

class ReadOnlyGuestSessionProxy extends SessionHandlerProxy
{
    private $user;

    public function __construct(\SessionHandlerInterface $handler, User $user)
    {
        $this->user = $user;

        parent::__construct($handler);
    }

    public function write($id, $data)
    {
        if ($this->user->isGuest()) {
            return;
        }

        return parent::write($id, $data);
    }
}
Making the Locale “Sticky” during a User’s Session

Prior to Symfony 2.1, the locale was stored in a session attribute called _locale. Since 2.1, it is stored in the Request, which means that it’s not “sticky” during a user’s request. In this article, you’ll learn how to make the locale of a user “sticky” so that once it’s set, that same locale will be used for every subsequent request.

Creating a LocaleListener

To simulate that the locale is stored in a session, you need to create and register a new event listener. The listener will look something like this. Typically, _locale is used as a routing parameter to signify the locale, though it doesn’t really matter how you determine the desired locale from the request:

// src/AppBundle/EventListener/LocaleListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class LocaleListener implements EventSubscriberInterface
{
    private $defaultLocale;

    public function __construct($defaultLocale = 'en')
    {
        $this->defaultLocale = $defaultLocale;
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        if (!$request->hasPreviousSession()) {
            return;
        }

        // try to see if the locale has been set as a _locale routing parameter
        if ($locale = $request->attributes->get('_locale')) {
            $request->getSession()->set('_locale', $locale);
        } else {
            // if no explicit locale has been set on this request, use one from the session
            $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            // must be registered before the default Locale listener
            KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
        );
    }
}

Then register the listener:

  • YAML
    services:
        app.locale_listener:
            class: AppBundle\EventListener\LocaleListener
            arguments: ["%kernel.default_locale%"]
            tags:
                - { name: kernel.event_subscriber }
    
  • XML
    <service id="app.locale_listener"
        class="AppBundle\EventListener\LocaleListener">
        <argument>%kernel.default_locale%</argument>
    
        <tag name="kernel.event_subscriber" />
    </service>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container
        ->setDefinition('app.locale_listener', new Definition(
            'AppBundle\EventListener\LocaleListener',
            array('%kernel.default_locale%')
        ))
        ->addTag('kernel.event_subscriber')
    ;
    

That’s it! Now celebrate by changing the user’s locale and seeing that it’s sticky throughout the request. Remember, to get the user’s locale, always use the Request::getLocale method:

// from a controller...
use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $locale = $request->getLocale();
}
Configuring the Directory where Session Files are Saved

By default, the Symfony Standard Edition uses the global php.ini values for session.save_handler and session.save_path to determine where to store session data. This is because of the following configuration:

  • YAML
    # app/config/config.yml
    framework:
        session:
            # handler_id set to null will use default session handler from php.ini
            handler_id: ~
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    >
        <framework:config>
            <!-- handler-id set to null will use default session handler from php.ini -->
            <framework:session handler-id="null" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'session' => array(
            // handler_id set to null will use default session handler from php.ini
            'handler_id' => null,
        ),
    ));
    

With this configuration, changing where your session metadata is stored is entirely up to your php.ini configuration.

However, if you have the following configuration, Symfony will store the session data in files in the cache directory %kernel.cache_dir%/sessions. This means that when you clear the cache, any current sessions will also be deleted:

  • YAML
    # app/config/config.yml
    framework:
        session: ~
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    >
        <framework:config>
            <framework:session />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'session' => array(),
    ));
    

Using a different directory to save session data is one method to ensure that your current sessions aren’t lost when you clear Symfony’s cache.

小技巧

Using a different session save handler is an excellent (yet more complex) method of session management available within Symfony. See Configuring Sessions and Save Handlers for a discussion of session save handlers. There is also an entry in the cookbook about storing sessions in the database.

To change the directory in which Symfony saves session data, you only need change the framework configuration. In this example, you will change the session directory to app/sessions:

  • YAML
    # app/config/config.yml
    framework:
        session:
            handler_id: session.handler.native_file
            save_path: "%kernel.root_dir%/sessions"
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    >
        <framework:config>
            <framework:session handler-id="session.handler.native_file"
                save-path="%kernel.root_dir%/sessions"
            />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'session' => array(
            'handler_id' => 'session.handler.native_file',
            'save_path'  => '%kernel.root_dir%/sessions',
        ),
    ));
    
Bridge a legacy Application with Symfony Sessions

2.3 新版功能: The ability to integrate with a legacy PHP session was introduced in Symfony 2.3.

If you’re integrating the Symfony full-stack Framework into a legacy application that starts the session with session_start(), you may still be able to use Symfony’s session management by using the PHP Bridge session.

If the application has sets it’s own PHP save handler, you can specify null for the handler_id:

  • YAML
    framework:
        session:
            storage_id: session.storage.php_bridge
            handler_id: ~
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:framework="http://symfony.com/schema/dic/symfony">
    
        <framework:config>
            <framework:session storage-id="session.storage.php_bridge"
                handler-id="null"
            />
        </framework:config>
    </container>
    
  • PHP
    $container->loadFromExtension('framework', array(
        'session' => array(
            'storage_id' => 'session.storage.php_bridge',
            'handler_id' => null,
    ));
    

Otherwise, if the problem is simply that you cannot avoid the application starting the session with session_start(), you can still make use of a Symfony based session save handler by specifying the save handler as in the example below:

  • YAML
    framework:
        session:
            storage_id: session.storage.php_bridge
            handler_id: session.handler.native_file
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:framework="http://symfony.com/schema/dic/symfony">
    
        <framework:config>
            <framework:session storage-id="session.storage.php_bridge"
                handler-id="session.storage.native_file"
            />
        </framework:config>
    </container>
    
  • PHP
    $container->loadFromExtension('framework', array(
        'session' => array(
            'storage_id' => 'session.storage.php_bridge',
            'handler_id' => 'session.storage.native_file',
    ));
    

注解

If the legacy application requires its own session save-handler, do not override this. Instead set handler_id: ~. Note that a save handler cannot be changed once the session has been started. If the application starts the session before Symfony is initialized, the save-handler will have already been set. In this case, you will need handler_id: ~. Only override the save-handler if you are sure the legacy application can use the Symfony save-handler without side effects and that the session has not been started before Symfony is initialized.

For more details, see Integrating with Legacy Sessions.

Avoid Starting Sessions for Anonymous Users

Sessions are automatically started whenever you read, write or even check for the existence of data in the session. This means that if you need to avoid creating a session cookie for some users, it can be difficult: you must completely avoid accessing the session.

For example, one common problem in this situation involves checking for flash messages, which are stored in the session. The following code would guarantee that a session is always started:

{% for flashMessage in app.session.flashbag.get('notice') %}
    <div class="flash-notice">
        {{ flashMessage }}
    </div>
{% endfor %}

Even if the user is not logged in and even if you haven’t created any flash messages, just calling the get() (or even has()) method of the flashbag will start a session. This may hurt your application performance because all users will receive a session cookie. To avoid this behavior, add a check before trying to access the flash messages:

{% if app.request.hasPreviousSession %}
    {% for flashMessage in app.session.flashbag.get('notice') %}
        <div class="flash-notice">
            {{ flashMessage }}
        </div>
    {% endfor %}
{% endif %}

How Symfony2 Differs from Symfony1

The Symfony2 framework embodies a significant evolution when compared with the first version of the framework. Fortunately, with the MVC architecture at its core, the skills used to master a symfony1 project continue to be very relevant when developing in Symfony2. Sure, app.yml is gone, but routing, controllers and templates all remain.

This chapter walks through the differences between symfony1 and Symfony2. As you’ll see, many tasks are tackled in a slightly different way. You’ll come to appreciate these minor differences as they promote stable, predictable, testable and decoupled code in your Symfony2 applications.

So, sit back and relax as you travel from “then” to “now”.

Directory Structure

When looking at a Symfony2 project - for example, the Symfony2 Standard Edition - you’ll notice a very different directory structure than in symfony1. The differences, however, are somewhat superficial.

The app/ Directory

In symfony1, your project has one or more applications, and each lives inside the apps/ directory (e.g. apps/frontend). By default in Symfony2, you have just one application represented by the app/ directory. Like in symfony1, the app/ directory contains configuration specific to that application. It also contains application-specific cache, log and template directories as well as a Kernel class (AppKernel), which is the base object that represents the application.

Unlike symfony1, almost no PHP code lives in the app/ directory. This directory is not meant to house modules or library files as it did in symfony1. Instead, it’s simply the home of configuration and other resources (templates, translation files).

The src/ Directory

Put simply, your actual code goes here. In Symfony2, all actual application-code lives inside a bundle (roughly equivalent to a symfony1 plugin) and, by default, each bundle lives inside the src directory. In that way, the src directory is a bit like the plugins directory in symfony1, but much more flexible. Additionally, while your bundles will live in the src/ directory, third-party bundles will live somewhere in the vendor/ directory.

To get a better picture of the src/ directory, first think of the structure of a symfony1 application. First, part of your code likely lives inside one or more applications. Most commonly these include modules, but could also include any other PHP classes you put in your application. You may have also created a schema.yml file in the config directory of your project and built several model files. Finally, to help with some common functionality, you’re using several third-party plugins that live in the plugins/ directory. In other words, the code that drives your application lives in many different places.

In Symfony2, life is much simpler because all Symfony2 code must live in a bundle. In the pretend symfony1 project, all the code could be moved into one or more plugins (which is a very good practice, in fact). Assuming that all modules, PHP classes, schema, routing configuration, etc. were moved into a plugin, the symfony1 plugins/ directory would be very similar to the Symfony2 src/ directory.

Put simply again, the src/ directory is where your code, assets, templates and most anything else specific to your project will live.

The vendor/ Directory

The vendor/ directory is basically equivalent to the lib/vendor/ directory in symfony1, which was the conventional directory for all vendor libraries and bundles. By default, you’ll find the Symfony2 library files in this directory, along with several other dependent libraries such as Doctrine2, Twig and Swift Mailer. 3rd party Symfony2 bundles live somewhere in the vendor/.

The web/ Directory

Not much has changed in the web/ directory. The most noticeable difference is the absence of the css/, js/ and images/ directories. This is intentional. Like with your PHP code, all assets should also live inside a bundle. With the help of a console command, the Resources/public/ directory of each bundle is copied or symbolically-linked to the web/bundles/ directory. This allows you to keep assets organized inside your bundle, but still make them available to the public. To make sure that all bundles are available, run the following command:

$ php app/console assets:install web

注解

This command is the Symfony2 equivalent to the symfony1 plugin:publish-assets command.

Autoloading

One of the advantages of modern frameworks is never needing to worry about requiring files. By making use of an autoloader, you can refer to any class in your project and trust that it’s available. Autoloading has changed in Symfony2 to be more universal, faster, and independent of needing to clear your cache.

In symfony1, autoloading was done by searching the entire project for the presence of PHP class files and caching this information in a giant array. That array told symfony1 exactly which file contained each class. In the production environment, this caused you to need to clear the cache when classes were added or moved.

In Symfony2, a tool named Composer handles this process. The idea behind the autoloader is simple: the name of your class (including the namespace) must match up with the path to the file containing that class. Take the FrameworkExtraBundle from the Symfony2 Standard Edition as an example:

namespace Sensio\Bundle\FrameworkExtraBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
// ...

class SensioFrameworkExtraBundle extends Bundle
{
    // ...
}

The file itself lives at vendor/sensio/framework-extra-bundle/Sensio/Bundle/FrameworkExtraBundle/SensioFrameworkExtraBundle.php. As you can see, the second part of the path follows the namespace of the class. The first part is equal to the package name of the SensioFrameworkExtraBundle.

The namespace, Sensio\Bundle\FrameworkExtraBundle, and package name, sensio/framework-extra-bundle, spells out the directory that the file should live in (vendor/sensio/framework-extra-bundle/Sensio/Bundle/FrameworkExtraBundle/). Composer can then look for the file at this specific place and load it very fast.

If the file did not live at this exact location, you’d receive a Class "Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle" does not exist. error. In Symfony2, a “class does not exist” error means that the namespace of the class and physical location do not match. Basically, Symfony2 is looking in one exact location for that class, but that location doesn’t exist (or contains a different class). In order for a class to be autoloaded, you never need to clear your cache in Symfony2.

As mentioned before, for the autoloader to work, it needs to know that the Sensio namespace lives in the vendor/sensio/framework-extra-bundle directory and that, for example, the Doctrine namespace lives in the vendor/doctrine/orm/lib/ directory. This mapping is entirely controlled by Composer. Each third-party library you load through Composer has its settings defined and Composer takes care of everything for you.

For this to work, all third-party libraries used by your project must be defined in the composer.json file.

If you look at the HelloController from the Symfony2 Standard Edition you can see that it lives in the Acme\DemoBundle\Controller namespace. Yet, the AcmeDemoBundle is not defined in your composer.json file. Nonetheless are the files autoloaded. This is because you can tell Composer to autoload files from specific directories without defining a dependency:

"autoload": {
    "psr-0": { "": "src/" }
}

This means that if a class is not found in the vendor directory, Composer will search in the src directory before throwing a “class does not exist” exception. Read more about configuring the Composer autoloader in the Composer documentation.

Using the Console

In symfony1, the console is in the root directory of your project and is called symfony:

$ php symfony

In Symfony2, the console is now in the app sub-directory and is called console:

$ php app/console
Applications

In a symfony1 project, it is common to have several applications: one for the frontend and one for the backend for instance.

In a Symfony2 project, you only need to create one application (a blog application, an intranet application, ...). Most of the time, if you want to create a second application, you might instead create another project and share some bundles between them.

And if you need to separate the frontend and the backend features of some bundles, you can create sub-namespaces for controllers, sub-directories for templates, different semantic configurations, separate routing configurations, and so on.

Of course, there’s nothing wrong with having multiple applications in your project, that’s entirely up to you. A second application would mean a new directory, e.g. my_app/, with the same basic setup as the app/ directory.

小技巧

Read the definition of a Project, an Application, and a Bundle in the glossary.

Bundles and Plugins

In a symfony1 project, a plugin could contain configuration, modules, PHP libraries, assets and anything else related to your project. In Symfony2, the idea of a plugin is replaced by the “bundle”. A bundle is even more powerful than a plugin because the core Symfony2 framework is brought in via a series of bundles. In Symfony2, bundles are first-class citizens that are so flexible that even core code itself is a bundle.

In symfony1, a plugin must be enabled inside the ProjectConfiguration class:

// config/ProjectConfiguration.class.php
public function setup()
{
    // some plugins here
    $this->enableAllPluginsExcept(array(...));
}

In Symfony2, the bundles are activated inside the application kernel:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Symfony\Bundle\TwigBundle\TwigBundle(),
        ...,
        new Acme\DemoBundle\AcmeDemoBundle(),
    );

    return $bundles;
}
Routing (routing.yml) and Configuration (config.yml)

In symfony1, the routing.yml and app.yml configuration files were automatically loaded inside any plugin. In Symfony2, routing and application configuration inside a bundle must be included manually. For example, to include a routing resource from a bundle called AcmeDemoBundle, you can do the following:

  • YAML
    # app/config/routing.yml
    _hello:
        resource: "@AcmeDemoBundle/Resources/config/routing.yml"
    
  • XML
    <!-- app/config/routing.yml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <import resource="@AcmeDemoBundle/Resources/config/routing.xml" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"));
    
    return $collection;
    

This will load the routes found in the Resources/config/routing.yml file of the AcmeDemoBundle. The special @AcmeDemoBundle is a shortcut syntax that, internally, resolves to the full path to that bundle.

You can use this same strategy to bring in configuration from a bundle:

  • YAML
    # app/config/config.yml
    imports:
        - { resource: "@AcmeDemoBundle/Resources/config/config.yml" }
    
  • XML
    <!-- app/config/config.xml -->
    <imports>
        <import resource="@AcmeDemoBundle/Resources/config/config.xml" />
    </imports>
    
  • PHP
    // app/config/config.php
    $this->import('@AcmeDemoBundle/Resources/config/config.php')
    

In Symfony2, configuration is a bit like app.yml in symfony1, except much more systematic. With app.yml, you could simply create any keys you wanted. By default, these entries were meaningless and depended entirely on how you used them in your application:

# some app.yml file from symfony1
all:
  email:
    from_address:  foo.bar@example.com

In Symfony2, you can also create arbitrary entries under the parameters key of your configuration:

  • YAML
    parameters:
        email.from_address: foo.bar@example.com
    
  • XML
    <parameters>
        <parameter key="email.from_address">foo.bar@example.com</parameter>
    </parameters>
    
  • PHP
    $container->setParameter('email.from_address', 'foo.bar@example.com');
    

You can now access this from a controller, for example:

public function helloAction($name)
{
    $fromAddress = $this->container->getParameter('email.from_address');
}

In reality, the Symfony2 configuration is much more powerful and is used primarily to configure objects that you can use. For more information, see the chapter titled “Service Container”.

Templating

How to Inject Variables into all Templates (i.e. global Variables)

Sometimes you want a variable to be accessible to all the templates you use. This is possible inside your app/config/config.yml file:

  • YAML
    # app/config/config.yml
    twig:
        # ...
        globals:
            ga_tracking: UA-xxxxx-x
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config>
        <!-- ... -->
        <twig:global key="ga_tracking">UA-xxxxx-x</twig:global>
    </twig:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
         // ...
         'globals' => array(
             'ga_tracking' => 'UA-xxxxx-x',
         ),
    ));
    

Now, the variable ga_tracking is available in all Twig templates:

<p>The google tracking code is: {{ ga_tracking }}</p>

It’s that easy!

Using Service Container Parameters

You can also take advantage of the built-in Service Parameters system, which lets you isolate or reuse the value:

# app/config/parameters.yml
parameters:
    ga_tracking: UA-xxxxx-x
  • YAML
    # app/config/config.yml
    twig:
        globals:
            ga_tracking: "%ga_tracking%"
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config>
        <twig:global key="ga_tracking">%ga_tracking%</twig:global>
    </twig:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
         'globals' => array(
             'ga_tracking' => '%ga_tracking%',
         ),
    ));
    

The same variable is available exactly as before.

Referencing Services

Instead of using static values, you can also set the value to a service. Whenever the global variable is accessed in the template, the service will be requested from the service container and you get access to that object.

注解

The service is not loaded lazily. In other words, as soon as Twig is loaded, your service is instantiated, even if you never use that global variable.

To define a service as a global Twig variable, prefix the string with @. This should feel familiar, as it’s the same syntax you use in service configuration.

  • YAML
    # app/config/config.yml
    twig:
        # ...
        globals:
            user_management: "@acme_user.user_management"
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config>
        <!-- ... -->
        <twig:global key="user_management">@acme_user.user_management</twig:global>
    </twig:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
         // ...
         'globals' => array(
             'user_management' => '@acme_user.user_management',
         ),
    ));
    
Using a Twig Extension

If the global variable you want to set is more complicated - say an object - then you won’t be able to use the above method. Instead, you’ll need to create a Twig Extension and return the global variable as one of the entries in the getGlobals method.

How to Use and Register Namespaced Twig Paths

2.2 新版功能: Namespaced path support was introduced in 2.2.

Usually, when you refer to a template, you’ll use the MyBundle:Subdir:filename.html.twig format (see Template Naming and Locations).

Twig also natively offers a feature called “namespaced paths”, and support is built-in automatically for all of your bundles.

Take the following paths as an example:

{% extends "AppBundle::layout.html.twig" %}
{% include "AppBundle:Foo:bar.html.twig" %}

With namespaced paths, the following works as well:

{% extends "@App/layout.html.twig" %}
{% include "@App/Foo/bar.html.twig" %}

Both paths are valid and functional by default in Symfony.

小技巧

As an added bonus, the namespaced syntax is faster.

Registering your own Namespaces

You can also register your own custom namespaces. Suppose that you’re using some third-party library that includes Twig templates that live in vendor/acme/foo-bar/templates. First, register a namespace for this directory:

  • YAML
    # app/config/config.yml
    twig:
        # ...
        paths:
            "%kernel.root_dir%/../vendor/acme/foo-bar/templates": foo_bar
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:twig="http://symfony.com/schema/dic/twig"
    >
    
        <twig:config debug="%kernel.debug%" strict-variables="%kernel.debug%">
            <twig:path namespace="foo_bar">%kernel.root_dir%/../vendor/acme/foo-bar/templates</twig:path>
        </twig:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'paths' => array(
            '%kernel.root_dir%/../vendor/acme/foo-bar/templates' => 'foo_bar',
        );
    ));
    

The registered namespace is called foo_bar, which refers to the vendor/acme/foo-bar/templates directory. Assuming there’s a file called sidebar.twig in that directory, you can use it easily:

{% include '@foo_bar/sidebar.twig' %}
Multiple Paths per Namespace

You can also assign several paths to the same template namespace. The order in which paths are configured is very important, because Twig will always load the first template that exists, starting from the first configured path. This feature can be used as a fallback mechanism to load generic templates when the specific template doesn’t exist.

  • YAML
    # app/config/config.yml
    twig:
        # ...
        paths:
            "%kernel.root_dir%/../vendor/acme/themes/theme1": theme
            "%kernel.root_dir%/../vendor/acme/themes/theme2": theme
            "%kernel.root_dir%/../vendor/acme/themes/common": theme
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
               xmlns:twig="http://symfony.com/schema/dic/twig"
    >
    
        <twig:config debug="%kernel.debug%" strict-variables="%kernel.debug%">
            <twig:path namespace="theme">%kernel.root_dir%/../vendor/acme/themes/theme1</twig:path>
            <twig:path namespace="theme">%kernel.root_dir%/../vendor/acme/themes/theme2</twig:path>
            <twig:path namespace="theme">%kernel.root_dir%/../vendor/acme/themes/common</twig:path>
        </twig:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'paths' => array(
            '%kernel.root_dir%/../vendor/acme/themes/theme1' => 'theme',
            '%kernel.root_dir%/../vendor/acme/themes/theme2' => 'theme',
            '%kernel.root_dir%/../vendor/acme/themes/common' => 'theme',
        ),
    ));
    

Now, you can use the same @theme namespace to refer to any template located in the previous three directories:

{% include '@theme/header.twig' %}
How to Use PHP instead of Twig for Templates

Symfony defaults to Twig for its template engine, but you can still use plain PHP code if you want. Both templating engines are supported equally in Symfony. Symfony adds some nice features on top of PHP to make writing templates with PHP more powerful.

Rendering PHP Templates

If you want to use the PHP templating engine, first, make sure to enable it in your application configuration file:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        templating:
            engines: ['twig', 'php']
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <!-- ... -->
        <framework:templating>
            <framework:engine id="twig" />
            <framework:engine id="php" />
        </framework:templating>
    </framework:config>
    
  • PHP
    $container->loadFromExtension('framework', array(
        // ...
        'templating' => array(
            'engines' => array('twig', 'php'),
        ),
    ));
    

You can now render a PHP template instead of a Twig one simply by using the .php extension in the template name instead of .twig. The controller below renders the index.html.php template:

// src/AppBundle/Controller/HelloController.php

// ...
public function indexAction($name)
{
    return $this->render(
        'AppBundle:Hello:index.html.php',
        array('name' => $name)
    );
}

You can also use the @Template shortcut to render the default AppBundle:Hello:index.html.php template:

// src/AppBundle/Controller/HelloController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

// ...

/**
 * @Template(engine="php")
 */
public function indexAction($name)
{
    return array('name' => $name);
}

警告

Enabling the php and twig template engines simultaneously is allowed, but it will produce an undesirable side effect in your application: the @ notation for Twig namespaces will no longer be supported for the render() method:

public function indexAction()
{
    // ...

    // namespaced templates will no longer work in controllers
    $this->render('@App/Default/index.html.twig');

    // you must use the traditional template notation
    $this->render('AppBundle:Default:index.html.twig');
}
{# inside a Twig template, namespaced templates work as expected #}
{{ include('@App/Default/index.html.twig') }}

{# traditional template notation will also work #}
{{ include('AppBundle:Default:index.html.twig') }}
Decorating Templates

More often than not, templates in a project share common elements, like the well-known header and footer. In Symfony, this problem is thought about differently: a template can be decorated by another one.

The index.html.php template is decorated by layout.html.php, thanks to the extend() call:

<!-- src/AppBundle/Resources/views/Hello/index.html.php -->
<?php $view->extend('AppBundle::layout.html.php') ?>

Hello <?php echo $name ?>!

The AppBundle::layout.html.php notation sounds familiar, doesn’t it? It is the same notation used to reference a template. The :: part simply means that the controller element is empty, so the corresponding file is directly stored under views/.

Now, have a look at the layout.html.php file:

<!-- src/AppBundle/Resources/views/layout.html.php -->
<?php $view->extend('::base.html.php') ?>

<h1>Hello Application</h1>

<?php $view['slots']->output('_content') ?>

The layout is itself decorated by another one (::base.html.php). Symfony supports multiple decoration levels: a layout can itself be decorated by another one. When the bundle part of the template name is empty, views are looked for in the app/Resources/views/ directory. This directory stores global views for your entire project:

<!-- app/Resources/views/base.html.php -->
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title><?php $view['slots']->output('title', 'Hello Application') ?></title>
    </head>
    <body>
        <?php $view['slots']->output('_content') ?>
    </body>
</html>

For both layouts, the $view['slots']->output('_content') expression is replaced by the content of the child template, index.html.php and layout.html.php respectively (more on slots in the next section).

As you can see, Symfony provides methods on a mysterious $view object. In a template, the $view variable is always available and refers to a special object that provides a bunch of methods that makes the template engine tick.

Working with Slots

A slot is a snippet of code, defined in a template, and reusable in any layout decorating the template. In the index.html.php template, define a title slot:

<!-- src/AppBundle/Resources/views/Hello/index.html.php -->
<?php $view->extend('AppBundle::layout.html.php') ?>

<?php $view['slots']->set('title', 'Hello World Application') ?>

Hello <?php echo $name ?>!

The base layout already has the code to output the title in the header:

<!-- app/Resources/views/base.html.php -->
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title><?php $view['slots']->output('title', 'Hello Application') ?></title>
</head>

The output() method inserts the content of a slot and optionally takes a default value if the slot is not defined. And _content is just a special slot that contains the rendered child template.

For large slots, there is also an extended syntax:

<?php $view['slots']->start('title') ?>
    Some large amount of HTML
<?php $view['slots']->stop() ?>
Including other Templates

The best way to share a snippet of template code is to define a template that can then be included into other templates.

Create a hello.html.php template:

<!-- src/AppBundle/Resources/views/Hello/hello.html.php -->
Hello <?php echo $name ?>!

And change the index.html.php template to include it:

<!-- src/AppBundle/Resources/views/Hello/index.html.php -->
<?php $view->extend('AppBundle::layout.html.php') ?>

<?php echo $view->render('AppBundle:Hello:hello.html.php', array('name' => $name)) ?>

The render() method evaluates and returns the content of another template (this is the exact same method as the one used in the controller).

Embedding other Controllers

And what if you want to embed the result of another controller in a template? That’s very useful when working with Ajax, or when the embedded template needs some variable not available in the main template.

If you create a fancy action, and want to include it into the index.html.php template, simply use the following code:

<!-- src/AppBundle/Resources/views/Hello/index.html.php -->
<?php echo $view['actions']->render(
    new \Symfony\Component\HttpKernel\Controller\ControllerReference('AppBundle:Hello:fancy', array(
        'name'  => $name,
        'color' => 'green',
    ))
) ?>

Here, the AppBundle:Hello:fancy string refers to the fancy action of the Hello controller:

// src/AppBundle/Controller/HelloController.php

class HelloController extends Controller
{
    public function fancyAction($name, $color)
    {
        // create some object, based on the $color variable
        $object = ...;

        return $this->render('AppBundle:Hello:fancy.html.php', array(
            'name'   => $name,
            'object' => $object
        ));
    }

    // ...
}

But where is the $view['actions'] array element defined? Like $view['slots'], it’s called a template helper, and the next section tells you more about those.

Using Template Helpers

The Symfony templating system can be easily extended via helpers. Helpers are PHP objects that provide features useful in a template context. actions and slots are two of the built-in Symfony helpers.

Using Assets: Images, JavaScripts and Stylesheets

What would the Internet be without images, JavaScripts, and stylesheets? Symfony provides the assets tag to deal with them easily:

<link href="<?php echo $view['assets']->getUrl('css/blog.css') ?>" rel="stylesheet" type="text/css" />

<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" />

The assets helper’s main purpose is to make your application more portable. Thanks to this helper, you can move the application root directory anywhere under your web root directory without changing anything in your template’s code.

Output Escaping

When using PHP templates, escape variables whenever they are displayed to the user:

<?php echo $view->escape($var) ?>

By default, the escape() method assumes that the variable is outputted within an HTML context. The second argument lets you change the context. For instance, to output something in a JavaScript script, use the js context:

<?php echo $view->escape($var, 'js') ?>
How to Write a custom Twig Extension

The main motivation for writing an extension is to move often used code into a reusable class like adding support for internationalization. An extension can define tags, filters, tests, operators, global variables, functions, and node visitors.

Creating an extension also makes for a better separation of code that is executed at compilation time and code needed at runtime. As such, it makes your code faster.

小技巧

Before writing your own extensions, have a look at the Twig official extension repository.

Create the Extension Class

注解

This cookbook describes how to write a custom Twig extension as of Twig 1.12. If you are using an older version, please read Twig extensions documentation legacy.

To get your custom functionality you must first create a Twig Extension class. As an example you’ll create a price filter to format a given number into price:

// src/AppBundle/Twig/AppExtension.php
namespace AppBundle\Twig;

class AppExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter('price', array($this, 'priceFilter')),
        );
    }

    public function priceFilter($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',')
    {
        $price = number_format($number, $decimals, $decPoint, $thousandsSep);
        $price = '$'.$price;

        return $price;
    }

    public function getName()
    {
        return 'app_extension';
    }
}

小技巧

Along with custom filters, you can also add custom functions and register global variables.

Register an Extension as a Service

Now you must let the Service Container know about your newly created Twig Extension:

  • YAML
    # app/config/services.yml
    services:
        app.twig_extension:
            class: AppBundle\Twig\AppExtension
            public: false
            tags:
                - { name: twig.extension }
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="app.twig_extension"
            class="AppBundle\Twig\AppExtension"
            public="false">
            <tag name="twig.extension" />
        </service>
    </services>
    
  • PHP
    // app/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container
        ->register('app.twig_extension', '\AppBundle\Twig\AppExtension')
        ->setPublic(false)
        ->addTag('twig.extension');
    

注解

Keep in mind that Twig Extensions are not lazily loaded. This means that there’s a higher chance that you’ll get a ServiceCircularReferenceException or a ScopeWideningInjectionException if any services (or your Twig Extension in this case) are dependent on the request service. For more information take a look at How to Work with Scopes.

Using the custom Extension

Using your newly created Twig Extension is no different than any other:

{# outputs $5,500.00 #}
{{ '5500'|price }}

Passing other arguments to your filter:

{# outputs $5500,2516 #}
{{ '5500.25155'|price(4, ',', '') }}
Learning further

For a more in-depth look into Twig Extensions, please take a look at the Twig extensions documentation.

How to Render a Template without a custom Controller

Usually, when you need to create a page, you need to create a controller and render a template from within that controller. But if you’re rendering a simple template that doesn’t need any data passed into it, you can avoid creating the controller entirely, by using the built-in FrameworkBundle:Template:template controller.

For example, suppose you want to render a AppBundle:Static:privacy.html.twig template, which doesn’t require that any variables are passed to it. You can do this without creating a controller:

  • YAML
    acme_privacy:
        path: /privacy
        defaults:
            _controller: FrameworkBundle:Template:template
            template:    'AppBundle:Static:privacy.html.twig'
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="acme_privacy" path="/privacy">
            <default key="_controller">FrameworkBundle:Template:template</default>
            <default key="template">AppBundle:Static:privacy.html.twig</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('acme_privacy', new Route('/privacy', array(
        '_controller'  => 'FrameworkBundle:Template:template',
        'template'     => 'AppBundle:Static:privacy.html.twig',
    )));
    
    return $collection;
    

The FrameworkBundle:Template:template controller will simply render whatever template you’ve passed as the template default value.

You can of course also use this trick when rendering embedded controllers from within a template. But since the purpose of rendering a controller from within a template is typically to prepare some data in a custom controller, this is probably only useful if you’d like to cache this page partial (see Caching the static Template).

  • Twig
    {{ render(url('acme_privacy')) }}
    
  • PHP
    <?php echo $view['actions']->render(
        $view['router']->generate('acme_privacy', array(), true)
    ) ?>
    
Caching the static Template

2.2 新版功能: The ability to cache templates rendered via FrameworkBundle:Template:template was introduced in Symfony 2.2.

Since templates that are rendered in this way are typically static, it might make sense to cache them. Fortunately, this is easy! By configuring a few other variables in your route, you can control exactly how your page is cached:

  • YAML
    acme_privacy:
        path: /privacy
        defaults:
            _controller:  FrameworkBundle:Template:template
            template:     'AppBundle:Static:privacy.html.twig'
            maxAge:       86400
            sharedAge:    86400
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="acme_privacy" path="/privacy">
            <default key="_controller">FrameworkBundle:Template:template</default>
            <default key="template">AppBundle:Static:privacy.html.twig</default>
            <default key="maxAge">86400</default>
            <default key="sharedAge">86400</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('acme_privacy', new Route('/privacy', array(
        '_controller'  => 'FrameworkBundle:Template:template',
        'template'     => 'AppBundle:Static:privacy.html.twig',
        'maxAge'       => 86400,
        'sharedAge' => 86400,
    )));
    
    return $collection;
    

The maxAge and sharedAge values are used to modify the Response object created in the controller. For more information on caching, see HTTP Cache.

There is also a private variable (not shown here). By default, the Response will be made public, as long as maxAge or sharedAge are passed. If set to true, the Response will be marked as private.

Testing

How to Simulate HTTP Authentication in a Functional Test

If your application needs HTTP authentication, pass the username and password as server variables to createClient():

$client = static::createClient(array(), array(
    'PHP_AUTH_USER' => 'username',
    'PHP_AUTH_PW'   => 'pa$$word',
));

You can also override it on a per request basis:

$client->request('DELETE', '/post/12', array(), array(), array(
    'PHP_AUTH_USER' => 'username',
    'PHP_AUTH_PW'   => 'pa$$word',
));

When your application is using a form_login, you can simplify your tests by allowing your test configuration to make use of HTTP authentication. This way you can use the above to authenticate in tests, but still have your users log in via the normal form_login. The trick is to include the http_basic key in your firewall, along with the form_login key:

  • YAML
    # app/config/config_test.yml
    security:
        firewalls:
            your_firewall_name:
                http_basic: ~
    
  • XML
    <!-- app/config/config_test.xml -->
    <security:config>
        <security:firewall name="your_firewall_name">
          <security:http-basic />
       </security:firewall>
    </security:config>
    
  • PHP
    // app/config/config_test.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'your_firewall_name' => array(
                'http_basic' => array(),
            ),
        ),
    ));
    
How to Simulate Authentication with a Token in a Functional Test

Authenticating requests in functional tests might slow down the suite. It could become an issue especially when form_login is used, since it requires additional requests to fill in and submit the form.

One of the solutions is to configure your firewall to use http_basic in the test environment as explained in How to Simulate HTTP Authentication in a Functional Test. Another way would be to create a token yourself and store it in a session. While doing this, you have to make sure that an appropriate cookie is sent with a request. The following example demonstrates this technique:

// src/AppBundle/Tests/Controller/DefaultControllerTest.php
namespace Appbundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class DefaultControllerTest extends WebTestCase
{
    private $client = null;

    public function setUp()
    {
        $this->client = static::createClient();
    }

    public function testSecuredHello()
    {
        $this->logIn();

        $crawler = $this->client->request('GET', '/admin');

        $this->assertTrue($this->client->getResponse()->isSuccessful());
        $this->assertGreaterThan(0, $crawler->filter('html:contains("Admin Dashboard")')->count());
    }

    private function logIn()
    {
        $session = $this->client->getContainer()->get('session');

        $firewall = 'secured_area';
        $token = new UsernamePasswordToken('admin', null, $firewall, array('ROLE_ADMIN'));
        $session->set('_security_'.$firewall, serialize($token));
        $session->save();

        $cookie = new Cookie($session->getName(), $session->getId());
        $this->client->getCookieJar()->set($cookie);
    }
}

注解

The technique described in How to Simulate HTTP Authentication in a Functional Test is cleaner and therefore the preferred way.

How to Test the Interaction of several Clients

If you need to simulate an interaction between different clients (think of a chat for instance), create several clients:

$harry = static::createClient();
$sally = static::createClient();

$harry->request('POST', '/say/sally/Hello');
$sally->request('GET', '/messages');

$this->assertEquals(201, $harry->getResponse()->getStatusCode());
$this->assertRegExp('/Hello/', $sally->getResponse()->getContent());

This works except when your code maintains a global state or if it depends on a third-party library that has some kind of global state. In such a case, you can insulate your clients:

$harry = static::createClient();
$sally = static::createClient();

$harry->insulate();
$sally->insulate();

$harry->request('POST', '/say/sally/Hello');
$sally->request('GET', '/messages');

$this->assertEquals(201, $harry->getResponse()->getStatusCode());
$this->assertRegExp('/Hello/', $sally->getResponse()->getContent());

Insulated clients transparently execute their requests in a dedicated and clean PHP process, thus avoiding any side-effects.

小技巧

As an insulated client is slower, you can keep one client in the main process, and insulate the other ones.

How to Use the Profiler in a Functional Test

It’s highly recommended that a functional test only tests the Response. But if you write functional tests that monitor your production servers, you might want to write tests on the profiling data as it gives you a great way to check various things and enforce some metrics.

The Symfony Profiler gathers a lot of data for each request. Use this data to check the number of database calls, the time spent in the framework, etc. But before writing assertions, enable the profiler and check that the profiler is indeed available (it is enabled by default in the test environment):

class HelloControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        // Enable the profiler for the next request (it does nothing if the profiler is not available)
        $client->enableProfiler();

        $crawler = $client->request('GET', '/hello/Fabien');

        // ... write some assertions about the Response

        // Check that the profiler is enabled
        if ($profile = $client->getProfile()) {
            // check the number of requests
            $this->assertLessThan(
                10,
                $profile->getCollector('db')->getQueryCount()
            );

            // check the time spent in the framework
            $this->assertLessThan(
                500,
                $profile->getCollector('time')->getDuration()
            );
        }
    }
}

If a test fails because of profiling data (too many DB queries for instance), you might want to use the Web Profiler to analyze the request after the tests finish. It’s easy to achieve if you embed the token in the error message:

$this->assertLessThan(
    30,
    $profile->getCollector('db')->getQueryCount(),
    sprintf(
        'Checks that query count is less than 30 (token %s)',
        $profile->getToken()
    )
);

警告

The profiler store can be different depending on the environment (especially if you use the SQLite store, which is the default configured one).

注解

The profiler information is available even if you insulate the client or if you use an HTTP layer for your tests.

小技巧

Read the API for built-in data collectors to learn more about their interfaces.

Speeding up Tests by not Collecting Profiler Data

To avoid collecting data in each test you can set the collect parameter to false:

  • YAML
    # app/config/config_test.yml
    
    # ...
    framework:
        profiler:
            enabled: true
            collect: false
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                    http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <!-- ... -->
    
        <framework:config>
            <framework:profiler enabled="true" collect="false" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    
    // ...
    $container->loadFromExtension('framework', array(
        'profiler' => array(
            'enabled' => true,
            'collect' => false,
        ),
    ));
    

In this way only tests that call $client->enableProfiler() will collect data.

How to Test Code that Interacts with the Database

If your code interacts with the database, e.g. reads data from or stores data into it, you need to adjust your tests to take this into account. There are many ways how to deal with this. In a unit test, you can create a mock for a Repository and use it to return expected objects. In a functional test, you may need to prepare a test database with predefined values to ensure that your test always has the same data to work with.

注解

If you want to test your queries directly, see How to Test Doctrine Repositories.

Mocking the Repository in a Unit Test

If you want to test code which depends on a Doctrine repository in isolation, you need to mock the Repository. Normally you inject the EntityManager into your class and use it to get the repository. This makes things a little more difficult as you need to mock both the EntityManager and your repository class.

小技巧

It is possible (and a good idea) to inject your repository directly by registering your repository as a factory service. This is a little bit more work to setup, but makes testing easier as you only need to mock the repository.

Suppose the class you want to test looks like this:

namespace AppBundle\Salary;

use Doctrine\Common\Persistence\ObjectManager;

class SalaryCalculator
{
    private $entityManager;

    public function __construct(ObjectManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function calculateTotalSalary($id)
    {
        $employeeRepository = $this->entityManager->getRepository('AppBundle::Employee');
        $employee = $employeeRepository->find($id);

        return $employee->getSalary() + $employee->getBonus();
    }
}

Since the ObjectManager gets injected into the class through the constructor, it’s easy to pass a mock object within a test:

use AppBundle\Salary\SalaryCalculator;

class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testCalculateTotalSalary()
    {
        // First, mock the object to be used in the test
        $employee = $this->getMock('\AppBundle\Entity\Employee');
        $employee->expects($this->once())
            ->method('getSalary')
            ->will($this->returnValue(1000));
        $employee->expects($this->once())
            ->method('getBonus')
            ->will($this->returnValue(1100));

        // Now, mock the repository so it returns the mock of the employee
        $employeeRepository = $this->getMockBuilder('\Doctrine\ORM\EntityRepository')
            ->disableOriginalConstructor()
            ->getMock();
        $employeeRepository->expects($this->once())
            ->method('find')
            ->will($this->returnValue($employee));

        // Last, mock the EntityManager to return the mock of the repository
        $entityManager = $this->getMockBuilder('\Doctrine\Common\Persistence\ObjectManager')
            ->disableOriginalConstructor()
            ->getMock();
        $entityManager->expects($this->once())
            ->method('getRepository')
            ->will($this->returnValue($employeeRepository));

        $salaryCalculator = new SalaryCalculator($entityManager);
        $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1));
    }
}

In this example, you are building the mocks from the inside out, first creating the employee which gets returned by the Repository, which itself gets returned by the EntityManager. This way, no real class is involved in testing.

Changing Database Settings for Functional Tests

If you have functional tests, you want them to interact with a real database. Most of the time you want to use a dedicated database connection to make sure not to overwrite data you entered when developing the application and also to be able to clear the database before every test.

To do this, you can specify a database configuration which overwrites the default configuration:

  • YAML
    # app/config/config_test.yml
    doctrine:
        # ...
        dbal:
            host:     localhost
            dbname:   testdb
            user:     testdb
            password: testdb
    
  • XML
    <!-- app/config/config_test.xml -->
    <doctrine:config>
        <doctrine:dbal
            host="localhost"
            dbname="testdb"
            user="testdb"
            password="testdb"
        />
    </doctrine:config>
    
  • PHP
    // app/config/config_test.php
    $configuration->loadFromExtension('doctrine', array(
        'dbal' => array(
            'host'     => 'localhost',
            'dbname'   => 'testdb',
            'user'     => 'testdb',
            'password' => 'testdb',
        ),
    ));
    

Make sure that your database runs on localhost and has the defined database and user credentials set up.

How to Test Doctrine Repositories

Unit testing Doctrine repositories in a Symfony project is not recommended. When you’re dealing with a repository, you’re really dealing with something that’s meant to be tested against a real database connection.

Fortunately, you can easily test your queries against a real database, as described below.

Functional Testing

If you need to actually execute a query, you will need to boot the kernel to get a valid connection. In this case, you’ll extend the WebTestCase, which makes all of this quite easy:

// src/Acme/StoreBundle/Tests/Entity/ProductRepositoryFunctionalTest.php
namespace Acme\StoreBundle\Tests\Entity;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ProductRepositoryFunctionalTest extends WebTestCase
{
    /**
     * @var \Doctrine\ORM\EntityManager
     */
    private $em;

    /**
     * {@inheritDoc}
     */
    public function setUp()
    {
        static::$kernel = static::createKernel();
        static::$kernel->boot();
        $this->em = static::$kernel->getContainer()
            ->get('doctrine')
            ->getManager()
        ;
    }

    public function testSearchByCategoryName()
    {
        $products = $this->em
            ->getRepository('AcmeStoreBundle:Product')
            ->searchByCategoryName('foo')
        ;

        $this->assertCount(1, $products);
    }

    /**
     * {@inheritDoc}
     */
    protected function tearDown()
    {
        parent::tearDown();
        $this->em->close();
    }
}
How to Customize the Bootstrap Process before Running Tests

Sometimes when running tests, you need to do additional bootstrap work before running those tests. For example, if you’re running a functional test and have introduced a new translation resource, then you will need to clear your cache before running those tests. This cookbook covers how to do that.

First, add the following file:

// app/tests.bootstrap.php
if (isset($_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'])) {
    passthru(sprintf(
        'php "%s/console" cache:clear --env=%s --no-warmup',
        __DIR__,
        $_ENV['BOOTSTRAP_CLEAR_CACHE_ENV']
    ));
}

require __DIR__.'/bootstrap.php.cache';

Replace the test bootstrap file bootstrap.php.cache in app/phpunit.xml.dist with tests.bootstrap.php:

<!-- app/phpunit.xml.dist -->

<!-- ... -->
<phpunit
    ...
    bootstrap = "tests.bootstrap.php"
>

Now, you can define in your phpunit.xml.dist file which environment you want the cache to be cleared:

<!-- app/phpunit.xml.dist -->
<php>
    <env name="BOOTSTRAP_CLEAR_CACHE_ENV" value="test"/>
</php>

This now becomes an environment variable (i.e. $_ENV) that’s available in the custom bootstrap file (tests.bootstrap.php).

How to Upgrade Your Symfony Project

So a new Symfony release has come out and you want to upgrade, great! Fortunately, because Symfony protects backwards-compatibility very closely, this should be quite easy.

There are two types of upgrades, and both are a little different:

Upgrading a Patch Version (e.g. 2.6.0 to 2.6.1)

If you’re upgrading and only the patch version (the last number) is changing, then it’s really easy:

$ composer update symfony/symfony

That’s it! You should not encounter any backwards-compatibility breaks or need to change anything else in your code. That’s because when you started your project, your composer.json included Symfony using a constraint like 2.6.*, where only the last version number will change when you update.

You may also want to upgrade the rest of your libraries. If you’ve done a good job with your version constraints in composer.json, you can do this safely by running:

$ composer update

But beware. If you have some bad version constraints in your composer.json, (e.g. dev-master), then this could upgrade some non-Symfony libraries to new versions that contain backwards-compatibility breaking changes.

Upgrading a Minor Version (e.g. 2.5.3 to 2.6.1)

If you’re upgrading a minor version (where the middle number changes), then you should also not encounter significant backwards compatibility changes. For details, see our Our backwards Compatibility Promise.

However, some backwards-compatibility breaks are possible, and you’ll learn in a second how to prepare for them.

There are two steps to upgrading:

1) Update the Symfony Library via Composer; 2) Updating Your Code to Work with the new Version

1) Update the Symfony Library via Composer

First, you need to update Symfony by modifying your composer.json file to use the new version:

{
    "...": "...",

    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*",
        "...": "... no changes to anything else..."
    },
    "...": "...",
}

Next, use Composer to download new versions of the libraries:

$ composer update symfony/symfony

You may also want to upgrade the rest of your libraries. If you’ve done a good job with your version constraints in composer.json, you can do this safely by running:

$ composer update

But beware. If you have some bad version constraints in your composer.json, (e.g. dev-master), then this could upgrade some non-Symfony libraries to new versions that contain backwards-compatibility breaking changes.

2) Updating Your Code to Work with the new Version

In theory, you should be done! However, you may need to make a few changes to your code to get everything working. Additionally, some features you’re using might still work, but might now be deprecated. That’s actually ok, but if you know about these deprecations, you can start to fix them over time.

Every version of Symfony comes with an UPGRADE file that describes these changes. Below are links to the file for each version, which you’ll need to read to see if you need any code changes.

小技巧

Don’t see the version here that you’re upgrading to? Just find the UPGRADE-X.X.md file for the appropriate version on the Symfony Repository.

Upgrading to Symfony 2.6

First, of course, update your composer.json file with the 2.6 version of Symfony as described above in 1) Update the Symfony Library via Composer.

Next, check the UPGRADE-2.6 document for details about any code changes that you might need to make in your project.

Upgrading to Symfony 2.5

First, of course, update your composer.json file with the 2.5 version of Symfony as described above in 1) Update the Symfony Library via Composer.

Next, check the UPGRADE-2.5 document for details about any code changes that you might need to make in your project.

Validation

How to Create a custom Validation Constraint

You can create a custom constraint by extending the base constraint class, Constraint. As an example you’re going to create a simple validator that checks if a string contains only alphanumeric characters.

Creating the Constraint Class

First you need to create a Constraint class and extend Constraint:

// src/AppBundle/Validator/Constraints/ContainsAlphanumeric.php
namespace AppBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class ContainsAlphanumeric extends Constraint
{
    public $message = 'The string "%string%" contains an illegal character: it can only contain letters or numbers.';
}

注解

The @Annotation annotation is necessary for this new constraint in order to make it available for use in classes via annotations. Options for your constraint are represented as public properties on the constraint class.

Creating the Validator itself

As you can see, a constraint class is fairly minimal. The actual validation is performed by another “constraint validator” class. The constraint validator class is specified by the constraint’s validatedBy() method, which includes some simple default logic:

// in the base Symfony\Component\Validator\Constraint class
public function validatedBy()
{
    return get_class($this).'Validator';
}

In other words, if you create a custom Constraint (e.g. MyConstraint), Symfony will automatically look for another class, MyConstraintValidator when actually performing the validation.

The validator class is also simple, and only has one required method validate():

// src/AppBundle/Validator/Constraints/ContainsAlphanumericValidator.php
namespace AppBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class ContainsAlphanumericValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if (!preg_match('/^[a-zA-Za0-9]+$/', $value, $matches)) {
            $this->context->addViolation(
                $constraint->message,
                array('%string%' => $value)
            );
        }
    }
}

注解

The validate method does not return a value; instead, it adds violations to the validator’s context property with an addViolation method call if there are validation failures. Therefore, a value could be considered as being valid if it causes no violations to be added to the context. The first parameter of the addViolation call is the error message to use for that violation.

Using the new Validator

Using custom validators is very easy, just as the ones provided by Symfony itself:

  • YAML
    # src/AppBundle/Resources/config/validation.yml
    AppBundle\Entity\AcmeEntity:
        properties:
            name:
                - NotBlank: ~
                - AppBundle\Validator\Constraints\ContainsAlphanumeric: ~
    
  • Annotations
    // src/AppBundle/Entity/AcmeEntity.php
    use Symfony\Component\Validator\Constraints as Assert;
    use AppBundle\Validator\Constraints as AcmeAssert;
    
    class AcmeEntity
    {
        // ...
    
        /**
         * @Assert\NotBlank
         * @AcmeAssert\ContainsAlphanumeric
         */
        protected $name;
    
        // ...
    }
    
  • XML
    <!-- src/AppBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="AppBundle\Entity\AcmeEntity">
            <property name="name">
                <constraint name="NotBlank" />
                <constraint name="AppBundle\Validator\Constraints\ContainsAlphanumeric" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/AppBundle/Entity/AcmeEntity.php
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use AppBundle\Validator\Constraints\ContainsAlphanumeric;
    
    class AcmeEntity
    {
        public $name;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('name', new NotBlank());
            $metadata->addPropertyConstraint('name', new ContainsAlphanumeric());
        }
    }
    

If your constraint contains options, then they should be public properties on the custom Constraint class you created earlier. These options can be configured like options on core Symfony constraints.

Constraint Validators with Dependencies

If your constraint validator has dependencies, such as a database connection, it will need to be configured as a service in the dependency injection container. This service must include the validator.constraint_validator tag and an alias attribute:

  • YAML
    # app/config/services.yml
    services:
        validator.unique.your_validator_name:
            class: Fully\Qualified\Validator\Class\Name
            tags:
                - { name: validator.constraint_validator, alias: alias_name }
    
  • XML
    <!-- app/config/services.xml -->
    <service id="validator.unique.your_validator_name" class="Fully\Qualified\Validator\Class\Name">
        <argument type="service" id="doctrine.orm.default_entity_manager" />
        <tag name="validator.constraint_validator" alias="alias_name" />
    </service>
    
  • PHP
    // app/config/services.php
    $container
        ->register('validator.unique.your_validator_name', 'Fully\Qualified\Validator\Class\Name')
        ->addTag('validator.constraint_validator', array('alias' => 'alias_name'));
    

Your constraint class should now use this alias to reference the appropriate validator:

public function validatedBy()
{
    return 'alias_name';
}

As mentioned above, Symfony will automatically look for a class named after the constraint, with Validator appended. If your constraint validator is defined as a service, it’s important that you override the validatedBy() method to return the alias used when defining your service, otherwise Symfony won’t use the constraint validator service, and will instantiate the class instead, without any dependencies injected.

Class Constraint Validator

Beside validating a class property, a constraint can have a class scope by providing a target in its Constraint class:

public function getTargets()
{
    return self::CLASS_CONSTRAINT;
}

With this, the validator validate() method gets an object as its first argument:

class ProtocolClassValidator extends ConstraintValidator
{
    public function validate($protocol, Constraint $constraint)
    {
        if ($protocol->getFoo() != $protocol->getBar()) {
            $this->context->addViolationAt(
                'foo',
                $constraint->message,
                array(),
                null
            );
        }
    }
}

Note that a class constraint validator is applied to the class itself, and not to the property:

  • YAML
    # src/AppBundle/Resources/config/validation.yml
    AppBundle\Entity\AcmeEntity:
        constraints:
            - AppBundle\Validator\Constraints\ContainsAlphanumeric: ~
    
  • Annotations
    /**
     * @AcmeAssert\ContainsAlphanumeric
     */
    class AcmeEntity
    {
        // ...
    }
    
  • XML
    <!-- src/AppBundle/Resources/config/validation.xml -->
    <class name="AppBundle\Entity\AcmeEntity">
        <constraint name="AppBundle\Validator\Constraints\ContainsAlphanumeric" />
    </class>
    

Web Server

How to Use PHP’s built-in Web Server

Since PHP 5.4 the CLI SAPI comes with a built-in web server. It can be used to run your PHP applications locally during development, for testing or for application demonstrations. This way, you don’t have to bother configuring a full-featured web server such as Apache or Nginx.

警告

The built-in web server is meant to be run in a controlled environment. It is not designed to be used on public networks.

Starting the Web Server

Running a Symfony application using PHP’s built-in web server is as easy as executing the server:run command:

$ php app/console server:run

This starts a server at localhost:8000 that executes your Symfony application. The command will wait and will respond to incoming HTTP requests until you terminate it (this is usually done by pressing Ctrl and C).

By default, the web server listens on port 8000 on the loopback device. You can change the socket passing an IP address and a port as a command-line argument:

$ php app/console server:run 192.168.0.1:8080
Command Options

The built-in web server expects a “router” script (read about the “router” script on php.net) as an argument. Symfony already passes such a router script when the command is executed in the prod or in the dev environment. Use the --router option in any other environment or to use another router script:

$ php app/console server:run --env=test --router=app/config/router_test.php

If your application’s document root differs from the standard directory layout, you have to pass the correct location using the --docroot option:

$ php app/console server:run --docroot=public_html

Web Services

How to Create a SOAP Web Service in a Symfony Controller

Setting up a controller to act as a SOAP server is simple with a couple tools. You must, of course, have the PHP SOAP extension installed. As the PHP SOAP extension can not currently generate a WSDL, you must either create one from scratch or use a 3rd party generator.

注解

There are several SOAP server implementations available for use with PHP. Zend SOAP and NuSOAP are two examples. Although the PHP SOAP extension is used in these examples, the general idea should still be applicable to other implementations.

SOAP works by exposing the methods of a PHP object to an external entity (i.e. the person using the SOAP service). To start, create a class - HelloService - which represents the functionality that you’ll expose in your SOAP service. In this case, the SOAP service will allow the client to call a method called hello, which happens to send an email:

// src/Acme/SoapBundle/Services/HelloService.php
namespace Acme\SoapBundle\Services;

class HelloService
{
    private $mailer;

    public function __construct(\Swift_Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function hello($name)
    {

        $message = \Swift_Message::newInstance()
                                ->setTo('me@example.com')
                                ->setSubject('Hello Service')
                                ->setBody($name . ' says hi!');

        $this->mailer->send($message);

        return 'Hello, '.$name;
    }
}

Next, you can train Symfony to be able to create an instance of this class. Since the class sends an e-mail, it’s been designed to accept a Swift_Mailer instance. Using the Service Container, you can configure Symfony to construct a HelloService object properly:

  • YAML
    # app/config/services.yml
    services:
        hello_service:
            class: Acme\SoapBundle\Services\HelloService
            arguments: ["@mailer"]
    
  • XML
    <!-- app/config/services.xml -->
    <services>
        <service id="hello_service" class="Acme\SoapBundle\Services\HelloService">
            <argument type="service" id="mailer"/>
        </service>
    </services>
    
  • PHP
    // app/config/services.php
    $container
        ->register('hello_service', 'Acme\SoapBundle\Services\HelloService')
        ->addArgument(new Reference('mailer'));
    

Below is an example of a controller that is capable of handling a SOAP request. If indexAction() is accessible via the route /soap, then the WSDL document can be retrieved via /soap?wsdl.

namespace Acme\SoapBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class HelloServiceController extends Controller
{
    public function indexAction()
    {
        $server = new \SoapServer('/path/to/hello.wsdl');
        $server->setObject($this->get('hello_service'));

        $response = new Response();
        $response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1');

        ob_start();
        $server->handle();
        $response->setContent(ob_get_clean());

        return $response;
    }
}

Take note of the calls to ob_start() and ob_get_clean(). These methods control output buffering which allows you to “trap” the echoed output of $server->handle(). This is necessary because Symfony expects your controller to return a Response object with the output as its “content”. You must also remember to set the “Content-Type” header to “text/xml”, as this is what the client will expect. So, you use ob_start() to start buffering the STDOUT and use ob_get_clean() to dump the echoed output into the content of the Response and clear the output buffer. Finally, you’re ready to return the Response.

Below is an example calling the service using a NuSOAP client. This example assumes that the indexAction in the controller above is accessible via the route /soap:

$client = new \Soapclient('http://example.com/app.php/soap?wsdl', true);

$result = $client->call('hello', array('name' => 'Scott'));

An example WSDL is below.

<?xml version="1.0" encoding="ISO-8859-1"?>
<definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
    xmlns:tns="urn:arnleadservicewsdl"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    xmlns="http://schemas.xmlsoap.org/wsdl/"
    targetNamespace="urn:helloservicewsdl">

    <types>
        <xsd:schema targetNamespace="urn:hellowsdl">
            <xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
            <xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" />
        </xsd:schema>
    </types>

    <message name="helloRequest">
        <part name="name" type="xsd:string" />
    </message>

    <message name="helloResponse">
        <part name="return" type="xsd:string" />
    </message>

    <portType name="hellowsdlPortType">
        <operation name="hello">
            <documentation>Hello World</documentation>
            <input message="tns:helloRequest"/>
            <output message="tns:helloResponse"/>
        </operation>
    </portType>

    <binding name="hellowsdlBinding" type="tns:hellowsdlPortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="hello">
            <soap:operation soapAction="urn:arnleadservicewsdl#hello" style="rpc"/>

            <input>
                <soap:body use="encoded" namespace="urn:hellowsdl"
                    encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            </input>

            <output>
                <soap:body use="encoded" namespace="urn:hellowsdl"
                    encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            </output>
        </operation>
    </binding>

    <service name="hellowsdl">
        <port name="hellowsdlPort" binding="tns:hellowsdlBinding">
            <soap:address location="http://example.com/app.php/soap" />
        </port>
    </service>
</definitions>

Workflow

How to Create and Store a Symfony Project in Git

小技巧

Though this entry is specifically about Git, the same generic principles will apply if you’re storing your project in Subversion.

Once you’ve read through Creating Pages in Symfony and become familiar with using Symfony, you’ll no-doubt be ready to start your own project. In this cookbook article, you’ll learn the best way to start a new Symfony project that’s stored using the Git source control management system.

Initial Project Setup

To get started, you’ll need to download Symfony and get things running. See the Installing and Configuring Symfony chapter for details.

Once your project is running, just follow these simple steps:

  1. Initialize your Git repository:

    $ git init
    
  2. Add all of the initial files to Git:

    $ git add .
    

    小技巧

    As you might have noticed, not all files that were downloaded by Composer in step 1, have been staged for commit by Git. Certain files and folders, such as the project’s dependencies (which are managed by Composer), parameters.yml (which contains sensitive information such as database credentials), log and cache files and dumped assets (which are created automatically by your project), should not be committed in Git. To help you prevent committing those files and folders by accident, the Standard Distribution comes with a file called .gitignore, which contains a list of files and folders that Git should ignore.

    小技巧

    You may also want to create a .gitignore file that can be used system-wide. This allows you to exclude files/folders for all your projects that are created by your IDE or operating system. For details, see GitHub .gitignore.

  3. Create an initial commit with your started project:

    $ git commit -m "Initial commit"
    

At this point, you have a fully-functional Symfony project that’s correctly committed to Git. You can immediately begin development, committing the new changes to your Git repository.

You can continue to follow along with the Creating Pages in Symfony chapter to learn more about how to configure and develop inside your application.

小技巧

The Symfony Standard Edition comes with some example functionality. To remove the sample code, follow the instructions in the “How to Remove the AcmeDemoBundle” article.

Managing Vendor Libraries with composer.json
How Does it Work?

Every Symfony project uses a group of third-party “vendor” libraries. One way or another the goal is to download these files into your vendor/ directory and, ideally, to give you some sane way to manage the exact version you need for each.

By default, these libraries are downloaded by running a composer install “downloader” binary. This composer file is from a library called Composer and you can read more about installing it in the Installation chapter.

The composer command reads from the composer.json file at the root of your project. This is an JSON-formatted file, which holds a list of each of the external packages you need, the version to be downloaded and more. composer also reads from a composer.lock file, which allows you to pin each library to an exact version. In fact, if a composer.lock file exists, the versions inside will override those in composer.json. To upgrade your libraries to new versions, run composer update.

小技巧

If you want to add a new package to your application, run the composer require command:

$ composer require doctrine/doctrine-fixtures-bundle

To learn more about Composer, see GetComposer.org:

It’s important to realize that these vendor libraries are not actually part of your repository. Instead, they’re simply un-tracked files that are downloaded into the vendor/. But since all the information needed to download these files is saved in composer.json and composer.lock (which are stored in the repository), any other developer can use the project, run composer install, and download the exact same set of vendor libraries. This means that you’re controlling exactly what each vendor library looks like, without needing to actually commit them to your repository.

So, whenever a developer uses your project, they should run the composer install script to ensure that all of the needed vendor libraries are downloaded.

Storing your Project on a remote Server

You now have a fully-functional Symfony project stored in Git. However, in most cases, you’ll also want to store your project on a remote server both for backup purposes, and so that other developers can collaborate on the project.

The easiest way to store your project on a remote server is via a web-based hosting service like GitHub or Bitbucket. Of course, there are more services out there, you can start your research with a comparison of hosting services.

Alternatively, you can store your Git repository on any server by creating a barebones repository and then pushing to it. One library that helps manage this is Gitolite.

How to Create and Store a Symfony Project in Subversion

小技巧

This entry is specifically about Subversion, and based on principles found in How to Create and Store a Symfony Project in Git.

Once you’ve read through Creating Pages in Symfony and become familiar with using Symfony, you’ll no-doubt be ready to start your own project. The preferred method to manage Symfony projects is using Git but some prefer to use Subversion which is totally fine!. In this cookbook article, you’ll learn how to manage your project using SVN in a similar manner you would do with Git.

小技巧

This is a method to tracking your Symfony project in a Subversion repository. There are several ways to do and this one is simply one that works.

The Subversion Repository

For this article it’s assumed that your repository layout follows the widespread standard structure:

myproject/
    branches/
    tags/
    trunk/

小技巧

Most Subversion hosting should follow this standard practice. This is the recommended layout in Version Control with Subversion and the layout used by most free hosting (see Subversion Hosting Solutions).

Initial Project Setup

To get started, you’ll need to download Symfony and get the basic Subversion setup. First, download and get your Symfony project running by following the Installation chapter.

Once you have your new project directory and things are working, follow along with these steps:

  1. Checkout the Subversion repository that will host this project. Suppose it is hosted on Google code and called myproject:

    $ svn checkout http://myproject.googlecode.com/svn/trunk myproject
    
  2. Copy the Symfony project files in the Subversion folder:

    $ mv Symfony/* myproject/
    
  3. Now, set the ignore rules. Not everything should be stored in your Subversion repository. Some files (like the cache) are generated and others (like the database configuration) are meant to be customized on each machine. This makes use of the svn:ignore property, so that specific files can be ignored.

    $ cd myproject/
    $ svn add --depth=empty app app/cache app/logs app/config web
    
    $ svn propset svn:ignore "vendor" .
    $ svn propset svn:ignore "bootstrap*" app/
    $ svn propset svn:ignore "parameters.yml" app/config/
    $ svn propset svn:ignore "*" app/cache/
    $ svn propset svn:ignore "*" app/logs/
    
    $ svn propset svn:ignore "bundles" web
    
    $ svn ci -m "commit basic Symfony ignore list (vendor, app/bootstrap*, app/config/parameters.yml, app/cache/*, app/logs/*, web/bundles)"
    
  4. The rest of the files can now be added and committed to the project:

    $ svn add --force .
    $ svn ci -m "add basic Symfony Standard 2.X.Y"
    

That’s it! Since the app/config/parameters.yml file is ignored, you can store machine-specific settings like database passwords here without committing them. The parameters.yml.dist file is committed, but is not read by Symfony. And by adding any new keys you need to both files, new developers can quickly clone the project, copy this file to parameters.yml, customize it, and start developing.

At this point, you have a fully-functional Symfony project stored in your Subversion repository. The development can start with commits in the Subversion repository.

You can continue to follow along with the Creating Pages in Symfony chapter to learn more about how to configure and develop inside your application.

小技巧

The Symfony Standard Edition comes with some example functionality. To remove the sample code, follow the instructions in the “How to Remove the AcmeDemoBundle” article.

Managing Vendor Libraries with composer.json
How Does it Work?

Every Symfony project uses a group of third-party “vendor” libraries. One way or another the goal is to download these files into your vendor/ directory and, ideally, to give you some sane way to manage the exact version you need for each.

By default, these libraries are downloaded by running a composer install “downloader” binary. This composer file is from a library called Composer and you can read more about installing it in the Installation chapter.

The composer command reads from the composer.json file at the root of your project. This is an JSON-formatted file, which holds a list of each of the external packages you need, the version to be downloaded and more. composer also reads from a composer.lock file, which allows you to pin each library to an exact version. In fact, if a composer.lock file exists, the versions inside will override those in composer.json. To upgrade your libraries to new versions, run composer update.

小技巧

If you want to add a new package to your application, run the composer require command:

$ composer require doctrine/doctrine-fixtures-bundle

To learn more about Composer, see GetComposer.org:

It’s important to realize that these vendor libraries are not actually part of your repository. Instead, they’re simply un-tracked files that are downloaded into the vendor/. But since all the information needed to download these files is saved in composer.json and composer.lock (which are stored in the repository), any other developer can use the project, run composer install, and download the exact same set of vendor libraries. This means that you’re controlling exactly what each vendor library looks like, without needing to actually commit them to your repository.

So, whenever a developer uses your project, they should run the composer install script to ensure that all of the needed vendor libraries are downloaded.

Subversion Hosting Solutions

The biggest difference between Git and SVN is that Subversion needs a central repository to work. You then have several solutions:

  • Self hosting: create your own repository and access it either through the filesystem or the network. To help in this task you can read Version Control with Subversion.
  • Third party hosting: there are a lot of serious free hosting solutions available like GitHub, Google code, SourceForge or Gna. Some of them offer Git hosting as well.

Read the Cookbook.

Best Practices

Official Symfony Best Practices

The Symfony Framework Best Practices

The Symfony framework is well-known for being really flexible and is used to build micro-sites, enterprise applications that handle billions of connections and even as the basis for other frameworks. Since its release in July 2011, the community has learned a lot about what’s possible and how to do things best.

These community resources - like blog posts or presentations - have created an unofficial set of recommendations for developing Symfony applications. Unfortunately, a lot of these recommendations are unneeded for web applications. Much of the time, they unnecessarily overcomplicate things and don’t follow the original pragmatic philosophy of Symfony.

What is this Guide About?

This guide aims to fix that by describing the best practices for developing web apps with the Symfony full-stack framework. These are best practices that fit the philosophy of the framework as envisioned by its original creator Fabien Potencier.

注解

Best practice is a noun that means “a well defined procedure that is known to produce near-optimum results”. And that’s exactly what this guide aims to provide. Even if you don’t agree with every recommendation, we believe these will help you build great applications with less complexity.

This guide is specially suited for:

  • Websites and web applications developed with the full-stack Symfony framework.

For other situations, this guide might be a good starting point that you can then extend and fit to your specific needs:

  • Bundles shared publicly to the Symfony community;
  • Advanced developers or teams who have created their own standards;
  • Some complex applications that have highly customized requirements;
  • Bundles that may be shared internally within a company.

We know that old habits die hard and some of you will be shocked by some of these best practices. But by following these, you’ll be able to develop apps faster, with less complexity and with the same or even higher quality. It’s also a moving target that will continue to improve.

Keep in mind that these are optional recommendations that you and your team may or may not follow to develop Symfony applications. If you want to continue using your own best practices and methodologies, you can of course do it. Symfony is flexible enough to adapt to your needs. That will never change.

Who this Book Is for (Hint: It’s not a Tutorial)

Any Symfony developer, whether you are an expert or a newcomer, can read this guide. But since this isn’t a tutorial, you’ll need some basic knowledge of Symfony to follow everything. If you are totally new to Symfony, welcome! Start with The Quick Tour tutorial first.

We’ve deliberately kept this guide short. We won’t repeat explanations that you can find in the vast Symfony documentation, like discussions about dependency injection or front controllers. We’ll solely focus on explaining how to do what you already know.

The Application

In addition to this guide, you’ll find a sample application developed with all these best practices in mind. The application is a simple blog engine, because that will allow us to focus on the Symfony concepts and features without getting buried in difficult details.

Instead of developing the application step by step in this guide, you’ll find selected snippets of code through the chapters. Please refer to the last chapter of this guide to find more details about this application and the instructions to install it.

Don’t Update Your Existing Applications

After reading this handbook, some of you may be considering refactoring your existing Symfony applications. Our recommendation is sound and clear: you should not refactor your existing applications to comply with these best practices. The reasons for not doing it are various:

  • Your existing applications are not wrong, they just follow another set of guidelines;
  • A full codebase refactorization is prone to introduce errors in your applications;
  • The amount of work spent on this could be better dedicated to improving your tests or adding features that provide real value to the end users.

Creating the Project

Installing Symfony

In the past, Symfony projects were created with Composer, the dependency manager for PHP applications. However, the current recommendation is to use the Symfony Installer, which has to be installed before creating your first project.

Linux and Mac OS X Systems

Open your command console and execute the following:

$ curl -LsS http://symfony.com/installer > symfony.phar
$ sudo mv symfony.phar /usr/local/bin/symfony
$ chmod a+x /usr/local/bin/symfony

Now you can execute the Symfony Installer as a global system command called symfony.

Windows Systems

Open your command console and execute the following:

c:\> php -r "readfile('http://symfony.com/installer');" > symfony.phar

Then, move the downloaded symfony.phar file to your projects directory and execute it as follows:

c:\> php symfony.phar
Creating the Blog Application

Now that everything is correctly set up, you can create a new project based on Symfony. In your command console, browse to a directory where you have permission to create files and execute the following commands:

# Linux, Mac OS X
$ cd projects/
$ symfony new blog

# Windows
c:\> cd projects/
c:\projects\> php symfony.phar new blog

This command creates a new directory called blog that contains a fresh new project based on the most recent stable Symfony version available. In addition, the installer checks if your system meets the technical requirements to execute Symfony applications. If not, you’ll see the list of changes needed to meet those requirements.

小技巧

Symfony releases are digitally signed for security reasons. If you want to verify the integrity of your Symfony installation, take a look at the public checksums repository and follow these steps to verify the signatures.

Structuring the Application

After creating the application, enter the blog/ directory and you’ll see a number of files and directories generated automatically:

blog/
├─ app/
│  ├─ console
│  ├─ cache/
│  ├─ config/
│  ├─ logs/
│  └─ Resources/
├─ src/
│  └─ AppBundle/
├─ vendor/
└─ web/

This file and directory hierarchy is the convention proposed by Symfony to structure your applications. The recommended purpose of each directory is the following:

  • app/cache/, stores all the cache files generated by the application;
  • app/config/, stores all the configuration defined for any environment;
  • app/logs/, stores all the log files generated by the application;
  • app/Resources/, stores all the templates and the translation files for the application;
  • src/AppBundle/, stores the Symfony specific code (controllers and routes), your domain code (e.g. Doctrine classes) and all your business logic;
  • vendor/, this is the directory where Composer installs the application’s dependencies and you should never modify any of its contents;
  • web/, stores all the front controller files and all the web assets, such as stylesheets, JavaScript files and images.
Application Bundles

When Symfony 2.0 was released, most developers naturally adopted the symfony 1.x way of dividing applications into logical modules. That’s why many Symfony apps use bundles to divide their code into logical features: UserBundle, ProductBundle, InvoiceBundle, etc.

But a bundle is meant to be something that can be reused as a stand-alone piece of software. If UserBundle cannot be used “as is” in other Symfony apps, then it shouldn’t be its own bundle. Moreover InvoiceBundle depends on ProductBundle, then there’s no advantage to having two separate bundles.

Best Practice

Create only one bundle called AppBundle for your application logic

Implementing a single AppBundle bundle in your projects will make your code more concise and easier to understand. Starting in Symfony 2.6, the official Symfony documentation uses the AppBundle name.

注解

There is no need to prefix the AppBundle with your own vendor (e.g. AcmeAppBundle), because this application bundle is never going to be shared.

All in all, this is the typical directory structure of a Symfony application that follows these best practices:

blog/
├─ app/
│  ├─ console
│  ├─ cache/
│  ├─ config/
│  ├─ logs/
│  └─ Resources/
├─ src/
│  └─ AppBundle/
├─ vendor/
└─ web/
   ├─ app.php
   └─ app_dev.php

小技巧

If your Symfony installation doesn’t come with a pre-generated AppBundle, you can generate it by hand executing this command:

$ php app/console generate:bundle --namespace=AppBundle --dir=src --format=annotation --no-interaction
Extending the Directory Structure

If your project or infrastructure requires some changes to the default directory structure of Symfony, you can override the location of the main directories: cache/, logs/ and web/.

In addition, Symfony3 will use a slightly different directory structure when it’s released:

blog-symfony3/
├─ app/
│  ├─ config/
│  └─ Resources/
├─ bin/
│  └─ console
├─ src/
├─ var/
│  ├─ cache/
│  └─ logs/
├─ vendor/
└─ web/

The changes are pretty superficial, but for now, we recommend that you use the Symfony directory structure.

Configuration

Configuration usually involves different application parts (such as infrastructure and security credentials) and different environments (development, production). That’s why Symfony recommends that you split the application configuration into three parts.

Semantic Configuration: Don’t Do It

Best Practice

Don’t define a semantic dependency injection configuration for your bundles.

As explained in How to Load Service Configuration inside a Bundle article, Symfony bundles have two choices on how to handle configuration: normal service configuration through the services.yml file and semantic configuration through a special *Extension class.

Although semantic configuration is much more powerful and provides nice features such as configuration validation, the amount of work needed to define that configuration isn’t worth it for bundles that aren’t meant to be shared as third-party bundles.

Moving Sensitive Options Outside of Symfony Entirely

When dealing with sensitive options, like database credentials, we also recommend that you store them outside the Symfony project and make them available through environment variables. Learn how to do it in the following article: How to Set external Parameters in the Service Container

Organizing Your Business Logic

In computer software, business logic or domain logic is “the part of the program that encodes the real-world business rules that determine how data can be created, displayed, stored, and changed” (read full definition).

In Symfony applications, business logic is all the custom code you write for your app that’s not specific to the framework (e.g. routing and controllers). Domain classes, Doctrine entities and regular PHP classes that are used as services are good examples of business logic.

For most projects, you should store everything inside the AppBundle. Inside here, you can create whatever directories you want to organize things:

symfony2-project/
├─ app/
├─ src/
│  └─ AppBundle/
│     └─ Utils/
│        └─ MyClass.php
├─ vendor/
└─ web/
Storing Classes Outside of the Bundle?

But there’s no technical reason for putting business logic inside of a bundle. If you like, you can create your own namespace inside the src/ directory and put things there:

symfony2-project/
├─ app/
├─ src/
│  ├─ Acme/
│  │   └─ Utils/
│  │      └─ MyClass.php
│  └─ AppBundle/
├─ vendor/
└─ web/

小技巧

The recommended approach of using the AppBundle/ directory is for simplicity. If you’re advanced enough to know what needs to live in a bundle and what can live outside of one, then feel free to do that.

Services: Naming and Format

The blog application needs a utility that can transform a post title (e.g. “Hello World”) into a slug (e.g. “hello-world”). The slug will be used as part of the post URL.

Let’s create a new Slugger class inside src/AppBundle/Utils/ and add the following slugify() method:

// src/AppBundle/Utils/Slugger.php
namespace AppBundle\Utils;

class Slugger
{
    public function slugify($string)
    {
        return preg_replace(
            '/[^a-z0-9]/', '-', strtolower(trim(strip_tags($string)))
        );
    }
}

Next, define a new service for that class.

# app/config/services.yml
services:
    # keep your service names short
    app.slugger:
        class: AppBundle\Utils\Slugger

Traditionally, the naming convention for a service involved following the class name and location to avoid name collisions. Thus, the service would have been called app.utils.slugger. But by using short service names, your code will be easier to read and use.

Best Practice

The name of your application’s services should be as short as possible, but unique enough that you can search your project for the service if you ever need to.

Now you can use the custom slugger in any controller class, such as the AdminController:

public function createAction(Request $request)
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        $slug = $this->get('app.slugger')->slugify($post->getTitle());
        $post->setSlug($slug);

        // ...
    }
}
Service Format: YAML

In the previous section, YAML was used to define the service.

Best Practice

Use the YAML format to define your own services.

This is controversial, and in our experience, YAML and XML usage is evenly distributed among developers, with a slight preference towards YAML. Both formats have the same performance, so this is ultimately a matter of personal taste.

We recommend YAML because it’s friendly to newcomers and concise. You can of course use whatever format you like.

Service: No Class Parameter

You may have noticed that the previous service definition doesn’t configure the class namespace as a parameter:

# app/config/services.yml

# service definition with class namespace as parameter
parameters:
    slugger.class: AppBundle\Utils\Slugger

services:
    app.slugger:
        class: "%slugger.class%"

This practice is cumbersome and completely unnecessary for your own services:

Best Practice

Don’t define parameters for the classes of your services.

This practice was wrongly adopted from third-party bundles. When Symfony introduced its service container, some developers used this technique to easily allow overriding services. However, overriding a service by just changing its class name is a very rare use case because, frequently, the new service has different constructor arguments.

Using a Persistence Layer

Symfony is an HTTP framework that only cares about generating an HTTP response for each HTTP request. That’s why Symfony doesn’t provide a way to talk to a persistence layer (e.g. database, external API). You can choose whatever library or strategy you want for this.

In practice, many Symfony applications rely on the independent Doctrine project to define their model using entities and repositories. Just like with business logic, we recommend storing Doctrine entities in the AppBundle.

The three entities defined by our sample blog application are a good example:

symfony2-project/
├─ ...
└─ src/
   └─ AppBundle/
      └─ Entity/
         ├─ Comment.php
         ├─ Post.php
         └─ User.php

小技巧

If you’re more advanced, you can of course store them under your own namespace in src/.

Doctrine Mapping Information

Doctrine Entities are plain PHP objects that you store in some “database”. Doctrine only knows about your entities through the mapping metadata configured for your model classes. Doctrine supports four metadata formats: YAML, XML, PHP and annotations.

Best Practice

Use annotations to define the mapping information of the Doctrine entities.

Annotations are by far the most convenient and agile way of setting up and looking for mapping information:

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 */
class Post
{
    const NUM_ITEMS = 10;

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $title;

    /**
     * @ORM\Column(type="string")
     */
    private $slug;

    /**
     * @ORM\Column(type="text")
     */
    private $content;

    /**
     * @ORM\Column(type="string")
     */
    private $authorEmail;

    /**
     * @ORM\Column(type="datetime")
     */
    private $publishedAt;

    /**
     * @ORM\OneToMany(
     *      targetEntity="Comment",
     *      mappedBy="post",
     *      orphanRemoval=true
     * )
     * @ORM\OrderBy({"publishedAt" = "ASC"})
     */
    private $comments;

    public function __construct()
    {
        $this->publishedAt = new \DateTime();
        $this->comments = new ArrayCollection();
    }

    // getters and setters ...
}

All formats have the same performance, so this is once again ultimately a matter of taste.

Data Fixtures

As fixtures support is not enabled by default in Symfony, you should execute the following command to install the Doctrine fixtures bundle:

$ composer require "doctrine/doctrine-fixtures-bundle"

Then, enable the bundle in AppKernel.php, but only for the dev and test environments:

use Symfony\Component\HttpKernel\Kernel;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
        );

        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            // ...
            $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
        }

        return $bundles;
    }

    // ...
}

We recommend creating just one fixture class for simplicity, though you’re welcome to have more if that class gets quite large.

Assuming you have at least one fixtures class and that the database access is configured properly, you can load your fixtures by executing the following command:

$ php app/console doctrine:fixtures:load

Careful, database will be purged. Do you want to continue Y/N ? Y
  > purging database
  > loading AppBundle\DataFixtures\ORM\LoadFixtures
Coding Standards

The Symfony source code follows the PSR-1 and PSR-2 coding standards that were defined by the PHP community. You can learn more about the Symfony Coding standards and even use the PHP-CS-Fixer, which is a command-line utility that can fix the coding standards of an entire codebase in a matter of seconds.

Controllers

Symfony follows the philosophy of “thin controllers and fat models”. This means that controllers should hold just the thin layer of glue-code needed to coordinate the different parts of the application.

As a rule of thumb, you should follow the 5-10-20 rule, where controllers should only define 5 variables or less, contain 10 actions or less and include 20 lines of code or less in each action. This isn’t an exact science, but it should help you realize when code should be refactored out of the controller and into a service.

Best Practice

Make your controller extend the FrameworkBundle base controller and use annotations to configure routing, caching and security whenever possible.

Coupling the controllers to the underlying framework allows you to leverage all of its features and increases your productivity.

And since your controllers should be thin and contain nothing more than a few lines of glue-code, spending hours trying to decouple them from your framework doesn’t benefit you in the long run. The amount of time wasted isn’t worth the benefit.

In addition, using annotations for routing, caching and security simplifies configuration. You don’t need to browse tens of files created with different formats (YAML, XML, PHP): all the configuration is just where you need it and it only uses one format.

Overall, this means you should aggressively decouple your business logic from the framework while, at the same time, aggressively coupling your controllers and routing to the framework in order to get the most out of it.

Routing Configuration

To load routes defined as annotations in your controllers, add the following configuration to the main routing configuration file:

# app/config/routing.yml
app:
    resource: "@AppBundle/Controller/"
    type:     annotation

This configuration will load annotations from any controller stored inside the src/AppBundle/Controller/ directory and even from its subdirectories. So if your application defines lots of controllers, it’s perfectly ok to reorganize them into subdirectories:

<your-project>/
├─ ...
└─ src/
   └─ AppBundle/
      ├─ ...
      └─ Controller/
         ├─ DefaultController.php
         ├─ ...
         ├─ Api/
         │  ├─ ...
         │  └─ ...
         └─ Backend/
            ├─ ...
            └─ ...
Template Configuration

Best Practice

Don’t use the @Template() annotation to configure the template used by the controller.

The @Template annotation is useful, but also involves some magic. For that reason, we don’t recommend using it.

Most of the time, @Template is used without any parameters, which makes it more difficult to know which template is being rendered. It also makes it less obvious to beginners that a controller should always return a Response object (unless you’re using a view layer).

Lastly, the @Template annotation uses a TemplateListener class that hooks into the kernel.view event dispatched by the framework. This listener introduces a measurable performance impact. In the sample blog application, rendering the homepage took 5 milliseconds using the $this->render() method and 26 milliseconds using the @Template annotation.

How the Controller Looks

Considering all this, here is an example of how the controller should look for the homepage of our app:

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        $posts = $this->getDoctrine()
            ->getRepository('AppBundle:Post')
            ->findLatest();

        return $this->render('default/index.html.twig', array(
            'posts' => $posts
        ));
    }
}
Using the ParamConverter

If you’re using Doctrine, then you can optionally use the ParamConverter to automatically query for an entity and pass it as an argument to your controller.

Best Practice

Use the ParamConverter trick to automatically query for Doctrine entities when it’s simple and convenient.

For example:

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/{id}", name="admin_post_show")
 */
public function showAction(Post $post)
{
    $deleteForm = $this->createDeleteForm($post);

    return $this->render('admin/post/show.html.twig', array(
        'post'        => $post,
        'delete_form' => $deleteForm->createView(),
    ));
}

Normally, you’d expect a $id argument to showAction. Instead, by creating a new argument ($post) and type-hinting it with the Post class (which is a Doctrine entity), the ParamConverter automatically queries for an object whose $id property matches the {id} value. It will also show a 404 page if no Post can be found.

When Things Get More Advanced

This works without any configuration because the wildcard name {id} matches the name of the property on the entity. If this isn’t true, or if you have even more complex logic, the easiest thing to do is just query for the entity manually. In our application, we have this situation in CommentController:

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 */
public function newAction(Request $request, $postSlug)
{
    $post = $this->getDoctrine()
        ->getRepository('AppBundle:Post')
        ->findOneBy(array('slug' => $postSlug));

    if (!$post) {
        throw $this->createNotFoundException();
    }

    // ...
}

You can also use the @ParamConverter configuration, which is infinitely flexible:

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 * @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
 */
public function newAction(Request $request, Post $post)
{
    // ...
}

The point is this: the ParamConverter shortcut is great for simple situations. But you shouldn’t forget that querying for entities directly is still very easy.

Pre and Post Hooks

If you need to execute some code before or after the execution of your controllers, you can use the EventDispatcher component to set up before and after filters.

Templates

When PHP was created 20 years ago, developers loved its simplicity and how well it blended HTML and dynamic code. But as time passed, other template languages - like Twig - were created to make templating even better.

Best Practice

Use Twig templating format for your templates.

Generally speaking, PHP templates are much more verbose than Twig templates because they lack native support for lots of modern features needed by templates, like inheritance, automatic escaping and named arguments for filters and functions.

Twig is the default templating format in Symfony and has the largest community support of all non-PHP template engines (it’s used in high profile projects such as Drupal 8).

In addition, Twig is the only template format with guaranteed support in Symfony 3.0. As a matter of fact, PHP may be removed from the officially supported template engines.

Template Locations

Best Practice

Store all your application’s templates in app/Resources/views/ directory.

Traditionally, Symfony developers stored the application templates in the Resources/views/ directory of each bundle. Then they used the logical name to refer to them (e.g. AcmeDemoBundle:Default:index.html.twig).

But for the templates used in your application, it’s much more convenient to store them in the app/Resources/views/ directory. For starters, this drastically simplifies their logical names:

Templates Stored inside Bundles Templates Stored in app/
AcmeDemoBundle:Default:index.html.twig default/index.html.twig
::layout.html.twig layout.html.twig
AcmeDemoBundle::index.html.twig index.html.twig
AcmeDemoBundle:Default:subdir/index.html.twig default/subdir/index.html.twig
AcmeDemoBundle:Default/subdir:index.html.twig default/subdir/index.html.twig

Another advantage is that centralizing your templates simplifies the work of your designers. They don’t need to look for templates in lots of directories scattered through lots of bundles.

Twig Extensions

Best Practice

Define your Twig extensions in the AppBundle/Twig/ directory and configure them using the app/config/services.yml file.

Our application needs a custom md2html Twig filter so that we can transform the Markdown contents of each post into HTML.

To do this, first, install the excellent Parsedown Markdown parser as a new dependency of the project:

$ composer require erusev/parsedown

Then, create a new Markdown service that will be used later by the Twig extension. The service definition only requires the path to the class:

# app/config/services.yml
services:
    # ...
    markdown:
        class: AppBundle\Utils\Markdown

And the Markdown class just needs to define one single method to transform Markdown content into HTML:

namespace AppBundle\Utils;

class Markdown
{
    private $parser;

    public function __construct()
    {
        $this->parser = new \Parsedown();
    }

    public function toHtml($text)
    {
        $html = $this->parser->text($text);

        return $html;
    }
}

Next, create a new Twig extension and define a new filter called md2html using the Twig_SimpleFilter class. Inject the newly defined markdown service in the constructor of the Twig extension:

namespace AppBundle\Twig;

use AppBundle\Utils\Markdown;

class AppExtension extends \Twig_Extension
{
    private $parser;

    public function __construct(Markdown $parser)
    {
        $this->parser = $parser;
    }

    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter(
                'md2html',
                array($this, 'markdownToHtml'),
                array('is_safe' => array('html'))
            ),
        );
    }

    public function markdownToHtml($content)
    {
        return $this->parser->toHtml($content);
    }

    public function getName()
    {
        return 'app_extension';
    }
}

Lastly define a new service to enable this Twig extension in the app (the service name is irrelevant because you never use it in your own code):

# app/config/services.yml
services:
    app.twig.app_extension:
        class:     AppBundle\Twig\AppExtension
        arguments: ["@markdown"]
        public:    false
        tags:
            - { name: twig.extension }

Forms

Forms are one of the most misused Symfony components due to its vast scope and endless list of features. In this chapter we’ll show you some of the best practices so you can leverage forms but get work done quickly.

Building Forms

Best Practice

Define your forms as PHP classes.

The Form component allows you to build forms right inside your controller code. This is perfectly fine if you don’t need to reuse the form somewhere else. But for organization and reuse, we recommend that you define each form in its own PHP class:

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('summary', 'textarea')
            ->add('content', 'textarea')
            ->add('authorEmail', 'email')
            ->add('publishedAt', 'datetime')
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Post'
        ));
    }

    public function getName()
    {
        return 'post';
    }
}

To use the class, use createForm and instantiate the new class:

use AppBundle\Form\PostType;
// ...

public function newAction(Request $request)
{
    $post = new Post();
    $form = $this->createForm(new PostType(), $post);

    // ...
}
Registering Forms as Services

You can also register your form type as a service. But this is not recommended unless you plan to reuse the new form type in many places or embed it in other forms directly or via the collection type.

For most forms that are used only to edit or create something, registering the form as a service is over-kill, and makes it more difficult to figure out exactly which form class is being used in a controller.

Form Button Configuration

Form classes should try to be agnostic to where they will be used. This makes them easier to re-use later.

Best Practice

Add buttons in the templates, not in the form classes or the controllers.

Since Symfony 2.5, you can add buttons as fields on your form. This is a nice way to simplify the template that renders your form. But if you add the buttons directly in your form class, this would effectively limit the scope of that form:

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('save', 'submit', array('label' => 'Create Post'))
        ;
    }

    // ...
}

This form may have been designed for creating posts, but if you wanted to reuse it for editing posts, the button label would be wrong. Instead, some developers configure form buttons in the controller:

namespace AppBundle\Controller\Admin;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use AppBundle\Entity\Post;
use AppBundle\Form\PostType;

class PostController extends Controller
{
    // ...

    public function newAction(Request $request)
    {
        $post = new Post();
        $form = $this->createForm(new PostType(), $post);
        $form->add('submit', 'submit', array(
            'label' => 'Create',
            'attr'  => array('class' => 'btn btn-default pull-right')
        ));

        // ...
    }
}

This is also an important error, because you are mixing presentation markup (labels, CSS classes, etc.) with pure PHP code. Separation of concerns is always a good practice to follow, so put all the view-related things in the view layer:

{{ form_start(form) }}
    {{ form_widget(form) }}

    <input type="submit" value="Create"
           class="btn btn-default pull-right" />
{{ form_end(form) }}
Rendering the Form

There are a lot of ways to render your form, ranging from rendering the entire thing in one line to rendering each part of each field independently. The best way depends on how much customization you need.

One of the simplest ways - which is especially useful during development - is to render the form tags and use form_widget() to render all of the fields:

{{ form_start(form, {'attr': {'class': 'my-form-class'} }) }}
    {{ form_widget(form) }}
{{ form_end(form) }}

If you need more control over how your fields are rendered, then you should remove the form_widget(form) function and render your fields individually. See the How to Customize Form Rendering article for more information on this and how you can control how the form renders at a global level using form theming.

Handling Form Submits

Handling a form submit usually follows a similar template:

public function newAction(Request $request)
{
    // build the form ...

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $em->persist($post);
        $em->flush();

        return $this->redirect($this->generateUrl(
            'admin_post_show',
            array('id' => $post->getId())
        ));
    }

    // render the template
}

There are really only two notable things here. First, we recommend that you use a single action for both rendering the form and handling the form submit. For example, you could have a newAction that only renders the form and a createAction that only processes the form submit. Both those actions will be almost identical. So it’s much simpler to let newAction handle everything.

Second, we recommend using $form->isSubmitted() in the if statement for clarity. This isn’t technically needed, since isValid() first calls isSubmitted(). But without this, the flow doesn’t read well as it looks like the form is always processed (even on the GET request).

Internationalization

Internationalization and localization adapt the applications and their contents to the specific region or language of the users. In Symfony this is an opt-in feature that needs to be enabled before using it. To do this, uncomment the following translator configuration option and set your application locale:

# app/config/config.yml
framework:
    # ...
    translator: { fallback: "%locale%" }

# app/config/parameters.yml
parameters:
    # ...
    locale:     en
Translation Source File Format

The Symfony Translation component supports lots of different translation formats: PHP, Qt, .po, .mo, JSON, CSV, INI, etc.

Best Practice

Use the XLIFF format for your translation files.

Of all the available translation formats, only XLIFF and gettext have broad support in the tools used by professional translators. And since it’s based on XML, you can validate XLIFF file contents as you write them.

Symfony 2.6 added support for notes inside XLIFF files, making them more user-friendly for translators. At the end, good translations are all about context, and these XLIFF notes allow you to define that context.

小技巧

The Apache-licensed JMSTranslationBundle offers you a web interface for viewing and editing these translation files. It also has advanced extractors that can read your project and automatically update the XLIFF files.

Translation Source File Location

Best Practice

Store the translation files in the app/Resources/translations/ directory.

Traditionally, Symfony developers have created these files in the Resources/translations/ directory of each bundle.

But since the app/Resources/ directory is considered the global location for the application’s resources, storing translations in app/Resources/translations/ centralizes them and gives them priority over any other translation file. This lets you override translations defined in third-party bundles.

Translation Keys

Best Practice

Always use keys for translations instead of content strings.

Using keys simplifies the management of the translation files because you can change the original contents without having to update all of the translation files.

Keys should always describe their purpose and not their location. For example, if a form has a field with the label “Username”, then a nice key would be label.username, not edit_form.label.username.

Example Translation File

Applying all the previous best practices, the sample translation file for English in the application would be:

<!-- app/Resources/translations/messages.en.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
        <body>
            <trans-unit id="1">
                <source>title.post_list</source>
                <target>Post List</target>
            </trans-unit>
        </body>
    </file>
</xliff>

Security

Authentication and Firewalls (i.e. Getting the User’s Credentials)

You can configure Symfony to authenticate your users using any method you want and to load user information from any source. This is a complex topic, but the Security Cookbook Section has a lot of information about this.

Regardless of your needs, authentication is configured in security.yml, primarily under the firewalls key.

Best Practice

Unless you have two legitimately different authentication systems and users (e.g. form login for the main site and a token system for your API only), we recommend having only one firewall entry with the anonymous key enabled.

Most applications only have one authentication system and one set of users. For this reason, you only need one firewall entry. There are exceptions of course, especially if you have separated web and API sections on your site. But the point is to keep things simple.

Additionally, you should use the anonymous key under your firewall. If you need to require users to be logged in for different sections of your site (or maybe nearly all sections), use the access_control area.

Best Practice

Use the bcrypt encoder for encoding your users’ passwords.

If your users have a password, then we recommend encoding it using the bcrypt encoder, instead of the traditional SHA-512 hashing encoder. The main advantages of bcrypt are the inclusion of a salt value to protect against rainbow table attacks, and its adaptive nature, which allows to make it slower to remain resistant to brute-force search attacks.

With this in mind, here is the authentication setup from our application, which uses a login form to load users from the database:

# app/config/security.yml
security:
    encoders:
        AppBundle\Entity\User: bcrypt

    providers:
        database_users:
            entity: { class: AppBundle:User, property: username }

    firewalls:
        secured_area:
            pattern: ^/
            anonymous: true
            form_login:
                check_path: security_login_check
                login_path: security_login_form

            logout:
                path: security_logout
                target: homepage

# ... access_control exists, but is not shown here

小技巧

The source code for our project contains comments that explain each part.

Authorization (i.e. Denying Access)

Symfony gives you several ways to enforce authorization, including the access_control configuration in security.yml and using isGranted on the security.context service directly.

Best Practice

  • For protecting broad URL patterns, use access_control;
  • Check security directly on the security.context service whenever you have a more complex situation.

There are also different ways to centralize your authorization logic, like with a custom security voter or with ACL.

Best Practice

  • For fine-grained restrictions, define a custom security voter;
  • For restricting access to any object by any user via an admin interface, use the Symfony ACL.
Manually Checking Permissions

If you cannot control the access based on URL patterns, you can always do the security checks in PHP:

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = $this->getDoctrine()->getRepository('AppBundle:Post')
        ->find($id);

    if (!$post) {
        throw $this->createNotFoundException();
    }

    if (!$post->isAuthor($this->getUser())) {
        throw $this->createAccessDeniedException();
    }

    // ...
}
Security Voters

If your security logic is complex and can’t be centralized into a method like isAuthor(), you should leverage custom voters. These are an order of magnitude easier than ACLs and will give you the flexibility you need in almost all cases.

First, create a voter class. The following example shows a voter that implements the same getAuthorEmail logic you used above:

namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
use Symfony\Component\Security\Core\User\UserInterface;

// AbstractVoter class requires Symfony 2.6 or higher version
class PostVoter extends AbstractVoter
{
    const CREATE = 'create';
    const EDIT   = 'edit';

    protected function getSupportedAttributes()
    {
        return array(self::CREATE, self::EDIT);
    }

    protected function getSupportedClasses()
    {
        return array('AppBundle\Entity\Post');
    }

    protected function isGranted($attribute, $post, $user = null)
    {
        if (!$user instanceof UserInterface) {
            return false;
        }

        if ($attribute === self::CREATE && in_array('ROLE_ADMIN', $user->getRoles(), true)) {
            return true;
        }

        if ($attribute === self::EDIT && $user->getEmail() === $post->getAuthorEmail()) {
            return true;
        }

        return false;
    }
}

To enable the security voter in the application, define a new service:

# app/config/services.yml
services:
    # ...
    post_voter:
        class:      AppBundle\Security\PostVoter
        public:     false
        tags:
           - { name: security.voter }

Now, you can use the voter with the security.context service:

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = // query for the post ...

    if (!$this->get('security.context')->isGranted('edit', $post)) {
        throw $this->createAccessDeniedException();
    }
}
Learn More

The FOSUserBundle, developed by the Symfony community, adds support for a database-backed user system in Symfony. It also handles common tasks like user registration and forgotten password functionality.

Enable the Remember Me feature to allow your users to stay logged in for a long period of time.

When providing customer support, sometimes it’s necessary to access the application as some other user so that you can reproduce the problem. Symfony provides the ability to impersonate users.

If your company uses a user login method not supported by Symfony, you can develop your own user provider and your own authentication provider.

Web Assets

Web assets are things like CSS, JavaScript and image files that make the frontend of your site look and work great. Symfony developers have traditionally stored these assets in the Resources/public/ directory of each bundle.

Best Practice

Store your assets in the web/ directory.

Scattering your web assets across tens of different bundles makes it more difficult to manage them. Your designers’ lives will be much easier if all the application assets are in one location.

Templates also benefit from centralizing your assets, because the links are much more concise:

<link rel="stylesheet" href="{{ asset('css/bootstrap.min.css') }}" />
<link rel="stylesheet" href="{{ asset('css/main.css') }}" />

{# ... #}

<script src="{{ asset('js/jquery.min.js') }}"></script>
<script src="{{ asset('js/bootstrap.min.js') }}"></script>

注解

Keep in mind that web/ is a public directory and that anything stored here will be publicly accessible. For that reason, you should put your compiled web assets here, but not their source files (e.g. SASS files).

Using Assetic

These days, you probably can’t simply create static CSS and JavaScript files and include them in your template. Instead, you’ll probably want to combine and minify these to improve client-side performance. You may also want to use LESS or Sass (for example), which means you’ll need some way to process these into CSS files.

A lot of tools exist to solve these problems, including pure-frontend (non-PHP) tools like GruntJS.

Best Practice

Use Assetic to compile, combine and minimize web assets, unless you’re comfortable with frontend tools like GruntJS.

Assetic is an asset manager capable of compiling assets developed with a lot of different frontend technologies like LESS, Sass and CoffeeScript. Combining all your assets with Assetic is a matter of wrapping all the assets with a single Twig tag:

{% stylesheets
    'css/bootstrap.min.css'
    'css/main.css'
    filter='cssrewrite' output='css/compiled/all.css' %}
    <link rel="stylesheet" href="{{ asset_url }}" />
{% endstylesheets %}

{# ... #}

{% javascripts
    'js/jquery.min.js'
    'js/bootstrap.min.js'
    output='js/compiled/all.js' %}
    <script src="{{ asset_url }}"></script>
{% endjavascripts %}
Frontend-Based Applications

Recently, frontend technologies like AngularJS have become pretty popular for developing frontend web applications that talk to an API.

If you are developing an application like this, you should use the tools that are recommended by the technology, such as Bower and GruntJS. You should develop your frontend application separately from your Symfony backend (even separating the repositories if you want).

Learn More about Assetic

Assetic can also minimize CSS and JavaScript assets using UglifyCSS/UglifyJS to speed up your websites. You can even compress images with Assetic to reduce their size before serving them to the user. Check out the official Assetic documentation to learn more about all the available features.

Tests

Roughly speaking, there are two types of test. Unit testing allows you to test the input and output of specific functions. Functional testing allows you to command a “browser” where you browse to pages on your site, click links, fill out forms and assert that you see certain things on the page.

Unit Tests

Unit tests are used to test your “business logic”, which should live in classes that are independent of Symfony. For that reason, Symfony doesn’t really have an opinion on what tools you use for unit testing. However, the most popular tools are PhpUnit and PhpSpec.

Functional Tests

Creating really good functional tests can be tough so some developers skip these completely. Don’t skip the functional tests! By defining some simple functional tests, you can quickly spot any big errors before you deploy them:

Best Practice

Define a functional test that at least checks if your application pages are successfully loading.

A functional test can be as easy as this:

/** @dataProvider provideUrls */
public function testPageIsSuccessful($url)
{
    $client = self::createClient();
    $client->request('GET', $url);

    $this->assertTrue($client->getResponse()->isSuccessful());
}

public function provideUrls()
{
    return array(
        array('/'),
        array('/posts'),
        array('/post/fixture-post-1'),
        array('/blog/category/fixture-category'),
        array('/archives'),
        // ...
    );
}

This code checks that all the given URLs load successfully, which means that their HTTP response status code is between 200 and 299. This may not look that useful, but given how little effort this took, it’s worth having it in your application.

In computer software, this kind of test is called smoke testing and consists of “preliminary testing to reveal simple failures severe enough to reject a prospective software release”.

Hardcode URLs in a Functional Test

Some of you may be asking why the previous functional test doesn’t use the URL generator service:

Best Practice

Hardcode the URLs used in the functional tests instead of using the URL generator.

Consider the following functional test that uses the router service to generate the URL of the tested page:

public function testBlogArchives()
{
    $client = self::createClient();
    $url = $client->getContainer()->get('router')->generate('blog_archives');
    $client->request('GET', $url);

    // ...
}

This will work, but it has one huge drawback. If a developer mistakenly changes the path of the blog_archives route, the test will still pass, but the original (old) URL won’t work! This means that any bookmarks for that URL will be broken and you’ll lose any search engine page ranking.

Testing JavaScript Functionality

The built-in functional testing client is great, but it can’t be used to test any JavaScript behavior on your pages. If you need to test this, consider using the Mink library from within PHPUnit.

Of course, if you have a heavy JavaScript frontend, you should consider using pure JavaScript-based testing tools.

Learn More about Functional Tests

Consider using Faker and Alice libraries to generate real-looking data for your test fixtures.

Read the Official Best Practices.

Components

The Components

How to Install and Use the Symfony Components

If you’re starting a new project (or already have a project) that will use one or more components, the easiest way to integrate everything is with Composer. Composer is smart enough to download the component(s) that you need and take care of autoloading so that you can begin using the libraries immediately.

This article will take you through using The Finder Component, though this applies to using any component.

Using the Finder Component

1. If you’re creating a new project, create a new empty directory for it.

2. Open a terminal and use Composer to grab the library.

$ composer require symfony/finder

The name symfony/finder is written at the top of the documentation for whatever component you want.

小技巧

Install composer if you don’t have it already present on your system. Depending on how you install, you may end up with a composer.phar file in your directory. In that case, no worries! Just run php composer.phar require symfony/finder.

If you know you need a specific version of the library, add that to the command:

$ composer require symfony/finder

3. Write your code!

Once Composer has downloaded the component(s), all you need to do is include the vendor/autoload.php file that was generated by Composer. This file takes care of autoloading all of the libraries so that you can use them immediately:

// File example: src/script.php

// update this to the path to the "vendor/" directory, relative to this file
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\Finder\Finder;

$finder = new Finder();
$finder->in('../data/');

// ...
Using all of the Components

If you want to use all of the Symfony Components, then instead of adding them one by one, you can include the symfony/symfony package:

$ composer require symfony/symfony

This will also include the Bundle and Bridge libraries, which you may or may not actually need.

Now what?

Now that the component is installed and autoloaded, read the specific component’s documentation to find out more about how to use it.

And have fun!

ClassLoader

The ClassLoader Component
The ClassLoader component provides tools to autoload your classes and cache their locations for performance.
Usage

Whenever you reference a class that has not been required or included yet, PHP uses the autoloading mechanism to delegate the loading of a file defining the class. Symfony provides two autoloaders, which are able to load your classes:

Additionally, the Symfony ClassLoader component ships with a set of wrapper classes which can be used to add additional functionality on top of existing autoloaders:

Installation

You can install the component in 2 different ways:

The PSR-0 Class Loader

2.1 新版功能: The ClassLoader class was introduced in Symfony 2.1.

If your classes and third-party libraries follow the PSR-0 standard, you can use the ClassLoader class to load all of your project’s classes.

小技巧

You can use both the ApcClassLoader and the XcacheClassLoader to cache a ClassLoader instance or the DebugClassLoader to debug it.

Usage

Registering the ClassLoader autoloader is straightforward:

require_once '/path/to/src/Symfony/Component/ClassLoader/ClassLoader.php';

use Symfony\Component\ClassLoader\ClassLoader;

$loader = new ClassLoader();

// to enable searching the include path (eg. for PEAR packages)
$loader->setUseIncludePath(true);

// ... register namespaces and prefixes here - see below

$loader->register();

注解

The autoloader is automatically registered in a Symfony application (see app/autoload.php).

Use the addPrefix() or addPrefixes() methods to register your classes:

// register a single namespaces
$loader->addPrefix('Symfony', __DIR__.'/vendor/symfony/symfony/src');

// register several namespaces at once
$loader->addPrefixes(array(
    'Symfony' => __DIR__.'/../vendor/symfony/symfony/src',
    'Monolog' => __DIR__.'/../vendor/monolog/monolog/src',
));

// register a prefix for a class following the PEAR naming conventions
$loader->addPrefix('Twig_', __DIR__.'/vendor/twig/twig/lib');

$loader->addPrefixes(array(
    'Swift_' => __DIR__.'/vendor/swiftmailer/swiftmailer/lib/classes',
    'Twig_'  => __DIR__.'/vendor/twig/twig/lib',
));

Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be looked for in a location list to ease the vendoring of a sub-set of classes for large projects:

$loader->addPrefixes(array(
    'Doctrine\\Common'           => __DIR__.'/vendor/doctrine/common/lib',
    'Doctrine\\DBAL\\Migrations' => __DIR__.'/vendor/doctrine/migrations/lib',
    'Doctrine\\DBAL'             => __DIR__.'/vendor/doctrine/dbal/lib',
    'Doctrine'                   => __DIR__.'/vendor/doctrine/orm/lib',
));

In this example, if you try to use a class in the Doctrine\Common namespace or one of its children, the autoloader will first look for the class under the doctrine-common directory. If not found, it will then fallback to the default Doctrine directory (the last one configured) before giving up. The order of the prefix registrations is significant in this case.

MapClassLoader

The MapClassLoader allows you to autoload files via a static map from classes to files. This is useful if you use third-party libraries which don’t follow the PSR-0 standards and so can’t use the PSR-0 class loader.

The MapClassLoader can be used along with the PSR-0 class loader by configuring and calling the register() method on both.

注解

The default behavior is to append the MapClassLoader on the autoload stack. If you want to use it as the first autoloader, pass true when calling the register() method. Your class loader will then be prepended on the autoload stack.

Usage

Using it is as easy as passing your mapping to its constructor when creating an instance of the MapClassLoader class:

require_once '/path/to/src/Symfony/Component/ClassLoader/MapClassLoader.php';

$mapping = array(
    'Foo' => '/path/to/Foo',
    'Bar' => '/path/to/Bar',
);

$loader = new MapClassLoader($mapping);

$loader->register();
Cache a Class Loader
Introduction

Finding the file for a particular class can be an expensive task. Luckily, the ClassLoader component comes with two classes to cache the mapping from a class to its containing file. Both the ApcClassLoader and the XcacheClassLoader wrap around an object which implements a findFile() method to find the file for a class.

注解

Both the ApcClassLoader and the XcacheClassLoader can be used to cache Composer’s autoloader.

ApcClassLoader

2.1 新版功能: The ApcClassLoader class was introduced in Symfony 2.1.

ApcClassLoader wraps an existing class loader and caches calls to its findFile() method using APC:

require_once '/path/to/src/Symfony/Component/ClassLoader/ApcClassLoader.php';

// instance of a class that implements a findFile() method, like the ClassLoader
$loader = ...;

// sha1(__FILE__) generates an APC namespace prefix
$cachedLoader = new ApcClassLoader(sha1(__FILE__), $loader);

// register the cached class loader
$cachedLoader->register();

// deactivate the original, non-cached loader if it was registered previously
$loader->unregister();
XcacheClassLoader

2.1 新版功能: The XcacheClassLoader class was introduced in Symfony 2.1.

XcacheClassLoader uses XCache to cache a class loader. Registering it is straightforward:

require_once '/path/to/src/Symfony/Component/ClassLoader/XcacheClassLoader.php';

// instance of a class that implements a findFile() method, like the ClassLoader
$loader = ...;

// sha1(__FILE__) generates an XCache namespace prefix
$cachedLoader = new XcacheClassLoader(sha1(__FILE__), $loader);

// register the cached class loader
$cachedLoader->register();

// deactivate the original, non-cached loader if it was registered previously
$loader->unregister();
Debugging a Class Loader

2.1 新版功能: The DebugClassLoader class was introduced in Symfony 2.1.

The DebugClassLoader attempts to throw more helpful exceptions when a class isn’t found by the registered autoloaders. All autoloaders that implement a findFile() method are replaced with a DebugClassLoader wrapper.

Using the DebugClassLoader is as easy as calling its static enable() method:

use Symfony\Component\ClassLoader\DebugClassLoader;

DebugClassLoader::enable();
The Class Map Generator

Loading a class usually is an easy task given the PSR-0 and PSR-4 standards. Thanks to the Symfony ClassLoader component or the autoloading mechanism provided by Composer, you don’t have to map your class names to actual PHP files manually. Nowadays, PHP libraries usually come with autoloading support through Composer.

But from time to time you may have to use a third-party library that comes without any autoloading support and therefore forces you to load each class manually. For example, imagine a library with the following directory structure:

library/
├── bar/
│   ├── baz/
│   │   └── Boo.php
│   └── Foo.php
└── foo/
    ├── bar/
    │   └── Foo.php
    └── Bar.php

These files contain the following classes:

File Class Name
library/bar/baz/Boo.php Acme\Bar\Baz
library/bar/Foo.php Acme\Bar
library/foo/bar/Foo.php Acme\Foo\Bar
library/foo/Bar.php Acme\Foo

To make your life easier, the ClassLoader component comes with a ClassMapGenerator class that makes it possible to create a map of class names to files.

Generating a Class Map

To generate the class map, simply pass the root directory of your class files to the createMap() method:

use Symfony\Component\ClassLoader\ClassMapGenerator;

print_r(ClassMapGenerator::createMap(__DIR__.'/library'));

Given the files and class from the table above, you should see an output like this:

Array
(
    [Acme\Foo] => /var/www/library/foo/Bar.php
    [Acme\Foo\Bar] => /var/www/library/foo/bar/Foo.php
    [Acme\Bar\Baz] => /var/www/library/bar/baz/Boo.php
    [Acme\Bar] => /var/www/library/bar/Foo.php
)
Dumping the Class Map

Writing the class map to the console output is not really sufficient when it comes to autoloading. Luckily, the ClassMapGenerator provides the dump() method to save the generated class map to the filesystem:

use Symfony\Component\ClassLoader\ClassMapGenerator;

ClassMapGenerator::dump(__DIR__.'/library', __DIR__.'/class_map.php');

This call to dump() generates the class map and writes it to the class_map.php file in the same directory with the following contents:

<?php return array (
'Acme\\Foo' => '/var/www/library/foo/Bar.php',
'Acme\\Foo\\Bar' => '/var/www/library/foo/bar/Foo.php',
'Acme\\Bar\\Baz' => '/var/www/library/bar/baz/Boo.php',
'Acme\\Bar' => '/var/www/library/bar/Foo.php',
);

Instead of loading each file manually, you’ll only have to register the generated class map with, for example, the MapClassLoader:

use Symfony\Component\ClassLoader\MapClassLoader;

$mapping = include __DIR__.'/class_map.php';
$loader = new MapClassLoader($mapping);
$loader->register();

// you can now use the classes:
use Acme\Foo;

$foo = new Foo();

// ...

注解

The example assumes that you already have autoloading working (e.g. through Composer or one of the other class loaders from the ClassLoader component.

Besides dumping the class map for one directory, you can also pass an array of directories for which to generate the class map (the result actually is the same as in the example above):

use Symfony\Component\ClassLoader\ClassMapGenerator;

ClassMapGenerator::dump(
    array(__DIR__.'/library/bar', __DIR__.'/library/foo'),
    __DIR__.'/class_map.php'
);

Config

The Config Component
The Config component provides several classes to help you find, load, combine, autofill and validate configuration values of any kind, whatever their source may be (YAML, XML, INI files, or for instance a database).

警告

The IniFileLoader parses the file contents using the parse_ini_file function, therefore, you can only set parameters to string values. To set parameters to other data types (e.g. boolean, integer, etc), the other loaders are recommended.

Installation

You can install the component in 2 different ways:

Loading Resources
Locating Resources

Loading the configuration normally starts with a search for resources – in most cases: files. This can be done with the FileLocator:

use Symfony\Component\Config\FileLocator;

$configDirectories = array(__DIR__.'/app/config');

$locator = new FileLocator($configDirectories);
$yamlUserFiles = $locator->locate('users.yml', null, false);

The locator receives a collection of locations where it should look for files. The first argument of locate() is the name of the file to look for. The second argument may be the current path and when supplied, the locator will look in this directory first. The third argument indicates whether or not the locator should return the first file it has found, or an array containing all matches.

Resource Loaders

For each type of resource (YAML, XML, annotation, etc.) a loader must be defined. Each loader should implement LoaderInterface or extend the abstract FileLoader class, which allows for recursively importing other resources:

use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Yaml\Yaml;

class YamlUserLoader extends FileLoader
{
    public function load($resource, $type = null)
    {
        $configValues = Yaml::parse(file_get_contents($resource));

        // ... handle the config values

        // maybe import some other resource:

        // $this->import('extra_users.yml');
    }

    public function supports($resource, $type = null)
    {
        return is_string($resource) && 'yml' === pathinfo(
            $resource,
            PATHINFO_EXTENSION
        );
    }
}
Finding the right Loader

The LoaderResolver receives as its first constructor argument a collection of loaders. When a resource (for instance an XML file) should be loaded, it loops through this collection of loaders and returns the loader which supports this particular resource type.

The DelegatingLoader makes use of the LoaderResolver. When it is asked to load a resource, it delegates this question to the LoaderResolver. In case the resolver has found a suitable loader, this loader will be asked to load the resource:

use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\Loader\DelegatingLoader;

$loaderResolver = new LoaderResolver(array(new YamlUserLoader($locator)));
$delegatingLoader = new DelegatingLoader($loaderResolver);

$delegatingLoader->load(__DIR__.'/users.yml');
/*
The YamlUserLoader will be used to load this resource,
since it supports files with a "yml" extension
*/
Caching Based on Resources

When all configuration resources are loaded, you may want to process the configuration values and combine them all in one file. This file acts like a cache. Its contents don’t have to be regenerated every time the application runs – only when the configuration resources are modified.

For example, the Symfony Routing component allows you to load all routes, and then dump a URL matcher or a URL generator based on these routes. In this case, when one of the resources is modified (and you are working in a development environment), the generated file should be invalidated and regenerated. This can be accomplished by making use of the ConfigCache class.

The example below shows you how to collect resources, then generate some code based on the resources that were loaded, and write this code to the cache. The cache also receives the collection of resources that were used for generating the code. By looking at the “last modified” timestamp of these resources, the cache can tell if it is still fresh or that its contents should be regenerated:

use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;

$cachePath = __DIR__.'/cache/appUserMatcher.php';

// the second argument indicates whether or not you want to use debug mode
$userMatcherCache = new ConfigCache($cachePath, true);

if (!$userMatcherCache->isFresh()) {
    // fill this with an array of 'users.yml' file paths
    $yamlUserFiles = ...;

    $resources = array();

    foreach ($yamlUserFiles as $yamlUserFile) {
        // see the previous article "Loading resources" to
        // see where $delegatingLoader comes from
        $delegatingLoader->load($yamlUserFile);
        $resources[] = new FileResource($yamlUserFile);
    }

    // the code for the UserMatcher is generated elsewhere
    $code = ...;

    $userMatcherCache->write($code, $resources);
}

// you may want to require the cached code:
require $cachePath;

In debug mode, a .meta file will be created in the same directory as the cache file itself. This .meta file contains the serialized resources, whose timestamps are used to determine if the cache is still fresh. When not in debug mode, the cache is considered to be “fresh” as soon as it exists, and therefore no .meta file will be generated.

Defining and Processing Configuration Values
Validating Configuration Values

After loading configuration values from all kinds of resources, the values and their structure can be validated using the “Definition” part of the Config Component. Configuration values are usually expected to show some kind of hierarchy. Also, values should be of a certain type, be restricted in number or be one of a given set of values. For example, the following configuration (in YAML) shows a clear hierarchy and some validation rules that should be applied to it (like: “the value for auto_connect must be a boolean value”):

auto_connect: true
default_connection: mysql
connections:
    mysql:
        host:     localhost
        driver:   mysql
        username: user
        password: pass
    sqlite:
        host:     localhost
        driver:   sqlite
        memory:   true
        username: user
        password: pass

When loading multiple configuration files, it should be possible to merge and overwrite some values. Other values should not be merged and stay as they are when first encountered. Also, some keys are only available when another key has a specific value (in the sample configuration above: the memory key only makes sense when the driver is sqlite).

Defining a Hierarchy of Configuration Values Using the TreeBuilder

All the rules concerning configuration values can be defined using the TreeBuilder.

A TreeBuilder instance should be returned from a custom Configuration class which implements the ConfigurationInterface:

namespace Acme\DatabaseConfiguration;

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;

class DatabaseConfiguration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('database');

        // ... add node definitions to the root of the tree

        return $treeBuilder;
    }
}
Adding Node Definitions to the Tree
Variable Nodes

A tree contains node definitions which can be laid out in a semantic way. This means, using indentation and the fluent notation, it is possible to reflect the real structure of the configuration values:

$rootNode
    ->children()
        ->booleanNode('auto_connect')
            ->defaultTrue()
        ->end()
        ->scalarNode('default_connection')
            ->defaultValue('default')
        ->end()
    ->end()
;

The root node itself is an array node, and has children, like the boolean node auto_connect and the scalar node default_connection. In general: after defining a node, a call to end() takes you one step up in the hierarchy.

Node Type

It is possible to validate the type of a provided value by using the appropriate node definition. Node type are available for:

  • scalar
  • boolean
  • integer (new in 2.2)
  • float (new in 2.2)
  • enum (new in 2.1)
  • array
  • variable (no validation)

and are created with node($name, $type) or their associated shortcut xxxxNode($name) method.

Numeric Node Constraints

2.2 新版功能: The numeric (float and integer) nodes were introduced in Symfony 2.2.

Numeric nodes (float and integer) provide two extra constraints - min() and max() - allowing to validate the value:

$rootNode
    ->children()
        ->integerNode('positive_value')
            ->min(0)
        ->end()
        ->floatNode('big_value')
            ->max(5E45)
        ->end()
        ->integerNode('value_inside_a_range')
            ->min(-50)->max(50)
        ->end()
    ->end()
;
Enum Nodes

2.1 新版功能: The enum node was introduced in Symfony 2.1.

Enum nodes provide a constraint to match the given input against a set of values:

$rootNode
    ->children()
        ->enumNode('gender')
            ->values(array('male', 'female'))
        ->end()
    ->end()
;

This will restrict the gender option to be either male or female.

Array Nodes

It is possible to add a deeper level to the hierarchy, by adding an array node. The array node itself, may have a pre-defined set of variable nodes:

$rootNode
    ->children()
        ->arrayNode('connection')
            ->children()
                ->scalarNode('driver')->end()
                ->scalarNode('host')->end()
                ->scalarNode('username')->end()
                ->scalarNode('password')->end()
            ->end()
        ->end()
    ->end()
;

Or you may define a prototype for each node inside an array node:

$rootNode
    ->children()
        ->arrayNode('connections')
            ->prototype('array')
                ->children()
                    ->scalarNode('driver')->end()
                    ->scalarNode('host')->end()
                    ->scalarNode('username')->end()
                    ->scalarNode('password')->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

A prototype can be used to add a definition which may be repeated many times inside the current node. According to the prototype definition in the example above, it is possible to have multiple connection arrays (containing a driver, host, etc.).

Array Node Options

Before defining the children of an array node, you can provide options like:

useAttributeAsKey()
Provide the name of a child node, whose value should be used as the key in the resulting array.
requiresAtLeastOneElement()
There should be at least one element in the array (works only when isRequired() is also called).
addDefaultsIfNotSet()
If any child nodes have default values, use them if explicit values haven’t been provided.

An example of this:

$rootNode
    ->children()
        ->arrayNode('parameters')
            ->isRequired()
            ->requiresAtLeastOneElement()
            ->useAttributeAsKey('name')
            ->prototype('array')
                ->children()
                    ->scalarNode('value')->isRequired()->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

In YAML, the configuration might look like this:

database:
    parameters:
        param1: { value: param1val }

In XML, each parameters node would have a name attribute (along with value), which would be removed and used as the key for that element in the final array. The useAttributeAsKey is useful for normalizing how arrays are specified between different formats like XML and YAML.

Default and required Values

For all node types, it is possible to define default values and replacement values in case a node has a certain value:

defaultValue()
Set a default value
isRequired()
Must be defined (but may be empty)
cannotBeEmpty()
May not contain an empty value
default*()
(null, true, false), shortcut for defaultValue()
treat*Like()
(null, true, false), provide a replacement value in case the value is *.
$rootNode
    ->children()
        ->arrayNode('connection')
            ->children()
                ->scalarNode('driver')
                    ->isRequired()
                    ->cannotBeEmpty()
                ->end()
                ->scalarNode('host')
                    ->defaultValue('localhost')
                ->end()
                ->scalarNode('username')->end()
                ->scalarNode('password')->end()
                ->booleanNode('memory')
                    ->defaultFalse()
                ->end()
            ->end()
        ->end()
        ->arrayNode('settings')
            ->addDefaultsIfNotSet()
            ->children()
                ->scalarNode('name')
                    ->isRequired()
                    ->cannotBeEmpty()
                    ->defaultValue('value')
                ->end()
            ->end()
        ->end()
    ->end()
;
Documenting the Option

All options can be documented using the info() method.

The info will be printed as a comment when dumping the configuration tree.

Optional Sections

2.2 新版功能: The canBeEnabled and canBeDisabled methods were introduced in Symfony 2.2.

If you have entire sections which are optional and can be enabled/disabled, you can take advantage of the shortcut canBeEnabled() and canBeDisabled() methods:

$arrayNode
    ->canBeEnabled()
;

// is equivalent to

$arrayNode
    ->treatFalseLike(array('enabled' => false))
    ->treatTrueLike(array('enabled' => true))
    ->treatNullLike(array('enabled' => true))
    ->children()
        ->booleanNode('enabled')
            ->defaultFalse()
;

The canBeDisabled method looks about the same except that the section would be enabled by default.

Merging Options

Extra options concerning the merge process may be provided. For arrays:

performNoDeepMerging()
When the value is also defined in a second configuration array, don’t try to merge an array, but overwrite it entirely

For all nodes:

cannotBeOverwritten()
don’t let other configuration arrays overwrite an existing value for this node
Appending Sections

If you have a complex configuration to validate then the tree can grow to be large and you may want to split it up into sections. You can do this by making a section a separate node and then appending it into the main tree with append():

public function getConfigTreeBuilder()
{
    $treeBuilder = new TreeBuilder();
    $rootNode = $treeBuilder->root('database');

    $rootNode
        ->children()
            ->arrayNode('connection')
                ->children()
                    ->scalarNode('driver')
                        ->isRequired()
                        ->cannotBeEmpty()
                    ->end()
                    ->scalarNode('host')
                        ->defaultValue('localhost')
                    ->end()
                    ->scalarNode('username')->end()
                    ->scalarNode('password')->end()
                    ->booleanNode('memory')
                        ->defaultFalse()
                    ->end()
                ->end()
                ->append($this->addParametersNode())
            ->end()
        ->end()
    ;

    return $treeBuilder;
}

public function addParametersNode()
{
    $builder = new TreeBuilder();
    $node = $builder->root('parameters');

    $node
        ->isRequired()
        ->requiresAtLeastOneElement()
        ->useAttributeAsKey('name')
        ->prototype('array')
            ->children()
                ->scalarNode('value')->isRequired()->end()
            ->end()
        ->end()
    ;

    return $node;
}

This is also useful to help you avoid repeating yourself if you have sections of the config that are repeated in different places.

Normalization

When the config files are processed they are first normalized, then merged and finally the tree is used to validate the resulting array. The normalization process is used to remove some of the differences that result from different configuration formats, mainly the differences between YAML and XML.

The separator used in keys is typically _ in YAML and - in XML. For example, auto_connect in YAML and auto-connect in XML. The normalization would make both of these auto_connect.

警告

The target key will not be altered if it’s mixed like foo-bar_moo or if it already exists.

Another difference between YAML and XML is in the way arrays of values may be represented. In YAML you may have:

twig:
    extensions: ['twig.extension.foo', 'twig.extension.bar']

and in XML:

<twig:config>
    <twig:extension>twig.extension.foo</twig:extension>
    <twig:extension>twig.extension.bar</twig:extension>
</twig:config>

This difference can be removed in normalization by pluralizing the key used in XML. You can specify that you want a key to be pluralized in this way with fixXmlConfig():

$rootNode
    ->fixXmlConfig('extension')
    ->children()
        ->arrayNode('extensions')
            ->prototype('scalar')->end()
        ->end()
    ->end()
;

If it is an irregular pluralization you can specify the plural to use as a second argument:

$rootNode
    ->fixXmlConfig('child', 'children')
    ->children()
        ->arrayNode('children')
            // ...
        ->end()
    ->end()
;

As well as fixing this, fixXmlConfig ensures that single XML elements are still turned into an array. So you may have:

<connection>default</connection>
<connection>extra</connection>

and sometimes only:

<connection>default</connection>

By default connection would be an array in the first case and a string in the second making it difficult to validate. You can ensure it is always an array with fixXmlConfig.

You can further control the normalization process if you need to. For example, you may want to allow a string to be set and used as a particular key or several keys to be set explicitly. So that, if everything apart from name is optional in this config:

connection:
    name:     my_mysql_connection
    host:     localhost
    driver:   mysql
    username: user
    password: pass

you can allow the following as well:

connection: my_mysql_connection

By changing a string value into an associative array with name as the key:

$rootNode
    ->children()
        ->arrayNode('connection')
            ->beforeNormalization()
                ->ifString()
                ->then(function ($v) { return array('name' => $v); })
            ->end()
            ->children()
                ->scalarNode('name')->isRequired()
                // ...
            ->end()
        ->end()
    ->end()
;
Validation Rules

More advanced validation rules can be provided using the ExprBuilder. This builder implements a fluent interface for a well-known control structure. The builder is used for adding advanced validation rules to node definitions, like:

$rootNode
    ->children()
        ->arrayNode('connection')
            ->children()
                ->scalarNode('driver')
                    ->isRequired()
                    ->validate()
                    ->ifNotInArray(array('mysql', 'sqlite', 'mssql'))
                        ->thenInvalid('Invalid database driver "%s"')
                    ->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

A validation rule always has an “if” part. You can specify this part in the following ways:

  • ifTrue()
  • ifString()
  • ifNull()
  • ifArray()
  • ifInArray()
  • ifNotInArray()
  • always()

A validation rule also requires a “then” part:

  • then()
  • thenEmptyArray()
  • thenInvalid()
  • thenUnset()

Usually, “then” is a closure. Its return value will be used as a new value for the node, instead of the node’s original value.

Processing Configuration Values

The Processor uses the tree as it was built using the TreeBuilder to process multiple arrays of configuration values that should be merged. If any value is not of the expected type, is mandatory and yet undefined, or could not be validated in some other way, an exception will be thrown. Otherwise the result is a clean array of configuration values:

use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Definition\Processor;
use Acme\DatabaseConfiguration;

$config1 = Yaml::parse(file_get_contents(__DIR__.'/src/Matthias/config/config.yml'));
$config2 = Yaml::parse(file_get_contents(__DIR__.'/src/Matthias/config/config_extra.yml'));

$configs = array($config1, $config2);

$processor = new Processor();
$configuration = new DatabaseConfiguration();
$processedConfiguration = $processor->processConfiguration(
    $configuration,
    $configs
);

Console

The Console Component
The Console component eases the creation of beautiful and testable command line interfaces.

The Console component allows you to create command-line commands. Your console commands can be used for any recurring task, such as cronjobs, imports, or other batch jobs.

Installation

You can install the component in 2 different ways:

注解

Windows does not support ANSI colors by default so the Console component detects and disables colors where Windows does not have support. However, if Windows is not configured with an ANSI driver and your console commands invoke other scripts which emit ANSI color sequences, they will be shown as raw escape characters.

To enable ANSI color support for Windows, please install ANSICON.

Creating a basic Command

To make a console command that greets you from the command line, create GreetCommand.php and add the following to it:

namespace Acme\Console\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class GreetCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('demo:greet')
            ->setDescription('Greet someone')
            ->addArgument(
                'name',
                InputArgument::OPTIONAL,
                'Who do you want to greet?'
            )
            ->addOption(
               'yell',
               null,
               InputOption::VALUE_NONE,
               'If set, the task will yell in uppercase letters'
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        if ($name) {
            $text = 'Hello '.$name;
        } else {
            $text = 'Hello';
        }

        if ($input->getOption('yell')) {
            $text = strtoupper($text);
        }

        $output->writeln($text);
    }
}

You also need to create the file to run at the command line which creates an Application and adds commands to it:

#!/usr/bin/env php
<?php
// application.php

require __DIR__.'/vendor/autoload.php';

use Acme\Console\Command\GreetCommand;
use Symfony\Component\Console\Application;

$application = new Application();
$application->add(new GreetCommand);
$application->run();

Test the new console command by running the following

$ php application.php demo:greet Fabien

This will print the following to the command line:

Hello Fabien

You can also use the --yell option to make everything uppercase:

$ php application.php demo:greet Fabien --yell

This prints:

HELLO FABIEN
Coloring the Output

Whenever you output text, you can surround the text with tags to color its output. For example:

// green text
$output->writeln('<info>foo</info>');

// yellow text
$output->writeln('<comment>foo</comment>');

// black text on a cyan background
$output->writeln('<question>foo</question>');

// white text on a red background
$output->writeln('<error>foo</error>');

It is possible to define your own styles using the class OutputFormatterStyle:

use Symfony\Component\Console\Formatter\OutputFormatterStyle;

// ...
$style = new OutputFormatterStyle('red', 'yellow', array('bold', 'blink'));
$output->getFormatter()->setStyle('fire', $style);
$output->writeln('<fire>foo</fire>');

Available foreground and background colors are: black, red, green, yellow, blue, magenta, cyan and white.

And available options are: bold, underscore, blink, reverse and conceal.

You can also set these colors and options inside the tagname:

// green text
$output->writeln('<fg=green>foo</fg=green>');

// black text on a cyan background
$output->writeln('<fg=black;bg=cyan>foo</fg=black;bg=cyan>');

// bold text on a yellow background
$output->writeln('<bg=yellow;options=bold>foo</bg=yellow;options=bold>');
Verbosity Levels

2.3 新版功能: The VERBOSITY_VERY_VERBOSE and VERBOSITY_DEBUG constants were introduced in version 2.3

The console has 5 levels of verbosity. These are defined in the OutputInterface:

Mode Value
OutputInterface::VERBOSITY_QUIET Do not output any messages
OutputInterface::VERBOSITY_NORMAL The default verbosity level
OutputInterface::VERBOSITY_VERBOSE Increased verbosity of messages
OutputInterface::VERBOSITY_VERY_VERBOSE Informative non essential messages
OutputInterface::VERBOSITY_DEBUG Debug messages

You can specify the quiet verbosity level with the --quiet or -q option. The --verbose or -v option is used when you want an increased level of verbosity.

小技巧

The full exception stacktrace is printed if the VERBOSITY_VERBOSE level or above is used.

It is possible to print a message in a command for only a specific verbosity level. For example:

if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
    $output->writeln(...);
}

When the quiet level is used, all output is suppressed as the default write() method returns without actually printing.

Using Command Arguments

The most interesting part of the commands are the arguments and options that you can make available. Arguments are the strings - separated by spaces - that come after the command name itself. They are ordered, and can be optional or required. For example, add an optional last_name argument to the command and make the name argument required:

$this
    // ...
    ->addArgument(
        'name',
        InputArgument::REQUIRED,
        'Who do you want to greet?'
    )
    ->addArgument(
        'last_name',
        InputArgument::OPTIONAL,
        'Your last name?'
    );

You now have access to a last_name argument in your command:

if ($lastName = $input->getArgument('last_name')) {
    $text .= ' '.$lastName;
}

The command can now be used in either of the following ways:

$ php application.php demo:greet Fabien
$ php application.php demo:greet Fabien Potencier

It is also possible to let an argument take a list of values (imagine you want to greet all your friends). For this it must be specified at the end of the argument list:

$this
    // ...
    ->addArgument(
        'names',
        InputArgument::IS_ARRAY,
        'Who do you want to greet (separate multiple names with a space)?'
    );

To use this, just specify as many names as you want:

$ php application.php demo:greet Fabien Ryan Bernhard

You can access the names argument as an array:

if ($names = $input->getArgument('names')) {
    $text .= ' '.implode(', ', $names);
}

There are 3 argument variants you can use:

Mode Value
InputArgument::REQUIRED The argument is required
InputArgument::OPTIONAL The argument is optional and therefore can be omitted
InputArgument::IS_ARRAY The argument can contain an indefinite number of arguments and must be used at the end of the argument list

You can combine IS_ARRAY with REQUIRED and OPTIONAL like this:

$this
    // ...
    ->addArgument(
        'names',
        InputArgument::IS_ARRAY | InputArgument::REQUIRED,
        'Who do you want to greet (separate multiple names with a space)?'
    );
Using Command Options

Unlike arguments, options are not ordered (meaning you can specify them in any order) and are specified with two dashes (e.g. --yell - you can also declare a one-letter shortcut that you can call with a single dash like -y). Options are always optional, and can be setup to accept a value (e.g. --dir=src) or simply as a boolean flag without a value (e.g. --yell).

小技巧

It is also possible to make an option optionally accept a value (so that --yell, --yell=loud or --yell loud work). Options can also be configured to accept an array of values.

For example, add a new option to the command that can be used to specify how many times in a row the message should be printed:

$this
    // ...
    ->addOption(
        'iterations',
        null,
        InputOption::VALUE_REQUIRED,
        'How many times should the message be printed?',
        1
    );

Next, use this in the command to print the message multiple times:

for ($i = 0; $i < $input->getOption('iterations'); $i++) {
    $output->writeln($text);
}

Now, when you run the task, you can optionally specify a --iterations flag:

$ php application.php demo:greet Fabien
$ php application.php demo:greet Fabien --iterations=5

The first example will only print once, since iterations is empty and defaults to 1 (the last argument of addOption). The second example will print five times.

Recall that options don’t care about their order. So, either of the following will work:

$ php application.php demo:greet Fabien --iterations=5 --yell
$ php application.php demo:greet Fabien --yell --iterations=5

There are 4 option variants you can use:

Option Value
InputOption::VALUE_IS_ARRAY This option accepts multiple values (e.g. --dir=/foo --dir=/bar)
InputOption::VALUE_NONE Do not accept input for this option (e.g. --yell)
InputOption::VALUE_REQUIRED This value is required (e.g. --iterations=5), the option itself is still optional
InputOption::VALUE_OPTIONAL This option may or may not have a value (e.g. --yell or --yell=loud)

You can combine VALUE_IS_ARRAY with VALUE_REQUIRED or VALUE_OPTIONAL like this:

$this
    // ...
    ->addOption(
        'colors',
        null,
        InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
        'Which colors do you like?',
        array('blue', 'red')
    );
Console Helpers

The console component also contains a set of “helpers” - different small tools capable of helping you with different tasks:

Testing Commands

Symfony provides several tools to help you test your commands. The most useful one is the CommandTester class. It uses special input and output classes to ease testing without a real console:

use Acme\Console\Command\GreetCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;

class ListCommandTest extends \PHPUnit_Framework_TestCase
{
    public function testExecute()
    {
        $application = new Application();
        $application->add(new GreetCommand());

        $command = $application->find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester->execute(array('command' => $command->getName()));

        $this->assertRegExp('/.../', $commandTester->getDisplay());

        // ...
    }
}

The getDisplay() method returns what would have been displayed during a normal call from the console.

You can test sending arguments and options to the command by passing them as an array to the execute() method:

use Acme\Console\Command\GreetCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;

class ListCommandTest extends \PHPUnit_Framework_TestCase
{
    // ...

    public function testNameIsOutput()
    {
        $application = new Application();
        $application->add(new GreetCommand());

        $command = $application->find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester->execute(array(
            'command'      => $command->getName(),
            'name'         => 'Fabien',
            '--iterations' => 5,
        ));

        $this->assertRegExp('/Fabien/', $commandTester->getDisplay());
    }
}

小技巧

You can also test a whole console application by using ApplicationTester.

Calling an Existing Command

If a command depends on another one being run before it, instead of asking the user to remember the order of execution, you can call it directly yourself. This is also useful if you want to create a “meta” command that just runs a bunch of other commands (for instance, all commands that need to be run when the project’s code has changed on the production servers: clearing the cache, generating Doctrine2 proxies, dumping Assetic assets, ...).

Calling a command from another one is straightforward:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $command = $this->getApplication()->find('demo:greet');

    $arguments = array(
        'command' => 'demo:greet',
        'name'    => 'Fabien',
        '--yell'  => true,
    );

    $input = new ArrayInput($arguments);
    $returnCode = $command->run($input, $output);

    // ...
}

First, you find() the command you want to execute by passing the command name.

Then, you need to create a new ArrayInput with the arguments and options you want to pass to the command.

Eventually, calling the run() method actually executes the command and returns the returned code from the command (return value from command’s execute() method).

注解

Most of the time, calling a command from code that is not executed on the command line is not a good idea for several reasons. First, the command’s output is optimized for the console. But more important, you can think of a command as being like a controller; it should use the model to do something and display feedback to the user. So, instead of calling a command from the Web, refactor your code and move the logic to a new class.

Using Console Commands, Shortcuts and Built-in Commands

In addition to the options you specify for your commands, there are some built-in options as well as a couple of built-in commands for the Console component.

注解

These examples assume you have added a file application.php to run at the cli:

#!/usr/bin/env php
<?php
// application.php

use Symfony\Component\Console\Application;

$application = new Application();
// ...
$application->run();
Built-in Commands

There is a built-in command list which outputs all the standard options and the registered commands:

$ php application.php list

You can get the same output by not running any command as well

$ php application.php

The help command lists the help information for the specified command. For example, to get the help for the list command:

$ php application.php help list

Running help without specifying a command will list the global options:

$ php application.php help
Global Options

You can get help information for any command with the --help option. To get help for the list command:

$ php application.php list --help
$ php application.php list -h

You can suppress output with:

$ php application.php list --quiet
$ php application.php list -q

You can get more verbose messages (if this is supported for a command) with:

$ php application.php list --verbose
$ php application.php list -v

The verbose flag can optionally take a value between 1 (default) and 3 to output even more verbose messages:

$ php application.php list --verbose=2
$ php application.php list -vv
$ php application.php list --verbose=3
$ php application.php list -vvv

If you set the optional arguments to give your application a name and version:

$application = new Application('Acme Console Application', '1.2');

then you can use:

$ php application.php list --version
$ php application.php list -V

to get this information output:

Acme Console Application version 1.2

If you do not provide both arguments then it will just output:

console tool

You can force turning on ANSI output coloring with:

$ php application.php list --ansi

or turn it off with:

$ php application.php list --no-ansi

You can suppress any interactive questions from the command you are running with:

$ php application.php list --no-interaction
$ php application.php list -n
Shortcut Syntax

You do not have to type out the full command names. You can just type the shortest unambiguous name to run a command. So if there are non-clashing commands, then you can run help like this:

$ php application.php h

If you have commands using : to namespace commands then you just have to type the shortest unambiguous text for each part. If you have created the demo:greet as shown in The Console Component then you can run it with:

$ php application.php d:g Fabien

If you enter a short command that’s ambiguous (i.e. there are more than one command that match), then no command will be run and some suggestions of the possible commands to choose from will be output.

Building a single Command Application

When building a command line tool, you may not need to provide several commands. In such case, having to pass the command name each time is tedious. Fortunately, it is possible to remove this need by extending the application:

namespace Acme\Tool;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;

class MyApplication extends Application
{
    /**
     * Gets the name of the command based on input.
     *
     * @param InputInterface $input The input interface
     *
     * @return string The command name
     */
    protected function getCommandName(InputInterface $input)
    {
        // This should return the name of your command.
        return 'my_command';
    }

    /**
     * Gets the default commands that should always be available.
     *
     * @return array An array of default Command instances
     */
    protected function getDefaultCommands()
    {
        // Keep the core default commands to have the HelpCommand
        // which is used when using the --help option
        $defaultCommands = parent::getDefaultCommands();

        $defaultCommands[] = new MyCommand();

        return $defaultCommands;
    }

    /**
     * Overridden so that the application doesn't expect the command
     * name to be the first argument.
     */
    public function getDefinition()
    {
        $inputDefinition = parent::getDefinition();
        // clear out the normal first argument, which is the command name
        $inputDefinition->setArguments();

        return $inputDefinition;
    }
}

When calling your console script, the command MyCommand will then always be used, without having to pass its name.

You can also simplify how you execute the application:

#!/usr/bin/env php
<?php
// command.php

use Acme\Tool\MyApplication;

$application = new MyApplication();
$application->run();
Understanding how Console Arguments Are Handled

It can be difficult to understand the way arguments are handled by the console application. The Symfony Console application, like many other CLI utility tools, follows the behavior described in the docopt standards.

Have a look at the following command that has three options:

namespace Acme\Console\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class DemoArgsCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('demo:args')
            ->setDescription('Describe args behaviors')
            ->setDefinition(
                new InputDefinition(array(
                    new InputOption('foo', 'f'),
                    new InputOption('bar', 'b', InputOption::VALUE_REQUIRED),
                    new InputOption('cat', 'c', InputOption::VALUE_OPTIONAL),
                ))
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
       // ...
    }
}

Since the foo option doesn’t accept a value, it will be either false (when it is not passed to the command) or true (when --foo was passed by the user). The value of the bar option (and its b shortcut respectively) is required. It can be separated from the option name either by spaces or = characters. The cat option (and its c shortcut) behaves similar except that it doesn’t require a value. Have a look at the following table to get an overview of the possible ways to pass options:

Input foo bar cat
--bar=Hello false "Hello" null
--bar Hello false "Hello" null
-b=Hello false "Hello" null
-b Hello false "Hello" null
-bHello false "Hello" null
-fcWorld -b Hello true "Hello" "World"
-cfWorld -b Hello false "Hello" "fWorld"
-cbWorld false null "bWorld"

Things get a little bit more tricky when the command also accepts an optional argument:

// ...

new InputDefinition(array(
    // ...
    new InputArgument('arg', InputArgument::OPTIONAL),
));

You might have to use the special -- separator to separate options from arguments. Have a look at the fifth example in the following table where it is used to tell the command that World is the value for arg and not the value of the optional cat option:

Input bar cat arg
--bar Hello "Hello" null null
--bar Hello World "Hello" null "World"
--bar "Hello World" "Hello World" null null
--bar Hello --cat World "Hello" "World" null
--bar Hello --cat -- World "Hello" null "World"
-b Hello -c World "Hello" "World" null
Using Events

2.3 新版功能: Console events were introduced in Symfony 2.3.

The Application class of the Console component allows you to optionally hook into the lifecycle of a console application via events. Instead of reinventing the wheel, it uses the Symfony EventDispatcher component to do the work:

use Symfony\Component\Console\Application;
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();

$application = new Application();
$application->setDispatcher($dispatcher);
$application->run();
The ConsoleEvents::COMMAND Event

Typical Purposes: Doing something before any command is run (like logging which command is going to be executed), or displaying something about the event to be executed.

Just before executing any command, the ConsoleEvents::COMMAND event is dispatched. Listeners receive a ConsoleCommandEvent event:

use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\ConsoleEvents;

$dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event) {
    // get the input instance
    $input = $event->getInput();

    // get the output instance
    $output = $event->getOutput();

    // get the command to be executed
    $command = $event->getCommand();

    // write something about the command
    $output->writeln(sprintf('Before running command <info>%s</info>', $command->getName()));

    // get the application
    $application = $command->getApplication();
});
The ConsoleEvents::TERMINATE Event

Typical Purposes: To perform some cleanup actions after the command has been executed.

After the command has been executed, the ConsoleEvents::TERMINATE event is dispatched. It can be used to do any actions that need to be executed for all commands or to cleanup what you initiated in a ConsoleEvents::COMMAND listener (like sending logs, closing a database connection, sending emails, ...). A listener might also change the exit code.

Listeners receive a ConsoleTerminateEvent event:

use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\ConsoleEvents;

$dispatcher->addListener(ConsoleEvents::TERMINATE, function (ConsoleTerminateEvent $event) {
    // get the output
    $output = $event->getOutput();

    // get the command that has been executed
    $command = $event->getCommand();

    // display something
    $output->writeln(sprintf('After running command <info>%s</info>', $command->getName()));

    // change the exit code
    $event->setExitCode(128);
});

小技巧

This event is also dispatched when an exception is thrown by the command. It is then dispatched just before the ConsoleEvents::EXCEPTION event. The exit code received in this case is the exception code.

The ConsoleEvents::EXCEPTION Event

Typical Purposes: Handle exceptions thrown during the execution of a command.

Whenever an exception is thrown by a command, the ConsoleEvents::EXCEPTION event is dispatched. A listener can wrap or change the exception or do anything useful before the exception is thrown by the application.

Listeners receive a ConsoleExceptionEvent event:

use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\ConsoleEvents;

$dispatcher->addListener(ConsoleEvents::EXCEPTION, function (ConsoleExceptionEvent $event) {
    $output = $event->getOutput();

    $command = $event->getCommand();

    $output->writeln(sprintf('Oops, exception thrown while running command <info>%s</info>', $command->getName()));

    // get the current exit code (the exception code or the exit code set by a ConsoleEvents::TERMINATE event)
    $exitCode = $event->getExitCode();

    // change the exception to another one
    $event->setException(new \LogicException('Caught exception', $exitCode, $event->getException()));
});
The Console Helpers
Dialog Helper

The DialogHelper provides functions to ask the user for more information. It is included in the default helper set, which you can get by calling getHelperSet():

$dialog = $this->getHelper('dialog');

All the methods inside the Dialog Helper have an OutputInterface as the first argument, the question as the second argument and the default value as the last argument.

Asking the User for Confirmation

Suppose you want to confirm an action before actually executing it. Add the following to your command:

// ...
if (!$dialog->askConfirmation(
        $output,
        '<question>Continue with this action?</question>',
        false
    )) {
    return;
}

In this case, the user will be asked “Continue with this action?”, and will return true if the user answers with y or false if the user answers with n. The third argument to askConfirmation() is the default value to return if the user doesn’t enter any input. Any other input will ask the same question again.

Asking the User for Information

You can also ask question with more than a simple yes/no answer. For instance, if you want to know a bundle name, you can add this to your command:

// ...
$bundle = $dialog->ask(
    $output,
    'Please enter the name of the bundle',
    'AcmeDemoBundle'
);

The user will be asked “Please enter the name of the bundle”. They can type some name which will be returned by the ask() method. If they leave it empty, the default value (AcmeDemoBundle here) is returned.

Autocompletion

2.2 新版功能: Autocompletion for questions was introduced in Symfony 2.2.

You can also specify an array of potential answers for a given question. These will be autocompleted as the user types:

$dialog = $this->getHelper('dialog');
$bundleNames = array('AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle');
$name = $dialog->ask(
    $output,
    'Please enter the name of a bundle',
    'FooBundle',
    $bundleNames
);
Hiding the User’s Response

2.2 新版功能: The askHiddenResponse method was introduced in Symfony 2.2.

You can also ask a question and hide the response. This is particularly convenient for passwords:

$dialog = $this->getHelper('dialog');
$password = $dialog->askHiddenResponse(
    $output,
    'What is the database password?',
    false
);

警告

When you ask for a hidden response, Symfony will use either a binary, change stty mode or use another trick to hide the response. If none is available, it will fallback and allow the response to be visible unless you pass false as the third argument like in the example above. In this case, a RuntimeException would be thrown.

Validating the Answer

You can even validate the answer. For instance, in the last example you asked for the bundle name. Following the Symfony naming conventions, it should be suffixed with Bundle. You can validate that by using the askAndValidate() method:

// ...
$bundle = $dialog->askAndValidate(
    $output,
    'Please enter the name of the bundle',
    function ($answer) {
        if ('Bundle' !== substr($answer, -6)) {
            throw new \RuntimeException(
                'The name of the bundle should be suffixed with \'Bundle\''
            );
        }

        return $answer;
    },
    false,
    'AcmeDemoBundle'
);

This methods has 2 new arguments, the full signature is:

askAndValidate(
    OutputInterface $output,
    string|array $question,
    callback $validator,
    integer $attempts = false,
    string $default = null,
    array $autocomplete = null
)

The $validator is a callback which handles the validation. It should throw an exception if there is something wrong. The exception message is displayed in the console, so it is a good practice to put some useful information in it. The callback function should also return the value of the user’s input if the validation was successful.

You can set the max number of times to ask in the $attempts argument. If you reach this max number it will use the default value. Using false means the amount of attempts is infinite. The user will be asked as long as they provide an invalid answer and will only be able to proceed if their input is valid.

Validating a Hidden Response

2.2 新版功能: The askHiddenResponseAndValidate method was introduced in Symfony 2.2.

You can also ask and validate a hidden response:

$dialog = $this->getHelper('dialog');

$validator = function ($value) {
    if ('' === trim($value)) {
        throw new \Exception('The password can not be empty');
    }

    return $value;
};

$password = $dialog->askHiddenResponseAndValidate(
    $output,
    'Please enter your password',
    $validator,
    20,
    false
);

If you want to allow the response to be visible if it cannot be hidden for some reason, pass true as the fifth argument.

Let the User Choose from a List of Answers

2.2 新版功能: The select() method was introduced in Symfony 2.2.

If you have a predefined set of answers the user can choose from, you could use the ask method described above or, to make sure the user provided a correct answer, the askAndValidate method. Both have the disadvantage that you need to handle incorrect values yourself.

Instead, you can use the select() method, which makes sure that the user can only enter a valid string from a predefined list:

$dialog = $this->getHelper('dialog');
$colors = array('red', 'blue', 'yellow');

$color = $dialog->select(
    $output,
    'Please select your favorite color (default to red)',
    $colors,
    0
);
$output->writeln('You have just selected: ' . $colors[$color]);

// ... do something with the color

The option which should be selected by default is provided with the fourth argument. The default is null, which means that no option is the default one.

If the user enters an invalid string, an error message is shown and the user is asked to provide the answer another time, until they enter a valid string or the maximum attempts is reached (which you can define in the fifth argument). The default value for the attempts is false, which means infinite attempts. You can define your own error message in the sixth argument.

2.3 新版功能: Multiselect support was introduced in Symfony 2.3.

Multiple Choices

Sometimes, multiple answers can be given. The DialogHelper provides this feature using comma separated values. This is disabled by default, to enable this set the seventh argument to true:

// ...

$selected = $dialog->select(
    $output,
    'Please select your favorite color (default to red)',
    $colors,
    0,
    false,
    'Value "%s" is invalid',
    true // enable multiselect
);

$selectedColors = array_map(function ($c) use ($colors) {
    return $colors[$c];
}, $selected);

$output->writeln(
    'You have just selected: ' . implode(', ', $selectedColors)
);

Now, when the user enters 1,2, the result will be: You have just selected: blue, yellow.

Testing a Command which Expects Input

If you want to write a unit test for a command which expects some kind of input from the command line, you need to overwrite the HelperSet used by the command:

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\DialogHelper;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Tester\CommandTester;

// ...
public function testExecute()
{
    // ...
    $application = new Application();
    $application->add(new MyCommand());
    $command = $application->find('my:command:name');
    $commandTester = new CommandTester($command);

    $dialog = $command->getHelper('dialog');
    $dialog->setInputStream($this->getInputStream("Test\n"));
    // Equals to a user inputting "Test" and hitting ENTER
    // If you need to enter a confirmation, "yes\n" will work

    $commandTester->execute(array('command' => $command->getName()));

    // $this->assertRegExp('/.../', $commandTester->getDisplay());
}

protected function getInputStream($input)
{
    $stream = fopen('php://memory', 'r+', false);
    fputs($stream, $input);
    rewind($stream);

    return $stream;
}

By setting the input stream of the DialogHelper, you imitate what the console would do internally with all user input through the cli. This way you can test any user interaction (even complex ones) by passing an appropriate input stream.

参见

You find more information about testing commands in the console component docs about testing console commands.

Formatter Helper

The Formatter helpers provides functions to format the output with colors. You can do more advanced things with this helper than you can in Coloring the Output.

The FormatterHelper is included in the default helper set, which you can get by calling getHelperSet():

$formatter = $this->getHelper('formatter');

The methods return a string, which you’ll usually render to the console by passing it to the OutputInterface::writeln method.

Progress Helper

2.2 新版功能: The progress helper was introduced in Symfony 2.2.

2.3 新版功能: The setCurrent method was introduced in Symfony 2.3.

When executing longer-running commands, it may be helpful to show progress information, which updates as your command runs:

_images/progress.png

To display progress details, use the ProgressHelper, pass it a total number of units, and advance the progress as your command executes:

$progress = $this->getHelper('progress');

$progress->start($output, 50);
$i = 0;
while ($i++ < 50) {
    // ... do some work

    // advance the progress bar 1 unit
    $progress->advance();
}

$progress->finish();

小技巧

You can also set the current progress by calling the setCurrent() method.

The appearance of the progress output can be customized as well, with a number of different levels of verbosity. Each of these displays different possible items - like percentage completion, a moving progress bar, or current/total information (e.g. 10/50):

$progress->setFormat(ProgressHelper::FORMAT_QUIET);
$progress->setFormat(ProgressHelper::FORMAT_NORMAL);
$progress->setFormat(ProgressHelper::FORMAT_VERBOSE);
$progress->setFormat(ProgressHelper::FORMAT_QUIET_NOMAX);
// the default value
$progress->setFormat(ProgressHelper::FORMAT_NORMAL_NOMAX);
$progress->setFormat(ProgressHelper::FORMAT_VERBOSE_NOMAX);

You can also control the different characters and the width used for the progress bar:

// the finished part of the bar
$progress->setBarCharacter('<comment>=</comment>');
// the unfinished part of the bar
$progress->setEmptyBarCharacter(' ');
$progress->setProgressCharacter('|');
$progress->setBarWidth(50);

To see other available options, check the API documentation for ProgressHelper.

警告

For performance reasons, be careful if you set the total number of steps to a high number. For example, if you’re iterating over a large number of items, consider setting the redraw frequency to a higher value by calling setRedrawFrequency(), so it updates on only some iterations:

$progress->start($output, 50000);

// update every 100 iterations
$progress->setRedrawFrequency(100);

$i = 0;
while ($i++ < 50000) {
    // ... do some work

    $progress->advance();
}
Table Helper

2.3 新版功能: The table helper was introduced in Symfony 2.3.

When building a console application it may be useful to display tabular data:

_images/table.png

To display a table, use the TableHelper, set headers, rows and render:

$table = $this->getHelper('table');
$table
    ->setHeaders(array('ISBN', 'Title', 'Author'))
    ->setRows(array(
        array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'),
        array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'),
        array('960-425-059-0', 'The Lord of the Rings', 'J. R. R. Tolkien'),
        array('80-902734-1-6', 'And Then There Were None', 'Agatha Christie'),
    ))
;
$table->render($output);

The table layout can be customized as well. There are two ways to customize table rendering: using named layouts or by customizing rendering options.

Customize Table Layout using Named Layouts

The Table helper ships with two preconfigured table layouts:

  • TableHelper::LAYOUT_DEFAULT
  • TableHelper::LAYOUT_BORDERLESS

Layout can be set using setLayout() method.

Customize Table Layout using Rendering Options

You can also control table rendering by setting custom rendering option values:

The Console component comes with some useful helpers. These helpers contain function to ease some common tasks.

The CssSelector Component

The CssSelector component converts CSS selectors to XPath expressions.
Installation

You can install the component in 2 different ways:

Usage
Why to Use CSS selectors?

When you’re parsing an HTML or an XML document, by far the most powerful method is XPath.

XPath expressions are incredibly flexible, so there is almost always an XPath expression that will find the element you need. Unfortunately, they can also become very complicated, and the learning curve is steep. Even common operations (such as finding an element with a particular class) can require long and unwieldy expressions.

Many developers – particularly web developers – are more comfortable using CSS selectors to find elements. As well as working in stylesheets, CSS selectors are used in JavaScript with the querySelectorAll function and in popular JavaScript libraries such as jQuery, Prototype and MooTools.

CSS selectors are less powerful than XPath, but far easier to write, read and understand. Since they are less powerful, almost all CSS selectors can be converted to an XPath equivalent. This XPath expression can then be used with other functions and classes that use XPath to find elements in a document.

The CssSelector Component

The component’s only goal is to convert CSS selectors to their XPath equivalents:

use Symfony\Component\CssSelector\CssSelector;

print CssSelector::toXPath('div.item > h4 > a');

This gives the following output:

descendant-or-self::div[@class and contains(concat(' ',normalize-space(@class), ' '), ' item ')]/h4/a

You can use this expression with, for instance, DOMXPath or SimpleXMLElement to find elements in a document.

小技巧

The Crawler::filter() method uses the CssSelector component to find elements based on a CSS selector string. See the The DomCrawler Component for more details.

Limitations of the CssSelector Component

Not all CSS selectors can be converted to XPath equivalents.

There are several CSS selectors that only make sense in the context of a web-browser.

  • link-state selectors: :link, :visited, :target
  • selectors based on user action: :hover, :focus, :active
  • UI-state selectors: :invalid, :indeterminate (however, :enabled, :disabled, :checked and :unchecked are available)

Pseudo-elements (:before, :after, :first-line, :first-letter) are not supported because they select portions of text rather than elements.

Several pseudo-classes are not yet supported:

  • *:first-of-type, *:last-of-type, *:nth-of-type, *:nth-last-of-type, *:only-of-type. (These work with an element name (e.g. li:first-of-type) but not with *.

The Debug Component

The Debug component provides tools to ease debugging PHP code.

2.3 新版功能: The Debug component was introduced in Symfony 2.3. Previously, the classes were located in the HttpKernel component.

Installation

You can install the component in many different ways:

Usage

The Debug component provides several tools to help you debug PHP code. Enabling them all is as easy as it can get:

use Symfony\Component\Debug\Debug;

Debug::enable();

The enable() method registers an error handler and an exception handler. If the ClassLoader component is available, a special class loader is also registered.

Read the following sections for more information about the different available tools.

警告

You should never enable the debug tools in a production environment as they might disclose sensitive information to the user.

Enabling the Error Handler

The ErrorHandler class catches PHP errors and converts them to exceptions (of class ErrorException or FatalErrorException for PHP fatal errors):

use Symfony\Component\Debug\ErrorHandler;

ErrorHandler::register();
Enabling the Exception Handler

The ExceptionHandler class catches uncaught PHP exceptions and converts them to a nice PHP response. It is useful in debug mode to replace the default PHP/XDebug output with something prettier and more useful:

use Symfony\Component\Debug\ExceptionHandler;

ExceptionHandler::register();

注解

If the HttpFoundation component is available, the handler uses a Symfony Response object; if not, it falls back to a regular PHP response.

DependencyInjection

The DependencyInjection Component
The DependencyInjection component allows you to standardize and centralize the way objects are constructed in your application.

For an introduction to Dependency Injection and service containers see Service Container.

Installation

You can install the component in 2 different ways:

Basic Usage

You might have a simple class like the following Mailer that you want to make available as a service:

class Mailer
{
    private $transport;

    public function __construct()
    {
        $this->transport = 'sendmail';
    }

    // ...
}

You can register this in the container as a service:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->register('mailer', 'Mailer');

An improvement to the class to make it more flexible would be to allow the container to set the transport used. If you change the class so this is passed into the constructor:

class Mailer
{
    private $transport;

    public function __construct($transport)
    {
        $this->transport = $transport;
    }

    // ...
}

Then you can set the choice of transport in the container:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container
    ->register('mailer', 'Mailer')
    ->addArgument('sendmail');

This class is now much more flexible as you have separated the choice of transport out of the implementation and into the container.

Which mail transport you have chosen may be something other services need to know about. You can avoid having to change it in multiple places by making it a parameter in the container and then referring to this parameter for the Mailer service’s constructor argument:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->setParameter('mailer.transport', 'sendmail');
$container
    ->register('mailer', 'Mailer')
    ->addArgument('%mailer.transport%');

Now that the mailer service is in the container you can inject it as a dependency of other classes. If you have a NewsletterManager class like this:

class NewsletterManager
{
    private $mailer;

    public function __construct(\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Then you can register this as a service as well and pass the mailer service into it:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();

$container->setParameter('mailer.transport', 'sendmail');
$container
    ->register('mailer', 'Mailer')
    ->addArgument('%mailer.transport%');

$container
    ->register('newsletter_manager', 'NewsletterManager')
    ->addArgument(new Reference('mailer'));

If the NewsletterManager did not require the Mailer and injecting it was only optional then you could use setter injection instead:

class NewsletterManager
{
    private $mailer;

    public function setMailer(\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

You can now choose not to inject a Mailer into the NewsletterManager. If you do want to though then the container can call the setter method:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();

$container->setParameter('mailer.transport', 'sendmail');
$container
    ->register('mailer', 'Mailer')
    ->addArgument('%mailer.transport%');

$container
    ->register('newsletter_manager', 'NewsletterManager')
    ->addMethodCall('setMailer', array(new Reference('mailer')));

You could then get your newsletter_manager service from the container like this:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();

// ...

$newsletterManager = $container->get('newsletter_manager');
Avoiding your Code Becoming Dependent on the Container

Whilst you can retrieve services from the container directly it is best to minimize this. For example, in the NewsletterManager you injected the mailer service in rather than asking for it from the container. You could have injected the container in and retrieved the mailer service from it but it would then be tied to this particular container making it difficult to reuse the class elsewhere.

You will need to get a service from the container at some point but this should be as few times as possible at the entry point to your application.

Setting up the Container with Configuration Files

As well as setting up the services using PHP as above you can also use configuration files. This allows you to use XML or YAML to write the definitions for the services rather than using PHP to define the services as in the above examples. In anything but the smallest applications it makes sense to organize the service definitions by moving them into one or more configuration files. To do this you also need to install the Config component.

Loading an XML config file:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.xml');

Loading a YAML config file:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.yml');

注解

If you want to load YAML config files then you will also need to install the Yaml component.

If you do want to use PHP to create the services then you can move this into a separate config file and load it in a similar way:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

$container = new ContainerBuilder();
$loader = new PhpFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.php');

You can now set up the newsletter_manager and mailer services using config files:

  • YAML
    parameters:
        # ...
        mailer.transport: sendmail
    
    services:
        mailer:
            class:     Mailer
            arguments: ["%mailer.transport%"]
        newsletter_manager:
            class:     NewsletterManager
            calls:
                - [setMailer, ["@mailer"]]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <!-- ... -->
            <parameter key="mailer.transport">sendmail</parameter>
        </parameters>
    
        <services>
            <service id="mailer" class="Mailer">
                <argument>%mailer.transport%</argument>
            </service>
    
            <service id="newsletter_manager" class="NewsletterManager">
                <call method="setMailer">
                    <argument type="service" id="mailer" />
                </call>
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->setParameter('mailer.transport', 'sendmail');
    $container
        ->register('mailer', 'Mailer')
        ->addArgument('%mailer.transport%');
    
    $container
        ->register('newsletter_manager', 'NewsletterManager')
        ->addMethodCall('setMailer', array(new Reference('mailer')));
    
Types of Injection

Making a class’s dependencies explicit and requiring that they be injected into it is a good way of making a class more reusable, testable and decoupled from others.

There are several ways that the dependencies can be injected. Each injection point has advantages and disadvantages to consider, as well as different ways of working with them when using the service container.

Constructor Injection

The most common way to inject dependencies is via a class’s constructor. To do this you need to add an argument to the constructor signature to accept the dependency:

class NewsletterManager
{
    protected $mailer;

    public function __construct(\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

You can specify what service you would like to inject into this in the service container configuration:

  • YAML
    services:
         my_mailer:
             # ...
         newsletter_manager:
             class:     NewsletterManager
             arguments: ["@my_mailer"]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
                <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="NewsletterManager">
                <argument type="service" id="my_mailer"/>
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('my_mailer', ...);
    $container->setDefinition('newsletter_manager', new Definition(
        'NewsletterManager',
        array(new Reference('my_mailer'))
    ));
    

小技巧

Type hinting the injected object means that you can be sure that a suitable dependency has been injected. By type-hinting, you’ll get a clear error immediately if an unsuitable dependency is injected. By type hinting using an interface rather than a class you can make the choice of dependency more flexible. And assuming you only use methods defined in the interface, you can gain that flexibility and still safely use the object.

There are several advantages to using constructor injection:

  • If the dependency is a requirement and the class cannot work without it then injecting it via the constructor ensures it is present when the class is used as the class cannot be constructed without it.
  • The constructor is only ever called once when the object is created, so you can be sure that the dependency will not change during the object’s lifetime.

These advantages do mean that constructor injection is not suitable for working with optional dependencies. It is also more difficult to use in combination with class hierarchies: if a class uses constructor injection then extending it and overriding the constructor becomes problematic.

Setter Injection

Another possible injection point into a class is by adding a setter method that accepts the dependency:

class NewsletterManager
{
    protected $mailer;

    public function setMailer(\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}
  • YAML
    services:
         my_mailer:
             # ...
         newsletter_manager:
             class:     NewsletterManager
             calls:
                 - [setMailer, ["@my_mailer"]]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
                <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="NewsletterManager">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('my_mailer', ...);
    $container->setDefinition('newsletter_manager', new Definition(
        'NewsletterManager'
    ))->addMethodCall('setMailer', array(new Reference('my_mailer')));
    

This time the advantages are:

  • Setter injection works well with optional dependencies. If you do not need the dependency, then just do not call the setter.
  • You can call the setter multiple times. This is particularly useful if the method adds the dependency to a collection. You can then have a variable number of dependencies.

The disadvantages of setter injection are:

  • The setter can be called more than just at the time of construction so you cannot be sure the dependency is not replaced during the lifetime of the object (except by explicitly writing the setter method to check if it has already been called).
  • You cannot be sure the setter will be called and so you need to add checks that any required dependencies are injected.
Property Injection

Another possibility is just setting public fields of the class directly:

class NewsletterManager
{
    public $mailer;

    // ...
}
  • YAML
    services:
         my_mailer:
             # ...
         newsletter_manager:
             class: NewsletterManager
             properties:
                 mailer: "@my_mailer"
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
                <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="NewsletterManager">
                <property name="mailer" type="service" id="my_mailer" />
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setDefinition('my_mailer', ...);
    $container->setDefinition('newsletter_manager', new Definition(
        'NewsletterManager'
    ))->setProperty('mailer', new Reference('my_mailer'));
    

There are mainly only disadvantages to using property injection, it is similar to setter injection but with these additional important problems:

  • You cannot control when the dependency is set at all, it can be changed at any point in the object’s lifetime.
  • You cannot use type hinting so you cannot be sure what dependency is injected except by writing into the class code to explicitly test the class instance before using it.

But, it is useful to know that this can be done with the service container, especially if you are working with code that is out of your control, such as in a third party library, which uses public properties for its dependencies.

Introduction to Parameters

You can define parameters in the service container which can then be used directly or as part of service definitions. This can help to separate out values that you will want to change more regularly.

Getting and Setting Container Parameters

Working with container parameters is straightforward using the container’s accessor methods for parameters. You can check if a parameter has been defined in the container with:

$container->hasParameter('mailer.transport');

You can retrieve a parameter set in the container with:

$container->getParameter('mailer.transport');

and set a parameter in the container with:

$container->setParameter('mailer.transport', 'sendmail');

警告

The used . notation is just a Symfony convention to make parameters easier to read. Parameters are just flat key-value elements, they can’t be organized into a nested array

注解

You can only set a parameter before the container is compiled. To learn more about compiling the container see Compiling the Container.

Parameters in Configuration Files

You can also use the parameters section of a config file to set parameters:

  • YAML
    parameters:
        mailer.transport: sendmail
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="mailer.transport">sendmail</parameter>
        </parameters>
    </container>
    
  • PHP
    $container->setParameter('mailer.transport', 'sendmail');
    

As well as retrieving the parameter values directly from the container you can use them in the config files. You can refer to parameters elsewhere by surrounding them with percent (%) signs, e.g. %mailer.transport%. One use for this is to inject the values into your services. This allows you to configure different versions of services between applications or multiple services based on the same class but configured differently within a single application. You could inject the choice of mail transport into the Mailer class directly. But declaring it as a parameter makes it easier to change rather than being tied up and hidden with the service definition:

  • YAML
    parameters:
        mailer.transport: sendmail
    
    services:
        mailer:
            class:     Mailer
            arguments: ['%mailer.transport%']
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="mailer.transport">sendmail</parameter>
        </parameters>
    
        <services>
            <service id="mailer" class="Mailer">
                <argument>%mailer.transport%</argument>
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->setParameter('mailer.transport', 'sendmail');
    
    $container
        ->register('mailer', 'Mailer')
        ->addArgument('%mailer.transport%');
    

警告

The values between parameter tags in XML configuration files are not trimmed.

This means that the following configuration sample will have the value \n    sendmail\n:

<parameter key="mailer.transport">
    sendmail
</parameter>

In some cases (for constants or class names), this could throw errors. In order to prevent this, you must always inline your parameters as follow:

<parameter key="mailer.transport">sendmail</parameter>

If you were using this elsewhere as well, then you would only need to change the parameter value in one place if needed.

注解

The percent sign inside a parameter or argument, as part of the string, must be escaped with another percent sign:

  • YAML
    arguments: ["http://symfony.com/?foo=%%s&bar=%%d"]
    
  • XML
    <argument>http://symfony.com/?foo=%%s&bar=%%d</argument>
    
  • PHP
    ->addArgument('http://symfony.com/?foo=%%s&bar=%%d');
    
Array Parameters

Parameters do not need to be flat strings, they can also contain array values. For the XML format, you need to use the type="collection" attribute for all parameters that are arrays.

  • YAML
    parameters:
        my_mailer.gateways:
            - mail1
            - mail2
            - mail3
        my_multilang.language_fallback:
            en:
                - en
                - fr
            fr:
                - fr
                - en
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="my_mailer.gateways" type="collection">
                <parameter>mail1</parameter>
                <parameter>mail2</parameter>
                <parameter>mail3</parameter>
            </parameter>
            <parameter key="my_multilang.language_fallback" type="collection">
                <parameter key="en" type="collection">
                    <parameter>en</parameter>
                    <parameter>fr</parameter>
                </parameter>
                <parameter key="fr" type="collection">
                    <parameter>fr</parameter>
                    <parameter>en</parameter>
                </parameter>
            </parameter>
        </parameters>
    </container>
    
  • PHP
    $container->setParameter('my_mailer.gateways', array('mail1', 'mail2', 'mail3'));
    $container->setParameter('my_multilang.language_fallback', array(
        'en' => array('en', 'fr'),
        'fr' => array('fr', 'en'),
    ));
    
Constants as Parameters

The container also has support for setting PHP constants as parameters. To take advantage of this feature, map the name of your constant to a parameter key, and define the type as constant.

  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="global.constant.value" type="constant">GLOBAL_CONSTANT</parameter>
            <parameter key="my_class.constant.value" type="constant">My_Class::CONSTANT_NAME</parameter>
        </parameters>
    </container>
    
  • PHP
    $container->setParameter('global.constant.value', GLOBAL_CONSTANT);
    $container->setParameter('my_class.constant.value', My_Class::CONSTANT_NAME);
    

注解

This does not work for YAML configurations. If you’re using YAML, you can import an XML file to take advantage of this functionality:

imports:
    - { resource: parameters.xml }
PHP Keywords in XML

By default, true, false and null in XML are converted to the PHP keywords (respectively true, false and null):

<parameters>
    <parameter key="mailer.send_all_in_once">false</parameter>
</parameters>

<!-- after parsing
$container->getParameter('mailer.send_all_in_once'); // returns false
-->

To disable this behavior, use the string type:

<parameters>
    <parameter key="mailer.some_parameter" type="string">true</parameter>
</parameters>

<!-- after parsing
$container->getParameter('mailer.some_parameter'); // returns "true"
-->

注解

This is not available for YAML and PHP, because they already have built-in support for the PHP keywords.

Working with Container Service Definitions
Getting and Setting Service Definitions

There are some helpful methods for working with the service definitions.

To find out if there is a definition for a service id:

$container->hasDefinition($serviceId);

This is useful if you only want to do something if a particular definition exists.

You can retrieve a definition with:

$container->getDefinition($serviceId);

or:

$container->findDefinition($serviceId);

which unlike getDefinition() also resolves aliases so if the $serviceId argument is an alias you will get the underlying definition.

The service definitions themselves are objects so if you retrieve a definition with these methods and make changes to it these will be reflected in the container. If, however, you are creating a new definition then you can add it to the container using:

$container->setDefinition($id, $definition);
Working with a Definition
Creating a new Definition

If you need to create a new definition rather than manipulate one retrieved from the container then the definition class is Definition.

Class

First up is the class of a definition, this is the class of the object returned when the service is requested from the container.

To find out what class is set for a definition:

$definition->getClass();

and to set a different class:

$definition->setClass($class); // Fully qualified class name as string
Constructor Arguments

To get an array of the constructor arguments for a definition you can use:

$definition->getArguments();

or to get a single argument by its position:

$definition->getArgument($index);
// e.g. $definition->getArgument(0) for the first argument

You can add a new argument to the end of the arguments array using:

$definition->addArgument($argument);

The argument can be a string, an array, a service parameter by using %parameter_name% or a service id by using:

use Symfony\Component\DependencyInjection\Reference;

// ...

$definition->addArgument(new Reference('service_id'));

In a similar way you can replace an already set argument by index using:

$definition->replaceArgument($index, $argument);

You can also replace all the arguments (or set some if there are none) with an array of arguments:

$definition->setArguments($arguments);
Method Calls

If the service you are working with uses setter injection then you can manipulate any method calls in the definitions as well.

You can get an array of all the method calls with:

$definition->getMethodCalls();

Add a method call with:

$definition->addMethodCall($method, $arguments);

Where $method is the method name and $arguments is an array of the arguments to call the method with. The arguments can be strings, arrays, parameters or service ids as with the constructor arguments.

You can also replace any existing method calls with an array of new ones with:

$definition->setMethodCalls($methodCalls);

小技巧

There are more examples of specific ways of working with definitions in the PHP code blocks of the configuration examples on pages such as Using a Factory to Create Services and Managing common Dependencies with parent Services.

注解

The methods here that change service definitions can only be used before the container is compiled. Once the container is compiled you cannot manipulate service definitions further. To learn more about compiling the container see Compiling the Container.

Compiling the Container

The service container can be compiled for various reasons. These reasons include checking for any potential issues such as circular references and making the container more efficient by resolving parameters and removing unused services. Also, certain features - like using parent services - require the container to be compiled.

It is compiled by running:

$container->compile();

The compile method uses Compiler Passes for the compilation. The DependencyInjection component comes with several passes which are automatically registered for compilation. For example the CheckDefinitionValidityPass checks for various potential issues with the definitions that have been set in the container. After this and several other passes that check the container’s validity, further compiler passes are used to optimize the configuration before it is cached. For example, private services and abstract services are removed, and aliases are resolved.

Managing Configuration with Extensions

As well as loading configuration directly into the container as shown in The DependencyInjection Component, you can manage it by registering extensions with the container. The first step in the compilation process is to load configuration from any extension classes registered with the container. Unlike the configuration loaded directly, they are only processed when the container is compiled. If your application is modular then extensions allow each module to register and manage their own service configuration.

The extensions must implement ExtensionInterface and can be registered with the container with:

$container->registerExtension($extension);

The main work of the extension is done in the load method. In the load method you can load configuration from one or more configuration files as well as manipulate the container definitions using the methods shown in Working with Container Service Definitions.

The load method is passed a fresh container to set up, which is then merged afterwards into the container it is registered with. This allows you to have several extensions managing container definitions independently. The extensions do not add to the containers configuration when they are added but are processed when the container’s compile method is called.

A very simple extension may just load configuration files into the container:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\Config\FileLocator;

class AcmeDemoExtension implements ExtensionInterface
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.xml');
    }

    // ...
}

This does not gain very much compared to loading the file directly into the overall container being built. It just allows the files to be split up amongst the modules/bundles. Being able to affect the configuration of a module from configuration files outside of the module/bundle is needed to make a complex application configurable. This can be done by specifying sections of config files loaded directly into the container as being for a particular extension. These sections on the config will not be processed directly by the container but by the relevant Extension.

The Extension must specify a getAlias method to implement the interface:

// ...

class AcmeDemoExtension implements ExtensionInterface
{
    // ...

    public function getAlias()
    {
        return 'acme_demo';
    }
}

For YAML configuration files specifying the alias for the Extension as a key will mean that those values are passed to the Extension’s load method:

# ...
acme_demo:
    foo: fooValue
    bar: barValue

If this file is loaded into the configuration then the values in it are only processed when the container is compiled at which point the Extensions are loaded:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$container = new ContainerBuilder();
$container->registerExtension(new AcmeDemoExtension);

$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('config.yml');

// ...
$container->compile();

注解

When loading a config file that uses an extension alias as a key, the extension must already have been registered with the container builder or an exception will be thrown.

The values from those sections of the config files are passed into the first argument of the load method of the extension:

public function load(array $configs, ContainerBuilder $container)
{
    $foo = $configs[0]['foo']; //fooValue
    $bar = $configs[0]['bar']; //barValue
}

The $configs argument is an array containing each different config file that was loaded into the container. You are only loading a single config file in the above example but it will still be within an array. The array will look like this:

array(
    array(
        'foo' => 'fooValue',
        'bar' => 'barValue',
    ),
)

Whilst you can manually manage merging the different files, it is much better to use the Config component to merge and validate the config values. Using the configuration processing you could access the config value this way:

use Symfony\Component\Config\Definition\Processor;
// ...

public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $foo = $config['foo']; //fooValue
    $bar = $config['bar']; //barValue

    // ...
}

There are a further two methods you must implement. One to return the XML namespace so that the relevant parts of an XML config file are passed to the extension. The other to specify the base path to XSD files to validate the XML configuration:

public function getXsdValidationBasePath()
{
    return __DIR__.'/../Resources/config/';
}

public function getNamespace()
{
    return 'http://www.example.com/symfony/schema/';
}

注解

XSD validation is optional, returning false from the getXsdValidationBasePath method will disable it.

The XML version of the config would then look like this:

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:acme_demo="http://www.example.com/symfony/schema/"
    xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony/schema/hello-1.0.xsd">

    <acme_demo:config>
        <acme_demo:foo>fooValue</acme_hello:foo>
        <acme_demo:bar>barValue</acme_demo:bar>
    </acme_demo:config>
</container>

注解

In the Symfony full stack framework there is a base Extension class which implements these methods as well as a shortcut method for processing the configuration. See How to Load Service Configuration inside a Bundle for more details.

The processed config value can now be added as container parameters as if it were listed in a parameters section of the config file but with the additional benefit of merging multiple files and validation of the configuration:

public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $container->setParameter('acme_demo.FOO', $config['foo']);

    // ...
}

More complex configuration requirements can be catered for in the Extension classes. For example, you may choose to load a main service configuration file but also load a secondary one only if a certain parameter is set:

public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $loader = new XmlFileLoader(
        $container,
        new FileLocator(__DIR__.'/../Resources/config')
    );
    $loader->load('services.xml');

    if ($config['advanced']) {
        $loader->load('advanced.xml');
    }
}

注解

Just registering an extension with the container is not enough to get it included in the processed extensions when the container is compiled. Loading config which uses the extension’s alias as a key as in the above examples will ensure it is loaded. The container builder can also be told to load it with its loadFromExtension() method:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$extension = new AcmeDemoExtension();
$container->registerExtension($extension);
$container->loadFromExtension($extension->getAlias());
$container->compile();

注解

If you need to manipulate the configuration loaded by an extension then you cannot do it from another extension as it uses a fresh container. You should instead use a compiler pass which works with the full container after the extensions have been processed.

Prepending Configuration Passed to the Extension

2.2 新版功能: The ability to prepend the configuration of a bundle was introduced in Symfony 2.2.

An Extension can prepend the configuration of any Bundle before the load() method is called by implementing PrependExtensionInterface:

use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
// ...

class AcmeDemoExtension implements ExtensionInterface, PrependExtensionInterface
{
    // ...

    public function prepend()
    {
        // ...

        $container->prependExtensionConfig($name, $config);

        // ...
    }
}

For more details, see How to Simplify Configuration of multiple Bundles, which is specific to the Symfony Framework, but contains more details about this feature.

Creating a Compiler Pass

You can also create and register your own compiler passes with the container. To create a compiler pass it needs to implement the CompilerPassInterface interface. The compiler pass gives you an opportunity to manipulate the service definitions that have been compiled. This can be very powerful, but is not something needed in everyday use.

The compiler pass must have the process method which is passed the container being compiled:

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class CustomCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
       // ...
    }
}

The container’s parameters and definitions can be manipulated using the methods described in the Working with Container Service Definitions. One common thing to do in a compiler pass is to search for all services that have a certain tag in order to process them in some way or dynamically plug each into some other service.

Registering a Compiler Pass

You need to register your custom pass with the container. Its process method will then be called when the container is compiled:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->addCompilerPass(new CustomCompilerPass);

注解

Compiler passes are registered differently if you are using the full stack framework, see How to Work with Compiler Passes in Bundles for more details.

Controlling the Pass Ordering

The default compiler passes are grouped into optimization passes and removal passes. The optimization passes run first and include tasks such as resolving references within the definitions. The removal passes perform tasks such as removing private aliases and unused services. You can choose where in the order any custom passes you add are run. By default they will be run before the optimization passes.

You can use the following constants as the second argument when registering a pass with the container to control where it goes in the order:

  • PassConfig::TYPE_BEFORE_OPTIMIZATION
  • PassConfig::TYPE_OPTIMIZE
  • PassConfig::TYPE_BEFORE_REMOVING
  • PassConfig::TYPE_REMOVE
  • PassConfig::TYPE_AFTER_REMOVING

For example, to run your custom pass after the default removal passes have been run:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;

$container = new ContainerBuilder();
$container->addCompilerPass(
    new CustomCompilerPass,
    PassConfig::TYPE_AFTER_REMOVING
);
Dumping the Configuration for Performance

Using configuration files to manage the service container can be much easier to understand than using PHP once there are a lot of services. This ease comes at a price though when it comes to performance as the config files need to be parsed and the PHP configuration built from them. The compilation process makes the container more efficient but it takes time to run. You can have the best of both worlds though by using configuration files and then dumping and caching the resulting configuration. The PhpDumper makes dumping the compiled container easy:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;

$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new ProjectServiceContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    file_put_contents($file, $dumper->dump());
}

ProjectServiceContainer is the default name given to the dumped container class, you can change this though this with the class option when you dump it:

// ...
$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    file_put_contents(
        $file,
        $dumper->dump(array('class' => 'MyCachedContainer'))
    );
}

You will now get the speed of the PHP configured container with the ease of using configuration files. Additionally dumping the container in this way further optimizes how the services are created by the container.

In the above example you will need to delete the cached container file whenever you make any changes. Adding a check for a variable that determines if you are in debug mode allows you to keep the speed of the cached container in production but getting an up to date configuration whilst developing your application:

// ...

// based on something in your project
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';

if (!$isDebug && file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    if (!$isDebug) {
        $dumper = new PhpDumper($container);
        file_put_contents(
            $file,
            $dumper->dump(array('class' => 'MyCachedContainer'))
        );
    }
}

This could be further improved by only recompiling the container in debug mode when changes have been made to its configuration rather than on every request. This can be done by caching the resource files used to configure the container in the way described in “Caching Based on Resources” in the config component documentation.

You do not need to work out which files to cache as the container builder keeps track of all the resources used to configure it, not just the configuration files but the extension classes and compiler passes as well. This means that any changes to any of these files will invalidate the cache and trigger the container being rebuilt. You just need to ask the container for these resources and use them as metadata for the cache:

// ...

// based on something in your project
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';
$containerConfigCache = new ConfigCache($file, $isDebug);

if (!$containerConfigCache->isFresh()) {
    $containerBuilder = new ContainerBuilder();
    // ...
    $containerBuilder->compile();

    $dumper = new PhpDumper($containerBuilder);
    $containerConfigCache->write(
        $dumper->dump(array('class' => 'MyCachedContainer')),
        $containerBuilder->getResources()
    );
}

require_once $file;
$container = new MyCachedContainer();

Now the cached dumped container is used regardless of whether debug mode is on or not. The difference is that the ConfigCache is set to debug mode with its second constructor argument. When the cache is not in debug mode the cached container will always be used if it exists. In debug mode, an additional metadata file is written with the timestamps of all the resource files. These are then checked to see if the files have changed, if they have the cache will be considered stale.

注解

In the full stack framework the compilation and caching of the container is taken care of for you.

Working with Tagged Services

Tags are a generic string (along with some options) that can be applied to any service. By themselves, tags don’t actually alter the functionality of your services in any way. But if you choose to, you can ask a container builder for a list of all services that were tagged with some specific tag. This is useful in compiler passes where you can find these services and use or modify them in some specific way.

For example, if you are using Swift Mailer you might imagine that you want to implement a “transport chain”, which is a collection of classes implementing \Swift_Transport. Using the chain, you’ll want Swift Mailer to try several ways of transporting the message until one succeeds.

To begin with, define the TransportChain class:

class TransportChain
{
    private $transports;

    public function __construct()
    {
        $this->transports = array();
    }

    public function addTransport(\Swift_Transport $transport)
    {
        $this->transports[] = $transport;
    }
}

Then, define the chain as a service:

  • YAML
    services:
        acme_mailer.transport_chain:
            class: TransportChain
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="acme_mailer.transport_chain" class="TransportChain" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition('acme_mailer.transport_chain', new Definition('TransportChain'));
    
Define Services with a custom Tag

Now you might want several of the \Swift_Transport classes to be instantiated and added to the chain automatically using the addTransport() method. For example you may add the following transports as services:

  • YAML
    services:
        acme_mailer.transport.smtp:
            class: \Swift_SmtpTransport
            arguments:
                - "%mailer_host%"
            tags:
                -  { name: acme_mailer.transport }
        acme_mailer.transport.sendmail:
            class: \Swift_SendmailTransport
            tags:
                -  { name: acme_mailer.transport }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
                <argument>%mailer_host%</argument>
                <tag name="acme_mailer.transport" />
            </service>
    
            <service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
                <tag name="acme_mailer.transport" />
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%'));
    $definitionSmtp->addTag('acme_mailer.transport');
    $container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp);
    
    $definitionSendmail = new Definition('\Swift_SendmailTransport');
    $definitionSendmail->addTag('acme_mailer.transport');
    $container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail);
    

Notice that each was given a tag named acme_mailer.transport. This is the custom tag that you’ll use in your compiler pass. The compiler pass is what makes this tag “mean” something.

Create a CompilerPass

Your compiler pass can now ask the container for any services with the custom tag:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('acme_mailer.transport_chain')) {
            return;
        }

        $definition = $container->getDefinition(
            'acme_mailer.transport_chain'
        );

        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tags) {
            $definition->addMethodCall(
                'addTransport',
                array(new Reference($id))
            );
        }
    }
}

The process() method checks for the existence of the acme_mailer.transport_chain service, then looks for all services tagged acme_mailer.transport. It adds to the definition of the acme_mailer.transport_chain service a call to addTransport() for each “acme_mailer.transport” service it has found. The first argument of each of these calls will be the mailer transport service itself.

Register the Pass with the Container

You also need to register the pass with the container, it will then be run when the container is compiled:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->addCompilerPass(new TransportCompilerPass());

注解

Compiler passes are registered differently if you are using the full stack framework. See How to Work with Compiler Passes in Bundles for more details.

Adding additional Attributes on Tags

Sometimes you need additional information about each service that’s tagged with your tag. For example, you might want to add an alias to each member of the transport chain.

To begin with, change the TransportChain class:

class TransportChain
{
    private $transports;

    public function __construct()
    {
        $this->transports = array();
    }

    public function addTransport(\Swift_Transport $transport, $alias)
    {
        $this->transports[$alias] = $transport;
    }

    public function getTransport($alias)
    {
        if (array_key_exists($alias, $this->transports)) {
            return $this->transports[$alias];
        }
    }
}

As you can see, when addTransport is called, it takes not only a Swift_Transport object, but also a string alias for that transport. So, how can you allow each tagged transport service to also supply an alias?

To answer this, change the service declaration:

  • YAML
    services:
        acme_mailer.transport.smtp:
            class: \Swift_SmtpTransport
            arguments:
                - "%mailer_host%"
            tags:
                -  { name: acme_mailer.transport, alias: foo }
        acme_mailer.transport.sendmail:
            class: \Swift_SendmailTransport
            tags:
                -  { name: acme_mailer.transport, alias: bar }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
                <argument>%mailer_host%</argument>
                <tag name="acme_mailer.transport" alias="foo" />
            </service>
    
            <service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
                <tag name="acme_mailer.transport" alias="bar" />
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%'));
    $definitionSmtp->addTag('acme_mailer.transport', array('alias' => 'foo'));
    $container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp);
    
    $definitionSendmail = new Definition('\Swift_SendmailTransport');
    $definitionSendmail->addTag('acme_mailer.transport', array('alias' => 'bar'));
    $container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail);
    

Notice that you’ve added a generic alias key to the tag. To actually use this, update the compiler:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('acme_mailer.transport_chain')) {
            return;
        }

        $definition = $container->getDefinition(
            'acme_mailer.transport_chain'
        );

        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tags) {
            foreach ($tags as $attributes) {
                $definition->addMethodCall(
                    'addTransport',
                    array(new Reference($id), $attributes["alias"])
                );
            }
        }
    }
}

The double loop may be confusing. This is because a service can have more than one tag. You tag a service twice or more with the acme_mailer.transport tag. The second foreach loop iterates over the acme_mailer.transport tags set for the current service and gives you the attributes.

Using a Factory to Create Services

Symfony’s Service Container provides a powerful way of controlling the creation of objects, allowing you to specify arguments passed to the constructor as well as calling methods and setting parameters. Sometimes, however, this will not provide you with everything you need to construct your objects. For this situation, you can use a factory to create the object and tell the service container to call a method on the factory rather than directly instantiating the class.

Suppose you have a factory that configures and returns a new NewsletterManager object:

class NewsletterManagerFactory
{
    public static function createNewsletterManager()
    {
        $newsletterManager = new NewsletterManager();

        // ...

        return $newsletterManager;
    }
}

To make the NewsletterManager object available as a service, you can configure the service container to use the NewsletterManagerFactory factory class:

  • YAML
    services:
        newsletter_manager:
            class:          NewsletterManager
            factory_class:  NewsletterManagerFactory
            factory_method: createNewsletterManager
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="newsletter_manager"
                class="NewsletterManager"
                factory-class="NewsletterManagerFactory"
                factory-method="createNewsletterManager" />
        </services>
    </services>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    // ...
    $definition = new Definition('NewsletterManager');
    $definition->setFactoryClass('NewsletterManagerFactory');
    $definition->setFactoryMethod('createNewsletterManager');
    
    $container->setDefinition('newsletter_manager', $definition);
    

When you specify the class to use for the factory (via factory_class) the method will be called statically. If the factory itself should be instantiated and the resulting object’s method called, configure the factory itself as a service. In this case, the method (e.g. createNewsletterManager) should be changed to be non-static:

  • YAML
    services:
        newsletter_manager_factory:
            class:            NewsletterManagerFactory
        newsletter_manager:
            class:            NewsletterManager
            factory_service:  newsletter_manager_factory
            factory_method:   createNewsletterManager
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="newsletter_manager_factory" class="NewsletterManagerFactory" />
    
            <service
                id="newsletter_manager"
                class="NewsletterManager"
                factory-service="newsletter_manager_factory"
                factory-method="createNewsletterManager" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition('newsletter_manager_factory', new Definition(
        'NewsletterManager'
    ));
    $container->setDefinition('newsletter_manager', new Definition(
        'NewsletterManagerFactory'
    ))->setFactoryService(
        'newsletter_manager_factory'
    )->setFactoryMethod(
        'createNewsletterManager'
    );
    

注解

The factory service is specified by its id name and not a reference to the service itself. So, you do not need to use the @ syntax for this in YAML configurations.

Passing Arguments to the Factory Method

If you need to pass arguments to the factory method, you can use the arguments options inside the service container. For example, suppose the createNewsletterManager method in the previous example takes the templating service as an argument:

  • YAML
    services:
        newsletter_manager_factory:
            class:            NewsletterManagerFactory
        newsletter_manager:
            class:            NewsletterManager
            factory_service:  newsletter_manager_factory
            factory_method:   createNewsletterManager
            arguments:
                - "@templating"
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="newsletter_manager_factory" class="NewsletterManagerFactory" />
    
            <service
                id="newsletter_manager"
                class="NewsletterManager"
                factory-service="newsletter_manager_factory"
                factory-method="createNewsletterManager">
    
                <argument type="service" id="templating" />
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    // ...
    $container->setDefinition('newsletter_manager_factory', new Definition(
        'NewsletterManagerFactory'
    ));
    $container->setDefinition('newsletter_manager', new Definition(
        'NewsletterManager',
        array(new Reference('templating'))
    ))->setFactoryService(
        'newsletter_manager_factory'
    )->setFactoryMethod(
        'createNewsletterManager'
    );
    
Configuring Services with a Service Configurator

The Service Configurator is a feature of the Dependency Injection Container that allows you to use a callable to configure a service after its instantiation.

You can specify a method in another service, a PHP function or a static method in a class. The service instance is passed to the callable, allowing the configurator to do whatever it needs to configure the service after its creation.

A Service Configurator can be used, for example, when you have a service that requires complex setup based on configuration settings coming from different sources/services. Using an external configurator, you can maintain the service implementation cleanly and keep it decoupled from the other objects that provide the configuration needed.

Another interesting use case is when you have multiple objects that share a common configuration or that should be configured in a similar way at runtime.

For example, suppose you have an application where you send different types of emails to users. Emails are passed through different formatters that could be enabled or not depending on some dynamic application settings. You start defining a NewsletterManager class like this:

class NewsletterManager implements EmailFormatterAwareInterface
{
    protected $mailer;
    protected $enabledFormatters;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function setEnabledFormatters(array $enabledFormatters)
    {
        $this->enabledFormatters = $enabledFormatters;
    }

    // ...
}

and also a GreetingCardManager class:

class GreetingCardManager implements EmailFormatterAwareInterface
{
    protected $mailer;
    protected $enabledFormatters;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function setEnabledFormatters(array $enabledFormatters)
    {
        $this->enabledFormatters = $enabledFormatters;
    }

    // ...
}

As mentioned before, the goal is to set the formatters at runtime depending on application settings. To do this, you also have an EmailFormatterManager class which is responsible for loading and validating formatters enabled in the application:

class EmailFormatterManager
{
    protected $enabledFormatters;

    public function loadFormatters()
    {
        // code to configure which formatters to use
        $enabledFormatters = array(...);
        // ...

        $this->enabledFormatters = $enabledFormatters;
    }

    public function getEnabledFormatters()
    {
        return $this->enabledFormatters;
    }

    // ...
}

If your goal is to avoid having to couple NewsletterManager and GreetingCardManager with EmailFormatterManager, then you might want to create a configurator class to configure these instances:

class EmailConfigurator
{
    private $formatterManager;

    public function __construct(EmailFormatterManager $formatterManager)
    {
        $this->formatterManager = $formatterManager;
    }

    public function configure(EmailFormatterAwareInterface $emailManager)
    {
        $emailManager->setEnabledFormatters(
            $this->formatterManager->getEnabledFormatters()
        );
    }

    // ...
}

The EmailConfigurator‘s job is to inject the enabled filters into NewsletterManager and GreetingCardManager because they are not aware of where the enabled filters come from. In the other hand, the EmailFormatterManager holds the knowledge about the enabled formatters and how to load them, keeping the single responsibility principle.

Configurator Service Config

The service config for the above classes would look something like this:

  • YAML
    services:
        my_mailer:
            # ...
    
        email_formatter_manager:
            class:     EmailFormatterManager
            # ...
    
        email_configurator:
            class:     EmailConfigurator
            arguments: ["@email_formatter_manager"]
            # ...
    
        newsletter_manager:
            class:     NewsletterManager
            calls:
                - [setMailer, ["@my_mailer"]]
            configurator: ["@email_configurator", configure]
    
        greeting_card_manager:
            class:     GreetingCardManager
            calls:
                - [setMailer, ["@my_mailer"]]
            configurator: ["@email_configurator", configure]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
                <!-- ... -->
            </service>
    
            <service id="email_formatter_manager" class="EmailFormatterManager">
                <!-- ... -->
            </service>
    
            <service id="email_configurator" class="EmailConfigurator">
                <argument type="service" id="email_formatter_manager" />
                <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="NewsletterManager">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
                <configurator service="email_configurator" method="configure" />
            </service>
    
            <service id="greeting_card_manager" class="GreetingCardManager">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
                <configurator service="email_configurator" method="configure" />
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->setDefinition('my_mailer', ...);
    $container->setDefinition('email_formatter_manager', new Definition(
        'EmailFormatterManager'
    ));
    $container->setDefinition('email_configurator', new Definition(
        'EmailConfigurator'
    ));
    $container->setDefinition('newsletter_manager', new Definition(
        'NewsletterManager'
    ))->addMethodCall('setMailer', array(
        new Reference('my_mailer'),
    ))->setConfigurator(array(
        new Reference('email_configurator'),
        'configure',
    )));
    $container->setDefinition('greeting_card_manager', new Definition(
        'GreetingCardManager'
    ))->addMethodCall('setMailer', array(
        new Reference('my_mailer'),
    ))->setConfigurator(array(
        new Reference('email_configurator'),
        'configure',
    )));
    
Managing common Dependencies with parent Services

As you add more functionality to your application, you may well start to have related classes that share some of the same dependencies. For example you may have a Newsletter Manager which uses setter injection to set its dependencies:

class NewsletterManager
{
    protected $mailer;
    protected $emailFormatter;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }

    // ...
}

and also a Greeting Card class which shares the same dependencies:

class GreetingCardManager
{
    protected $mailer;
    protected $emailFormatter;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }

    // ...
}

The service config for these classes would look something like this:

  • YAML
    services:
        my_mailer:
            # ...
    
        my_email_formatter:
            # ...
    
        newsletter_manager:
            class: NewsletterManager
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
    
        greeting_card_manager:
            class: "GreetingCardManager"
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_mailer">
                <!-- ... -->
            </service>
    
            <service id="my_email_formatter">
                <!-- ... -->
            </service>
    
            <service id="newsletter_manager" class="NewsletterManager">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
                <call method="setEmailFormatter">
                    <argument type="service" id="my_email_formatter" />
                </call>
            </service>
    
            <service id="greeting_card_manager" class="GreetingCardManager">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
    
                <call method="setEmailFormatter">
                    <argument type="service" id="my_email_formatter" />
                </call>
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->register('my_mailer', ...);
    $container->register('my_email_formatter', ...);
    
    $container
        ->register('newsletter_manager', 'NewsletterManager')
        ->addMethodCall('setMailer', array(
            new Reference('my_mailer'),
        ))
        ->addMethodCall('setEmailFormatter', array(
            new Reference('my_email_formatter'),
        ))
    ;
    
    $container
        ->register('greeting_card_manager', 'GreetingCardManager')
        ->addMethodCall('setMailer', array(
            new Reference('my_mailer'),
        ))
        ->addMethodCall('setEmailFormatter', array(
            new Reference('my_email_formatter'),
        ))
    ;
    

There is a lot of repetition in both the classes and the configuration. This means that if you changed, for example, the Mailer of EmailFormatter classes to be injected via the constructor, you would need to update the config in two places. Likewise if you needed to make changes to the setter methods you would need to do this in both classes. The typical way to deal with the common methods of these related classes would be to extract them to a super class:

abstract class MailManager
{
    protected $mailer;
    protected $emailFormatter;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function setEmailFormatter(EmailFormatter $emailFormatter)
    {
        $this->emailFormatter = $emailFormatter;
    }

    // ...
}

The NewsletterManager and GreetingCardManager can then extend this super class:

class NewsletterManager extends MailManager
{
    // ...
}

and:

class GreetingCardManager extends MailManager
{
    // ...
}

In a similar fashion, the Symfony service container also supports extending services in the configuration so you can also reduce the repetition by specifying a parent for a service.

  • YAML
    # ...
    services:
        # ...
        mail_manager:
            abstract:  true
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
    
        newsletter_manager:
            class:  "NewsletterManager"
            parent: mail_manager
    
        greeting_card_manager:
            class:  "GreetingCardManager"
            parent: mail_manager
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <!-- ... -->
        <services>
            <!-- ... -->
            <service id="mail_manager" abstract="true">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
    
                <call method="setEmailFormatter">
                    <argument type="service" id="my_email_formatter" />
                </call>
            </service>
    
            <service
                id="newsletter_manager"
                class="NewsletterManager"
                parent="mail_manager" />
    
            <service
                id="greeting_card_manager"
                class="GreetingCardManager"
                parent="mail_manager" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\DefinitionDecorator;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $mailManager = new Definition();
    $mailManager
        ->setAbstract(true);
        ->addMethodCall('setMailer', array(
            new Reference('my_mailer'),
        ))
        ->addMethodCall('setEmailFormatter', array(
            new Reference('my_email_formatter'),
        ))
    ;
    $container->setDefinition('mail_manager', $mailManager);
    
    $newsletterManager = new DefinitionDecorator('mail_manager');
    $newsletterManager->setClass('NewsletterManager');
    $container->setDefinition('newsletter_manager', $newsletterManager);
    
    $greetingCardManager = new DefinitionDecorator('mail_manager');
    $greetingCardManager->setClass('GreetingCardManager');
    $container->setDefinition('greeting_card_manager', $greetingCardManager);
    

In this context, having a parent service implies that the arguments and method calls of the parent service should be used for the child services. Specifically, the setter methods defined for the parent service will be called when the child services are instantiated.

注解

If you remove the parent config key, the services will still be instantiated and they will still of course extend the MailManager class. The difference is that omitting the parent config key will mean that the calls defined on the mail_manager service will not be executed when the child services are instantiated.

警告

The scope, abstract and tags attributes are always taken from the child service.

The parent service is abstract as it should not be directly retrieved from the container or passed into another service. It exists merely as a “template” that other services can use. This is why it can have no class configured which would cause an exception to be raised for a non-abstract service.

注解

In order for parent dependencies to resolve, the ContainerBuilder must first be compiled. See Compiling the Container for more details.

小技巧

In the examples shown, the classes sharing the same configuration also extend from the same parent class in PHP. This isn’t necessary at all. You can just extract common parts of similar service definitions into a parent service without also extending a parent class in PHP.

Overriding parent Dependencies

There may be times where you want to override what class is passed in for a dependency of one child service only. Fortunately, by adding the method call config for the child service, the dependencies set by the parent class will be overridden. So if you needed to pass a different dependency just to the NewsletterManager class, the config would look like this:

  • YAML
    # ...
    services:
        # ...
        my_alternative_mailer:
            # ...
    
        mail_manager:
            abstract: true
            calls:
                - [setMailer, ["@my_mailer"]]
                - [setEmailFormatter, ["@my_email_formatter"]]
    
        newsletter_manager:
            class:  "NewsletterManager"
            parent: mail_manager
            calls:
                - [setMailer, ["@my_alternative_mailer"]]
    
        greeting_card_manager:
            class:  "GreetingCardManager"
            parent: mail_manager
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <!-- ... -->
        <services>
            <!-- ... -->
            <service id="my_alternative_mailer">
                <!-- ... -->
            </service>
    
            <service id="mail_manager" abstract="true">
                <call method="setMailer">
                    <argument type="service" id="my_mailer" />
                </call>
    
                <call method="setEmailFormatter">
                    <argument type="service" id="my_email_formatter" />
                </call>
            </service>
    
            <service
                id="newsletter_manager"
                class="NewsletterManager"
                parent="mail_manager">
    
                <call method="setMailer">
                    <argument type="service" id="my_alternative_mailer" />
                </call>
            </service>
    
            <service
                id="greeting_card_manager"
                class="GreetingCardManager"
                parent="mail_manager" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\DefinitionDecorator;
    use Symfony\Component\DependencyInjection\Reference;
    
    // ...
    $container->setDefinition('my_alternative_mailer', ...);
    
    $mailManager = new Definition();
    $mailManager
        ->setAbstract(true);
        ->addMethodCall('setMailer', array(
            new Reference('my_mailer'),
        ))
        ->addMethodCall('setEmailFormatter', array(
            new Reference('my_email_formatter'),
        ))
    ;
    $container->setDefinition('mail_manager', $mailManager);
    
    $newsletterManager = new DefinitionDecorator('mail_manager');
    $newsletterManager->setClass('NewsletterManager');
        ->addMethodCall('setMailer', array(
            new Reference('my_alternative_mailer'),
        ))
    ;
    $container->setDefinition('newsletter_manager', $newsletterManager);
    
    $greetingCardManager = new DefinitionDecorator('mail_manager');
    $greetingCardManager->setClass('GreetingCardManager');
    $container->setDefinition('greeting_card_manager', $greetingCardManager);
    

The GreetingCardManager will receive the same dependencies as before, but the NewsletterManager will be passed the my_alternative_mailer instead of the my_mailer service.

警告

You can’t override method calls. When you defined new method calls in the child service, it’ll be added to the current set of configured method calls. This means it works perfectly when the setter overrides the current property, but it doesn’t work as expected when the setter appends it to the existing data (e.g. an addFilters() method). In those cases, the only solution is to not extend the parent service and configuring the service just like you did before knowing this feature.

Advanced Container Configuration
Marking Services as public / private

When defining services, you’ll usually want to be able to access these definitions within your application code. These services are called public. For example, the doctrine service registered with the container when using the DoctrineBundle is a public service. This means that you can fetch it from the container using the get() method:

$doctrine = $container->get('doctrine');

In some cases, a service only exists to be injected into another service and is not intended to be fetched directly from the container as shown above.

In these cases, to get a minor performance boost, you can set the service to be not public (i.e. private):

  • YAML
    services:
       foo:
         class: Example\Foo
         public: false
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="foo" class="Example\Foo" public="false" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $definition = new Definition('Example\Foo');
    $definition->setPublic(false);
    $container->setDefinition('foo', $definition);
    

What makes private services special is that, if they are only injected once, they are converted from services to inlined instantiations (e.g. new PrivateThing()). This increases the container’s performance.

Now that the service is private, you should not fetch the service directly from the container:

$container->get('foo');

This may or may not work, depending on if the service could be inlined. Simply said: A service can be marked as private if you do not want to access it directly from your code.

However, if a service has been marked as private, you can still alias it (see below) to access this service (via the alias).

注解

Services are by default public.

Synthetic Services

Synthetic services are services that are injected into the container instead of being created by the container.

For example, if you’re using the HttpKernel component with the DependencyInjection component, then the request service is injected in the ContainerAwareHttpKernel::handle() method when entering the request scope. The class does not exist when there is no request, so it can’t be included in the container configuration. Also, the service should be different for every subrequest in the application.

To create a synthetic service, set synthetic to true:

  • YAML
    services:
        request:
            synthetic: true
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="request" synthetic="true" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container
        ->setDefinition('request', new Definition())
        ->setSynthetic(true);
    

As you see, only the synthetic option is set. All other options are only used to configure how a service is created by the container. As the service isn’t created by the container, these options are omitted.

Now, you can inject the class by using Container::set:

// ...
$container->set('request', new MyRequest(...));
Aliasing

You may sometimes want to use shortcuts to access some services. You can do so by aliasing them and, furthermore, you can even alias non-public services.

  • YAML
    services:
       foo:
         class: Example\Foo
       bar:
         alias: foo
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="foo" class="Example\Foo" />
    
            <service id="bar" alias="foo" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition('foo', new Definition('Example\Foo'));
    
    $containerBuilder->setAlias('bar', 'foo');
    

This means that when using the container directly, you can access the foo service by asking for the bar service like this:

$container->get('bar'); // Would return the foo service

小技巧

In YAML, you can also use a shortcut to alias a service:

services:
   foo:
     class: Example\Foo
   bar: "@foo"
Requiring Files

There might be use cases when you need to include another file just before the service itself gets loaded. To do so, you can use the file directive.

  • YAML
    services:
       foo:
         class: Example\Foo\Bar
         file: "%kernel.root_dir%/src/path/to/file/foo.php"
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="foo" class="Example\Foo\Bar">
                <file>%kernel.root_dir%/src/path/to/file/foo.php</file>
            </service>
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $definition = new Definition('Example\Foo\Bar');
    $definition->setFile('%kernel.root_dir%/src/path/to/file/foo.php');
    $container->setDefinition('foo', $definition);
    

Notice that Symfony will internally call the PHP statement require_once, which means that your file will be included only once per request.

Lazy Services

2.3 新版功能: Lazy services were introduced in Symfony 2.3.

Why lazy Services?

In some cases, you may want to inject a service that is a bit heavy to instantiate, but is not always used inside your object. For example, imagine you have a NewsletterManager and you inject a mailer service into it. Only a few methods on your NewsletterManager actually use the mailer, but even when you don’t need it, a mailer service is always instantiated in order to construct your NewsletterManager.

Configuring lazy services is one answer to this. With a lazy service, a “proxy” of the mailer service is actually injected. It looks and acts just like the mailer, except that the mailer isn’t actually instantiated until you interact with the proxy in some way.

Installation

In order to use the lazy service instantiation, you will first need to install the ProxyManager bridge:

$ composer require symfony/proxy-manager-bridge:~2.3

注解

If you’re using the full-stack framework, the proxy manager bridge is already included but the actual proxy manager needs to be included. So, run:

$ php composer.phar require ocramius/proxy-manager:~0.5

Afterwards compile your container and check to make sure that you get a proxy for your lazy services.

Configuration

You can mark the service as lazy by manipulating its definition:

  • YAML
    services:
       foo:
         class: Acme\Foo
         lazy: true
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="foo" class="Acme\Foo" lazy="true" />
        </services>
    </container>
    
  • PHP
    use Symfony\Component\DependencyInjection\Definition;
    
    $definition = new Definition('Acme\Foo');
    $definition->setLazy(true);
    $container->setDefinition('foo', $definition);
    

You can then require the service from the container:

$service = $container->get('foo');

At this point the retrieved $service should be a virtual proxy with the same signature of the class representing the service. You can also inject the service just like normal into other services. The object that’s actually injected will be the proxy.

To check if your proxy works you can simply check the interface of the received object.

var_dump(class_implements($service));

If the class implements the ProxyManager\Proxy\LazyLoadingInterface your lazy loaded services are working.

注解

If you don’t install the ProxyManager bridge, the container will just skip over the lazy flag and simply instantiate the service as it would normally do.

The proxy gets initialized and the actual service is instantiated as soon as you interact in any way with this object.

Additional Resources

You can read more about how proxies are instantiated, generated and initialized in the documentation of ProxyManager.

Container Building Workflow

In the preceding pages of this section, there has been little to say about where the various files and classes should be located. This is because this depends on the application, library or framework in which you want to use the container. Looking at how the container is configured and built in the Symfony full stack framework will help you see how this all fits together, whether you are using the full stack framework or looking to use the service container in another application.

The full stack framework uses the HttpKernel component to manage the loading of the service container configuration from the application and bundles and also handles the compilation and caching. Even if you are not using HttpKernel, it should give you an idea of one way of organizing configuration in a modular application.

Working with a Cached Container

Before building it, the kernel checks to see if a cached version of the container exists. The HttpKernel has a debug setting and if this is false, the cached version is used if it exists. If debug is true then the kernel checks to see if configuration is fresh and if it is, the cached version of the container is used. If not then the container is built from the application-level configuration and the bundles’s extension configuration.

Read Dumping the Configuration for Performance for more details.

Application-level Configuration

Application level config is loaded from the app/config directory. Multiple files are loaded which are then merged when the extensions are processed. This allows for different configuration for different environments e.g. dev, prod.

These files contain parameters and services that are loaded directly into the container as per Setting Up the Container with Configuration Files. They also contain configuration that is processed by extensions as per Managing Configuration with Extensions. These are considered to be bundle configuration since each bundle contains an Extension class.

Bundle-level Configuration with Extensions

By convention, each bundle contains an Extension class which is in the bundle’s DependencyInjection directory. These are registered with the ContainerBuilder when the kernel is booted. When the ContainerBuilder is compiled, the application-level configuration relevant to the bundle’s extension is passed to the Extension which also usually loads its own config file(s), typically from the bundle’s Resources/config directory. The application-level config is usually processed with a Configuration object also stored in the bundle’s DependencyInjection directory.

Compiler Passes to Allow Interaction between Bundles

Compiler passes are used to allow interaction between different bundles as they cannot affect each other’s configuration in the extension classes. One of the main uses is to process tagged services, allowing bundles to register services to be picked up by other bundles, such as Monolog loggers, Twig extensions and Data Collectors for the Web Profiler. Compiler passes are usually placed in the bundle’s DependencyInjection/Compiler directory.

Compilation and Caching

After the compilation process has loaded the services from the configuration, extensions and the compiler passes, it is dumped so that the cache can be used next time. The dumped version is then used during subsequent requests as it is more efficient.

The DomCrawler Component

The DomCrawler component eases DOM navigation for HTML and XML documents.

注解

While possible, the DomCrawler component is not designed for manipulation of the DOM or re-dumping HTML/XML.

Installation

You can install the component in 2 different ways:

Usage

The Crawler class provides methods to query and manipulate HTML and XML documents.

An instance of the Crawler represents a set (SplObjectStorage) of DOMElement objects, which are basically nodes that you can traverse easily:

use Symfony\Component\DomCrawler\Crawler;

$html = <<<'HTML'
<!DOCTYPE html>
<html>
    <body>
        <p class="message">Hello World!</p>
        <p>Hello Crawler!</p>
    </body>
</html>
HTML;

$crawler = new Crawler($html);

foreach ($crawler as $domElement) {
    print $domElement->nodeName;
}

Specialized Link and Form classes are useful for interacting with html links and forms as you traverse through the HTML tree.

注解

The DomCrawler will attempt to automatically fix your HTML to match the official specification. For example, if you nest a <p> tag inside another <p> tag, it will be moved to be a sibling of the parent tag. This is expected and is part of the HTML5 spec. But if you’re getting unexpected behavior, this could be a cause. And while the DomCrawler isn’t meant to dump content, you can see the “fixed” version of your HTML by dumping it.

Node Filtering

Using XPath expressions is really easy:

$crawler = $crawler->filterXPath('descendant-or-self::body/p');

小技巧

DOMXPath::query is used internally to actually perform an XPath query.

Filtering is even easier if you have the CssSelector component installed. This allows you to use jQuery-like selectors to traverse:

$crawler = $crawler->filter('body > p');

Anonymous function can be used to filter with more complex criteria:

use Symfony\Component\DomCrawler\Crawler;
// ...

$crawler = $crawler
    ->filter('body > p')
    ->reduce(function (Crawler $node, $i) {
        // filter even nodes
        return ($i % 2) == 0;
    });

To remove a node the anonymous function must return false.

注解

All filter methods return a new Crawler instance with filtered content.

Node Traversing

Access node by its position on the list:

$crawler->filter('body > p')->eq(0);

Get the first or last node of the current selection:

$crawler->filter('body > p')->first();
$crawler->filter('body > p')->last();

Get the nodes of the same level as the current selection:

$crawler->filter('body > p')->siblings();

Get the same level nodes after or before the current selection:

$crawler->filter('body > p')->nextAll();
$crawler->filter('body > p')->previousAll();

Get all the child or parent nodes:

$crawler->filter('body')->children();
$crawler->filter('body > p')->parents();

注解

All the traversal methods return a new Crawler instance.

Accessing Node Values

Access the value of the first node of the current selection:

$message = $crawler->filterXPath('//body/p')->text();

Access the attribute value of the first node of the current selection:

$class = $crawler->filterXPath('//body/p')->attr('class');

Extract attribute and/or node values from the list of nodes:

$attributes = $crawler
    ->filterXpath('//body/p')
    ->extract(array('_text', 'class'))
;

注解

Special attribute _text represents a node value.

Call an anonymous function on each node of the list:

use Symfony\Component\DomCrawler\Crawler;
// ...

$nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i) {
    return $node->text();
});

2.3 新版功能: As seen here, in Symfony 2.3, the each and reduce Closure functions are passed a Crawler as the first argument. Previously, that argument was a DOMNode.

The anonymous function receives the node (as a Crawler) and the position as arguments. The result is an array of values returned by the anonymous function calls.

Adding the Content

The crawler supports multiple ways of adding the content:

$crawler = new Crawler('<html><body /></html>');

$crawler->addHtmlContent('<html><body /></html>');
$crawler->addXmlContent('<root><node /></root>');

$crawler->addContent('<html><body /></html>');
$crawler->addContent('<root><node /></root>', 'text/xml');

$crawler->add('<html><body /></html>');
$crawler->add('<root><node /></root>');

注解

When dealing with character sets other than ISO-8859-1, always add HTML content using the addHTMLContent() method where you can specify the second parameter to be your target character set.

As the Crawler’s implementation is based on the DOM extension, it is also able to interact with native DOMDocument, DOMNodeList and DOMNode objects:

$document = new \DOMDocument();
$document->loadXml('<root><node /><node /></root>');
$nodeList = $document->getElementsByTagName('node');
$node = $document->getElementsByTagName('node')->item(0);

$crawler->addDocument($document);
$crawler->addNodeList($nodeList);
$crawler->addNodes(array($node));
$crawler->addNode($node);
$crawler->add($document);
Forms

Special treatment is also given to forms. A selectButton() method is available on the Crawler which returns another Crawler that matches a button (input[type=submit], input[type=image], or a button) with the given text. This method is especially useful because you can use it to return a Form object that represents the form that the button lives in:

$form = $crawler->selectButton('validate')->form();

// or "fill" the form fields with data
$form = $crawler->selectButton('validate')->form(array(
    'name' => 'Ryan',
));

The Form object has lots of very useful methods for working with forms:

$uri = $form->getUri();

$method = $form->getMethod();

The getUri() method does more than just return the action attribute of the form. If the form method is GET, then it mimics the browser’s behavior and returns the action attribute followed by a query string of all of the form’s values.

You can virtually set and get values on the form:

// set values on the form internally
$form->setValues(array(
    'registration[username]' => 'symfonyfan',
    'registration[terms]'    => 1,
));

// get back an array of values - in the "flat" array like above
$values = $form->getValues();

// returns the values like PHP would see them,
// where "registration" is its own array
$values = $form->getPhpValues();

To work with multi-dimensional fields:

<form>
    <input name="multi[]" />
    <input name="multi[]" />
    <input name="multi[dimensional]" />
</form>

Pass an array of values:

// Set a single field
$form->setValues(array('multi' => array('value')));

// Set multiple fields at once
$form->setValues(array('multi' => array(
    1             => 'value',
    'dimensional' => 'an other value'
)));

This is great, but it gets better! The Form object allows you to interact with your form like a browser, selecting radio values, ticking checkboxes, and uploading files:

$form['registration[username]']->setValue('symfonyfan');

// check or uncheck a checkbox
$form['registration[terms]']->tick();
$form['registration[terms]']->untick();

// select an option
$form['registration[birthday][year]']->select(1984);

// select many options from a "multiple" select
$form['registration[interests]']->select(array('symfony', 'cookies'));

// even fake a file upload
$form['registration[photo]']->upload('/path/to/lucas.jpg');

What’s the point of doing all of this? If you’re testing internally, you can grab the information off of your form as if it had just been submitted by using the PHP values:

$values = $form->getPhpValues();
$files = $form->getPhpFiles();

If you’re using an external HTTP client, you can use the form to grab all of the information you need to create a POST request for the form:

$uri = $form->getUri();
$method = $form->getMethod();
$values = $form->getValues();
$files = $form->getFiles();

// now use some HTTP client and post using this information

One great example of an integrated system that uses all of this is Goutte. Goutte understands the Symfony Crawler object and can use it to submit forms directly:

use Goutte\Client;

// make a real request to an external site
$client = new Client();
$crawler = $client->request('GET', 'https://github.com/login');

// select the form and fill in some values
$form = $crawler->selectButton('Log in')->form();
$form['login'] = 'symfonyfan';
$form['password'] = 'anypass';

// submit that form
$crawler = $client->submit($form);

EventDispatcher

The EventDispatcher Component
The EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them.
Introduction

Object Oriented code has gone a long way to ensuring code extensibility. By creating classes that have well defined responsibilities, your code becomes more flexible and a developer can extend them with subclasses to modify their behaviors. But if they want to share the changes with other developers who have also made their own subclasses, code inheritance is no longer the answer.

Consider the real-world example where you want to provide a plugin system for your project. A plugin should be able to add methods, or do something before or after a method is executed, without interfering with other plugins. This is not an easy problem to solve with single inheritance, and multiple inheritance (were it possible with PHP) has its own drawbacks.

The Symfony EventDispatcher component implements the Mediator pattern in a simple and effective way to make all these things possible and to make your projects truly extensible.

Take a simple example from The HttpKernel Component. Once a Response object has been created, it may be useful to allow other elements in the system to modify it (e.g. add some cache headers) before it’s actually used. To make this possible, the Symfony kernel throws an event - kernel.response. Here’s how it works:

  • A listener (PHP object) tells a central dispatcher object that it wants to listen to the kernel.response event;
  • At some point, the Symfony kernel tells the dispatcher object to dispatch the kernel.response event, passing with it an Event object that has access to the Response object;
  • The dispatcher notifies (i.e. calls a method on) all listeners of the kernel.response event, allowing each of them to make modifications to the Response object.
Installation

You can install the component in 2 different ways:

Usage
Events

When an event is dispatched, it’s identified by a unique name (e.g. kernel.response), which any number of listeners might be listening to. An Event instance is also created and passed to all of the listeners. As you’ll see later, the Event object itself often contains data about the event being dispatched.

Naming Conventions

The unique event name can be any string, but optionally follows a few simple naming conventions:

  • use only lowercase letters, numbers, dots (.), and underscores (_);
  • prefix names with a namespace followed by a dot (e.g. kernel.);
  • end names with a verb that indicates what action is being taken (e.g. request).

Here are some examples of good event names:

  • kernel.response
  • form.pre_set_data
Event Names and Event Objects

When the dispatcher notifies listeners, it passes an actual Event object to those listeners. The base Event class is very simple: it contains a method for stopping event propagation, but not much else.

Often times, data about a specific event needs to be passed along with the Event object so that the listeners have needed information. In the case of the kernel.response event, the Event object that’s created and passed to each listener is actually of type FilterResponseEvent, a subclass of the base Event object. This class contains methods such as getResponse and setResponse, allowing listeners to get or even replace the Response object.

The moral of the story is this: When creating a listener to an event, the Event object that’s passed to the listener may be a special subclass that has additional methods for retrieving information from and responding to the event.

The Dispatcher

The dispatcher is the central object of the event dispatcher system. In general, a single dispatcher is created, which maintains a registry of listeners. When an event is dispatched via the dispatcher, it notifies all listeners registered with that event:

use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
Connecting Listeners

To take advantage of an existing event, you need to connect a listener to the dispatcher so that it can be notified when the event is dispatched. A call to the dispatcher’s addListener() method associates any valid PHP callable to an event:

$listener = new AcmeListener();
$dispatcher->addListener('foo.action', array($listener, 'onFooAction'));

The addListener() method takes up to three arguments:

  • The event name (string) that this listener wants to listen to;
  • A PHP callable that will be notified when an event is thrown that it listens to;
  • An optional priority integer (higher equals more important, and therefore that the listener will be triggered earlier) that determines when a listener is triggered versus other listeners (defaults to 0). If two listeners have the same priority, they are executed in the order that they were added to the dispatcher.

注解

A PHP callable is a PHP variable that can be used by the call_user_func() function and returns true when passed to the is_callable() function. It can be a \Closure instance, an object implementing an __invoke method (which is what closures are in fact), a string representing a function, or an array representing an object method or a class method.

So far, you’ve seen how PHP objects can be registered as listeners. You can also register PHP Closures as event listeners:

use Symfony\Component\EventDispatcher\Event;

$dispatcher->addListener('foo.action', function (Event $event) {
    // will be executed when the foo.action event is dispatched
});

Once a listener is registered with the dispatcher, it waits until the event is notified. In the above example, when the foo.action event is dispatched, the dispatcher calls the AcmeListener::onFooAction method and passes the Event object as the single argument:

use Symfony\Component\EventDispatcher\Event;

class AcmeListener
{
    // ...

    public function onFooAction(Event $event)
    {
        // ... do something
    }
}

In many cases, a special Event subclass that’s specific to the given event is passed to the listener. This gives the listener access to special information about the event. Check the documentation or implementation of each event to determine the exact Symfony\Component\EventDispatcher\Event instance that’s being passed. For example, the kernel.response event passes an instance of Symfony\Component\HttpKernel\Event\FilterResponseEvent:

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

public function onKernelResponse(FilterResponseEvent $event)
{
    $response = $event->getResponse();
    $request = $event->getRequest();

    // ...
}
Creating and Dispatching an Event

In addition to registering listeners with existing events, you can create and dispatch your own events. This is useful when creating third-party libraries and also when you want to keep different components of your own system flexible and decoupled.

The Static Events Class

Suppose you want to create a new Event - store.order - that is dispatched each time an order is created inside your application. To keep things organized, start by creating a StoreEvents class inside your application that serves to define and document your event:

namespace Acme\StoreBundle;

final class StoreEvents
{
    /**
     * The store.order event is thrown each time an order is created
     * in the system.
     *
     * The event listener receives an
     * Acme\StoreBundle\Event\FilterOrderEvent instance.
     *
     * @var string
     */
    const STORE_ORDER = 'store.order';
}

Notice that this class doesn’t actually do anything. The purpose of the StoreEvents class is just to be a location where information about common events can be centralized. Notice also that a special FilterOrderEvent class will be passed to each listener of this event.

Creating an Event Object

Later, when you dispatch this new event, you’ll create an Event instance and pass it to the dispatcher. The dispatcher then passes this same instance to each of the listeners of the event. If you don’t need to pass any information to your listeners, you can use the default Symfony\Component\EventDispatcher\Event class. Most of the time, however, you will need to pass information about the event to each listener. To accomplish this, you’ll create a new class that extends Symfony\Component\EventDispatcher\Event.

In this example, each listener will need access to some pretend Order object. Create an Event class that makes this possible:

namespace Acme\StoreBundle\Event;

use Symfony\Component\EventDispatcher\Event;
use Acme\StoreBundle\Order;

class FilterOrderEvent extends Event
{
    protected $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function getOrder()
    {
        return $this->order;
    }
}

Each listener now has access to the Order object via the getOrder method.

Dispatch the Event

The dispatch() method notifies all listeners of the given event. It takes two arguments: the name of the event to dispatch and the Event instance to pass to each listener of that event:

use Acme\StoreBundle\StoreEvents;
use Acme\StoreBundle\Order;
use Acme\StoreBundle\Event\FilterOrderEvent;

// the order is somehow created or retrieved
$order = new Order();
// ...

// create the FilterOrderEvent and dispatch it
$event = new FilterOrderEvent($order);
$dispatcher->dispatch(StoreEvents::STORE_ORDER, $event);

Notice that the special FilterOrderEvent object is created and passed to the dispatch method. Now, any listener to the store.order event will receive the FilterOrderEvent and have access to the Order object via the getOrder method:

// some listener class that's been registered for "store.order" event
use Acme\StoreBundle\Event\FilterOrderEvent;

public function onStoreOrder(FilterOrderEvent $event)
{
    $order = $event->getOrder();
    // do something to or with the order
}
Using Event Subscribers

The most common way to listen to an event is to register an event listener with the dispatcher. This listener can listen to one or more events and is notified each time those events are dispatched.

Another way to listen to events is via an event subscriber. An event subscriber is a PHP class that’s able to tell the dispatcher exactly which events it should subscribe to. It implements the EventSubscriberInterface interface, which requires a single static method called getSubscribedEvents. Take the following example of a subscriber that subscribes to the kernel.response and store.order events:

namespace Acme\StoreBundle\Event;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class StoreSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            'kernel.response' => array(
                array('onKernelResponsePre', 10),
                array('onKernelResponseMid', 5),
                array('onKernelResponsePost', 0),
            ),
            'store.order'     => array('onStoreOrder', 0),
        );
    }

    public function onKernelResponsePre(FilterResponseEvent $event)
    {
        // ...
    }

    public function onKernelResponseMid(FilterResponseEvent $event)
    {
        // ...
    }

    public function onKernelResponsePost(FilterResponseEvent $event)
    {
        // ...
    }

    public function onStoreOrder(FilterOrderEvent $event)
    {
        // ...
    }
}

This is very similar to a listener class, except that the class itself can tell the dispatcher which events it should listen to. To register a subscriber with the dispatcher, use the addSubscriber() method:

use Acme\StoreBundle\Event\StoreSubscriber;

$subscriber = new StoreSubscriber();
$dispatcher->addSubscriber($subscriber);

The dispatcher will automatically register the subscriber for each event returned by the getSubscribedEvents method. This method returns an array indexed by event names and whose values are either the method name to call or an array composed of the method name to call and a priority. The example above shows how to register several listener methods for the same event in subscriber and also shows how to pass the priority of each listener method. The higher the priority, the earlier the method is called. In the above example, when the kernel.response event is triggered, the methods onKernelResponsePre, onKernelResponseMid, and onKernelResponsePost are called in that order.

Stopping Event Flow/Propagation

In some cases, it may make sense for a listener to prevent any other listeners from being called. In other words, the listener needs to be able to tell the dispatcher to stop all propagation of the event to future listeners (i.e. to not notify any more listeners). This can be accomplished from inside a listener via the stopPropagation() method:

use Acme\StoreBundle\Event\FilterOrderEvent;

public function onStoreOrder(FilterOrderEvent $event)
{
    // ...

    $event->stopPropagation();
}

Now, any listeners to store.order that have not yet been called will not be called.

It is possible to detect if an event was stopped by using the isPropagationStopped() method which returns a boolean value:

$dispatcher->dispatch('foo.event', $event);
if ($event->isPropagationStopped()) {
    // ...
}
EventDispatcher aware Events and Listeners

The EventDispatcher always injects a reference to itself in the passed event object. This means that all listeners have direct access to the EventDispatcher object that notified the listener via the passed Event object’s getDispatcher() method.

This can lead to some advanced applications of the EventDispatcher including letting listeners dispatch other events, event chaining or even lazy loading of more listeners into the dispatcher object. Examples follow:

Lazy loading listeners:

use Symfony\Component\EventDispatcher\Event;
use Acme\StoreBundle\Event\StoreSubscriber;

class Foo
{
    private $started = false;

    public function myLazyListener(Event $event)
    {
        if (false === $this->started) {
            $subscriber = new StoreSubscriber();
            $event->getDispatcher()->addSubscriber($subscriber);
        }

        $this->started = true;

        // ... more code
    }
}

Dispatching another event from within a listener:

use Symfony\Component\EventDispatcher\Event;

class Foo
{
    public function myFooListener(Event $event)
    {
        $event->getDispatcher()->dispatch('log', $event);

        // ... more code
    }
}

While this above is sufficient for most uses, if your application uses multiple EventDispatcher instances, you might need to specifically inject a known instance of the EventDispatcher into your listeners. This could be done using constructor or setter injection as follows:

Constructor injection:

use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class Foo
{
    protected $dispatcher = null;

    public function __construct(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }
}

Or setter injection:

use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class Foo
{
    protected $dispatcher = null;

    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }
}

Choosing between the two is really a matter of taste. Many tend to prefer the constructor injection as the objects are fully initialized at construction time. But when you have a long list of dependencies, using setter injection can be the way to go, especially for optional dependencies.

Dispatcher Shortcuts

The EventDispatcher::dispatch method always returns an Event object. This allows for various shortcuts. For example, if one does not need a custom event object, one can simply rely on a plain Event object. You do not even need to pass this to the dispatcher as it will create one by default unless you specifically pass one:

$dispatcher->dispatch('foo.event');

Moreover, the EventDispatcher always returns whichever event object that was dispatched, i.e. either the event that was passed or the event that was created internally by the dispatcher. This allows for nice shortcuts:

if (!$dispatcher->dispatch('foo.event')->isPropagationStopped()) {
    // ...
}

Or:

$barEvent = new BarEvent();
$bar = $dispatcher->dispatch('bar.event', $barEvent)->getBar();

Or:

$bar = $dispatcher->dispatch('bar.event', new BarEvent())->getBar();

and so on...

Event Name Introspection

Since the EventDispatcher already knows the name of the event when dispatching it, the event name is also injected into the Event objects, making it available to event listeners via the getName() method.

The event name, (as with any other data in a custom event object) can be used as part of the listener’s processing logic:

use Symfony\Component\EventDispatcher\Event;

class Foo
{
    public function myEventListener(Event $event)
    {
        echo $event->getName();
    }
}
Other Dispatchers

Besides the commonly used EventDispatcher, the component comes with 2 other dispatchers:

The Container Aware Event Dispatcher
Introduction

The ContainerAwareEventDispatcher is a special EventDispatcher implementation which is coupled to the service container that is part of the DependencyInjection component. It allows services to be specified as event listeners making the EventDispatcher extremely powerful.

Services are lazy loaded meaning the services attached as listeners will only be created if an event is dispatched that requires those listeners.

Setup

Setup is straightforward by injecting a ContainerInterface into the ContainerAwareEventDispatcher:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;

$container = new ContainerBuilder();
$dispatcher = new ContainerAwareEventDispatcher($container);
Adding Listeners

The Container Aware EventDispatcher can either load specified services directly, or services that implement EventSubscriberInterface.

The following examples assume the service container has been loaded with any services that are mentioned.

注解

Services must be marked as public in the container.

Adding Services

To connect existing service definitions, use the addListenerService() method where the $callback is an array of array($serviceId, $methodName):

$dispatcher->addListenerService($eventName, array('foo', 'logListener'));
Adding Subscriber Services

EventSubscribers can be added using the addSubscriberService() method where the first argument is the service ID of the subscriber service, and the second argument is the service’s class name (which must implement EventSubscriberInterface) as follows:

$dispatcher->addSubscriberService(
    'kernel.store_subscriber',
    'StoreSubscriber'
);

The EventSubscriberInterface will be exactly as you would expect:

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
// ...

class StoreSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            'kernel.response' => array(
                array('onKernelResponsePre', 10),
                array('onKernelResponsePost', 0),
            ),
            'store.order'     => array('onStoreOrder', 0),
        );
    }

    public function onKernelResponsePre(FilterResponseEvent $event)
    {
        // ...
    }

    public function onKernelResponsePost(FilterResponseEvent $event)
    {
        // ...
    }

    public function onStoreOrder(FilterOrderEvent $event)
    {
        // ...
    }
}
The Generic Event Object

The base Event class provided by the EventDispatcher component is deliberately sparse to allow the creation of API specific event objects by inheritance using OOP. This allows for elegant and readable code in complex applications.

The GenericEvent is available for convenience for those who wish to use just one event object throughout their application. It is suitable for most purposes straight out of the box, because it follows the standard observer pattern where the event object encapsulates an event ‘subject’, but has the addition of optional extra arguments.

GenericEvent has a simple API in addition to the base class Event

The GenericEvent also implements ArrayAccess on the event arguments which makes it very convenient to pass extra arguments regarding the event subject.

The following examples show use-cases to give a general idea of the flexibility. The examples assume event listeners have been added to the dispatcher.

Simply passing a subject:

use Symfony\Component\EventDispatcher\GenericEvent;

$event = new GenericEvent($subject);
$dispatcher->dispatch('foo', $event);

class FooListener
{
    public function handler(GenericEvent $event)
    {
        if ($event->getSubject() instanceof Foo) {
            // ...
        }
    }
}

Passing and processing arguments using the ArrayAccess API to access the event arguments:

use Symfony\Component\EventDispatcher\GenericEvent;

$event = new GenericEvent(
    $subject,
    array('type' => 'foo', 'counter' => 0)
);
$dispatcher->dispatch('foo', $event);

echo $event['counter'];

class FooListener
{
    public function handler(GenericEvent $event)
    {
        if (isset($event['type']) && $event['type'] === 'foo') {
            // ... do something
        }

        $event['counter']++;
    }
}

Filtering data:

use Symfony\Component\EventDispatcher\GenericEvent;

$event = new GenericEvent($subject, array('data' => 'Foo'));
$dispatcher->dispatch('foo', $event);

echo $event['data'];

class FooListener
{
    public function filter(GenericEvent $event)
    {
        $event['data'] = strtolower($event['data']);
    }
}
The Immutable Event Dispatcher

2.1 新版功能: This feature was introduced in Symfony 2.1.

The ImmutableEventDispatcher is a locked or frozen event dispatcher. The dispatcher cannot register new listeners or subscribers.

The ImmutableEventDispatcher takes another event dispatcher with all the listeners and subscribers. The immutable dispatcher is just a proxy of this original dispatcher.

To use it, first create a normal dispatcher (EventDispatcher or ContainerAwareEventDispatcher) and register some listeners or subscribers:

use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
$dispatcher->addListener('foo.action', function ($event) {
    // ...
});

// ...

Now, inject that into an ImmutableEventDispatcher:

use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
// ...

$immutableDispatcher = new ImmutableEventDispatcher($dispatcher);

You’ll need to use this new dispatcher in your project.

If you are trying to execute one of the methods which modifies the dispatcher (e.g. addListener), a BadMethodCallException is thrown.

The Traceable Event Dispatcher

The TraceableEventDispatcher is an event dispatcher that wraps any other event dispatcher and can then be used to determine which event listeners have been called by the dispatcher. Pass the event dispatcher to be wrapped and an instance of the Stopwatch to its constructor:

use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher;
use Symfony\Component\Stopwatch\Stopwatch;

// the event dispatcher to debug
$eventDispatcher = ...;

$traceableEventDispatcher = new TraceableEventDispatcher(
    $eventDispatcher,
    new Stopwatch()
);

Now, the TraceableEventDispatcher can be used like any other event dispatcher to register event listeners and dispatch events:

// ...

// register an event listener
$eventListener = ...;
$priority = ...;
$traceableEventDispatcher->addListener(
    'event.the_name',
    $eventListener,
    $priority
);

// dispatch an event
$event = ...;
$traceableEventDispatcher->dispatch('event.the_name', $event);

After your application has been processed, you can use the getCalledListeners() method to retrieve an array of event listeners that have been called in your application. Similarly, the getNotCalledListeners() method returns an array of event listeners that have not been called:

// ...

$calledListeners = $traceableEventDispatcher->getCalledListeners();
$notCalledListeners = $traceableEventDispatcher->getNotCalledListeners();

The Filesystem Component

The Filesystem component provides basic utilities for the filesystem.

2.1 新版功能: The Filesystem component was introduced in Symfony 2.1. Previously, the Filesystem class was located in the HttpKernel component.

Installation

You can install the component in 2 different ways:

Usage

The Filesystem class is the unique endpoint for filesystem operations:

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOException;

$fs = new Filesystem();

try {
    $fs->mkdir('/tmp/random/dir/'.mt_rand());
} catch (IOException $e) {
    echo "An error occurred while creating your directory";
}

注解

Methods mkdir(), exists(), touch(), remove(), chmod(), chown() and chgrp() can receive a string, an array or any object implementing Traversable as the target argument.

mkdir

mkdir() creates a directory. On POSIX filesystems, directories are created with a default mode value 0777. You can use the second argument to set your own mode:

$fs->mkdir('/tmp/photos', 0700);

注解

You can pass an array or any Traversable object as the first argument.

exists

exists() checks for the presence of all files or directories and returns false if a file is missing:

// this directory exists, return true
$fs->exists('/tmp/photos');

// rabbit.jpg exists, bottle.png does not exists, return false
$fs->exists(array('rabbit.jpg', 'bottle.png'));

注解

You can pass an array or any Traversable object as the first argument.

copy

copy() is used to copy files. If the target already exists, the file is copied only if the source modification date is later than the target. This behavior can be overridden by the third boolean argument:

// works only if image-ICC has been modified after image.jpg
$fs->copy('image-ICC.jpg', 'image.jpg');

// image.jpg will be overridden
$fs->copy('image-ICC.jpg', 'image.jpg', true);
touch

touch() sets access and modification time for a file. The current time is used by default. You can set your own with the second argument. The third argument is the access time:

// set modification time to the current timestamp
$fs->touch('file.txt');
// set modification time 10 seconds in the future
$fs->touch('file.txt', time() + 10);
// set access time 10 seconds in the past
$fs->touch('file.txt', time(), time() - 10);

注解

You can pass an array or any Traversable object as the first argument.

chown

chown() is used to change the owner of a file. The third argument is a boolean recursive option:

// set the owner of the lolcat video to www-data
$fs->chown('lolcat.mp4', 'www-data');
// change the owner of the video directory recursively
$fs->chown('/video', 'www-data', true);

注解

You can pass an array or any Traversable object as the first argument.

chgrp

chgrp() is used to change the group of a file. The third argument is a boolean recursive option:

// set the group of the lolcat video to nginx
$fs->chgrp('lolcat.mp4', 'nginx');
// change the group of the video directory recursively
$fs->chgrp('/video', 'nginx', true);

注解

You can pass an array or any Traversable object as the first argument.

chmod

chmod() is used to change the mode of a file. The fourth argument is a boolean recursive option:

// set the mode of the video to 0600
$fs->chmod('video.ogg', 0600);
// change the mod of the src directory recursively
$fs->chmod('src', 0700, 0000, true);

注解

You can pass an array or any Traversable object as the first argument.

remove

remove() is used to remove files, symlinks, directories easily:

$fs->remove(array('symlink', '/path/to/directory', 'activity.log'));

注解

You can pass an array or any Traversable object as the first argument.

rename

rename() is used to rename files and directories:

// rename a file
$fs->rename('/tmp/processed_video.ogg', '/path/to/store/video_647.ogg');
// rename a directory
$fs->rename('/tmp/files', '/path/to/store/files');
makePathRelative

makePathRelative() returns the relative path of a directory given another one:

// returns '../'
$fs->makePathRelative(
    '/var/lib/symfony/src/Symfony/',
    '/var/lib/symfony/src/Symfony/Component'
);
// returns 'videos/'
$fs->makePathRelative('/tmp/videos', '/tmp')
mirror

mirror() mirrors a directory:

$fs->mirror('/path/to/source', '/path/to/target');
isAbsolutePath

isAbsolutePath() returns true if the given path is absolute, false otherwise:

// return true
$fs->isAbsolutePath('/tmp');
// return true
$fs->isAbsolutePath('c:\\Windows');
// return false
$fs->isAbsolutePath('tmp');
// return false
$fs->isAbsolutePath('../dir');
dumpFile

2.3 新版功能: The dumpFile() was introduced in Symfony 2.3.

dumpFile() allows you to dump contents to a file. It does this in an atomic manner: it writes a temporary file first and then moves it to the new file location when it’s finished. This means that the user will always see either the complete old file or complete new file (but never a partially-written file):

$fs->dumpFile('file.txt', 'Hello World');

The file.txt file contains Hello World now.

A desired file mode can be passed as the third argument.

Error Handling

Whenever something wrong happens, an exception implementing ExceptionInterface is thrown.

注解

Prior to version 2.1, mkdir returned a boolean and did not throw exceptions. As of 2.1, a IOException is thrown if a directory creation fails.

The Finder Component

The Finder component finds files and directories via an intuitive fluent interface.
Installation

You can install the component in 2 different ways:

Usage

The Finder class finds files and/or directories:

use Symfony\Component\Finder\Finder;

$finder = new Finder();
$finder->files()->in(__DIR__);

foreach ($finder as $file) {
    // Print the absolute path
    print $file->getRealpath()."\n";

    // Print the relative path to the file, omitting the filename
    print $file->getRelativePath()."\n";

    // Print the relative path to the file
    print $file->getRelativePathname()."\n";
}

The $file is an instance of SplFileInfo which extends SplFileInfo to provide methods to work with relative paths.

The above code prints the names of all the files in the current directory recursively. The Finder class uses a fluent interface, so all methods return the Finder instance.

小技巧

A Finder instance is a PHP Iterator. So, instead of iterating over the Finder with foreach, you can also convert it to an array with the iterator_to_array method, or get the number of items with iterator_count.

警告

When searching through multiple locations passed to the in() method, a separate iterator is created internally for every location. This means we have multiple result sets aggregated into one. Since iterator_to_array uses keys of result sets by default, when converting to an array, some keys might be duplicated and their values overwritten. This can be avoided by passing false as a second parameter to iterator_to_array.

Criteria

There are lots of ways to filter and sort your results.

Location

The location is the only mandatory criteria. It tells the finder which directory to use for the search:

$finder->in(__DIR__);

Search in several locations by chaining calls to in():

$finder->files()->in(__DIR__)->in('/elsewhere');

2.2 新版功能: Wildcard support was introduced in version 2.2.

Use wildcard characters to search in the directories matching a pattern:

$finder->in('src/Symfony/*/*/Resources');

Each pattern has to resolve to at least one directory path.

Exclude directories from matching with the exclude() method:

$finder->in(__DIR__)->exclude('ruby');

2.3 新版功能: The ignoreUnreadableDirs() method was introduced in Symfony 2.3.

It’s also possible to ignore directories that you don’t have permission to read:

$finder->ignoreUnreadableDirs()->in(__DIR__);

As the Finder uses PHP iterators, you can pass any URL with a supported protocol:

$finder->in('ftp://example.com/pub/');

And it also works with user-defined streams:

use Symfony\Component\Finder\Finder;

$s3 = new \Zend_Service_Amazon_S3($key, $secret);
$s3->registerStreamWrapper("s3");

$finder = new Finder();
$finder->name('photos*')->size('< 100K')->date('since 1 hour ago');
foreach ($finder->in('s3://bucket-name') as $file) {
    // ... do something

    print $file->getFilename()."\n";
}

注解

Read the Streams documentation to learn how to create your own streams.

Files or Directories

By default, the Finder returns files and directories; but the files() and directories() methods control that:

$finder->files();

$finder->directories();

If you want to follow links, use the followLinks() method:

$finder->files()->followLinks();

By default, the iterator ignores popular VCS files. This can be changed with the ignoreVCS() method:

$finder->ignoreVCS(false);
Sorting

Sort the result by name or by type (directories first, then files):

$finder->sortByName();

$finder->sortByType();

注解

Notice that the sort* methods need to get all matching elements to do their jobs. For large iterators, it is slow.

You can also define your own sorting algorithm with sort() method:

$sort = function (\SplFileInfo $a, \SplFileInfo $b)
{
    return strcmp($a->getRealpath(), $b->getRealpath());
};

$finder->sort($sort);
File Name

Restrict files by name with the name() method:

$finder->files()->name('*.php');

The name() method accepts globs, strings, or regexes:

$finder->files()->name('/\.php$/');

The notName() method excludes files matching a pattern:

$finder->files()->notName('*.rb');
File Contents

Restrict files by contents with the contains() method:

$finder->files()->contains('lorem ipsum');

The contains() method accepts strings or regexes:

$finder->files()->contains('/lorem\s+ipsum$/i');

The notContains() method excludes files containing given pattern:

$finder->files()->notContains('dolor sit amet');
Path

2.2 新版功能: The path() and notPath() methods were introduced in Symfony 2.2.

Restrict files and directories by path with the path() method:

$finder->path('some/special/dir');

On all platforms slash (i.e. /) should be used as the directory separator.

The path() method accepts a string or a regular expression:

$finder->path('foo/bar');
$finder->path('/^foo\/bar/');

Internally, strings are converted into regular expressions by escaping slashes and adding delimiters:

dirname    ===>    /dirname/
a/b/c      ===>    /a\/b\/c/

The notPath() method excludes files by path:

$finder->notPath('other/dir');
File Size

Restrict files by size with the size() method:

$finder->files()->size('< 1.5K');

Restrict by a size range by chaining calls:

$finder->files()->size('>= 1K')->size('<= 2K');

The comparison operator can be any of the following: >, >=, <, <=, ==, !=.

The target value may use magnitudes of kilobytes (k, ki), megabytes (m, mi), or gigabytes (g, gi). Those suffixed with an i use the appropriate 2**n version in accordance with the IEC standard.

File Date

Restrict files by last modified dates with the date() method:

$finder->date('since yesterday');

The comparison operator can be any of the following: >, >=, <, <=, ==. You can also use since or after as an alias for >, and until or before as an alias for <.

The target value can be any date supported by the strtotime function.

Directory Depth

By default, the Finder recursively traverse directories. Restrict the depth of traversing with depth():

$finder->depth('== 0');
$finder->depth('< 3');
Custom Filtering

To restrict the matching file with your own strategy, use filter():

$filter = function (\SplFileInfo $file)
{
    if (strlen($file) > 10) {
        return false;
    }
};

$finder->files()->filter($filter);

The filter() method takes a Closure as an argument. For each matching file, it is called with the file as a SplFileInfo instance. The file is excluded from the result set if the Closure returns false.

Reading Contents of Returned Files

The contents of returned files can be read with getContents():

use Symfony\Component\Finder\Finder;

$finder = new Finder();
$finder->files()->in(__DIR__);

foreach ($finder as $file) {
    $contents = $file->getContents();

    // ...
}

Form

The Form Component
The Form component allows you to easily create, process and reuse HTML forms.

The Form component is a tool to help you solve the problem of allowing end-users to interact with the data and modify the data in your application. And though traditionally this has been through HTML forms, the component focuses on processing data to and from your client and application, whether that data be from a normal form post or from an API.

Installation

You can install the component in 2 different ways:

Configuration

小技巧

If you are working with the full-stack Symfony framework, the Form component is already configured for you. In this case, skip to Creating a simple Form.

In Symfony, forms are represented by objects and these objects are built by using a form factory. Building a form factory is simple:

use Symfony\Component\Form\Forms;

$formFactory = Forms::createFormFactory();

This factory can already be used to create basic forms, but it is lacking support for very important features:

  • Request Handling: Support for request handling and file uploads;
  • CSRF Protection: Support for protection against Cross-Site-Request-Forgery (CSRF) attacks;
  • Templating: Integration with a templating layer that allows you to reuse HTML fragments when rendering a form;
  • Translation: Support for translating error messages, field labels and other strings;
  • Validation: Integration with a validation library to generate error messages for submitted data.

The Symfony Form component relies on other libraries to solve these problems. Most of the time you will use Twig and the Symfony HttpFoundation, Translation and Validator components, but you can replace any of these with a different library of your choice.

The following sections explain how to plug these libraries into the form factory.

小技巧

For a working example, see https://github.com/bschussek/standalone-forms

Request Handling

2.3 新版功能: The handleRequest() method was introduced in Symfony 2.3.

To process form data, you’ll need to call the handleRequest() method:

$form->handleRequest();

Behind the scenes, this uses a NativeRequestHandler object to read data off of the correct PHP superglobals (i.e. $_POST or $_GET) based on the HTTP method configured on the form (POST is default).

参见

If you need more control over exactly when your form is submitted or which data is passed to it, you can use the submit() for this. Read more about it in the cookbook.

CSRF Protection

Protection against CSRF attacks is built into the Form component, but you need to explicitly enable it or replace it with a custom solution. The following snippet adds CSRF protection to the form factory:

use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider;
use Symfony\Component\HttpFoundation\Session\Session;

// generate a CSRF secret from somewhere
$csrfSecret = '<generated token>';

// create a Session object from the HttpFoundation component
$session = new Session();

$csrfProvider = new SessionCsrfProvider($session, $csrfSecret);

$formFactory = Forms::createFormFactoryBuilder()
    // ...
    ->addExtension(new CsrfExtension($csrfProvider))
    ->getFormFactory();

To secure your application against CSRF attacks, you need to define a CSRF secret. Generate a random string with at least 32 characters, insert it in the above snippet and make sure that nobody except your web server can access the secret.

Internally, this extension will automatically add a hidden field to every form (called __token by default) whose value is automatically generated and validated when binding the form.

小技巧

If you’re not using the HttpFoundation component, you can use DefaultCsrfProvider instead, which relies on PHP’s native session handling:

use Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider;

$csrfProvider = new DefaultCsrfProvider($csrfSecret);
Twig Templating

If you’re using the Form component to process HTML forms, you’ll need a way to easily render your form as HTML form fields (complete with field values, errors, and labels). If you use Twig as your template engine, the Form component offers a rich integration.

To use the integration, you’ll need the TwigBridge, which provides integration between Twig and several Symfony components. If you’re using Composer, you could install the latest 2.3 version by adding the following require line to your composer.json file:

{
    "require": {
        "symfony/twig-bridge": "2.3.*"
    }
}

The TwigBridge integration provides you with several Twig Functions that help you render the HTML widget, label and error for each field (as well as a few other things). To configure the integration, you’ll need to bootstrap or access Twig and add the FormExtension:

use Symfony\Component\Form\Forms;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;

// the Twig file that holds all the default markup for rendering forms
// this file comes with TwigBridge
$defaultFormTheme = 'form_div_layout.html.twig';

$vendorDir = realpath(__DIR__.'/../vendor');
// the path to TwigBridge so Twig can locate the
// form_div_layout.html.twig file
$vendorTwigBridgeDir =
    $vendorDir.'/symfony/twig-bridge/Symfony/Bridge/Twig';
// the path to your other templates
$viewsDir = realpath(__DIR__.'/../views');

$twig = new Twig_Environment(new Twig_Loader_Filesystem(array(
    $viewsDir,
    $vendorTwigBridgeDir.'/Resources/views/Form',
)));
$formEngine = new TwigRendererEngine(array($defaultFormTheme));
$formEngine->setEnvironment($twig);
// add the FormExtension to Twig
$twig->addExtension(
    new FormExtension(new TwigRenderer($formEngine, $csrfProvider))
);

// create your form factory as normal
$formFactory = Forms::createFormFactoryBuilder()
    // ...
    ->getFormFactory();

The exact details of your Twig Configuration will vary, but the goal is always to add the FormExtension to Twig, which gives you access to the Twig functions for rendering forms. To do this, you first need to create a TwigRendererEngine, where you define your form themes (i.e. resources/files that define form HTML markup).

For general details on rendering forms, see How to Customize Form Rendering.

注解

If you use the Twig integration, read “Translation” below for details on the needed translation filters.

Translation

If you’re using the Twig integration with one of the default form theme files (e.g. form_div_layout.html.twig), there are 2 Twig filters (trans and transChoice) that are used for translating form labels, errors, option text and other strings.

To add these Twig filters, you can either use the built-in TranslationExtension that integrates with Symfony’s Translation component, or add the 2 Twig filters yourself, via your own Twig extension.

To use the built-in integration, be sure that your project has Symfony’s Translation and Config components installed. If you’re using Composer, you could get the latest 2.3 version of each of these by adding the following to your composer.json file:

{
    "require": {
        "symfony/translation": "2.3.*",
        "symfony/config": "2.3.*"
    }
}

Next, add the TranslationExtension to your Twig_Environment instance:

use Symfony\Component\Form\Forms;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\XliffFileLoader;
use Symfony\Bridge\Twig\Extension\TranslationExtension;

// create the Translator
$translator = new Translator('en');
// somehow load some translations into it
$translator->addLoader('xlf', new XliffFileLoader());
$translator->addResource(
    'xlf',
    __DIR__.'/path/to/translations/messages.en.xlf',
    'en'
);

// add the TranslationExtension (gives us trans and transChoice filters)
$twig->addExtension(new TranslationExtension($translator));

$formFactory = Forms::createFormFactoryBuilder()
    // ...
    ->getFormFactory();

Depending on how your translations are being loaded, you can now add string keys, such as field labels, and their translations to your translation files.

For more details on translations, see Translations.

Validation

The Form component comes with tight (but optional) integration with Symfony’s Validator component. If you’re using a different solution for validation, no problem! Simply take the submitted/bound data of your form (which is an array or object) and pass it through your own validation system.

To use the integration with Symfony’s Validator component, first make sure it’s installed in your application. If you’re using Composer and want to install the latest 2.3 version, add this to your composer.json:

{
    "require": {
        "symfony/validator": "2.3.*"
    }
}

If you’re not familiar with Symfony’s Validator component, read more about it: Validation. The Form component comes with a ValidatorExtension class, which automatically applies validation to your data on bind. These errors are then mapped to the correct field and rendered.

Your integration with the Validation component will look something like this:

use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Validator\Validation;

$vendorDir = realpath(__DIR__.'/../vendor');
$vendorFormDir = $vendorDir.'/symfony/form/Symfony/Component/Form';
$vendorValidatorDir =
    $vendorDir.'/symfony/validator/Symfony/Component/Validator';

// create the validator - details will vary
$validator = Validation::createValidator();

// there are built-in translations for the core error messages
$translator->addResource(
    'xlf',
    $vendorFormDir.'/Resources/translations/validators.en.xlf',
    'en',
    'validators'
);
$translator->addResource(
    'xlf',
    $vendorValidatorDir.'/Resources/translations/validators.en.xlf',
    'en',
    'validators'
);

$formFactory = Forms::createFormFactoryBuilder()
    // ...
    ->addExtension(new ValidatorExtension($validator))
    ->getFormFactory();

To learn more, skip down to the Form Validation section.

Accessing the Form Factory

Your application only needs one form factory, and that one factory object should be used to create any and all form objects in your application. This means that you should create it in some central, bootstrap part of your application and then access it whenever you need to build a form.

注解

In this document, the form factory is always a local variable called $formFactory. The point here is that you will probably need to create this object in some more “global” way so you can access it from anywhere.

Exactly how you gain access to your one form factory is up to you. If you’re using a Service Container, then you should add the form factory to your container and grab it out whenever you need to. If your application uses global or static variables (not usually a good idea), then you can store the object on some static class or do something similar.

Regardless of how you architect your application, just remember that you should only have one form factory and that you’ll need to be able to access it throughout your application.

Creating a simple Form

小技巧

If you’re using the Symfony framework, then the form factory is available automatically as a service called form.factory. Also, the default base controller class has a createFormBuilder() method, which is a shortcut to fetch the form factory and call createBuilder on it.

Creating a form is done via a FormBuilder object, where you build and configure different fields. The form builder is created from the form factory.

  • Standalone Use
    $form = $formFactory->createBuilder()
        ->add('task', 'text')
        ->add('dueDate', 'date')
        ->getForm();
    
    echo $twig->render('new.html.twig', array(
        'form' => $form->createView(),
    ));
    
  • Framework Use
    // src/Acme/TaskBundle/Controller/DefaultController.php
    namespace Acme\TaskBundle\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\HttpFoundation\Request;
    
    class DefaultController extends Controller
    {
        public function newAction(Request $request)
        {
            // createFormBuilder is a shortcut to get the "form factory"
            // and then call "createBuilder()" on it
            $form = $this->createFormBuilder()
                ->add('task', 'text')
                ->add('dueDate', 'date')
                ->getForm();
    
            return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
                'form' => $form->createView(),
            ));
        }
    }
    

As you can see, creating a form is like writing a recipe: you call add for each new field you want to create. The first argument to add is the name of your field, and the second is the field “type”. The Form component comes with a lot of built-in types.

Now that you’ve built your form, learn how to render it and process the form submission.

Setting default Values

If you need your form to load with some default values (or you’re building an “edit” form), simply pass in the default data when creating your form builder:

  • Standalone Use
    $defaults = array(
        'dueDate' => new \DateTime('tomorrow'),
    );
    
    $form = $formFactory->createBuilder('form', $defaults)
        ->add('task', 'text')
        ->add('dueDate', 'date')
        ->getForm();
    
  • Framework Use
    $defaults = array(
        'dueDate' => new \DateTime('tomorrow'),
    );
    
    $form = $this->createFormBuilder($defaults)
        ->add('task', 'text')
        ->add('dueDate', 'date')
        ->getForm();
    

小技巧

In this example, the default data is an array. Later, when you use the data_class option to bind data directly to objects, your default data will be an instance of that object.

Rendering the Form

Now that the form has been created, the next step is to render it. This is done by passing a special form “view” object to your template (notice the $form->createView() in the controller above) and using a set of form helper functions:

<form action="#" method="post" {{ form_enctype(form) }}>
    {{ form_widget(form) }}

    <input type="submit" />
</form>
_images/form-simple.png

That’s it! By printing form_widget(form), each field in the form is rendered, along with a label and error message (if there is one). As easy as this is, it’s not very flexible (yet). Usually, you’ll want to render each form field individually so you can control how the form looks. You’ll learn how to do that in the “Rendering a Form in a Template” section.

Changing a Form’s Method and Action

2.3 新版功能: The ability to configure the form method and action was introduced in Symfony 2.3.

By default, a form is submitted to the same URI that rendered the form with an HTTP POST request. This behavior can be changed using the action and method options (the method option is also used by handleRequest() to determine whether a form has been submitted):

  • Standalone Use
    $formBuilder = $formFactory->createBuilder('form', null, array(
        'action' => '/search',
        'method' => 'GET',
    ));
    
    // ...
    
  • Framework Use
    // ...
    
    public function searchAction()
    {
        $formBuilder = $this->createFormBuilder('form', null, array(
            'action' => '/search',
            'method' => 'GET',
        ));
    
        // ...
    }
    
Handling Form Submissions

To handle form submissions, use the handleRequest() method:

  • Standalone Use
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\RedirectResponse;
    
    $form = $formFactory->createBuilder()
        ->add('task', 'text')
        ->add('dueDate', 'date')
        ->getForm();
    
    $request = Request::createFromGlobals();
    
    $form->handleRequest($request);
    
    if ($form->isValid()) {
        $data = $form->getData();
    
        // ... perform some action, such as saving the data to the database
    
        $response = new RedirectResponse('/task/success');
        $response->prepare($request);
    
        return $response->send();
    }
    
    // ...
    
  • Framework Use
    // ...
    
    public function newAction(Request $request)
    {
        $form = $this->createFormBuilder()
            ->add('task', 'text')
            ->add('dueDate', 'date')
            ->getForm();
    
        $form->handleRequest($request);
    
        if ($form->isValid()) {
            $data = $form->getData();
    
            // ... perform some action, such as saving the data to the database
    
            return $this->redirect($this->generateUrl('task_success'));
        }
    
        // ...
    }
    

This defines a common form “workflow”, which contains 3 different possibilities:

  1. On the initial GET request (i.e. when the user “surfs” to your page), build your form and render it;

If the request is a POST, process the submitted data (via handleRequest()). Then:

  1. if the form is invalid, re-render the form (which will now contain errors);
  2. if the form is valid, perform some action and redirect.

Luckily, you don’t need to decide whether or not a form has been submitted. Just pass the current request to the handleRequest() method. Then, the Form component will do all the necessary work for you.

Form Validation

The easiest way to add validation to your form is via the constraints option when building each field:

  • Standalone Use
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\Type;
    
    $form = $formFactory->createBuilder()
        ->add('task', 'text', array(
            'constraints' => new NotBlank(),
        ))
        ->add('dueDate', 'date', array(
            'constraints' => array(
                new NotBlank(),
                new Type('\DateTime'),
            )
        ))
        ->getForm();
    
  • Framework Use
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\Type;
    
    $form = $this->createFormBuilder()
        ->add('task', 'text', array(
            'constraints' => new NotBlank(),
        ))
        ->add('dueDate', 'date', array(
            'constraints' => array(
                new NotBlank(),
                new Type('\DateTime'),
            )
        ))
        ->getForm();
    

When the form is bound, these validation constraints will be applied automatically and the errors will display next to the fields on error.

注解

For a list of all of the built-in validation constraints, see Validation Constraints Reference.

Accessing Form Errors

You can use the getErrors() method to access the list of errors. Each element is a FormError object:

$form = ...;

// ...

// an array of FormError objects, but only errors attached to this
// form level (e.g. "global errors)
$errors = $form->getErrors();

// an array of FormError objects, but only errors attached to the
// "firstName" field
$errors = $form['firstName']->getErrors();

// a string representation of all errors of the whole form tree
$errors = $form->getErrorsAsString();

注解

If you enable the error_bubbling option on a field, calling getErrors() on the parent form will include errors from that field. However, there is no way to determine which field an error was originally attached to.

Creating a custom Type Guesser

The Form component can guess the type and some options of a form field by using type guessers. The component already includes a type guesser using the assertions of the Validation component, but you can also add your own custom type guessers.

Create a PHPDoc Type Guesser

In this section, you are going to build a guesser that reads information about fields from the PHPDoc of the properties. At first, you need to create a class which implements FormTypeGuesserInterface. This interface requires 4 methods:

Start by creating the class and these methods. Next, you’ll learn how to fill each on.

namespace Acme\Form;

use Symfony\Component\Form\FormTypeGuesserInterface;

class PHPDocTypeGuesser implements FormTypeGuesserInterface
{
    public function guessType($class, $property)
    {
    }

    public function guessRequired($class, $property)
    {
    }

    public function guessMaxLength($class, $property)
    {
    }

    public function guessPattern($class, $property)
    {
    }
}
Guessing the Type

When guessing a type, the method returns either an instance of TypeGuess or nothing, to determine that the type guesser cannot guess the type.

The TypeGuess constructor requires 3 options:

  • The type name (one of the form types);
  • Additional options (for instance, when the type is entity, you also want to set the class option). If no types are guessed, this should be set to an empty array;
  • The confidence that the guessed type is correct. This can be one of the constants of the Guess class: LOW_CONFIDENCE, MEDIUM_CONFIDENCE, HIGH_CONFIDENCE, VERY_HIGH_CONFIDENCE. After all type guessers have been executed, the type with the highest confidence is used.

With this knowledge, you can easily implement the guessType method of the PHPDocTypeGuesser:

namespace Acme\Form;

use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;

class PHPDocTypeGuesser implements FormTypeGuesserInterface
{
    public function guessType($class, $property)
    {
        $annotations = $this->readPhpDocAnnotations($class, $property);

        if (!isset($annotations['var'])) {
            return; // guess nothing if the @var annotation is not available
        }

        // otherwise, base the type on the @var annotation
        switch ($annotations['var']) {
            case 'string':
                // there is a high confidence that the type is text when
                // @var string is used
                return new TypeGuess('text', array(), Guess::HIGH_CONFIDENCE);

            case 'int':
            case 'integer':
                // integers can also be the id of an entity or a checkbox (0 or 1)
                return new TypeGuess('integer', array(), Guess::MEDIUM_CONFIDENCE);

            case 'float':
            case 'double':
            case 'real':
                return new TypeGuess('number', array(), Guess::MEDIUM_CONFIDENCE);

            case 'boolean':
            case 'bool':
                return new TypeGuess('checkbox', array(), Guess::HIGH_CONFIDENCE);

            default:
                // there is a very low confidence that this one is correct
                return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE);
        }
    }

    protected function readPhpDocAnnotations($class, $property)
    {
        $reflectionProperty = new \ReflectionProperty($class, $property);
        $phpdoc = $reflectionProperty->getDocComment();

        // parse the $phpdoc into an array like:
        // array('type' => 'string', 'since' => '1.0')
        $phpdocTags = ...;

        return $phpdocTags;
    }
}

This type guesser can now guess the field type for a property if it has PHPdoc!

Guessing Field Options

The other 3 methods (guessMaxLength, guessRequired and guessPattern) return a ValueGuess instance with the value of the option. This constructor has 2 arguments:

  • The value of the option;
  • The confidence that the guessed value is correct (using the constants of the Guess class).

null is guessed when you believe the value of the option should not be set.

警告

You should be very careful using the guessPattern method. When the type is a float, you cannot use it to determine a min or max value of the float (e.g. you want a float to be greater than 5, 4.512313 is not valid but length(4.512314) > length(5) is, so the pattern will succeed). In this case, the value should be set to null with a MEDIUM_CONFIDENCE.

Registering a Type Guesser

The last thing you need to do is registering your custom type guesser by using addTypeGuesser() or addTypeGuessers():

use Symfony\Component\Form\Forms;
use Acme\Form\PHPDocTypeGuesser;

$formFactory = Forms::createFormFactoryBuilder()
    // ...
    ->addTypeGuesser(new PHPDocTypeGuesser())
    ->getFormFactory();

// ...

注解

When you use the Symfony framework, you need to register your type guesser and tag it with form.type_guesser. For more information see the tag reference.

Form Events

The Form component provides a structured process to let you customize your forms, by making use of the EventDispatcher component. Using form events, you may modify information or fields at different steps of the workflow: from the population of the form to the submission of the data from the request.

Registering an event listener is very easy using the Form component.

For example, if you wish to register a function to the FormEvents::PRE_SUBMIT event, the following code lets you add a field, depending on the request values:

// ...

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

$listener = function (FormEvent $event) {
    // ...
};

$form = $formFactory->createBuilder()
    // add form fields
    ->addEventListener(FormEvents::PRE_SUBMIT, $listener);

// ...
The Form Workflow
The Form Submission Workflow _images/general_flow.png
1) Pre-populating the Form (FormEvents::PRE_SET_DATA and FormEvents::POST_SET_DATA) _images/set_data_flow.png

Two events are dispatched during pre-population of a form, when Form::setData() is called: FormEvents::PRE_SET_DATA and FormEvents::POST_SET_DATA.

A) The FormEvents::PRE_SET_DATA Event

The FormEvents::PRE_SET_DATA event is dispatched at the beginning of the Form::setData() method. It can be used to:

  • Modify the data given during pre-population;
  • Modify a form depending on the pre-populated data (adding or removing fields dynamically).

Form Events Information Table

Data Type Value
Model data null
Normalized data null
View data null

警告

During FormEvents::PRE_SET_DATA, Form::setData() is locked and will throw an exception if used. If you wish to modify data, you should use FormEvent::setData() instead.

B) The FormEvents::POST_SET_DATA Event

The FormEvents::POST_SET_DATA event is dispatched at the end of the Form::setData() method. This event is mostly here for reading data after having pre-populated the form.

Form Events Information Table

Data Type Value
Model data Model data injected into setData()
Normalized data Model data transformed using a model transformer
View data Normalized data transformed using a view transformer
2) Submitting a Form (FormEvents::PRE_SUBMIT, FormEvents::SUBMIT and FormEvents::POST_SUBMIT) _images/submission_flow.png

Three events are dispatched when Form::handleRequest() or Form::submit() are called: FormEvents::PRE_SUBMIT, FormEvents::SUBMIT, FormEvents::POST_SUBMIT.

A) The FormEvents::PRE_SUBMIT Event

The FormEvents::PRE_SUBMIT event is dispatched at the beginning of the Form::submit() method.

It can be used to:

  • Change data from the request, before submitting the data to the form;
  • Add or remove form fields, before submitting the data to the form.

Form Events Information Table

Data Type Value
Model data Same as in FormEvents::POST_SET_DATA
Normalized data Same as in FormEvents::POST_SET_DATA
View data Same as in FormEvents::POST_SET_DATA
B) The FormEvents::SUBMIT Event

The FormEvents::SUBMIT event is dispatched just before the Form::submit() method transforms back the normalized data to the model and view data.

It can be used to change data from the normalized representation of the data.

Form Events Information Table

Data Type Value
Model data Same as in FormEvents::POST_SET_DATA
Normalized data Data from the request reverse-transformed from the request using a view transformer
View data Same as in FormEvents::POST_SET_DATA

警告

At this point, you cannot add or remove fields to the form.

C) The FormEvents::POST_SUBMIT Event

The FormEvents::POST_SUBMIT event is dispatched after the Form::submit() once the model and view data have been denormalized.

It can be used to fetch data after denormalization.

Form Events Information Table

Data Type Value
Model data Normalized data reverse-transformed using a model transformer
Normalized data Same as in FormEvents::POST_SUBMIT
View data Normalized data transformed using a view transformer

警告

At this point, you cannot add or remove fields to the form.

Registering Event Listeners or Event Subscribers

In order to be able to use Form events, you need to create an event listener or an event subscriber, and register it to an event.

The name of each of the “form” events is defined as a constant on the FormEvents class. Additionally, each event callback (listener or subscriber method) is passed a single argument, which is an instance of FormEvent. The event object contains a reference to the current state of the form, and the current data being processed.

Name FormEvents Constant Event’s Data
form.pre_set_data FormEvents::PRE_SET_DATA Model data
form.post_set_data FormEvents::POST_SET_DATA Model data
form.pre_bind FormEvents::PRE_SUBMIT Request data
form.bind FormEvents::SUBMIT Normalized data
form.post_bind FormEvents::POST_SUBMIT View data

2.3 新版功能: Before Symfony 2.3, FormEvents::PRE_SUBMIT, FormEvents::SUBMIT and FormEvents::POST_SUBMIT were called FormEvents::PRE_BIND, FormEvents::BIND and FormEvents::POST_BIND.

警告

The FormEvents::PRE_BIND, FormEvents::BIND and FormEvents::POST_BIND constants will be removed in version 3.0 of Symfony. The event names still keep their original values, so make sure you use the FormEvents constants in your code for forward compatibility.

Event Listeners

An event listener may be any type of valid callable.

Creating and binding an event listener to the form is very easy:

// ...

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

$form = $formFactory->createBuilder()
    ->add('username', 'text')
    ->add('show_email', 'checkbox')
    ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
        $user = $event->getData();
        $form = $event->getForm();

        if (!$user) {
            return;
        }

        // Check whether the user has chosen to display his email or not.
        // If the data was submitted previously, the additional value that is
        // included in the request variables needs to be removed.
        if (true === $user['show_email']) {
            $form->add('email', 'email');
        } else {
            unset($user['email']);
            $event->setData($user);
        }
    })
    ->getForm();

// ...

When you have created a form type class, you can use one of its methods as a callback for better readability:

// ...

class SubscriptionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('username', 'text');
        $builder->add('show_email', 'checkbox');
        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            array($this, 'onPreSetData')
        );
    }

    public function onPreSetData(FormEvent $event)
    {
        // ...
    }
}
Event Subscribers

Event subscribers have different uses:

  • Improving readability;
  • Listening to multiple events;
  • Regrouping multiple listeners inside a single class.
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class AddEmailFieldListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'onPreSetData',
            FormEvents::PRE_SUBMIT   => 'onPreSubmit',
        );
    }

    public function onPreSetData(FormEvent $event)
    {
        $user = $event->getData();
        $form = $event->getForm();

        // Check whether the user from the initial data has chosen to
        // display his email or not.
        if (true === $user->isShowEmail()) {
            $form->add('email', 'email');
        }
    }

    public function onPreSubmit(FormEvent $event)
    {
        $user = $event->getData();
        $form = $event->getForm();

        if (!$user) {
            return;
        }

        // Check whether the user has chosen to display his email or not.
        // If the data was submitted previously, the additional value that
        // is included in the request variables needs to be removed.
        if (true === $user['show_email']) {
            $form->add('email', 'email');
        } else {
            unset($user['email']);
            $event->setData($user);
        }
    }
}

To register the event subscriber, use the addEventSubscriber() method:

// ...

$form = $formFactory->createBuilder()
    ->add('username', 'text')
    ->add('show_email', 'checkbox')
    ->addEventSubscriber(new AddEmailFieldListener())
    ->getForm();

// ...

HttpFoundation

The HttpFoundation Component
The HttpFoundation component defines an object-oriented layer for the HTTP specification.

In PHP, the request is represented by some global variables ($_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, ...) and the response is generated by some functions (echo, header, setcookie, ...).

The Symfony HttpFoundation component replaces these default PHP global variables and functions by an object-oriented layer.

Installation

You can install the component in 2 different ways:

Request

The most common way to create a request is to base it on the current PHP global variables with createFromGlobals():

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();

which is almost equivalent to the more verbose, but also more flexible, __construct() call:

$request = new Request(
    $_GET,
    $_POST,
    array(),
    $_COOKIE,
    $_FILES,
    $_SERVER
);
Accessing Request Data

A Request object holds information about the client request. This information can be accessed via several public properties:

  • request: equivalent of $_POST;
  • query: equivalent of $_GET ($request->query->get('name'));
  • cookies: equivalent of $_COOKIE;
  • attributes: no equivalent - used by your app to store other data (see below);
  • files: equivalent of $_FILES;
  • server: equivalent of $_SERVER;
  • headers: mostly equivalent to a sub-set of $_SERVER ($request->headers->get('User-Agent')).

Each property is a ParameterBag instance (or a sub-class of), which is a data holder class:

All ParameterBag instances have methods to retrieve and update its data:

all()
Returns the parameters.
keys()
Returns the parameter keys.
replace()
Replaces the current parameters by a new set.
add()
Adds parameters.
get()
Returns a parameter by name.
set()
Sets a parameter by name.
has()
Returns true if the parameter is defined.
remove()
Removes a parameter.

The ParameterBag instance also has some methods to filter the input values:

getAlpha()
Returns the alphabetic characters of the parameter value;
getAlnum()
Returns the alphabetic characters and digits of the parameter value;
getDigits()
Returns the digits of the parameter value;
getInt()
Returns the parameter value converted to integer;
filter()
Filters the parameter by using the PHP filter_var function.

All getters takes up to three arguments: the first one is the parameter name and the second one is the default value to return if the parameter does not exist:

// the query string is '?foo=bar'

$request->query->get('foo');
// returns bar

$request->query->get('bar');
// returns null

$request->query->get('bar', 'bar');
// returns 'bar'

When PHP imports the request query, it handles request parameters like foo[bar]=bar in a special way as it creates an array. So you can get the foo parameter and you will get back an array with a bar element. But sometimes, you might want to get the value for the “original” parameter name: foo[bar]. This is possible with all the ParameterBag getters like get() via the third argument:

// the query string is '?foo[bar]=bar'

$request->query->get('foo');
// returns array('bar' => 'bar')

$request->query->get('foo[bar]');
// returns null

$request->query->get('foo[bar]', null, true);
// returns 'bar'

Thanks to the public attributes property, you can store additional data in the request, which is also an instance of ParameterBag. This is mostly used to attach information that belongs to the Request and that needs to be accessed from many different points in your application. For information on how this is used in the Symfony framework, see the Symfony book.

Finally, the raw data sent with the request body can be accessed using getContent():

$content = $request->getContent();

For instance, this may be useful to process a JSON string sent to the application by a remote service using the HTTP POST method.

Identifying a Request

In your application, you need a way to identify a request; most of the time, this is done via the “path info” of the request, which can be accessed via the getPathInfo() method:

// for a request to http://example.com/blog/index.php/post/hello-world
// the path info is "/post/hello-world"
$request->getPathInfo();
Simulating a Request

Instead of creating a request based on the PHP globals, you can also simulate a request:

$request = Request::create(
    '/hello-world',
    'GET',
    array('name' => 'Fabien')
);

The create() method creates a request based on a URI, a method and some parameters (the query parameters or the request ones depending on the HTTP method); and of course, you can also override all other variables as well (by default, Symfony creates sensible defaults for all the PHP global variables).

Based on such a request, you can override the PHP global variables via overrideGlobals():

$request->overrideGlobals();

小技巧

You can also duplicate an existing request via duplicate() or change a bunch of parameters with a single call to initialize().

Accessing the Session

If you have a session attached to the request, you can access it via the getSession() method; the hasPreviousSession() method tells you if the request contains a session which was started in one of the previous requests.

Accessing Accept-* Headers Data

You can easily access basic data extracted from Accept-* headers by using the following methods:

getAcceptableContentTypes()
Returns the list of accepted content types ordered by descending quality.
getLanguages()
Returns the list of accepted languages ordered by descending quality.
getCharsets()
Returns the list of accepted charsets ordered by descending quality.

2.2 新版功能: The AcceptHeader class was introduced in Symfony 2.2.

If you need to get full access to parsed data from Accept, Accept-Language, Accept-Charset or Accept-Encoding, you can use AcceptHeader utility class:

use Symfony\Component\HttpFoundation\AcceptHeader;

$accept = AcceptHeader::fromString($request->headers->get('Accept'));
if ($accept->has('text/html')) {
    $item = $accept->get('text/html');
    $charset = $item->getAttribute('charset', 'utf-8');
    $quality = $item->getQuality();
}

// Accept header items are sorted by descending quality
$accepts = AcceptHeader::fromString($request->headers->get('Accept'))
    ->all();
Accessing other Data

The Request class has many other methods that you can use to access the request information. Have a look at the Request API for more information about them.

Response

A Response object holds all the information that needs to be sent back to the client from a given request. The constructor takes up to three arguments: the response content, the status code, and an array of HTTP headers:

use Symfony\Component\HttpFoundation\Response;

$response = new Response(
    'Content',
    200,
    array('content-type' => 'text/html')
);

This information can also be manipulated after the Response object creation:

$response->setContent('Hello World');

// the headers public attribute is a ResponseHeaderBag
$response->headers->set('Content-Type', 'text/plain');

$response->setStatusCode(404);

When setting the Content-Type of the Response, you can set the charset, but it is better to set it via the setCharset() method:

$response->setCharset('ISO-8859-1');

Note that by default, Symfony assumes that your Responses are encoded in UTF-8.

Sending the Response

Before sending the Response, you can ensure that it is compliant with the HTTP specification by calling the prepare() method:

$response->prepare($request);

Sending the response to the client is then as simple as calling send():

$response->send();
Setting Cookies

The response cookies can be manipulated through the headers public attribute:

use Symfony\Component\HttpFoundation\Cookie;

$response->headers->setCookie(new Cookie('foo', 'bar'));

The setCookie() method takes an instance of Cookie as an argument.

You can clear a cookie via the clearCookie() method.

Managing the HTTP Cache

The Response class has a rich set of methods to manipulate the HTTP headers related to the cache:

The setCache() method can be used to set the most commonly used cache information in one method call:

$response->setCache(array(
    'etag'          => 'abcdef',
    'last_modified' => new \DateTime(),
    'max_age'       => 600,
    's_maxage'      => 600,
    'private'       => false,
    'public'        => true,
));

To check if the Response validators (ETag, Last-Modified) match a conditional value specified in the client Request, use the isNotModified() method:

if ($response->isNotModified($request)) {
    $response->send();
}

If the Response is not modified, it sets the status code to 304 and removes the actual response content.

Redirecting the User

To redirect the client to another URL, you can use the RedirectResponse class:

use Symfony\Component\HttpFoundation\RedirectResponse;

$response = new RedirectResponse('http://example.com/');
Streaming a Response

The StreamedResponse class allows you to stream the Response back to the client. The response content is represented by a PHP callable instead of a string:

use Symfony\Component\HttpFoundation\StreamedResponse;

$response = new StreamedResponse();
$response->setCallback(function () {
    echo 'Hello World';
    flush();
    sleep(2);
    echo 'Hello World';
    flush();
});
$response->send();

注解

The flush() function does not flush buffering. If ob_start() has been called before or the output_buffering php.ini option is enabled, you must call ob_flush() before flush().

Additionally, PHP isn’t the only layer that can buffer output. Your web server might also buffer based on its configuration. Even more, if you use fastcgi, buffering can’t be disabled at all.

Serving Files

When sending a file, you must add a Content-Disposition header to your response. While creating this header for basic file downloads is easy, using non-ASCII filenames is more involving. The makeDisposition() abstracts the hard work behind a simple API:

use Symfony\Component\HttpFoundation\ResponseHeaderBag;

$d = $response->headers->makeDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'foo.pdf'
);

$response->headers->set('Content-Disposition', $d);

2.2 新版功能: The BinaryFileResponse class was introduced in Symfony 2.2.

Alternatively, if you are serving a static file, you can use a BinaryFileResponse:

use Symfony\Component\HttpFoundation\BinaryFileResponse;

$file = 'path/to/file.txt';
$response = new BinaryFileResponse($file);

The BinaryFileResponse will automatically handle Range and If-Range headers from the request. It also supports X-Sendfile (see for Nginx and Apache). To make use of it, you need to determine whether or not the X-Sendfile-Type header should be trusted and call trustXSendfileTypeHeader() if it should:

BinaryFileResponse::trustXSendfileTypeHeader();

You can still set the Content-Type of the sent file, or change its Content-Disposition:

$response->headers->set('Content-Type', 'text/plain');
$response->setContentDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'filename.txt'
);
Creating a JSON Response

Any type of response can be created via the Response class by setting the right content and headers. A JSON response might look like this:

use Symfony\Component\HttpFoundation\Response;

$response = new Response();
$response->setContent(json_encode(array(
    'data' => 123,
)));
$response->headers->set('Content-Type', 'application/json');

There is also a helpful JsonResponse class, which can make this even easier:

use Symfony\Component\HttpFoundation\JsonResponse;

$response = new JsonResponse();
$response->setData(array(
    'data' => 123
));

This encodes your array of data to JSON and sets the Content-Type header to application/json.

警告

To avoid XSSI JSON Hijacking, you should pass an associative array as the outer-most array to JsonResponse and not an indexed array so that the final result is an object (e.g. {"object": "not inside an array"}) instead of an array (e.g. [{"object": "inside an array"}]). Read the OWASP guidelines for more information.

Only methods that respond to GET requests are vulnerable to XSSI ‘JSON Hijacking’. Methods responding to POST requests only remain unaffected.

JSONP Callback

If you’re using JSONP, you can set the callback function that the data should be passed to:

$response->setCallback('handleResponse');

In this case, the Content-Type header will be text/javascript and the response content will look like this:

handleResponse({'data': 123});
Session

The session information is in its own document: Session Management.

Session Management

The Symfony HttpFoundation component has a very powerful and flexible session subsystem which is designed to provide session management through a simple object-oriented interface using a variety of session storage drivers.

Sessions are used via the simple Session implementation of SessionInterface interface.

警告

Make sure your PHP session isn’t already started before using the Session class. If you have a legacy session system that starts your session, see Legacy Sessions.

Quick example:

use Symfony\Component\HttpFoundation\Session\Session;

$session = new Session();
$session->start();

// set and get session attributes
$session->set('name', 'Drak');
$session->get('name');

// set flash messages
$session->getFlashBag()->add('notice', 'Profile updated');

// retrieve messages
foreach ($session->getFlashBag()->get('notice', array()) as $message) {
    echo '<div class="flash-notice">'.$message.'</div>';
}

注解

Symfony sessions are designed to replace several native PHP functions. Applications should avoid using session_start(), session_regenerate_id(), session_id(), session_name(), and session_destroy() and instead use the APIs in the following section.

注解

While it is recommended to explicitly start a session, a session will actually start on demand, that is, if any session request is made to read/write session data.

警告

Symfony sessions are incompatible with php.ini directive session.auto_start = 1 This directive should be turned off in php.ini, in the webserver directives or in .htaccess.

Session API

The Session class implements SessionInterface.

The Session has a simple API as follows divided into a couple of groups.

Session Workflow
start()
Starts the session - do not use session_start().
migrate()
Regenerates the session ID - do not use session_regenerate_id(). This method can optionally change the lifetime of the new cookie that will be emitted by calling this method.
invalidate()
Clears all session data and regenerates session ID. Do not use session_destroy().
getId()
Gets the session ID. Do not use session_id().
setId()
Sets the session ID. Do not use session_id().
getName()
Gets the session name. Do not use session_name().
setName()
Sets the session name. Do not use session_name().
Session Attributes
set()
Sets an attribute by key.
get()
Gets an attribute by key.
all()
Gets all attributes as an array of key => value.
has()
Returns true if the attribute exists.
replace()
Sets multiple attributes at once: takes a keyed array and sets each key => value pair.
remove()
Deletes an attribute by key.
clear()
Clear all attributes.

The attributes are stored internally in a “Bag”, a PHP object that acts like an array. A few methods exist for “Bag” management:

registerBag()
Registers a SessionBagInterface.
getBag()
Gets a SessionBagInterface by bag name.
getFlashBag()
Gets the FlashBagInterface. This is just a shortcut for convenience.
Session Metadata
getMetadataBag()
Gets the MetadataBag which contains information about the session.
Session Data Management

PHP’s session management requires the use of the $_SESSION super-global, however, this interferes somewhat with code testability and encapsulation in an OOP paradigm. To help overcome this, Symfony uses session bags linked to the session to encapsulate a specific dataset of attributes or flash messages.

This approach also mitigates namespace pollution within the $_SESSION super-global because each bag stores all its data under a unique namespace. This allows Symfony to peacefully co-exist with other applications or libraries that might use the $_SESSION super-global and all data remains completely compatible with Symfony’s session management.

Symfony provides two kinds of storage bags, with two separate implementations. Everything is written against interfaces so you may extend or create your own bag types if necessary.

SessionBagInterface has the following API which is intended mainly for internal purposes:

getStorageKey()
Returns the key which the bag will ultimately store its array under in $_SESSION. Generally this value can be left at its default and is for internal use.
initialize()
This is called internally by Symfony session storage classes to link bag data to the session.
getName()
Returns the name of the session bag.
Attributes

The purpose of the bags implementing the AttributeBagInterface is to handle session attribute storage. This might include things like user ID, and remember me login settings or other user based state information.

AttributeBag
This is the standard default implementation.
NamespacedAttributeBag
This implementation allows for attributes to be stored in a structured namespace.

Any plain key-value storage system is limited in the extent to which complex data can be stored since each key must be unique. You can achieve namespacing by introducing a naming convention to the keys so different parts of your application could operate without clashing. For example, module1.foo and module2.foo. However, sometimes this is not very practical when the attributes data is an array, for example a set of tokens. In this case, managing the array becomes a burden because you have to retrieve the array then process it and store it again:

$tokens = array(
    'tokens' => array(
        'a' => 'a6c1e0b6',
        'b' => 'f4a7b1f3',
    ),
);

So any processing of this might quickly get ugly, even simply adding a token to the array:

$tokens = $session->get('tokens');
$tokens['c'] = $value;
$session->set('tokens', $tokens);

With structured namespacing, the key can be translated to the array structure like this using a namespace character (defaults to /):

$session->set('tokens/c', $value);

This way you can easily access a key within the stored array directly and easily.

AttributeBagInterface has a simple API

set()
Sets an attribute by key.
get()
Gets an attribute by key.
all()
Gets all attributes as an array of key => value.
has()
Returns true if the attribute exists.
keys()
Returns an array of stored attribute keys.
replace()
Sets multiple attributes at once: takes a keyed array and sets each key => value pair.
remove()
Deletes an attribute by key.
clear()
Clear the bag.
Flash Messages

The purpose of the FlashBagInterface is to provide a way of setting and retrieving messages on a per session basis. The usual workflow would be to set flash messages in a request and to display them after a page redirect. For example, a user submits a form which hits an update controller, and after processing the controller redirects the page to either the updated page or an error page. Flash messages set in the previous page request would be displayed immediately on the subsequent page load for that session. This is however just one application for flash messages.

AutoExpireFlashBag
In this implementation, messages set in one page-load will be available for display only on the next page load. These messages will auto expire regardless of if they are retrieved or not.
FlashBag
In this implementation, messages will remain in the session until they are explicitly retrieved or cleared. This makes it possible to use ESI caching.

FlashBagInterface has a simple API

add()
Adds a flash message to the stack of specified type.
set()
Sets flashes by type; This method conveniently takes both single messages as a string or multiple messages in an array.
get()
Gets flashes by type and clears those flashes from the bag.
setAll()
Sets all flashes, accepts a keyed array of arrays type => array(messages).
all()
Gets all flashes (as a keyed array of arrays) and clears the flashes from the bag.
peek()
Gets flashes by type (read only).
peekAll()
Gets all flashes (read only) as keyed array of arrays.
has()
Returns true if the type exists, false if not.
keys()
Returns an array of the stored flash types.
clear()
Clears the bag.

For simple applications it is usually sufficient to have one flash message per type, for example a confirmation notice after a form is submitted. However, flash messages are stored in a keyed array by flash $type which means your application can issue multiple messages for a given type. This allows the API to be used for more complex messaging in your application.

Examples of setting multiple flashes:

use Symfony\Component\HttpFoundation\Session\Session;

$session = new Session();
$session->start();

// add flash messages
$session->getFlashBag()->add(
    'warning',
    'Your config file is writable, it should be set read-only'
);
$session->getFlashBag()->add('error', 'Failed to update name');
$session->getFlashBag()->add('error', 'Another error');

Displaying the flash messages might look as follows.

Simple, display one type of message:

// display warnings
foreach ($session->getFlashBag()->get('warning', array()) as $message) {
    echo '<div class="flash-warning">'.$message.'</div>';
}

// display errors
foreach ($session->getFlashBag()->get('error', array()) as $message) {
    echo '<div class="flash-error">'.$message.'</div>';
}

Compact method to process display all flashes at once:

foreach ($session->getFlashBag()->all() as $type => $messages) {
    foreach ($messages as $message) {
        echo '<div class="flash-'.$type.'">'.$message.'</div>';
    }
}
Configuring Sessions and Save Handlers

This section deals with how to configure session management and fine tune it to your specific needs. This documentation covers save handlers, which store and retrieve session data, and configuring session behavior.

Save Handlers

The PHP session workflow has 6 possible operations that may occur. The normal session follows open, read, write and close, with the possibility of destroy and gc (garbage collection which will expire any old sessions: gc is called randomly according to PHP’s configuration and if called, it is invoked after the open operation). You can read more about this at php.net/session.customhandler

Native PHP Save Handlers

So-called native handlers, are save handlers which are either compiled into PHP or provided by PHP extensions, such as PHP-Sqlite, PHP-Memcached and so on.

All native save handlers are internal to PHP and as such, have no public facing API. They must be configured by php.ini directives, usually session.save_path and potentially other driver specific directives. Specific details can be found in the docblock of the setOptions() method of each class. For instance, the one provided by the Memcached extension can be found on php.net/memcached.setoption

While native save handlers can be activated by directly using ini_set('session.save_handler', $name);, Symfony provides a convenient way to activate these in the same way as it does for custom handlers.

Symfony provides drivers for the following native save handler as an example:

Example usage:

use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;

$storage = new NativeSessionStorage(array(), new NativeFileSessionHandler());
$session = new Session($storage);

注解

With the exception of the files handler which is built into PHP and always available, the availability of the other handlers depends on those PHP extensions being active at runtime.

注解

Native save handlers provide a quick solution to session storage, however, in complex systems where you need more control, custom save handlers may provide more freedom and flexibility. Symfony provides several implementations which you may further customize as required.

Custom Save Handlers

Custom handlers are those which completely replace PHP’s built-in session save handlers by providing six callback functions which PHP calls internally at various points in the session workflow.

The Symfony HttpFoundation component provides some by default and these can easily serve as examples if you wish to write your own.

Example usage:

use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;

$pdo = new \PDO(...);
$storage = new NativeSessionStorage(array(), new PdoSessionHandler($pdo));
$session = new Session($storage);
Configuring PHP Sessions

The NativeSessionStorage can configure most of the php.ini configuration directives which are documented at php.net/session.configuration.

To configure these settings, pass the keys (omitting the initial session. part of the key) as a key-value array to the $options constructor argument. Or set them via the setOptions() method.

For the sake of clarity, some key options are explained in this documentation.

Configuring Garbage Collection

When a session opens, PHP will call the gc handler randomly according to the probability set by session.gc_probability / session.gc_divisor. For example if these were set to 5/100 respectively, it would mean a probability of 5%. Similarly, 3/4 would mean a 3 in 4 chance of being called, i.e. 75%.

If the garbage collection handler is invoked, PHP will pass the value stored in the php.ini directive session.gc_maxlifetime. The meaning in this context is that any stored session that was saved more than gc_maxlifetime ago should be deleted. This allows one to expire records based on idle time.

You can configure these settings by passing gc_probability, gc_divisor and gc_maxlifetime in an array to the constructor of NativeSessionStorage or to the setOptions() method.

Session Lifetime

When a new session is created, meaning Symfony issues a new session cookie to the client, the cookie will be stamped with an expiry time. This is calculated by adding the PHP runtime configuration value in session.cookie_lifetime with the current server time.

注解

PHP will only issue a cookie once. The client is expected to store that cookie for the entire lifetime. A new cookie will only be issued when the session is destroyed, the browser cookie is deleted, or the session ID is regenerated using the migrate() or invalidate() methods of the Session class.

The initial cookie lifetime can be set by configuring NativeSessionStorage using the setOptions(array('cookie_lifetime' => 1234)) method.

注解

A cookie lifetime of 0 means the cookie expires when the browser is closed.

Session Idle Time/Keep Alive

There are often circumstances where you may want to protect, or minimize unauthorized use of a session when a user steps away from their terminal while logged in by destroying the session after a certain period of idle time. For example, it is common for banking applications to log the user out after just 5 to 10 minutes of inactivity. Setting the cookie lifetime here is not appropriate because that can be manipulated by the client, so we must do the expiry on the server side. The easiest way is to implement this via garbage collection which runs reasonably frequently. The cookie_lifetime would be set to a relatively high value, and the garbage collection gc_maxlifetime would be set to destroy sessions at whatever the desired idle period is.

The other option is specifically check if a session has expired after the session is started. The session can be destroyed as required. This method of processing can allow the expiry of sessions to be integrated into the user experience, for example, by displaying a message.

Symfony records some basic metadata about each session to give you complete freedom in this area.

Session Metadata

Sessions are decorated with some basic metadata to enable fine control over the security settings. The session object has a getter for the metadata, getMetadataBag() which exposes an instance of MetadataBag:

$session->getMetadataBag()->getCreated();
$session->getMetadataBag()->getLastUsed();

Both methods return a Unix timestamp (relative to the server).

This metadata can be used to explicitly expire a session on access, e.g.:

$session->start();
if (time() - $session->getMetadataBag()->getLastUsed() > $maxIdleTime) {
    $session->invalidate();
    throw new SessionExpired(); // redirect to expired session page
}

It is also possible to tell what the cookie_lifetime was set to for a particular cookie by reading the getLifetime() method:

$session->getMetadataBag()->getLifetime();

The expiry time of the cookie can be determined by adding the created timestamp and the lifetime.

PHP 5.4 Compatibility

Since PHP 5.4.0, SessionHandler and SessionHandlerInterface are available. Symfony provides forward compatibility for the SessionHandlerInterface so it can be used under PHP 5.3. This greatly improves interoperability with other libraries.

SessionHandler is a special PHP internal class which exposes native save handlers to PHP user-space.

In order to provide a solution for those using PHP 5.4, Symfony has a special class called NativeSessionHandler which under PHP 5.4, extends from \SessionHandler and under PHP 5.3 is just a empty base class. This provides some interesting opportunities to leverage PHP 5.4 functionality if it is available.

Save Handler Proxy

A Save Handler Proxy is basically a wrapper around a Save Handler that was introduced to seamlessly support the migration from PHP 5.3 to PHP 5.4+. It further creates an extension point from where custom logic can be added that works independently of which handler is being wrapped inside.

There are two kinds of save handler class proxies which inherit from AbstractProxy: they are NativeProxy and SessionHandlerProxy.

NativeSessionStorage automatically injects storage handlers into a save handler proxy unless already wrapped by one.

NativeProxy is used automatically under PHP 5.3 when internal PHP save handlers are specified using the Native*SessionHandler classes, while SessionHandlerProxy will be used to wrap any custom save handlers, that implement SessionHandlerInterface.

From PHP 5.4 and above, all session handlers implement SessionHandlerInterface including Native*SessionHandler classes which inherit from SessionHandler.

The proxy mechanism allows you to get more deeply involved in session save handler classes. A proxy for example could be used to encrypt any session transaction without knowledge of the specific save handler.

注解

Before PHP 5.4, you can only proxy user-land save handlers but not native PHP save handlers.

Testing with Sessions

Symfony is designed from the ground up with code-testability in mind. In order to make your code which utilizes session easily testable we provide two separate mock storage mechanisms for both unit testing and functional testing.

Testing code using real sessions is tricky because PHP’s workflow state is global and it is not possible to have multiple concurrent sessions in the same PHP process.

The mock storage engines simulate the PHP session workflow without actually starting one allowing you to test your code without complications. You may also run multiple instances in the same PHP process.

The mock storage drivers do not read or write the system globals session_id() or session_name(). Methods are provided to simulate this if required:

Unit Testing

For unit testing where it is not necessary to persist the session, you should simply swap out the default storage engine with MockArraySessionStorage:

use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpFoundation\Session\Session;

$session = new Session(new MockArraySessionStorage());
Functional Testing

For functional testing where you may need to persist session data across separate PHP processes, simply change the storage engine to MockFileSessionStorage:

use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage;

$session = new Session(new MockFileSessionStorage());
Integrating with Legacy Sessions

Sometimes it may be necessary to integrate Symfony into a legacy application where you do not initially have the level of control you require.

As stated elsewhere, Symfony Sessions are designed to replace the use of PHP’s native session_*() functions and use of the $_SESSION superglobal. Additionally, it is mandatory for Symfony to start the session.

However when there really are circumstances where this is not possible, you can use a special storage bridge PhpBridgeSessionStorage which is designed to allow Symfony to work with a session started outside of the Symfony Session framework. You are warned that things can interrupt this use-case unless you are careful: for example the legacy application erases $_SESSION.

A typical use of this might look like this:

use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage;

// legacy application configures session
ini_set('session.save_handler', 'files');
ini_set('session.save_path', '/tmp');
session_start();

// Get Symfony to interface with this existing session
$session = new Session(new PhpBridgeSessionStorage());

// symfony will now interface with the existing PHP session
$session->start();

This will allow you to start using the Symfony Session API and allow migration of your application to Symfony sessions.

注解

Symfony sessions store data like attributes in special ‘Bags’ which use a key in the $_SESSION superglobal. This means that a Symfony session cannot access arbitrary keys in $_SESSION that may be set by the legacy application, although all the $_SESSION contents will be saved when the session is saved.

Trusting Proxies

小技巧

If you’re using the Symfony Framework, start by reading How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy.

If you find yourself behind some sort of proxy - like a load balancer - then certain header information may be sent to you using special X-Forwarded-* headers. For example, the Host HTTP header is usually used to return the requested host. But when you’re behind a proxy, the true host may be stored in a X-Forwarded-Host header.

Since HTTP headers can be spoofed, Symfony does not trust these proxy headers by default. If you are behind a proxy, you should manually whitelist your proxy.

2.3 新版功能: CIDR notation support was introduced in Symfony 2.3, so you can whitelist whole subnets (e.g. 10.0.0.0/8, fc00::/7).

use Symfony\Component\HttpFoundation\Request;

// only trust proxy headers coming from this IP addresses
Request::setTrustedProxies(array('192.0.0.1', '10.0.0.0/8'));
Configuring Header Names

By default, the following proxy headers are trusted:

If your reverse proxy uses a different header name for any of these, you can configure that header name via setTrustedHeaderName():

Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, 'X-Proxy-For');
Request::setTrustedHeaderName(Request::HEADER_CLIENT_HOST, 'X-Proxy-Host');
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, 'X-Proxy-Port');
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X-Proxy-Proto');
Not Trusting certain Headers

By default, if you whitelist your proxy’s IP address, then all four headers listed above are trusted. If you need to trust some of these headers but not others, you can do that as well:

// disables trusting the ``X-Forwarded-Proto`` header, the default header is used
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, '');

HttpKernel

The HttpKernel Component
The HttpKernel component provides a structured process for converting a Request into a Response by making use of the EventDispatcher. It’s flexible enough to create a full-stack framework (Symfony), a micro-framework (Silex) or an advanced CMS system (Drupal).
Installation

You can install the component in 2 different ways:

The Workflow of a Request

Every HTTP web interaction begins with a request and ends with a response. Your job as a developer is to create PHP code that reads the request information (e.g. the URL) and creates and returns a response (e.g. an HTML page or JSON string).

_images/request-response-flow.png

Typically, some sort of framework or system is built to handle all the repetitive tasks (e.g. routing, security, etc) so that a developer can easily build each page of the application. Exactly how these systems are built varies greatly. The HttpKernel component provides an interface that formalizes the process of starting with a request and creating the appropriate response. The component is meant to be the heart of any application or framework, no matter how varied the architecture of that system:

namespace Symfony\Component\HttpKernel;

use Symfony\Component\HttpFoundation\Request;

interface HttpKernelInterface
{
    // ...

    /**
     * @return Response A Response instance
     */
    public function handle(
        Request $request,
        $type = self::MASTER_REQUEST,
        $catch = true
    );
}

Internally, HttpKernel::handle() - the concrete implementation of HttpKernelInterface::handle() - defines a workflow that starts with a Request and ends with a Response.

_images/01-workflow.png

The exact details of this workflow are the key to understanding how the kernel (and the Symfony Framework or any other library that uses the kernel) works.

HttpKernel: Driven by Events

The HttpKernel::handle() method works internally by dispatching events. This makes the method both flexible, but also a bit abstract, since all the “work” of a framework/application built with HttpKernel is actually done in event listeners.

To help explain this process, this document looks at each step of the process and talks about how one specific implementation of the HttpKernel - the Symfony Framework - works.

Initially, using the HttpKernel is really simple, and involves creating an EventDispatcher and a controller resolver (explained below). To complete your working kernel, you’ll add more event listeners to the events discussed below:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;

// create the Request object
$request = Request::createFromGlobals();

$dispatcher = new EventDispatcher();
// ... add some event listeners

// create your controller resolver
$resolver = new ControllerResolver();
// instantiate the kernel
$kernel = new HttpKernel($dispatcher, $resolver);

// actually execute the kernel, which turns the request into a response
// by dispatching events, calling a controller, and returning the response
$response = $kernel->handle($request);

// send the headers and echo the content
$response->send();

// triggers the kernel.terminate event
$kernel->terminate($request, $response);

See “A full Working Example” for a more concrete implementation.

For general information on adding listeners to the events below, see Creating an Event Listener.

小技巧

Fabien Potencier also wrote a wonderful series on using the HttpKernel component and other Symfony components to create your own framework. See Create your own framework... on top of the Symfony2 Components.

1) The kernel.request Event

Typical Purposes: To add more information to the Request, initialize parts of the system, or return a Response if possible (e.g. a security layer that denies access).

Kernel Events Information Table

The first event that is dispatched inside HttpKernel::handle is kernel.request, which may have a variety of different listeners.

_images/02-kernel-request.png

Listeners of this event can be quite varied. Some listeners - such as a security listener - might have enough information to create a Response object immediately. For example, if a security listener determined that a user doesn’t have access, that listener may return a RedirectResponse to the login page or a 403 Access Denied response.

If a Response is returned at this stage, the process skips directly to the kernel.response event.

_images/03-kernel-request-response.png

Other listeners simply initialize things or add more information to the request. For example, a listener might determine and set the locale on the Request object.

Another common listener is routing. A router listener may process the Request and determine the controller that should be rendered (see the next section). In fact, the Request object has an “attributes” bag which is a perfect spot to store this extra, application-specific data about the request. This means that if your router listener somehow determines the controller, it can store it on the Request attributes (which can be used by your controller resolver).

Overall, the purpose of the kernel.request event is either to create and return a Response directly, or to add information to the Request (e.g. setting the locale or setting some other information on the Request attributes).

注解

When setting a response for the kernel.request event, the propagation is stopped. This means listeners with lower priority won’t be executed.

2) Resolve the Controller

Assuming that no kernel.request listener was able to create a Response, the next step in HttpKernel is to determine and prepare (i.e. resolve) the controller. The controller is the part of the end-application’s code that is responsible for creating and returning the Response for a specific page. The only requirement is that it is a PHP callable - i.e. a function, method on an object, or a Closure.

But how you determine the exact controller for a request is entirely up to your application. This is the job of the “controller resolver” - a class that implements ControllerResolverInterface and is one of the constructor arguments to HttpKernel.

_images/04-resolve-controller.png

Your job is to create a class that implements the interface and fill in its two methods: getController and getArguments. In fact, one default implementation already exists, which you can use directly or learn from: ControllerResolver. This implementation is explained more in the sidebar below:

namespace Symfony\Component\HttpKernel\Controller;

use Symfony\Component\HttpFoundation\Request;

interface ControllerResolverInterface
{
    public function getController(Request $request);

    public function getArguments(Request $request, $controller);
}

Internally, the HttpKernel::handle method first calls getController() on the controller resolver. This method is passed the Request and is responsible for somehow determining and returning a PHP callable (the controller) based on the request’s information.

The second method, getArguments(), will be called after another event - kernel.controller - is dispatched.

3) The kernel.controller Event

Typical Purposes: Initialize things or change the controller just before the controller is executed.

Kernel Events Information Table

After the controller callable has been determined, HttpKernel::handle dispatches the kernel.controller event. Listeners to this event might initialize some part of the system that needs to be initialized after certain things have been determined (e.g. the controller, routing information) but before the controller is executed. For some examples, see the Symfony section below.

_images/06-kernel-controller.png

Listeners to this event can also change the controller callable completely by calling FilterControllerEvent::setController on the event object that’s passed to listeners on this event.

4) Getting the Controller Arguments

Next, HttpKernel::handle calls getArguments(). Remember that the controller returned in getController is a callable. The purpose of getArguments is to return the array of arguments that should be passed to that controller. Exactly how this is done is completely up to your design, though the built-in ControllerResolver is a good example.

_images/07-controller-arguments.png

At this point the kernel has a PHP callable (the controller) and an array of arguments that should be passed when executing that callable.

5) Calling the Controller

The next step is simple! HttpKernel::handle executes the controller.

_images/08-call-controller.png

The job of the controller is to build the response for the given resource. This could be an HTML page, a JSON string or anything else. Unlike every other part of the process so far, this step is implemented by the “end-developer”, for each page that is built.

Usually, the controller will return a Response object. If this is true, then the work of the kernel is just about done! In this case, the next step is the kernel.response event.

_images/09-controller-returns-response.png

But if the controller returns anything besides a Response, then the kernel has a little bit more work to do - kernel.view (since the end goal is always to generate a Response object).

注解

A controller must return something. If a controller returns null, an exception will be thrown immediately.

6) The kernel.view Event

Typical Purposes: Transform a non-Response return value from a controller into a Response

Kernel Events Information Table

If the controller doesn’t return a Response object, then the kernel dispatches another event - kernel.view. The job of a listener to this event is to use the return value of the controller (e.g. an array of data or an object) to create a Response.

_images/10-kernel-view.png

This can be useful if you want to use a “view” layer: instead of returning a Response from the controller, you return data that represents the page. A listener to this event could then use this data to create a Response that is in the correct format (e.g HTML, JSON, etc).

At this stage, if no listener sets a response on the event, then an exception is thrown: either the controller or one of the view listeners must always return a Response.

注解

When setting a response for the kernel.view event, the propagation is stopped. This means listeners with lower priority won’t be executed.

7) The kernel.response Event

Typical Purposes: Modify the Response object just before it is sent

Kernel Events Information Table

The end goal of the kernel is to transform a Request into a Response. The Response might be created during the kernel.request event, returned from the controller, or returned by one of the listeners to the kernel.view event.

Regardless of who creates the Response, another event - kernel.response is dispatched directly afterwards. A typical listener to this event will modify the Response object in some way, such as modifying headers, adding cookies, or even changing the content of the Response itself (e.g. injecting some JavaScript before the end </body> tag of an HTML response).

After this event is dispatched, the final Response object is returned from handle(). In the most typical use-case, you can then call the send() method, which sends the headers and prints the Response content.

8) The kernel.terminate Event

Typical Purposes: To perform some “heavy” action after the response has been streamed to the user

Kernel Events Information Table

The final event of the HttpKernel process is kernel.terminate and is unique because it occurs after the HttpKernel::handle method, and after the response is sent to the user. Recall from above, then the code that uses the kernel, ends like this:

// send the headers and echo the content
$response->send();

// triggers the kernel.terminate event
$kernel->terminate($request, $response);

As you can see, by calling $kernel->terminate after sending the response, you will trigger the kernel.terminate event where you can perform certain actions that you may have delayed in order to return the response as quickly as possible to the client (e.g. sending emails).

警告

Internally, the HttpKernel makes use of the fastcgi_finish_request PHP function. This means that at the moment, only the PHP FPM server API is able to send a response to the client while the server’s PHP process still performs some tasks. With all other server APIs, listeners to kernel.terminate are still executed, but the response is not sent to the client until they are all completed.

注解

Using the kernel.terminate event is optional, and should only be called if your kernel implements TerminableInterface.

Handling Exceptions: the kernel.exception Event

Typical Purposes: Handle some type of exception and create an appropriate Response to return for the exception

Kernel Events Information Table

If an exception is thrown at any point inside HttpKernel::handle, another event - kernel.exception is thrown. Internally, the body of the handle function is wrapped in a try-catch block. When any exception is thrown, the kernel.exception event is dispatched so that your system can somehow respond to the exception.

_images/11-kernel-exception.png

Each listener to this event is passed a GetResponseForExceptionEvent object, which you can use to access the original exception via the getException() method. A typical listener on this event will check for a certain type of exception and create an appropriate error Response.

For example, to generate a 404 page, you might throw a special type of exception and then add a listener on this event that looks for this exception and creates and returns a 404 Response. In fact, the HttpKernel component comes with an ExceptionListener, which if you choose to use, will do this and more by default (see the sidebar below for more details).

注解

When setting a response for the kernel.exception event, the propagation is stopped. This means listeners with lower priority won’t be executed.

Creating an Event Listener

As you’ve seen, you can create and attach event listeners to any of the events dispatched during the HttpKernel::handle cycle. Typically a listener is a PHP class with a method that’s executed, but it can be anything. For more information on creating and attaching event listeners, see The EventDispatcher Component.

The name of each of the “kernel” events is defined as a constant on the KernelEvents class. Additionally, each event listener is passed a single argument, which is some sub-class of KernelEvent. This object contains information about the current state of the system and each event has their own event object:

Name KernelEvents Constant Argument Passed to the Listener
kernel.request KernelEvents::REQUEST GetResponseEvent
kernel.controller KernelEvents::CONTROLLER FilterControllerEvent
kernel.view KernelEvents::VIEW GetResponseForControllerResultEvent
kernel.response KernelEvents::RESPONSE FilterResponseEvent
kernel.terminate KernelEvents::TERMINATE PostResponseEvent
kernel.exception KernelEvents::EXCEPTION GetResponseForExceptionEvent
A full Working Example

When using the HttpKernel component, you’re free to attach any listeners to the core events and use any controller resolver that implements the ControllerResolverInterface. However, the HttpKernel component comes with some built-in listeners and a built-in ControllerResolver that can be used to create a working example:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}', array(
        '_controller' => function (Request $request) {
            return new Response(
                sprintf("Hello %s", $request->get('name'))
            );
        }
    )
));

$request = Request::createFromGlobals();

$matcher = new UrlMatcher($routes, new RequestContext());

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher));

$resolver = new ControllerResolver();
$kernel = new HttpKernel($dispatcher, $resolver);

$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);
Sub Requests

In addition to the “main” request that’s sent into HttpKernel::handle, you can also send so-called “sub request”. A sub request looks and acts like any other request, but typically serves to render just one small portion of a page instead of a full page. You’ll most commonly make sub-requests from your controller (or perhaps from inside a template, that’s being rendered by your controller).

_images/sub-request.png

To execute a sub request, use HttpKernel::handle, but change the second argument as follows:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

// ...

// create some other request manually as needed
$request = new Request();
// for example, possibly set its _controller manually
$request->attributes->set('_controller', '...');

$response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST);
// do something with this response

This creates another full request-response cycle where this new Request is transformed into a Response. The only difference internally is that some listeners (e.g. security) may only act upon the master request. Each listener is passed some sub-class of KernelEvent, whose getRequestType() can be used to figure out if the current request is a “master” or “sub” request.

For example, a listener that only needs to act on the master request may look like this:

use Symfony\Component\HttpKernel\HttpKernelInterface;
// ...

public function onKernelRequest(GetResponseEvent $event)
{
    if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
        return;
    }

    // ...
}

The Intl Component

A PHP replacement layer for the C intl extension that also provides access to the localization data of the ICU library.

2.3 新版功能: The Intl component was introduced in Symfony 2.3. In earlier versions of Symfony, you should use the Locale component instead.

警告

The replacement layer is limited to the locale “en”. If you want to use other locales, you should install the intl extension instead.

Installation

You can install the component in two different ways:

If you install the component via Composer, the following classes and functions of the intl extension will be automatically provided if the intl extension is not loaded:

When the intl extension is not available, the following classes are used to replace the intl classes:

Composer automatically exposes these classes in the global namespace.

If you don’t use Composer but the Symfony ClassLoader component, you need to expose them manually by adding the following lines to your autoload code:

if (!function_exists('intl_is_failure')) {
    require '/path/to/Icu/Resources/stubs/functions.php';

    $loader->registerPrefixFallback('/path/to/Icu/Resources/stubs');
}
Writing and Reading Resource Bundles

The ResourceBundle class is not currently supported by this component. Instead, it includes a set of readers and writers for reading and writing arrays (or array-like objects) from/to resource bundle files. The following classes are supported:

Continue reading if you are interested in how to use these classes. Otherwise skip this section and jump to Accessing ICU Data.

TextBundleWriter

The TextBundleWriter writes an array or an array-like object to a plain-text resource bundle. The resulting .txt file can be converted to a binary .res file with the BundleCompiler class:

use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter;
use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompiler;

$writer = new TextBundleWriter();
$writer->write('/path/to/bundle', 'en', array(
    'Data' => array(
        'entry1',
        'entry2',
        // ...
    ),
));

$compiler = new BundleCompiler();
$compiler->compile('/path/to/bundle', '/path/to/binary/bundle');

The command “genrb” must be available for the BundleCompiler to work. If the command is located in a non-standard location, you can pass its path to the BundleCompiler constructor.

PhpBundleWriter

The PhpBundleWriter writes an array or an array-like object to a .php resource bundle:

use Symfony\Component\Intl\ResourceBundle\Writer\PhpBundleWriter;

$writer = new PhpBundleWriter();
$writer->write('/path/to/bundle', 'en', array(
    'Data' => array(
        'entry1',
        'entry2',
        // ...
    ),
));
BinaryBundleReader

The BinaryBundleReader reads binary resource bundle files and returns an array or an array-like object. This class currently only works with the intl extension installed:

use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;

$reader = new BinaryBundleReader();
$data = $reader->read('/path/to/bundle', 'en');

echo $data['Data']['entry1'];
PhpBundleReader

The PhpBundleReader reads resource bundles from .php files and returns an array or an array-like object:

use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader;

$reader = new PhpBundleReader();
$data = $reader->read('/path/to/bundle', 'en');

echo $data['Data']['entry1'];
BufferedBundleReader

The BufferedBundleReader wraps another reader, but keeps the last N reads in a buffer, where N is a buffer size passed to the constructor:

use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;
use Symfony\Component\Intl\ResourceBundle\Reader\BufferedBundleReader;

$reader = new BufferedBundleReader(new BinaryBundleReader(), 10);

// actually reads the file
$data = $reader->read('/path/to/bundle', 'en');

// returns data from the buffer
$data = $reader->read('/path/to/bundle', 'en');

// actually reads the file
$data = $reader->read('/path/to/bundle', 'fr');
StructuredBundleReader

The StructuredBundleReader wraps another reader and offers a readEntry() method for reading an entry of the resource bundle without having to worry whether array keys are set or not. If a path cannot be resolved, null is returned:

use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;
use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader;

$reader = new StructuredBundleReader(new BinaryBundleReader());

$data = $reader->read('/path/to/bundle', 'en');

// Produces an error if the key "Data" does not exist
echo $data['Data']['entry1'];

// Returns null if the key "Data" does not exist
echo $reader->readEntry('/path/to/bundle', 'en', array('Data', 'entry1'));

Additionally, the readEntry() method resolves fallback locales. For example, the fallback locale of “en_GB” is “en”. For single-valued entries (strings, numbers etc.), the entry will be read from the fallback locale if it cannot be found in the more specific locale. For multi-valued entries (arrays), the values of the more specific and the fallback locale will be merged. In order to suppress this behavior, the last parameter $fallback can be set to false:

echo $reader->readEntry(
    '/path/to/bundle',
    'en',
    array('Data', 'entry1'),
    false
);
Accessing ICU Data

The ICU data is located in several “resource bundles”. You can access a PHP wrapper of these bundles through the static Intl class. At the moment, the following data is supported:

Language and Script Names

The translations of language and script names can be found in the language bundle:

use Symfony\Component\Intl\Intl;

\Locale::setDefault('en');

$languages = Intl::getLanguageBundle()->getLanguageNames();
// => array('ab' => 'Abkhazian', ...)

$language = Intl::getLanguageBundle()->getLanguageName('de');
// => 'German'

$language = Intl::getLanguageBundle()->getLanguageName('de', 'AT');
// => 'Austrian German'

$scripts = Intl::getLanguageBundle()->getScriptNames();
// => array('Arab' => 'Arabic', ...)

$script = Intl::getLanguageBundle()->getScriptName('Hans');
// => 'Simplified'

All methods accept the translation locale as the last, optional parameter, which defaults to the current default locale:

$languages = Intl::getLanguageBundle()->getLanguageNames('de');
// => array('ab' => 'Abchasisch', ...)
Country Names

The translations of country names can be found in the region bundle:

use Symfony\Component\Intl\Intl;

\Locale::setDefault('en');

$countries = Intl::getRegionBundle()->getCountryNames();
// => array('AF' => 'Afghanistan', ...)

$country = Intl::getRegionBundle()->getCountryName('GB');
// => 'United Kingdom'

All methods accept the translation locale as the last, optional parameter, which defaults to the current default locale:

$countries = Intl::getRegionBundle()->getCountryNames('de');
// => array('AF' => 'Afghanistan', ...)
Locales

The translations of locale names can be found in the locale bundle:

use Symfony\Component\Intl\Intl;

\Locale::setDefault('en');

$locales = Intl::getLocaleBundle()->getLocaleNames();
// => array('af' => 'Afrikaans', ...)

$locale = Intl::getLocaleBundle()->getLocaleName('zh_Hans_MO');
// => 'Chinese (Simplified, Macau SAR China)'

All methods accept the translation locale as the last, optional parameter, which defaults to the current default locale:

$locales = Intl::getLocaleBundle()->getLocaleNames('de');
// => array('af' => 'Afrikaans', ...)
Currencies

The translations of currency names and other currency-related information can be found in the currency bundle:

use Symfony\Component\Intl\Intl;

\Locale::setDefault('en');

$currencies = Intl::getCurrencyBundle()->getCurrencyNames();
// => array('AFN' => 'Afghan Afghani', ...)

$currency = Intl::getCurrencyBundle()->getCurrencyName('INR');
// => 'Indian Rupee'

$symbol = Intl::getCurrencyBundle()->getCurrencySymbol('INR');
// => '₹'

$fractionDigits = Intl::getCurrencyBundle()->getFractionDigits('INR');
// => 2

$roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement('INR');
// => 0

All methods (except for getFractionDigits() and getRoundingIncrement()) accept the translation locale as the last, optional parameter, which defaults to the current default locale:

$currencies = Intl::getCurrencyBundle()->getCurrencyNames('de');
// => array('AFN' => 'Afghanische Afghani', ...)

That’s all you need to know for now. Have fun coding!

The OptionsResolver Component

The OptionsResolver component helps you configure objects with option arrays. It supports default values, option constraints and lazy options.
Installation

You can install the component in 2 different ways:

Usage

Imagine you have a Mailer class which has 2 options: host and password. These options are going to be handled by the OptionsResolver Component.

First, create the Mailer class:

class Mailer
{
    protected $options;

    public function __construct(array $options = array())
    {
    }
}

You could of course set the $options value directly on the property. Instead, use the OptionsResolver class and let it resolve the options by calling resolve(). The advantages of doing this will become more obvious as you continue:

use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function __construct(array $options = array())
{
    $resolver = new OptionsResolver();

    $this->options = $resolver->resolve($options);
}

The options property now is a well defined array with all resolved options readily available:

// ...
public function sendMail($from, $to)
{
    $mail = ...;
    $mail->setHost($this->options['host']);
    $mail->setUsername($this->options['username']);
    $mail->setPassword($this->options['password']);
    // ...
}
Configuring the OptionsResolver

Now, try to actually use the class:

$mailer = new Mailer(array(
    'host'     => 'smtp.example.org',
    'username' => 'user',
    'password' => 'pa$$word',
));

Right now, you’ll receive a InvalidOptionsException, which tells you that the options host and password do not exist. This is because you need to configure the OptionsResolver first, so it knows which options should be resolved.

小技巧

To check if an option exists, you can use the isKnown() function.

A best practice is to put the configuration in a method (e.g. configureOptions). You call this method in the constructor to configure the OptionsResolver class:

use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class Mailer
{
    protected $options;

    public function __construct(array $options = array())
    {
        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);

        $this->options = $resolver->resolve($options);
    }

    protected function configureOptions(OptionsResolverInterface $resolver)
    {
        // ... configure the resolver, you will learn this
        // in the sections below
    }
}
Set default Values

Most of the options have a default value. You can configure these options by calling setDefaults():

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...

    $resolver->setDefaults(array(
        'username' => 'root',
    ));
}

This would add an option - username - and give it a default value of root. If the user passes in a username option, that value will override this default. You don’t need to configure username as an optional option.

Required Options

The host option is required: the class can’t work without it. You can set the required options by calling setRequired():

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setRequired(array('host'));
}

You are now able to use the class without errors:

$mailer = new Mailer(array(
    'host' => 'smtp.example.org',
));

echo $mailer->getHost(); // 'smtp.example.org'

If you don’t pass a required option, a MissingOptionsException will be thrown.

小技巧

To determine if an option is required, you can use the isRequired() method.

Optional Options

Sometimes, an option can be optional (e.g. the password option in the Mailer class), but it doesn’t have a default value. You can configure these options by calling setOptional():

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...

    $resolver->setOptional(array('password'));
}

Options with defaults are already marked as optional.

小技巧

When setting an option as optional, you can’t be sure if it’s in the array or not. You have to check if the option exists before using it.

To avoid checking if it exists everytime, you can also set a default of null to an option using the setDefaults() method (see Set Default Values), this means the element always exists in the array, but with a default of null.

Default Values that Depend on another Option

Suppose you add a port option to the Mailer class, whose default value you guess based on the encryption. You can do that easily by using a closure as the default value:

use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...

    $resolver->setDefaults(array(
        'encryption' => null,
        'port' => function (Options $options) {
            if ('ssl' === $options['encryption']) {
                return 465;
            }

            return 25;
        },
    ));
}

The Options class implements ArrayAccess, Iterator and Countable. That means you can handle it just like a normal array containing the options.

警告

The first argument of the closure must be typehinted as Options, otherwise it is considered as the value.

Overwriting default Values

A previously set default value can be overwritten by invoking setDefaults() again. When using a closure as the new value it is passed 2 arguments:

  • $options: an Options instance with all the other default options
  • $previousValue: the previous set default value
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...
    $resolver->setDefaults(array(
        'encryption' => 'ssl',
        'host' => 'localhost',
    ));

    // ...
    $resolver->setDefaults(array(
        'encryption' => 'tls', // simple overwrite
        'host' => function (Options $options, $previousValue) {
            return 'localhost' == $previousValue
                ? '127.0.0.1'
                : $previousValue;
        },
    ));
}

小技巧

If the previous default value is calculated by an expensive closure and you don’t need access to it, you can use the replaceDefaults() method instead. It acts like setDefaults but simply erases the previous value to improve performance. This means that the previous default value is not available when overwriting with another closure:

use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...
    $resolver->setDefaults(array(
        'encryption' => 'ssl',
        'heavy' => function (Options $options) {
            // Some heavy calculations to create the $result

            return $result;
        },
    ));

    $resolver->replaceDefaults(array(
        'encryption' => 'tls', // simple overwrite
        'heavy' => function (Options $options) {
            // $previousValue not available
            // ...

            return $someOtherResult;
        },
    ));
}

注解

Existing option keys that you do not mention when overwriting are preserved.

Configure Allowed Values

Not all values are valid values for options. Suppose the Mailer class has a transport option, it can only be one of sendmail, mail or smtp. You can configure these allowed values by calling setAllowedValues():

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...

    $resolver->setAllowedValues(array(
        'encryption' => array(null, 'ssl', 'tls'),
    ));
}

There is also an addAllowedValues() method, which you can use if you want to add an allowed value to the previously configured allowed values.

Configure Allowed Types

You can also specify allowed types. For instance, the port option can be anything, but it must be an integer. You can configure these types by calling setAllowedTypes():

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...

    $resolver->setAllowedTypes(array(
        'port' => 'integer',
    ));
}

Possible types are the ones associated with the is_* PHP functions or a class name. You can also pass an array of types as the value. For instance, array('null', 'string') allows port to be null or a string.

There is also an addAllowedTypes() method, which you can use to add an allowed type to the previous allowed types.

Normalize the Options

Some values need to be normalized before you can use them. For instance, pretend that the host should always start with http://. To do that, you can write normalizers. These closures will be executed after all options are passed and should return the normalized value. You can configure these normalizers by calling setNormalizers():

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...

    $resolver->setNormalizers(array(
        'host' => function (Options $options, $value) {
            if ('http://' !== substr($value, 0, 7)) {
                $value = 'http://'.$value;
            }

            return $value;
        },
    ));
}

You see that the closure also gets an $options parameter. Sometimes, you need to use the other options for normalizing:

// ...
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
    // ...

    $resolver->setNormalizers(array(
        'host' => function (Options $options, $value) {
            if (!in_array(substr($value, 0, 7), array('http://', 'https://'))) {
                if ($options['ssl']) {
                    $value = 'https://'.$value;
                } else {
                    $value = 'http://'.$value;
                }
            }

            return $value;
        },
    ));
}

The Process Component

The Process component executes commands in sub-processes.
Installation

You can install the component in 2 different ways:

Usage

The Process class allows you to execute a command in a sub-process:

use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
$process->run();

// executes after the command finishes
if (!$process->isSuccessful()) {
    throw new \RuntimeException($process->getErrorOutput());
}

print $process->getOutput();

The component takes care of the subtle differences between the different platforms when executing the command.

2.2 新版功能: The getIncrementalOutput() and getIncrementalErrorOutput() methods were introduced in Symfony 2.2.

The getOutput() method always return the whole content of the standard output of the command and getErrorOutput() the content of the error output. Alternatively, the getIncrementalOutput() and getIncrementalErrorOutput() methods returns the new outputs since the last call.

Getting real-time Process Output

When executing a long running command (like rsync-ing files to a remote server), you can give feedback to the end user in real-time by passing an anonymous function to the run() method:

use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
$process->run(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

2.1 新版功能: The non-blocking feature was introduced in 2.1.

Running Processes Asynchronously

You can also start the subprocess and then let it run asynchronously, retrieving output and the status in your main process whenever you need it. Use the start() method to start an asynchronous process, the isRunning() method to check if the process is done and the getOutput() method to get the output:

$process = new Process('ls -lsa');
$process->start();

while ($process->isRunning()) {
    // waiting for process to finish
}

echo $process->getOutput();

You can also wait for a process to end if you started it asynchronously and are done doing other stuff:

$process = new Process('ls -lsa');
$process->start();

// ... do other things

$process->wait(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

注解

The wait() method is blocking, which means that your code will halt at this line until the external process is completed.

Stopping a Process

2.3 新版功能: The signal parameter of the stop method was introduced in Symfony 2.3.

Any asynchronous process can be stopped at any time with the stop() method. This method takes two arguments: a timeout and a signal. Once the timeout is reached, the signal is sent to the running process. The default signal sent to a process is SIGKILL. Please read the signal documentation below to find out more about signal handling in the Process component:

$process = new Process('ls -lsa');
$process->start();

// ... do other things

$process->stop(3, SIGINT);
Executing PHP Code in Isolation

If you want to execute some PHP code in isolation, use the PhpProcess instead:

use Symfony\Component\Process\PhpProcess;

$process = new PhpProcess(<<<EOF
    <?php echo 'Hello World'; ?>
EOF
);
$process->run();

To make your code work better on all platforms, you might want to use the ProcessBuilder class instead:

use Symfony\Component\Process\ProcessBuilder;

$builder = new ProcessBuilder(array('ls', '-lsa'));
$builder->getProcess()->run();

2.3 新版功能: The ProcessBuilder::setPrefix method was introduced in Symfony 2.3.

In case you are building a binary driver, you can use the setPrefix() method to prefix all the generated process commands.

The following example will generate two process commands for a tar binary adapter:

use Symfony\Component\Process\ProcessBuilder;

$builder = new ProcessBuilder();
$builder->setPrefix('/usr/bin/tar');

// '/usr/bin/tar' '--list' '--file=archive.tar.gz'
echo $builder
    ->setArguments(array('--list', '--file=archive.tar.gz'))
    ->getProcess()
    ->getCommandLine();

// '/usr/bin/tar' '-xzf' 'archive.tar.gz'
echo $builder
    ->setArguments(array('-xzf', 'archive.tar.gz'))
    ->getProcess()
    ->getCommandLine();
Process Timeout

You can limit the amount of time a process takes to complete by setting a timeout (in seconds):

use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
$process->setTimeout(3600);
$process->run();

If the timeout is reached, a RuntimeException is thrown.

For long running commands, it is your responsibility to perform the timeout check regularly:

$process->setTimeout(3600);
$process->start();

while ($condition) {
    // ...

    // check if the timeout is reached
    $process->checkTimeout();

    usleep(200000);
}
Process Signals

2.3 新版功能: The signal method was introduced in Symfony 2.3.

When running a program asynchronously, you can send it POSIX signals with the signal() method:

use Symfony\Component\Process\Process;

$process = new Process('find / -name "rabbit"');
$process->start();

// will send a SIGKILL to the process
$process->signal(SIGKILL);

警告

Due to some limitations in PHP, if you’re using signals with the Process component, you may have to prefix your commands with exec. Please read Symfony Issue#5759 and PHP Bug#39992 to understand why this is happening.

POSIX signals are not available on Windows platforms, please refer to the PHP documentation for available signals.

Process Pid

2.3 新版功能: The getPid method was introduced in Symfony 2.3.

You can access the pid of a running process with the getPid() method.

use Symfony\Component\Process\Process;

$process = new Process('/usr/bin/php worker.php');
$process->start();

$pid = $process->getPid();

警告

Due to some limitations in PHP, if you want to get the pid of a symfony Process, you may have to prefix your commands with exec. Please read Symfony Issue#5759 to understand why this is happening.

PropertyAccess

The PropertyAccess Component
The PropertyAccess component provides function to read and write from/to an object or array using a simple string notation.

2.2 新版功能: The PropertyAccess component was introduced in Symfony 2.2. Previously, the PropertyPath class was located in the Form component.

Installation

You can install the component in two different ways:

Usage

The entry point of this component is the PropertyAccess::createPropertyAccessor factory. This factory will create a new instance of the PropertyAccessor class with the default configuration:

use Symfony\Component\PropertyAccess\PropertyAccess;

$accessor = PropertyAccess::createPropertyAccessor();

2.3 新版功能: The createPropertyAccessor() method was introduced in Symfony 2.3. Previously, it was called getPropertyAccessor().

Reading from Arrays

You can read an array with the PropertyAccessor::getValue method. This is done using the index notation that is used in PHP:

// ...
$person = array(
    'first_name' => 'Wouter',
);

echo $accessor->getValue($person, '[first_name]'); // 'Wouter'
echo $accessor->getValue($person, '[age]'); // null

As you can see, the method will return null if the index does not exists.

You can also use multi dimensional arrays:

// ...
$persons = array(
    array(
        'first_name' => 'Wouter',
    ),
    array(
        'first_name' => 'Ryan',
    )
);

echo $accessor->getValue($persons, '[0][first_name]'); // 'Wouter'
echo $accessor->getValue($persons, '[1][first_name]'); // 'Ryan'
Reading from Objects

The getValue method is a very robust method, and you can see all of its features when working with objects.

Accessing public Properties

To read from properties, use the “dot” notation:

// ...
$person = new Person();
$person->firstName = 'Wouter';

echo $accessor->getValue($person, 'firstName'); // 'Wouter'

$child = new Person();
$child->firstName = 'Bar';
$person->children = array($child);

echo $accessor->getValue($person, 'children[0].firstName'); // 'Bar'

警告

Accessing public properties is the last option used by PropertyAccessor. It tries to access the value using the below methods first before using the property directly. For example, if you have a public property that has a getter method, it will use the getter.

Using Getters

The getValue method also supports reading using getters. The method will be created using common naming conventions for getters. It camelizes the property name (first_name becomes FirstName) and prefixes it with get. So the actual method becomes getFirstName:

// ...
class Person
{
    private $firstName = 'Wouter';

    public function getFirstName()
    {
        return $this->firstName;
    }
}

$person = new Person();

echo $accessor->getValue($person, 'first_name'); // 'Wouter'
Using Hassers/Issers

And it doesn’t even stop there. If there is no getter found, the accessor will look for an isser or hasser. This method is created using the same way as getters, this means that you can do something like this:

// ...
class Person
{
    private $author = true;
    private $children = array();

    public function isAuthor()
    {
        return $this->author;
    }

    public function hasChildren()
    {
        return 0 !== count($this->children);
    }
}

$person = new Person();

if ($accessor->getValue($person, 'author')) {
    echo 'He is an author';
}
if ($accessor->getValue($person, 'children')) {
    echo 'He has children';
}

This will produce: He is an author

Magic __get() Method

The getValue method can also use the magic __get method:

// ...
class Person
{
    private $children = array(
        'Wouter' => array(...),
    );

    public function __get($id)
    {
        return $this->children[$id];
    }
}

$person = new Person();

echo $accessor->getValue($person, 'Wouter'); // array(...)
Magic __call() Method

At last, getValue can use the magic __call method, but you need to enable this feature by using PropertyAccessorBuilder:

// ...
class Person
{
    private $children = array(
        'wouter' => array(...),
    );

    public function __call($name, $args)
    {
        $property = lcfirst(substr($name, 3));
        if ('get' === substr($name, 0, 3)) {
            return isset($this->children[$property])
                ? $this->children[$property]
                : null;
        } elseif ('set' === substr($name, 0, 3)) {
            $value = 1 == count($args) ? $args[0] : null;
            $this->children[$property] = $value;
        }
    }
}

$person = new Person();

// Enable magic __call
$accessor = PropertyAccess::createPropertyAccessorBuilder()
    ->enableMagicCall()
    ->getPropertyAccessor();

echo $accessor->getValue($person, 'wouter'); // array(...)

2.3 新版功能: The use of magic __call() method was introduced in Symfony 2.3.

警告

The __call feature is disabled by default, you can enable it by calling PropertyAccessorBuilder::enableMagicCallEnabled see Enable other Features.

Writing to Arrays

The PropertyAccessor class can do more than just read an array, it can also write to an array. This can be achieved using the PropertyAccessor::setValue method:

// ...
$person = array();

$accessor->setValue($person, '[first_name]', 'Wouter');

echo $accessor->getValue($person, '[first_name]'); // 'Wouter'
// or
// echo $person['first_name']; // 'Wouter'
Writing to Objects

The setValue method has the same features as the getValue method. You can use setters, the magic __set method or properties to set values:

// ...
class Person
{
    public $firstName;
    private $lastName;
    private $children = array();

    public function setLastName($name)
    {
        $this->lastName = $name;
    }

    public function __set($property, $value)
    {
        $this->$property = $value;
    }

    // ...
}

$person = new Person();

$accessor->setValue($person, 'firstName', 'Wouter');
$accessor->setValue($person, 'lastName', 'de Jong');
$accessor->setValue($person, 'children', array(new Person()));

echo $person->firstName; // 'Wouter'
echo $person->getLastName(); // 'de Jong'
echo $person->children; // array(Person());

You can also use __call to set values but you need to enable the feature, see Enable other Features.

// ...
class Person
{
    private $children = array();

    public function __call($name, $args)
    {
        $property = lcfirst(substr($name, 3));
        if ('get' === substr($name, 0, 3)) {
            return isset($this->children[$property])
                ? $this->children[$property]
                : null;
        } elseif ('set' === substr($name, 0, 3)) {
            $value = 1 == count($args) ? $args[0] : null;
            $this->children[$property] = $value;
        }
    }

}

$person = new Person();

// Enable magic __call
$accessor = PropertyAccess::createPropertyAccessorBuilder()
    ->enableMagicCall()
    ->getPropertyAccessor();

$accessor->setValue($person, 'wouter', array(...));

echo $person->getWouter(); // array(...)
Mixing Objects and Arrays

You can also mix objects and arrays:

// ...
class Person
{
    public $firstName;
    private $children = array();

    public function setChildren($children)
    {
        $this->children = $children;
    }

    public function getChildren()
    {
        return $this->children;
    }
}

$person = new Person();

$accessor->setValue($person, 'children[0]', new Person);
// equal to $person->getChildren()[0] = new Person()

$accessor->setValue($person, 'children[0].firstName', 'Wouter');
// equal to $person->getChildren()[0]->firstName = 'Wouter'

echo 'Hello '.$accessor->getValue($person, 'children[0].firstName'); // 'Wouter'
// equal to $person->getChildren()[0]->firstName
Enable other Features

The PropertyAccessor can be configured to enable extra features. To do that you could use the PropertyAccessorBuilder:

// ...
$accessorBuilder = PropertyAccess::createPropertyAccessorBuilder();

// Enable magic __call
$accessorBuilder->enableMagicCall();

// Disable magic __call
$accessorBuilder->disableMagicCall();

// Check if magic __call handling is enabled
$accessorBuilder->isMagicCallEnabled(); // true or false

// At the end get the configured property accessor
$accessor = $accessorBuilder->getPropertyAccessor();

// Or all in one
$accessor = PropertyAccess::createPropertyAccessorBuilder()
    ->enableMagicCall()
    ->getPropertyAccessor();

Or you can pass parameters directly to the constructor (not the recommended way):

// ...
$accessor = new PropertyAccessor(true); // this enables handling of magic __call

Routing

The Routing Component
The Routing component maps an HTTP request to a set of configuration variables.
Installation

You can install the component in 2 different ways:

Usage

In order to set up a basic routing system you need three parts:

  • A RouteCollection, which contains the route definitions (instances of the class Route)
  • A RequestContext, which has information about the request
  • A UrlMatcher, which performs the mapping of the request to a single route

Here is a quick example. Notice that this assumes that you’ve already configured your autoloader to load the Routing component:

use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$route = new Route('/foo', array('controller' => 'MyController'));
$routes = new RouteCollection();
$routes->add('route_name', $route);

$context = new RequestContext($_SERVER['REQUEST_URI']);

$matcher = new UrlMatcher($routes, $context);

$parameters = $matcher->match('/foo');
// array('controller' => 'MyController', '_route' => 'route_name')

注解

Be careful when using $_SERVER['REQUEST_URI'], as it may include any query parameters on the URL, which will cause problems with route matching. An easy way to solve this is to use the HttpFoundation component as explained below.

You can add as many routes as you like to a RouteCollection.

The RouteCollection::add() method takes two arguments. The first is the name of the route. The second is a Route object, which expects a URL path and some array of custom variables in its constructor. This array of custom variables can be anything that’s significant to your application, and is returned when that route is matched.

If no matching route can be found a ResourceNotFoundException will be thrown.

In addition to your array of custom variables, a _route key is added, which holds the name of the matched route.

Defining Routes

A full route definition can contain up to seven parts:

  1. The URL path route. This is matched against the URL passed to the RequestContext, and can contain named wildcard placeholders (e.g. {placeholders}) to match dynamic parts in the URL.
  2. An array of default values. This contains an array of arbitrary values that will be returned when the request matches the route.
  3. An array of requirements. These define constraints for the values of the placeholders as regular expressions.
  4. An array of options. These contain internal settings for the route and are the least commonly needed.
  5. A host. This is matched against the host of the request. See How to Match a Route Based on the Host for more details.
  6. An array of schemes. These enforce a certain HTTP scheme (http, https).
  7. An array of methods. These enforce a certain HTTP request method (HEAD, GET, POST, ...).

2.2 新版功能: Host matching support was introduced in Symfony 2.2

Take the following route, which combines several of these ideas:

$route = new Route(
    '/archive/{month}', // path
    array('controller' => 'showArchive'), // default values
    array('month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'), // requirements
    array(), // options
    '{subdomain}.example.com', // host
    array(), // schemes
    array() // methods
);

// ...

$parameters = $matcher->match('/archive/2012-01');
// array(
//     'controller' => 'showArchive',
//     'month' => '2012-01',
//     'subdomain' => 'www',
//     '_route' => ...
//  )

$parameters = $matcher->match('/archive/foo');
// throws ResourceNotFoundException

In this case, the route is matched by /archive/2012-01, because the {month} wildcard matches the regular expression wildcard given. However, /archive/foo does not match, because “foo” fails the month wildcard.

小技巧

If you want to match all URLs which start with a certain path and end in an arbitrary suffix you can use the following route definition:

$route = new Route(
    '/start/{suffix}',
    array('suffix' => ''),
    array('suffix' => '.*')
);
Using Prefixes

You can add routes or other instances of RouteCollection to another collection. This way you can build a tree of routes. Additionally you can define a prefix and default values for the parameters, requirements, options, schemes and the host to all routes of a subtree using methods provided by the RouteCollection class:

$rootCollection = new RouteCollection();

$subCollection = new RouteCollection();
$subCollection->add(...);
$subCollection->add(...);
$subCollection->addPrefix('/prefix');
$subCollection->addDefaults(array(...));
$subCollection->addRequirements(array(...));
$subCollection->addOptions(array(...));
$subCollection->setHost('admin.example.com');
$subCollection->setMethods(array('POST'));
$subCollection->setSchemes(array('https'));

$rootCollection->addCollection($subCollection);
Set the Request Parameters

The RequestContext provides information about the current request. You can define all parameters of an HTTP request with this class via its constructor:

public function __construct(
    $baseUrl = '',
    $method = 'GET',
    $host = 'localhost',
    $scheme = 'http',
    $httpPort = 80,
    $httpsPort = 443,
    $path = '/',
    $queryString = ''
)

Normally you can pass the values from the $_SERVER variable to populate the RequestContext. But If you use the HttpFoundation component, you can use its Request class to feed the RequestContext in a shortcut:

use Symfony\Component\HttpFoundation\Request;

$context = new RequestContext();
$context->fromRequest(Request::createFromGlobals());
Generate a URL

While the UrlMatcher tries to find a route that fits the given request you can also build a URL from a certain route:

use Symfony\Component\Routing\Generator\UrlGenerator;

$routes = new RouteCollection();
$routes->add('show_post', new Route('/show/{slug}'));

$context = new RequestContext($_SERVER['REQUEST_URI']);

$generator = new UrlGenerator($routes, $context);

$url = $generator->generate('show_post', array(
    'slug' => 'my-blog-post',
));
// /show/my-blog-post

注解

If you have defined a scheme, an absolute URL is generated if the scheme of the current RequestContext does not match the requirement.

Load Routes from a File

You’ve already seen how you can easily add routes to a collection right inside PHP. But you can also load routes from a number of different files.

The Routing component comes with a number of loader classes, each giving you the ability to load a collection of route definitions from an external file of some format. Each loader expects a FileLocator instance as the constructor argument. You can use the FileLocator to define an array of paths in which the loader will look for the requested files. If the file is found, the loader returns a RouteCollection.

If you’re using the YamlFileLoader, then route definitions look like this:

# routes.yml
route1:
    path:     /foo
    defaults: { _controller: 'MyController::fooAction' }

route2:
    path:     /foo/bar
    defaults: { _controller: 'MyController::foobarAction' }

To load this file, you can use the following code. This assumes that your routes.yml file is in the same directory as the below code:

use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;

// look inside *this* directory
$locator = new FileLocator(array(__DIR__));
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');

Besides YamlFileLoader there are two other loaders that work the same way:

If you use the PhpFileLoader you have to provide the name of a PHP file which returns a RouteCollection:

// RouteProvider.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();
$collection->add(
    'route_name',
    new Route('/foo', array('controller' => 'ExampleController'))
);
// ...

return $collection;
Routes as Closures

There is also the ClosureLoader, which calls a closure and uses the result as a RouteCollection:

use Symfony\Component\Routing\Loader\ClosureLoader;

$closure = function () {
    return new RouteCollection();
};

$loader = new ClosureLoader();
$collection = $loader->load($closure);
Routes as Annotations

Last but not least there are AnnotationDirectoryLoader and AnnotationFileLoader to load route definitions from class annotations. The specific details are left out here.

The all-in-one Router

The Router class is an all-in-one package to quickly use the Routing component. The constructor expects a loader instance, a path to the main route definition and some other settings:

public function __construct(
    LoaderInterface $loader,
    $resource,
    array $options = array(),
    RequestContext $context = null,
    array $defaults = array()
);

With the cache_dir option you can enable route caching (if you provide a path) or disable caching (if it’s set to null). The caching is done automatically in the background if you want to use it. A basic example of the Router class would look like:

$locator = new FileLocator(array(__DIR__));
$requestContext = new RequestContext($_SERVER['REQUEST_URI']);

$router = new Router(
    new YamlFileLoader($locator),
    'routes.yml',
    array('cache_dir' => __DIR__.'/cache'),
    $requestContext
);
$router->match('/foo/bar');

注解

If you use caching, the Routing component will compile new classes which are saved in the cache_dir. This means your script must have write permissions for that location.

How to Match a Route Based on the Host

2.2 新版功能: Host matching support was introduced in Symfony 2.2

You can also match on the HTTP host of the incoming request.

  • YAML
    mobile_homepage:
        path:     /
        host:     m.example.com
        defaults: { _controller: AcmeDemoBundle:Main:mobileHomepage }
    
    homepage:
        path:     /
        defaults: { _controller: AcmeDemoBundle:Main:homepage }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="mobile_homepage" path="/" host="m.example.com">
            <default key="_controller">AcmeDemoBundle:Main:mobileHomepage</default>
        </route>
    
        <route id="homepage" path="/">
            <default key="_controller">AcmeDemoBundle:Main:homepage</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('mobile_homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:mobileHomepage',
    ), array(), array(), 'm.example.com'));
    
    $collection->add('homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:homepage',
    )));
    
    return $collection;
    

Both routes match the same path /, however the first one will match only if the host is m.example.com.

Using Placeholders

The host option uses the same syntax as the path matching system. This means you can use placeholders in your hostname:

  • YAML
    projects_homepage:
        path:     /
        host:     "{project_name}.example.com"
        defaults: { _controller: AcmeDemoBundle:Main:mobileHomepage }
    
    homepage:
        path:     /
        defaults: { _controller: AcmeDemoBundle:Main:homepage }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="projects_homepage" path="/" host="{project_name}.example.com">
            <default key="_controller">AcmeDemoBundle:Main:mobileHomepage</default>
        </route>
    
        <route id="homepage" path="/">
            <default key="_controller">AcmeDemoBundle:Main:homepage</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('project_homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:mobileHomepage',
    ), array(), array(), '{project_name}.example.com'));
    
    $collection->add('homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:homepage',
    )));
    
    return $collection;
    

You can also set requirements and default options for these placeholders. For instance, if you want to match both m.example.com and mobile.example.com, you use this:

  • YAML
    mobile_homepage:
        path:     /
        host:     "{subdomain}.example.com"
        defaults:
            _controller: AcmeDemoBundle:Main:mobileHomepage
            subdomain: m
        requirements:
            subdomain: m|mobile
    
    homepage:
        path:     /
        defaults: { _controller: AcmeDemoBundle:Main:homepage }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="mobile_homepage" path="/" host="{subdomain}.example.com">
            <default key="_controller">AcmeDemoBundle:Main:mobileHomepage</default>
            <default key="subdomain">m</default>
            <requirement key="subdomain">m|mobile</requirement>
        </route>
    
        <route id="homepage" path="/">
            <default key="_controller">AcmeDemoBundle:Main:homepage</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('mobile_homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:mobileHomepage',
        'subdomain'   => 'm',
    ), array(
        'subdomain' => 'm|mobile',
    ), array(), '{subdomain}.example.com'));
    
    $collection->add('homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:homepage',
    )));
    
    return $collection;
    

小技巧

You can also use service parameters if you do not want to hardcode the hostname:

  • YAML
    mobile_homepage:
        path:     /
        host:     "m.{domain}"
        defaults:
            _controller: AcmeDemoBundle:Main:mobileHomepage
            domain: "%domain%"
        requirements:
            domain: "%domain%"
    
    homepage:
        path:  /
        defaults: { _controller: AcmeDemoBundle:Main:homepage }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="mobile_homepage" path="/" host="m.{domain}">
            <default key="_controller">AcmeDemoBundle:Main:mobileHomepage</default>
            <default key="domain">%domain%</default>
            <requirement key="domain">%domain%</requirement>
        </route>
    
        <route id="homepage" path="/">
            <default key="_controller">AcmeDemoBundle:Main:homepage</default>
        </route>
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('mobile_homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:mobileHomepage',
        'domain' => '%domain%',
    ), array(
        'domain' => '%domain%',
    ), array(), 'm.{domain}'));
    
    $collection->add('homepage', new Route('/', array(
        '_controller' => 'AcmeDemoBundle:Main:homepage',
    )));
    
    return $collection;
    

小技巧

Make sure you also include a default option for the domain placeholder, otherwise you need to include a domain value each time you generate a URL using the route.

Using Host Matching of Imported Routes

You can also set the host option on imported routes:

  • YAML
    acme_hello:
        resource: "@AcmeHelloBundle/Resources/config/routing.yml"
        host:     "hello.example.com"
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <import resource="@AcmeHelloBundle/Resources/config/routing.xml" host="hello.example.com" />
    </routes>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    
    $collection = new RouteCollection();
    $collection->addCollection($loader->import("@AcmeHelloBundle/Resources/config/routing.php"), '', array(), array(), array(), 'hello.example.com');
    
    return $collection;
    

The host hello.example.com will be set on each route loaded from the new routing resource.

Testing your Controllers

You need to set the Host HTTP header on your request objects if you want to get past url matching in your functional tests.

$crawler = $client->request(
    'GET',
    '/homepage',
    array(),
    array(),
    array('HTTP_HOST' => 'm.' . $client->getContainer()->getParameter('domain'))
);

Security

The Security Component
The Security component provides a complete security system for your web application. It ships with facilities for authenticating using HTTP basic or digest authentication, interactive form login or X.509 certificate login, but also allows you to implement your own authentication strategies. Furthermore, the component provides ways to authorize authenticated users based on their roles, and it contains an advanced ACL system.
Installation

You can install the component in 2 different ways:

The Firewall and Security Context

Central to the Security component is the security context, which is an instance of SecurityContextInterface. When all steps in the process of authenticating the user have been taken successfully, you can ask the security context if the authenticated user has access to a certain action or resource of the application:

use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

// instance of Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface
$authenticationManager = ...;

// instance of Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface
$accessDecisionManager = ...;

$securityContext = new SecurityContext(
    $authenticationManager,
    $accessDecisionManager
);

// ... authenticate the user

if (!$securityContext->isGranted('ROLE_ADMIN')) {
    throw new AccessDeniedException();
}

注解

Read the dedicated sections to learn more about Authentication and Authorization.

A Firewall for HTTP Requests

Authenticating a user is done by the firewall. An application may have multiple secured areas, so the firewall is configured using a map of these secured areas. For each of these areas, the map contains a request matcher and a collection of listeners. The request matcher gives the firewall the ability to find out if the current request points to a secured area. The listeners are then asked if the current request can be used to authenticate the user:

use Symfony\Component\Security\Http\FirewallMap;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;

$map = new FirewallMap();

$requestMatcher = new RequestMatcher('^/secured-area/');

// instances of Symfony\Component\Security\Http\Firewall\ListenerInterface
$listeners = array(...);

$exceptionListener = new ExceptionListener(...);

$map->add($requestMatcher, $listeners, $exceptionListener);

The firewall map will be given to the firewall as its first argument, together with the event dispatcher that is used by the HttpKernel:

use Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpKernel\KernelEvents;

// the EventDispatcher used by the HttpKernel
$dispatcher = ...;

$firewall = new Firewall($map, $dispatcher);

$dispatcher->addListener(
    KernelEvents::REQUEST,
    array($firewall, 'onKernelRequest')
);

The firewall is registered to listen to the kernel.request event that will be dispatched by the HttpKernel at the beginning of each request it processes. This way, the firewall may prevent the user from going any further than allowed.

Firewall Listeners

When the firewall gets notified of the kernel.request event, it asks the firewall map if the request matches one of the secured areas. The first secured area that matches the request will return a set of corresponding firewall listeners (which each implement ListenerInterface). These listeners will all be asked to handle the current request. This basically means: find out if the current request contains any information by which the user might be authenticated (for instance the Basic HTTP authentication listener checks if the request has a header called PHP_AUTH_USER).

Exception Listener

If any of the listeners throws an AuthenticationException, the exception listener that was provided when adding secured areas to the firewall map will jump in.

The exception listener determines what happens next, based on the arguments it received when it was created. It may start the authentication procedure, perhaps ask the user to supply their credentials again (when they have only been authenticated based on a “remember-me” cookie), or transform the exception into an AccessDeniedHttpException, which will eventually result in an “HTTP/1.1 403: Access Denied” response.

Entry Points

When the user is not authenticated at all (i.e. when the security context has no token yet), the firewall’s entry point will be called to “start” the authentication process. An entry point should implement AuthenticationEntryPointInterface, which has only one method: start(). This method receives the current Request object and the exception by which the exception listener was triggered. The method should return a Response object. This could be, for instance, the page containing the login form or, in the case of Basic HTTP authentication, a response with a WWW-Authenticate header, which will prompt the user to supply their username and password.

Flow: Firewall, Authentication, Authorization

Hopefully you can now see a little bit about how the “flow” of the security context works:

  1. The Firewall is registered as a listener on the kernel.request event;
  2. At the beginning of the request, the Firewall checks the firewall map to see if any firewall should be active for this URL;
  3. If a firewall is found in the map for this URL, its listeners are notified;
  4. Each listener checks to see if the current request contains any authentication information - a listener may (a) authenticate a user, (b) throw an AuthenticationException, or (c) do nothing (because there is no authentication information on the request);
  5. Once a user is authenticated, you’ll use Authorization to deny access to certain resources.

Read the next sections to find out more about Authentication and Authorization.

Authentication

When a request points to a secured area, and one of the listeners from the firewall map is able to extract the user’s credentials from the current Request object, it should create a token, containing these credentials. The next thing the listener should do is ask the authentication manager to validate the given token, and return an authenticated token if the supplied credentials were found to be valid. The listener should then store the authenticated token in the security context:

use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class SomeAuthenticationListener implements ListenerInterface
{
    /**
     * @var SecurityContextInterface
     */
    private $securityContext;

    /**
     * @var AuthenticationManagerInterface
     */
    private $authenticationManager;

    /**
     * @var string Uniquely identifies the secured area
     */
    private $providerKey;

    // ...

    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        $username = ...;
        $password = ...;

        $unauthenticatedToken = new UsernamePasswordToken(
            $username,
            $password,
            $this->providerKey
        );

        $authenticatedToken = $this
            ->authenticationManager
            ->authenticate($unauthenticatedToken);

        $this->securityContext->setToken($authenticatedToken);
    }
}

注解

A token can be of any class, as long as it implements TokenInterface.

The Authentication Manager

The default authentication manager is an instance of AuthenticationProviderManager:

use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;

// instances of Symfony\Component\Security\Core\Authentication\AuthenticationProviderInterface
$providers = array(...);

$authenticationManager = new AuthenticationProviderManager($providers);

try {
    $authenticatedToken = $authenticationManager
        ->authenticate($unauthenticatedToken);
} catch (AuthenticationException $failed) {
    // authentication failed
}

The AuthenticationProviderManager, when instantiated, receives several authentication providers, each supporting a different type of token.

注解

You may of course write your own authentication manager, it only has to implement AuthenticationManagerInterface.

Authentication Providers

Each provider (since it implements AuthenticationProviderInterface) has a method supports() by which the AuthenticationProviderManager can determine if it supports the given token. If this is the case, the manager then calls the provider’s method AuthenticationProviderInterface::authenticate. This method should return an authenticated token or throw an AuthenticationException (or any other exception extending it).

Authenticating Users by their Username and Password

An authentication provider will attempt to authenticate a user based on the credentials they provided. Usually these are a username and a password. Most web applications store their user’s username and a hash of the user’s password combined with a randomly generated salt. This means that the average authentication would consist of fetching the salt and the hashed password from the user data storage, hash the password the user has just provided (e.g. using a login form) with the salt and compare both to determine if the given password is valid.

This functionality is offered by the DaoAuthenticationProvider. It fetches the user’s data from a UserProviderInterface, uses a PasswordEncoderInterface to create a hash of the password and returns an authenticated token if the password was valid:

use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
use Symfony\Component\Security\Core\User\UserChecker;
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;

$userProvider = new InMemoryUserProvider(
    array(
        'admin' => array(
            // password is "foo"
            'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
            'roles'    => array('ROLE_ADMIN'),
        ),
    )
);

// for some extra checks: is account enabled, locked, expired, etc.?
$userChecker = new UserChecker();

// an array of password encoders (see below)
$encoderFactory = new EncoderFactory(...);

$provider = new DaoAuthenticationProvider(
    $userProvider,
    $userChecker,
    'secured_area',
    $encoderFactory
);

$provider->authenticate($unauthenticatedToken);

注解

The example above demonstrates the use of the “in-memory” user provider, but you may use any user provider, as long as it implements UserProviderInterface. It is also possible to let multiple user providers try to find the user’s data, using the ChainUserProvider.

The Password Encoder Factory

The DaoAuthenticationProvider uses an encoder factory to create a password encoder for a given type of user. This allows you to use different encoding strategies for different types of users. The default EncoderFactory receives an array of encoders:

use Symfony\Component\Security\Core\Encoder\EncoderFactory;
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;

$defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000);
$weakEncoder = new MessageDigestPasswordEncoder('md5', true, 1);

$encoders = array(
    'Symfony\\Component\\Security\\Core\\User\\User' => $defaultEncoder,
    'Acme\\Entity\\LegacyUser'                       => $weakEncoder,

    // ...
);

$encoderFactory = new EncoderFactory($encoders);

Each encoder should implement PasswordEncoderInterface or be an array with a class and an arguments key, which allows the encoder factory to construct the encoder only when it is needed.

Creating a custom Password Encoder

There are many built-in password encoders. But if you need to create your own, it just needs to follow these rules:

  1. The class must implement PasswordEncoderInterface;

  2. The implementations of encodePassword() and isPasswordValid() must first of all make sure the password is not too long, i.e. the password length is no longer than 4096 characters. This is for security reasons (see CVE-2013-5750), and you can use the isPasswordTooLong() method for this check:

    use Symfony\Component\Security\Core\Exception\BadCredentialsException;
    
    class FoobarEncoder extends BasePasswordEncoder
    {
        public function encodePassword($raw, $salt)
        {
            if ($this->isPasswordTooLong($raw)) {
                throw new BadCredentialsException('Invalid password.');
            }
    
            // ...
        }
    
        public function isPasswordValid($encoded, $raw, $salt)
        {
            if ($this->isPasswordTooLong($raw)) {
                return false;
            }
    
            // ...
    }
    
Using Password Encoders

When the getEncoder() method of the password encoder factory is called with the user object as its first argument, it will return an encoder of type PasswordEncoderInterface which should be used to encode this user’s password:

// a Acme\Entity\LegacyUser instance
$user = ...;

// the password that was submitted, e.g. when registering
$plainPassword = ...;

$encoder = $encoderFactory->getEncoder($user);

// will return $weakEncoder (see above)
$encodedPassword = $encoder->encodePassword($plainPassword, $user->getSalt());

$user->setPassword($encodedPassword);

// ... save the user

Now, when you want to check if the submitted password (e.g. when trying to log in) is correct, you can use:

// fetch the Acme\Entity\LegacyUser
$user = ...;

// the submitted password, e.g. from the login form
$plainPassword = ...;

$validPassword = $encoder->isPasswordValid(
    $user->getPassword(), // the encoded password
    $plainPassword,       // the submitted password
    $user->getSalt()
);
Authorization

When any of the authentication providers (see Authentication Providers) has verified the still-unauthenticated token, an authenticated token will be returned. The authentication listener should set this token directly in the SecurityContextInterface using its setToken() method.

From then on, the user is authenticated, i.e. identified. Now, other parts of the application can use the token to decide whether or not the user may request a certain URI, or modify a certain object. This decision will be made by an instance of AccessDecisionManagerInterface.

An authorization decision will always be based on a few things:

  • The current token

    For instance, the token’s getRoles() method may be used to retrieve the roles of the current user (e.g. ROLE_SUPER_ADMIN), or a decision may be based on the class of the token.

  • A set of attributes

    Each attribute stands for a certain right the user should have, e.g. ROLE_ADMIN to make sure the user is an administrator.

  • An object (optional)

    Any object for which access control needs to be checked, like an article or a comment object.

Access Decision Manager

Since deciding whether or not a user is authorized to perform a certain action can be a complicated process, the standard AccessDecisionManager itself depends on multiple voters, and makes a final verdict based on all the votes (either positive, negative or neutral) it has received. It recognizes several strategies:

affirmative (default)
grant access as soon as any voter returns an affirmative response;
consensus
grant access if there are more voters granting access than there are denying;
unanimous
only grant access if none of the voters has denied access;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;

// instances of Symfony\Component\Security\Core\Authorization\Voter\VoterInterface
$voters = array(...);

// one of "affirmative", "consensus", "unanimous"
$strategy = ...;

// whether or not to grant access when all voters abstain
$allowIfAllAbstainDecisions = ...;

// whether or not to grant access when there is no majority (applies only to the "consensus" strategy)
$allowIfEqualGrantedDeniedDecisions = ...;

$accessDecisionManager = new AccessDecisionManager(
    $voters,
    $strategy,
    $allowIfAllAbstainDecisions,
    $allowIfEqualGrantedDeniedDecisions
);

参见

You can change the default strategy in the configuration.

Voters

Voters are instances of VoterInterface, which means they have to implement a few methods which allows the decision manager to use them:

supportsAttribute($attribute)
will be used to check if the voter knows how to handle the given attribute;
supportsClass($class)
will be used to check if the voter is able to grant or deny access for an object of the given class;
vote(TokenInterface $token, $object, array $attributes)
this method will do the actual voting and return a value equal to one of the class constants of VoterInterface, i.e. VoterInterface::ACCESS_GRANTED, VoterInterface::ACCESS_DENIED or VoterInterface::ACCESS_ABSTAIN;

The Security component contains some standard voters which cover many use cases:

AuthenticatedVoter

The AuthenticatedVoter voter supports the attributes IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED, and IS_AUTHENTICATED_ANONYMOUSLY and grants access based on the current level of authentication, i.e. is the user fully authenticated, or only based on a “remember-me” cookie, or even authenticated anonymously?

use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;

$anonymousClass = 'Symfony\Component\Security\Core\Authentication\Token\AnonymousToken';
$rememberMeClass = 'Symfony\Component\Security\Core\Authentication\Token\RememberMeToken';

$trustResolver = new AuthenticationTrustResolver($anonymousClass, $rememberMeClass);

$authenticatedVoter = new AuthenticatedVoter($trustResolver);

// instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface
$token = ...;

// any object
$object = ...;

$vote = $authenticatedVoter->vote($token, $object, array('IS_AUTHENTICATED_FULLY');
RoleVoter

The RoleVoter supports attributes starting with ROLE_ and grants access to the user when the required ROLE_* attributes can all be found in the array of roles returned by the token’s getRoles() method:

use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter;

$roleVoter = new RoleVoter('ROLE_');

$roleVoter->vote($token, $object, array('ROLE_ADMIN'));
RoleHierarchyVoter

The RoleHierarchyVoter extends RoleVoter and provides some additional functionality: it knows how to handle a hierarchy of roles. For instance, a ROLE_SUPER_ADMIN role may have subroles ROLE_ADMIN and ROLE_USER, so that when a certain object requires the user to have the ROLE_ADMIN role, it grants access to users who in fact have the ROLE_ADMIN role, but also to users having the ROLE_SUPER_ADMIN role:

use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter;
use Symfony\Component\Security\Core\Role\RoleHierarchy;

$hierarchy = array(
    'ROLE_SUPER_ADMIN' => array('ROLE_ADMIN', 'ROLE_USER'),
);

$roleHierarchy = new RoleHierarchy($hierarchy);

$roleHierarchyVoter = new RoleHierarchyVoter($roleHierarchy);

注解

When you make your own voter, you may of course use its constructor to inject any dependencies it needs to come to a decision.

Roles

Roles are objects that give expression to a certain right the user has. The only requirement is that they implement RoleInterface, which means they should also have a getRole() method that returns a string representation of the role itself. The default Role simply returns its first constructor argument:

use Symfony\Component\Security\Core\Role\Role;

$role = new Role('ROLE_ADMIN');

// will echo 'ROLE_ADMIN'
echo $role->getRole();

注解

Most authentication tokens extend from AbstractToken, which means that the roles given to its constructor will be automatically converted from strings to these simple Role objects.

Using the Decision Manager
The Access Listener

The access decision manager can be used at any point in a request to decide whether or not the current user is entitled to access a given resource. One optional, but useful, method for restricting access based on a URL pattern is the AccessListener, which is one of the firewall listeners (see Firewall Listeners) that is triggered for each request matching the firewall map (see A Firewall for HTTP Requests).

It uses an access map (which should be an instance of AccessMapInterface) which contains request matchers and a corresponding set of attributes that are required for the current user to get access to the application:

use Symfony\Component\Security\Http\AccessMap;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\Security\Http\Firewall\AccessListener;

$accessMap = new AccessMap();
$requestMatcher = new RequestMatcher('^/admin');
$accessMap->add($requestMatcher, array('ROLE_ADMIN'));

$accessListener = new AccessListener(
    $securityContext,
    $accessDecisionManager,
    $accessMap,
    $authenticationManager
);
Security Context

The access decision manager is also available to other parts of the application via the isGranted() method of the SecurityContext. A call to this method will directly delegate the question to the access decision manager:

use Symfony\Component\Security\SecurityContext;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

$securityContext = new SecurityContext(
    $authenticationManager,
    $accessDecisionManager
);

if (!$securityContext->isGranted('ROLE_ADMIN')) {
    throw new AccessDeniedException();
}
Securely Comparing Strings and Generating Random Numbers

The Symfony Security component comes with a collection of nice utilities related to security. These utilities are used by Symfony, but you should also use them if you want to solve the problem they address.

Comparing Strings

The time it takes to compare two strings depends on their differences. This can be used by an attacker when the two strings represent a password for instance; it is known as a Timing attack.

Internally, when comparing two passwords, Symfony uses a constant-time algorithm; you can use the same strategy in your own code thanks to the StringUtils class:

use Symfony\Component\Security\Core\Util\StringUtils;

// is some known string (e.g. password) equal to some user input?
$bool = StringUtils::equals($knownString, $userInput);

警告

To avoid timing attacks, the known string must be the first argument and the user-entered string the second.

Generating a Secure random Number

Whenever you need to generate a secure random number, you are highly encouraged to use the Symfony SecureRandom class:

use Symfony\Component\Security\Core\Util\SecureRandom;

$generator = new SecureRandom();
$random = $generator->nextBytes(10);

The nextBytes() method returns a random string composed of the number of characters passed as an argument (10 in the above example).

The SecureRandom class works better when OpenSSL is installed. But when it’s not available, it falls back to an internal algorithm, which needs a seed file to work correctly. Just pass a file name to enable it:

use Symfony\Component\Security\Core\Util\SecureRandom;

$generator = new SecureRandom('/some/path/to/store/the/seed.txt');
$random = $generator->nextBytes(10);

注解

If you’re using the Symfony Framework, you can access a secure random instance directly from the container: its name is security.secure_random.

The Serializer Component

The Serializer component is meant to be used to turn objects into a specific format (XML, JSON, YAML, ...) and the other way around.

In order to do so, the Serializer component follows the following simple schema.

_images/serializer_workflow.png

As you can see in the picture above, an array is used as a man in the middle. This way, Encoders will only deal with turning specific formats into arrays and vice versa. The same way, Normalizers will deal with turning specific objects into arrays and vice versa.

Serialization is a complicated topic, and while this component may not work in all cases, it can be a useful tool while developing tools to serialize and deserialize your objects.

Installation

You can install the component in 2 different ways:

Usage

Using the Serializer component is really simple. You just need to set up the Serializer specifying which Encoders and Normalizer are going to be available:

use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;

$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new GetSetMethodNormalizer());

$serializer = new Serializer($normalizers, $encoders);
Serializing an Object

For the sake of this example, assume the following class already exists in your project:

namespace Acme;

class Person
{
    private $age;
    private $name;

    // Getters
    public function getName()
    {
        return $this->name;
    }

    public function getAge()
    {
        return $this->age;
    }

    // Setters
    public function setName($name)
    {
        $this->name = $name;
    }

    public function setAge($age)
    {
        $this->age = $age;
    }
}

Now, if you want to serialize this object into JSON, you only need to use the Serializer service created before:

$person = new Acme\Person();
$person->setName('foo');
$person->setAge(99);

$jsonContent = $serializer->serialize($person, 'json');

// $jsonContent contains {"name":"foo","age":99}

echo $jsonContent; // or return it in a Response

The first parameter of the serialize() is the object to be serialized and the second is used to choose the proper encoder, in this case JsonEncoder.

Ignoring Attributes when Serializing

2.3 新版功能: The GetSetMethodNormalizer::setIgnoredAttributes method was introduced in Symfony 2.3.

As an option, there’s a way to ignore attributes from the origin object when serializing. To remove those attributes use the setIgnoredAttributes() method on the normalizer definition:

use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;

$normalizer = new GetSetMethodNormalizer();
$normalizer->setIgnoredAttributes(array('age'));
$encoder = new JsonEncoder();

$serializer = new Serializer(array($normalizer), array($encoder));
$serializer->serialize($person, 'json'); // Output: {"name":"foo"}
Deserializing an Object

You’ll now learn how to do the exact opposite. This time, the information of the Person class would be encoded in XML format:

$data = <<<EOF
<person>
    <name>foo</name>
    <age>99</age>
</person>
EOF;

$person = $serializer->deserialize($data, 'Acme\Person', 'xml');

In this case, deserialize() needs three parameters:

  1. The information to be decoded
  2. The name of the class this information will be decoded to
  3. The encoder used to convert that information into an array
Using Camelized Method Names for Underscored Attributes

2.3 新版功能: The GetSetMethodNormalizer::setCamelizedAttributes method was introduced in Symfony 2.3.

Sometimes property names from the serialized content are underscored (e.g. first_name). Normally, these attributes will use get/set methods like getFirst_name, when getFirstName method is what you really want. To change that behavior use the setCamelizedAttributes() method on the normalizer definition:

$encoder = new JsonEncoder();
$normalizer = new GetSetMethodNormalizer();
$normalizer->setCamelizedAttributes(array('first_name'));

$serializer = new Serializer(array($normalizer), array($encoder));

$json = <<<EOT
{
    "name":       "foo",
    "age":        "19",
    "first_name": "bar"
}
EOT;

$person = $serializer->deserialize($json, 'Acme\Person', 'json');

As a final result, the deserializer uses the first_name attribute as if it were firstName and uses the getFirstName and setFirstName methods.

Using Callbacks to Serialize Properties with Object Instances

When serializing, you can set a callback to format a specific object property:

use Acme\Person;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;

$encoder = new JsonEncoder();
$normalizer = new GetSetMethodNormalizer();

$callback = function ($dateTime) {
    return $dateTime instanceof \DateTime
        ? $dateTime->format(\DateTime::ISO8601)
        : '';
};

$normalizer->setCallbacks(array('createdAt' => $callback));

$serializer = new Serializer(array($normalizer), array($encoder));

$person = new Person();
$person->setName('cordoval');
$person->setAge(34);
$person->setCreatedAt(new \DateTime('now'));

$serializer->serialize($person, 'json');
// Output: {"name":"cordoval", "age": 34, "createdAt": "2014-03-22T09:43:12-0500"}
JMSSerializer

A popular third-party library, JMS serializer, provides a more sophisticated albeit more complex solution. This library includes the ability to configure how your objects should be serialized/deserialized via annotations (as well as YAML, XML and PHP), integration with the Doctrine ORM, and handling of other complex cases (e.g. circular references).

The Stopwatch Component

The Stopwatch component provides a way to profile code.

2.2 新版功能: The Stopwatch component was introduced in Symfony 2.2. Previously, the Stopwatch class was located in the HttpKernel component (and was introduced in Symfony 2.1).

Installation

You can install the component in two different ways:

Usage

The Stopwatch component provides an easy and consistent way to measure execution time of certain parts of code so that you don’t constantly have to parse microtime by yourself. Instead, use the simple Stopwatch class:

use Symfony\Component\Stopwatch\Stopwatch;

$stopwatch = new Stopwatch();
// Start event named 'eventName'
$stopwatch->start('eventName');
// ... some code goes here
$event = $stopwatch->stop('eventName');

The StopwatchEvent object can be retrieved from the start(), stop() and lap() methods.

You can also provide a category name to an event:

$stopwatch->start('eventName', 'categoryName');

You can consider categories as a way of tagging events. For example, the Symfony Profiler tool uses categories to nicely color-code different events.

Periods

As you know from the real world, all stopwatches come with two buttons: one to start and stop the stopwatch, and another to measure the lap time. This is exactly what the lap() method does:

$stopwatch = new Stopwatch();
// Start event named 'foo'
$stopwatch->start('foo');
// ... some code goes here
$stopwatch->lap('foo');
// ... some code goes here
$stopwatch->lap('foo');
// ... some other code goes here
$event = $stopwatch->stop('foo');

Lap information is stored as “periods” within the event. To get lap information call:

$event->getPeriods();

In addition to periods, you can get other useful information from the event object. For example:

$event->getCategory();   // Returns the category the event was started in
$event->getOrigin();     // Returns the event start time in milliseconds
$event->ensureStopped(); // Stops all periods not already stopped
$event->getStartTime();  // Returns the start time of the very first period
$event->getEndTime();    // Returns the end time of the very last period
$event->getDuration();   // Returns the event duration, including all periods
$event->getMemory();     // Returns the max memory usage of all periods
Sections

Sections are a way to logically split the timeline into groups. You can see how Symfony uses sections to nicely visualize the framework lifecycle in the Symfony Profiler tool. Here is a basic usage example using sections:

$stopwatch = new Stopwatch();

$stopwatch->openSection();
$stopwatch->start('parsing_config_file', 'filesystem_operations');
$stopwatch->stopSection('routing');

$events = $stopwatch->getSectionEvents('routing');

You can reopen a closed section by calling the openSection() method and specifying the id of the section to be reopened:

$stopwatch->openSection('routing');
$stopwatch->start('building_config_tree');
$stopwatch->stopSection('routing');

Templating

The Templating Component

The Templating component provides all the tools needed to build any kind of template system.

It provides an infrastructure to load template files and optionally monitor them for changes. It also provides a concrete template engine implementation using PHP with additional tools for escaping and separating templates into blocks and layouts.

Installation

You can install the component in 2 different ways:

Usage

The PhpEngine class is the entry point of the component. It needs a template name parser (TemplateNameParserInterface) to convert a template name to a template reference (TemplateReferenceInterface). It also needs a template loader (LoaderInterface) which uses the template reference to actually find and load the template:

use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\Loader\FilesystemLoader;

$loader = new FilesystemLoader(__DIR__.'/views/%name%');

$templating = new PhpEngine(new TemplateNameParser(), $loader);

echo $templating->render('hello.php', array('firstname' => 'Fabien'));
<!-- views/hello.php -->
Hello, <?php echo $firstname ?>!

The render() method parses the views/hello.php file and returns the output text. The second argument of render is an array of variables to use in the template. In this example, the result will be Hello, Fabien!.

注解

Templates will be cached in the memory of the engine. This means that if you render the same template multiple times in the same request, the template will only be loaded once from the file system.

The $view Variable

In all templates parsed by the PhpEngine, you get access to a mysterious variable called $view. That variable holds the current PhpEngine instance. That means you get access to a bunch of methods that make your life easier.

Including Templates

The best way to share a snippet of template code is to create a template that can then be included by other templates. As the $view variable is an instance of PhpEngine, you can use the render method (which was used to render the template originally) inside the template to render another template:

<?php $names = array('Fabien', ...) ?>
<?php foreach ($names as $name) : ?>
    <?php echo $view->render('hello.php', array('firstname' => $name)) ?>
<?php endforeach ?>
Global Variables

Sometimes, you need to set a variable which is available in all templates rendered by an engine (like the $app variable when using the Symfony framework). These variables can be set by using the addGlobal() method and they can be accessed in the template as normal variables:

$templating->addGlobal('ga_tracking', 'UA-xxxxx-x');

In a template:

<p>The google tracking code is: <?php echo $ga_tracking ?></p>

警告

The global variables cannot be called this or view, since they are already used by the PHP engine.

注解

The global variables can be overridden by a local variable in the template with the same name.

Output Escaping

When you render variables, you should probably escape them so that HTML or JavaScript code isn’t written out to your page. This will prevent things like XSS attacks. To do this, use the escape() method:

<?php echo $view->escape($firstname) ?>

By default, the escape() method assumes that the variable is outputted within an HTML context. The second argument lets you change the context. For example, to output something inside JavaScript, use the js context:

<?php echo $view->escape($var, 'js') ?>

The component comes with an HTML and JS escaper. You can register your own escaper using the setEscaper() method:

$templating->setEscaper('css', function ($value) {
    // ... all CSS escaping

    return $escapedValue;
});
Helpers

The Templating component can be easily extended via helpers. Helpers are PHP objects that provide features useful in a template context. The component has 2 built-in helpers:

Before you can use these helpers, you need to register them using set():

use Symfony\Component\Templating\Helper\AssetsHelper;
// ...

$templating->set(new AssetsHelper());
Custom Helpers

You can create your own helpers by creating a class which implements HelperInterface. However, most of the time you’ll extend Helper.

The Helper has one required method: getName(). This is the name that is used to get the helper from the $view object.

Creating a Custom Engine

Besides providing a PHP templating engine, you can also create your own engine using the Templating component. To do that, create a new class which implements the EngineInterface. This requires 3 method:

Using Multiple Engines

It is possible to use multiple engines at the same time using the DelegatingEngine class. This class takes a list of engines and acts just like a normal templating engine. The only difference is that it delegates the calls to one of the other engines. To choose which one to use for the template, the EngineInterface::supports() method is used.

use Acme\Templating\CustomEngine;
use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\DelegatingEngine;

$templating = new DelegatingEngine(array(
    new PhpEngine(...),
    new CustomEngine(...),
));
The Templating Helpers
Slots Helper

More often than not, templates in a project share common elements, like the well-known header and footer. Using this helper, the static HTML code can be placed in a layout file along with “slots”, which represent the dynamic parts that will change on a page-by-page basis. These slots are then filled in by different children template. In other words, the layout file decorates the child template.

Displaying Slots

The slots are accessible by using the slots helper ($view['slots']). Use output() to display the content of the slot on that place:

<!-- views/layout.php -->
<!doctype html>
<html>
    <head>
        <title>
            <?php $view['slots']->output('title', 'Default title') ?>
        </title>
    </head>
    <body>
        <?php $view['slots']->output('_content') ?>
    </body>
</html>

The first argument of the method is the name of the slot. The method has an optional second argument, which is the default value to use if the slot is not available.

The _content slot is a special slot set by the PhpEngine. It contains the content of the subtemplate.

警告

If you’re using the standalone component, make sure you registered the SlotsHelper:

use Symfony\Component\Templating\Helper\SlotsHelper;

// ...
$templateEngine->set(new SlotsHelper());
Extending Templates

The extend() method is called in the sub-template to set its parent template. Then $view['slots']->set() can be used to set the content of a slot. All content which is not explicitly set in a slot is in the _content slot.

<!-- views/page.php -->
<?php $view->extend('layout.php') ?>

<?php $view['slots']->set('title', $page->title) ?>

<h1>
    <?php echo $page->title ?>
</h1>
<p>
    <?php echo $page->body ?>
</p>

注解

Multiple levels of inheritance is possible: a layout can extend another layout.

For large slots, there is also an extended syntax:

<?php $view['slots']->start('title') ?>
    Some large amount of HTML
<?php $view['slots']->stop() ?>
Assets Helper

The assets helper’s main purpose is to make your application more portable by generating asset paths:

<link href="<?php echo $view['assets']->getUrl('css/style.css') ?>" rel="stylesheet">

<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>">

The assets helper can then be configured to render paths to a CDN or modify the paths in case your assets live in a sub-directory of your host (e.g. http://example.com/app).

Configure Paths

By default, the assets helper will prefix all paths with a slash. You can configure this by passing a base assets path as the first argument of the constructor:

use Symfony\Component\Templating\Helper\AssetsHelper;

// ...
$templateEngine->set(new AssetsHelper('/foo/bar'));

Now, if you use the helper, everything will be prefixed with /foo/bar:

<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>">
<!-- renders as:
<img src="/foo/bar/images/logo.png">
-->
Absolute Urls

You can also specify a URL to use in the second parameter of the constructor:

// ...
$templateEngine->set(new AssetsHelper(null, 'http://cdn.example.com/'));

Now URLs are rendered like http://cdn.example.com/images/logo.png.

Versioning

To avoid using the cached resource after updating the old resource, you can use versions which you bump every time you release a new project. The version can be specified in the third argument:

// ...
$templateEngine->set(new AssetsHelper(null, null, '328rad75'));

Now, every URL is suffixed with ?328rad75. If you want to have a different format, you can specify the new format in fourth argument. It’s a string that is used in sprintf. The first argument is the path and the second is the version. For instance, %s?v=%s will be rendered as /images/logo.png?v=328rad75.

Multiple Packages

Asset path generation is handled internally by packages. The component provides 2 packages by default:

You can also use multiple packages:

use Symfony\Component\Templating\Asset\PathPackage;

// ...
$templateEngine->set(new AssetsHelper());

$templateEngine->get('assets')->addPackage('images', new PathPackage('/images/'));
$templateEngine->get('assets')->addPackage('scripts', new PathPackage('/scripts/'));

This will setup the assets helper with 3 packages: the default package which defaults to / (set by the constructor), the images package which prefixes it with /images/ and the scripts package which prefixes it with /scripts/.

If you want to set another default package, you can use setDefaultPackage().

You can specify which package you want to use in the second argument of getUrl():

<img src="<?php echo $view['assets']->getUrl('foo.png', 'images') ?>">
<!-- renders as:
<img src="/images/foo.png">
-->
Custom Packages

You can create your own package by extending Package.

The Templating component comes with some useful helpers. These helpers contain functions to ease some common tasks.

Translation

The Translation Component
The Translation component provides tools to internationalize your application.
Installation

You can install the component in 2 different ways:

Constructing the Translator

The main access point of the Translation component is Translator. Before you can use it, you need to configure it and load the messages to translate (called message catalogs).

Configuration

The constructor of the Translator class needs one argument: The locale.

use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\MessageSelector;

$translator = new Translator('fr_FR', new MessageSelector());

注解

The locale set here is the default locale to use. You can override this locale when translating strings.

注解

The term locale refers roughly to the user’s language and country. It can be any string that your application uses to manage translations and other format differences (e.g. currency format). The ISO 639-1 language code, an underscore (_), then the ISO 3166-1 alpha-2 country code (e.g. fr_FR for French/France) is recommended.

Loading Message Catalogs

The messages are stored in message catalogs inside the Translator class. A message catalog is like a dictionary of translations for a specific locale.

The Translation component uses Loader classes to load catalogs. You can load multiple resources for the same locale, which will then be combined into one catalog.

The component comes with some default loaders:

2.1 新版功能: The IcuDatFileLoader, IcuResFileLoader, IniFileLoader, MoFileLoader, PoFileLoader and QtFileLoader were introduced in Symfony 2.1.

All file loaders require the Config component.

You can also create your own Loader, in case the format is not already supported by one of the default loaders.

At first, you should add one or more loaders to the Translator:

// ...
$translator->addLoader('array', new ArrayLoader());

The first argument is the name to which you can refer the loader in the translator and the second argument is an instance of the loader itself. After this, you can add your resources using the correct loader.

Loading Messages with the ArrayLoader

Loading messages can be done by calling addResource(). The first argument is the loader name (this was the first argument of the addLoader method), the second is the resource and the third argument is the locale:

// ...
$translator->addResource('array', array(
    'Hello World!' => 'Bonjour',
), 'fr_FR');
Loading Messages with the File Loaders

If you use one of the file loaders, you should also use the addResource method. The only difference is that you should put the file name to the resource file as the second argument, instead of an array:

// ...
$translator->addLoader('yaml', new YamlFileLoader());
$translator->addResource('yaml', 'path/to/messages.fr.yml', 'fr_FR');
The Translation Process

To actually translate the message, the Translator uses a simple process:

  • A catalog of translated messages is loaded from translation resources defined for the locale (e.g. fr_FR). Messages from the Fallback Locales are also loaded and added to the catalog, if they don’t already exist. The end result is a large “dictionary” of translations;
  • If the message is located in the catalog, the translation is returned. If not, the translator returns the original message.

You start this process by calling trans() or transChoice(). Then, the Translator looks for the exact string inside the appropriate message catalog and returns it (if it exists).

Fallback Locales

If the message is not located in the catalog of the specific locale, the translator will look into the catalog of one or more fallback locales. For example, assume you’re trying to translate into the fr_FR locale:

  1. First, the translator looks for the translation in the fr_FR locale;
  2. If it wasn’t found, the translator looks for the translation in the fr locale;
  3. If the translation still isn’t found, the translator uses the one or more fallback locales set explicitly on the translator.

For (3), the fallback locales can be set by calling setFallbackLocale():

// ...
$translator->setFallbackLocale(array('en'));
Using Message Domains

As you’ve seen, message files are organized into the different locales that they translate. The message files can also be organized further into “domains”.

The domain is specified in the fourth argument of the addResource() method. The default domain is messages. For example, suppose that, for organization, translations were split into three different domains: messages, admin and navigation. The French translation would be loaded like this:

// ...
$translator->addLoader('xliff', new XliffLoader());

$translator->addResource('xliff', 'messages.fr.xliff', 'fr_FR');
$translator->addResource('xliff', 'admin.fr.xliff', 'fr_FR', 'admin');
$translator->addResource(
    'xliff',
    'navigation.fr.xliff',
    'fr_FR',
    'navigation'
);

When translating strings that are not in the default domain (messages), you must specify the domain as the third argument of trans():

$translator->trans('Symfony is great', array(), 'admin');

Symfony will now look for the message in the admin domain of the specified locale.

Usage

Read how to use the Translation component in Using the Translator.

Using the Translator

Imagine you want to translate the string “Symfony is great” into French:

use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\ArrayLoader;

$translator = new Translator('fr_FR');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array(
    'Symfony is great!' => 'J\'aime Symfony!',
), 'fr_FR');

echo $translator->trans('Symfony is great!');

In this example, the message “Symfony is great!” will be translated into the locale set in the constructor (fr_FR) if the message exists in one of the message catalogs.

Message Placeholders

Sometimes, a message containing a variable needs to be translated:

// ...
$translated = $translator->trans('Hello '.$name);

echo $translated;

However, creating a translation for this string is impossible since the translator will try to look up the exact message, including the variable portions (e.g. “Hello Ryan” or “Hello Fabien”). Instead of writing a translation for every possible iteration of the $name variable, you can replace the variable with a “placeholder”:

// ...
$translated = $translator->trans(
    'Hello %name%',
    array('%name%' => $name)
);

echo $translated;

Symfony will now look for a translation of the raw message (Hello %name%) and then replace the placeholders with their values. Creating a translation is done just as before:

  • XML
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>Hello %name%</source>
                    <target>Bonjour %name%</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • PHP
    return array(
        'Hello %name%' => 'Bonjour %name%',
    );
    
  • YAML
    'Hello %name%': Bonjour %name%
    

注解

The placeholders can take on any form as the full message is reconstructed using the PHP strtr function. But the %...% form is recommended, to avoid problems when using Twig.

As you’ve seen, creating a translation is a two-step process:

  1. Abstract the message that needs to be translated by processing it through the Translator.
  2. Create a translation for the message in each locale that you choose to support.

The second step is done by creating message catalogs that define the translations for any number of different locales.

Creating Translations

The act of creating translation files is an important part of “localization” (often abbreviated L10n). Translation files consist of a series of id-translation pairs for the given domain and locale. The source is the identifier for the individual translation, and can be the message in the main locale (e.g. “Symfony is great”) of your application or a unique identifier (e.g. symfony.great - see the sidebar below).

Translation files can be created in several different formats, XLIFF being the recommended format. These files are parsed by one of the loader classes.

  • XML
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>Symfony is great</source>
                    <target>J'aime Symfony</target>
                </trans-unit>
                <trans-unit id="2">
                    <source>symfony.great</source>
                    <target>J'aime Symfony</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • YAML
    Symfony is great: J'aime Symfony
    symfony.great:    J'aime Symfony
    
  • PHP
    return array(
        'Symfony is great' => 'J\'aime Symfony',
        'symfony.great'    => 'J\'aime Symfony',
    );
    
Pluralization

Message pluralization is a tough topic as the rules can be quite complex. For instance, here is the mathematical representation of the Russian pluralization rules:

(($number % 10 == 1) && ($number % 100 != 11))
    ? 0
    : ((($number % 10 >= 2)
        && ($number % 10 <= 4)
        && (($number % 100 < 10)
        || ($number % 100 >= 20)))
            ? 1
            : 2
);

As you can see, in Russian, you can have three different plural forms, each given an index of 0, 1 or 2. For each form, the plural is different, and so the translation is also different.

When a translation has different forms due to pluralization, you can provide all the forms as a string separated by a pipe (|):

'There is one apple|There are %count% apples'

To translate pluralized messages, use the transChoice() method:

$translator->transChoice(
    'There is one apple|There are %count% apples',
    10,
    array('%count%' => 10)
);

The second argument (10 in this example) is the number of objects being described and is used to determine which translation to use and also to populate the %count% placeholder.

Based on the given number, the translator chooses the right plural form. In English, most words have a singular form when there is exactly one object and a plural form for all other numbers (0, 2, 3...). So, if count is 1, the translator will use the first string (There is one apple) as the translation. Otherwise it will use There are %count% apples.

Here is the French translation:

'Il y a %count% pomme|Il y a %count% pommes'

Even if the string looks similar (it is made of two sub-strings separated by a pipe), the French rules are different: the first form (no plural) is used when count is 0 or 1. So, the translator will automatically use the first string (Il y a %count% pomme) when count is 0 or 1.

Each locale has its own set of rules, with some having as many as six different plural forms with complex rules behind which numbers map to which plural form. The rules are quite simple for English and French, but for Russian, you’d may want a hint to know which rule matches which string. To help translators, you can optionally “tag” each string:

'one: There is one apple|some: There are %count% apples'

'none_or_one: Il y a %count% pomme|some: Il y a %count% pommes'

The tags are really only hints for translators and don’t affect the logic used to determine which plural form to use. The tags can be any descriptive string that ends with a colon (:). The tags also do not need to be the same in the original message as in the translated one.

小技巧

As tags are optional, the translator doesn’t use them (the translator will only get a string based on its position in the string).

Explicit Interval Pluralization

The easiest way to pluralize a message is to let the Translator use internal logic to choose which string to use based on a given number. Sometimes, you’ll need more control or want a different translation for specific cases (for 0, or when the count is negative, for example). For such cases, you can use explicit math intervals:

'{0} There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] There are many apples'

The intervals follow the ISO 31-11 notation. The above string specifies four different intervals: exactly 0, exactly 1, 2-19, and 20 and higher.

You can also mix explicit math rules and standard rules. In this case, if the count is not matched by a specific interval, the standard rules take effect after removing the explicit rules:

'{0} There are no apples|[20,Inf] There are many apples|There is one apple|a_few: There are %count% apples'

For example, for 1 apple, the standard rule There is one apple will be used. For 2-19 apples, the second standard rule There are %count% apples will be selected.

An Interval can represent a finite set of numbers:

{1,2,3,4}

Or numbers between two other numbers:

[1, +Inf[
]-1,2[

The left delimiter can be [ (inclusive) or ] (exclusive). The right delimiter can be [ (exclusive) or ] (inclusive). Beside numbers, you can use -Inf and +Inf for the infinite.

Forcing the Translator Locale

When translating a message, the Translator uses the specified locale or the fallback locale if necessary. You can also manually specify the locale to use for translation:

$translator->trans(
    'Symfony is great',
    array(),
    'messages',
    'fr_FR'
);

$translator->transChoice(
    '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10),
    'messages',
    'fr_FR'
);
Adding Custom Format Support

Sometimes, you need to deal with custom formats for translation files. The Translation component is flexible enough to support this. Just create a loader (to load translations) and, optionally, a dumper (to dump translations).

Imagine that you have a custom format where translation messages are defined using one line for each translation and parentheses to wrap the key and the message. A translation file would look like this:

(welcome)(accueil)
(goodbye)(au revoir)
(hello)(bonjour)
Creating a Custom Loader

To define a custom loader that is able to read these kinds of files, you must create a new class that implements the LoaderInterface. The load() method will get a filename and parse it into an array. Then, it will create the catalog that will be returned:

use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Loader\LoaderInterface;

class MyFormatLoader implements LoaderInterface
{
    public function load($resource, $locale, $domain = 'messages')
    {
        $messages = array();
        $lines = file($resource);

        foreach ($lines as $line) {
            if (preg_match('/\(([^\)]+)\)\(([^\)]+)\)/', $line, $matches)) {
                $messages[$matches[1]] = $matches[2];
            }
        }

        $catalogue = new MessageCatalogue($locale);
        $catalogue->add($messages, $domain);

        return $catalogue;
    }

}

Once created, it can be used as any other loader:

use Symfony\Component\Translation\Translator;

$translator = new Translator('fr_FR');
$translator->addLoader('my_format', new MyFormatLoader());

$translator->addResource('my_format', __DIR__.'/translations/messages.txt', 'fr_FR');

echo $translator->trans('welcome');

It will print “accueil”.

Creating a Custom Dumper

It is also possible to create a custom dumper for your format, which is useful when using the extraction commands. To do so, a new class implementing the DumperInterface must be created. To write the dump contents into a file, extending the FileDumper class will save a few lines:

use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Dumper\FileDumper;

class MyFormatDumper extends FileDumper
{
    protected function format(MessageCatalogue $messages, $domain = 'messages')
    {
        $output = '';

        foreach ($messages->all($domain) as $source => $target) {
            $output .= sprintf("(%s)(%s)\n", $source, $target);
        }

        return $output;
    }

    protected function getExtension()
    {
        return 'txt';
    }
}

The format() method creates the output string, that will be used by the dump() method of the FileDumper class to create the file. The dumper can be used like any other built-in dumper. In the following example, the translation messages defined in the YAML file are dumped into a text file with the custom format:

use Symfony\Component\Translation\Loader\YamlFileLoader;

$loader = new YamlFileLoader();
$catalogue = $loader->load(__DIR__ . '/translations/messages.fr_FR.yml' , 'fr_FR');

$dumper = new MyFormatDumper();
$dumper->dump($catalogue, array('path' => __DIR__.'/dumps'));

Yaml

The Yaml Component
The Yaml component loads and dumps YAML files.
What is It?

The Symfony Yaml component parses YAML strings to convert them to PHP arrays. It is also able to convert PHP arrays to YAML strings.

YAML, YAML Ain’t Markup Language, is a human friendly data serialization standard for all programming languages. YAML is a great format for your configuration files. YAML files are as expressive as XML files and as readable as INI files.

The Symfony Yaml Component implements a selected subset of features defined in the YAML 1.2 version specification.

小技巧

Learn more about the Yaml component in the The YAML Format article.

Installation

You can install the component in 2 different ways:

Why?
Fast

One of the goals of Symfony Yaml is to find the right balance between speed and features. It supports just the needed features to handle configuration files. Notable lacking features are: document directives, multi-line quoted messages, compact block collections and multi-document files.

Real Parser

It sports a real parser and is able to parse a large subset of the YAML specification, for all your configuration needs. It also means that the parser is pretty robust, easy to understand, and simple enough to extend.

Clear Error Messages

Whenever you have a syntax problem with your YAML files, the library outputs a helpful message with the filename and the line number where the problem occurred. It eases the debugging a lot.

Dump Support

It is also able to dump PHP arrays to YAML with object support, and inline level configuration for pretty outputs.

Types Support

It supports most of the YAML built-in types like dates, integers, octals, booleans, and much more...

Full Merge Key Support

Full support for references, aliases, and full merge key. Don’t repeat yourself by referencing common configuration bits.

Using the Symfony YAML Component

The Symfony Yaml component is very simple and consists of two main classes: one parses YAML strings (Parser), and the other dumps a PHP array to a YAML string (Dumper).

On top of these two classes, the Yaml class acts as a thin wrapper that simplifies common uses.

Reading YAML Files

The parse() method parses a YAML string and converts it to a PHP array:

use Symfony\Component\Yaml\Parser;

$yaml = new Parser();

$value = $yaml->parse(file_get_contents('/path/to/file.yml'));

If an error occurs during parsing, the parser throws a ParseException exception indicating the error type and the line in the original YAML string where the error occurred:

use Symfony\Component\Yaml\Exception\ParseException;

try {
    $value = $yaml->parse(file_get_contents('/path/to/file.yml'));
} catch (ParseException $e) {
    printf("Unable to parse the YAML string: %s", $e->getMessage());
}

小技巧

As the parser is re-entrant, you can use the same parser object to load different YAML strings.

It may also be convenient to use the parse() wrapper method:

use Symfony\Component\Yaml\Yaml;

$yaml = Yaml::parse(file_get_contents('/path/to/file.yml'));

The parse() static method takes a YAML string or a file containing YAML. Internally, it calls the parse() method, but enhances the error if something goes wrong by adding the filename to the message.

警告

Because it is currently possible to pass a filename to this method, you must validate the input first. Passing a filename is deprecated in Symfony 2.2, and will be removed in Symfony 3.0.

Writing YAML Files

The dump() method dumps any PHP array to its YAML representation:

use Symfony\Component\Yaml\Dumper;

$array = array(
    'foo' => 'bar',
    'bar' => array('foo' => 'bar', 'bar' => 'baz'),
);

$dumper = new Dumper();

$yaml = $dumper->dump($array);

file_put_contents('/path/to/file.yml', $yaml);

注解

Of course, the Symfony Yaml dumper is not able to dump resources. Also, even if the dumper is able to dump PHP objects, it is considered to be a not supported feature.

If an error occurs during the dump, the parser throws a DumpException exception.

If you only need to dump one array, you can use the dump() static method shortcut:

use Symfony\Component\Yaml\Yaml;

$yaml = Yaml::dump($array, $inline);

The YAML format supports two kind of representation for arrays, the expanded one, and the inline one. By default, the dumper uses the inline representation:

{ foo: bar, bar: { foo: bar, bar: baz } }

The second argument of the dump() method customizes the level at which the output switches from the expanded representation to the inline one:

echo $dumper->dump($array, 1);
foo: bar
bar: { foo: bar, bar: baz }
echo $dumper->dump($array, 2);
foo: bar
bar:
    foo: bar
    bar: baz
The YAML Format

According to the official YAML website, YAML is “a human friendly data serialization standard for all programming languages”.

Even if the YAML format can describe complex nested data structure, this chapter only describes the minimum set of features needed to use YAML as a configuration file format.

YAML is a simple language that describes data. As PHP, it has a syntax for simple types like strings, booleans, floats, or integers. But unlike PHP, it makes a difference between arrays (sequences) and hashes (mappings).

Scalars

The syntax for scalars is similar to the PHP syntax.

Strings

Strings in YAML can be wrapped both in single and double quotes. In some cases, they can also be unquoted:

A string in YAML

'A singled-quoted string in YAML'

"A double-quoted string in YAML"

Quoted styles are useful when a string starts or end with one or more relevant spaces, because unquoted strings are trimmed on both end when parsing their contents. Quotes are required when the string contains special or reserved characters.

When using single-quoted strings, any single quote ' inside its contents must be doubled to escape it:

'A single quote '' inside a single-quoted string'

Strings containing any of the following characters must be quoted. Although you can use double quotes, for these characters it is more convenient to use single quotes, which avoids having to escape any backslash \:

  • :, {, }, [, ], ,, &, *, #, ?, |, -, <, >, =, !, %, @, \`

The double-quoted style provides a way to express arbitrary strings, by using \ to escape characters and sequences. For instance, it is very useful when you need to embed a \n or a Unicode character in a string.

"A double-quoted string in YAML\n"

If the string contains any of the following control characters, it must be escaped with double quotes:

  • \0, \x01, \x02, \x03, \x04, \x05, \x06, \a, \b, \t, \n, \v, \f, \r, \x0e, \x0f, \x10, \x11, \x12, \x13, \x14, \x15, \x16, \x17, \x18, \x19, \x1a, \e, \x1c, \x1d, \x1e, \x1f, \N, \_, \L, \P

Finally, there are other cases when the strings must be quoted, no matter if you’re using single or double quotes:

  • When the string is true or false (otherwise, it would be treated as a boolean value);
  • When the string is null or ~ (otherwise, it would be considered as a null value);
  • When the string looks like a number, such as integers (e.g. 2, 14, etc.), floats (e.g. 2.6, 14.9) and exponential numbers (e.g. 12e7, etc.) (otherwise, it would be treated as a numeric value);
  • When the string looks like a date (e.g. 2014-12-31) (otherwise it would be automatically converted into a Unix timestamp).

When a string contains line breaks, you can use the literal style, indicated by the pipe (|), to indicate that the string will span several lines. In literals, newlines are preserved:

|
  \/ /| |\/| |
  / / | |  | |__

Alternatively, strings can be written with the folded style, denoted by >, where each line break is replaced by a space:

>
  This is a very long sentence
  that spans several lines in the YAML
  but which will be rendered as a string
  without carriage returns.

注解

Notice the two spaces before each line in the previous examples. They won’t appear in the resulting PHP strings.

Numbers
# an integer
12
# an octal
014
# an hexadecimal
0xC
# a float
13.4
# an exponential number
1.2e+34
# infinity
.inf
Nulls

Nulls in YAML can be expressed with null or ~.

Booleans

Booleans in YAML are expressed with true and false.

Dates

YAML uses the ISO-8601 standard to express dates:

2001-12-14t21:59:43.10-05:00
# simple date
2002-12-14
Collections

A YAML file is rarely used to describe a simple scalar. Most of the time, it describes a collection. A collection can be a sequence or a mapping of elements. Both sequences and mappings are converted to PHP arrays.

Sequences use a dash followed by a space:

- PHP
- Perl
- Python

The previous YAML file is equivalent to the following PHP code:

array('PHP', 'Perl', 'Python');

Mappings use a colon followed by a space (: ) to mark each key/value pair:

PHP: 5.2
MySQL: 5.1
Apache: 2.2.20

which is equivalent to this PHP code:

array('PHP' => 5.2, 'MySQL' => 5.1, 'Apache' => '2.2.20');

注解

In a mapping, a key can be any valid scalar.

The number of spaces between the colon and the value does not matter:

PHP:    5.2
MySQL:  5.1
Apache: 2.2.20

YAML uses indentation with one or more spaces to describe nested collections:

"symfony 1.0":
  PHP:    5.0
  Propel: 1.2
"symfony 1.2":
  PHP:    5.2
  Propel: 1.3

The following YAML is equivalent to the following PHP code:

array(
    'symfony 1.0' => array(
        'PHP'    => 5.0,
        'Propel' => 1.2,
    ),
    'symfony 1.2' => array(
        'PHP'    => 5.2,
        'Propel' => 1.3,
    ),
);

There is one important thing you need to remember when using indentation in a YAML file: Indentation must be done with one or more spaces, but never with tabulations.

You can nest sequences and mappings as you like:

'Chapter 1':
  - Introduction
  - Event Types
'Chapter 2':
  - Introduction
  - Helpers

YAML can also use flow styles for collections, using explicit indicators rather than indentation to denote scope.

A sequence can be written as a comma separated list within square brackets ([]):

[PHP, Perl, Python]

A mapping can be written as a comma separated list of key/values within curly braces ({}):

{ PHP: 5.2, MySQL: 5.1, Apache: 2.2.20 }

You can mix and match styles to achieve a better readability:

'Chapter 1': [Introduction, Event Types]
'Chapter 2': [Introduction, Helpers]
"symfony 1.0": { PHP: 5.0, Propel: 1.2 }
"symfony 1.2": { PHP: 5.2, Propel: 1.3 }
Comments

Comments can be added in YAML by prefixing them with a hash mark (#):

# Comment on a line
"symfony 1.0": { PHP: 5.0, Propel: 1.2 } # Comment at the end of a line
"symfony 1.2": { PHP: 5.2, Propel: 1.3 }

注解

Comments are simply ignored by the YAML parser and do not need to be indented according to the current level of nesting in a collection.

Read the Components documentation.

Reference Documents

Get answers quickly with reference documents:

Reference Documents

FrameworkBundle Configuration (“framework”)

This reference document is a work in progress. It should be accurate, but all options are not yet fully covered.

The FrameworkBundle contains most of the “base” framework functionality and can be configured under the framework key in your application configuration. This includes settings related to sessions, translation, forms, validation, routing and more.

Configuration
secret

type: string required

This is a string that should be unique to your application. In practice, it’s used for generating the CSRF tokens, but it could be used in any other context where having a unique string is useful. It becomes the service container parameter named kernel.secret.

http_method_override

2.3 新版功能: The http_method_override option was introduced in Symfony 2.3.

type: Boolean default: true

This determines whether the _method request parameter is used as the intended HTTP method on POST requests. If enabled, the Request::enableHttpMethodParameterOverride method gets called automatically. It becomes the service container parameter named kernel.http_method_override. For more information, see How to Use HTTP Methods beyond GET and POST in Routes.

ide

type: string default: null

If you’re using an IDE like TextMate or Mac Vim, then Symfony can turn all of the file paths in an exception message into a link, which will open that file in your IDE.

Symfony contains preconfigured urls for some popular IDEs, you can set them using the following keys:

  • textmate
  • macvim
  • emacs
  • sublime

2.3.14 新版功能: The emacs and sublime editors were introduced in Symfony 2.3.14.

You can also specify a custom url string. If you do this, all percentage signs (%) must be doubled to escape that character. For example, if you have installed PhpStormOpener and use PHPstorm, you will do something like:

  • YAML
    # app/config/config.yml
    framework:
        ide: "pstorm://%%f:%%l"
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config ide="pstorm://%%f:%%l" />
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'ide' => 'pstorm://%%f:%%l',
    ));
    

Of course, since every developer uses a different IDE, it’s better to set this on a system level. This can be done by setting the xdebug.file_link_format in the php.ini configuration to the url string. If this configuration value is set, then the ide option will be ignored.

test

type: Boolean

If this configuration parameter is present (and not false), then the services related to testing your application (e.g. test.client) are loaded. This setting should be present in your test environment (usually via app/config/config_test.yml). For more information, see Testing.

default_locale

type: string default: en

The default locale is used if no _locale routing parameter has been set. It becomes the service container parameter named kernel.default_locale and it is also available with the Request::getDefaultLocale method.

trusted_proxies

type: array

Configures the IP addresses that should be trusted as proxies. For more details, see How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy.

2.3 新版功能: CIDR notation support was introduced in Symfony 2.3, so you can whitelist whole subnets (e.g. 10.0.0.0/8, fc00::/7).

  • YAML
    # app/config/config.yml
    framework:
        trusted_proxies:  [192.0.0.1, 10.0.0.0/8]
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config trusted-proxies="192.0.0.1, 10.0.0.0/8" />
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'trusted_proxies' => array('192.0.0.1', '10.0.0.0/8'),
    ));
    
form
csrf_protection
session
name

type: string default: null

This specifies the name of the session cookie. By default it will use the cookie name which is defined in the php.ini with the session.name directive.

gc_probability

type: integer default: 1

This defines the probability that the garbage collector (GC) process is started on every session initialization. The probability is calculated by using gc_probability / gc_divisor, e.g. 1/100 means there is a 1% chance that the GC process will start on each request.

gc_divisor

type: integer default: 100

See gc_probability.

gc_maxlifetime

type: integer default: 1440

This determines the number of seconds after which data will be seen as “garbage” and potentially cleaned up. Garbage collection may occur during session start and depends on gc_divisor and gc_probability.

save_path

type: string default: %kernel.cache.dir%/sessions

This determines the argument to be passed to the save handler. If you choose the default file handler, this is the path where the session files are created. For more information, see Configuring the Directory where Session Files are Saved.

You can also set this value to the save_path of your php.ini by setting the value to null:

  • YAML
    # app/config/config.yml
    framework:
        session:
            save_path: null
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:session save-path="null" />
        </framework:config>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'session' => array(
            'save_path' => null,
        ),
    ));
    
serializer
enabled

type: boolean default: false

Whether to enable the serializer service or not in the service container.

For more details, see How to Use the Serializer.

templating
assets_base_urls

default: { http: [], ssl: [] }

This option allows you to define base URLs to be used for assets referenced from http and ssl (https) pages. A string value may be provided in lieu of a single-element array. If multiple base URLs are provided, Symfony will select one from the collection each time it generates an asset’s path.

For your convenience, assets_base_urls can be set directly with a string or array of strings, which will be automatically organized into collections of base URLs for http and https requests. If a URL starts with https:// or is protocol-relative (i.e. starts with //) it will be added to both collections. URLs starting with http:// will only be added to the http collection.

assets_version

type: string

This option is used to bust the cache on assets by globally adding a query parameter to all rendered asset paths (e.g. /images/logo.png?v2). This applies only to assets rendered via the Twig asset function (or PHP equivalent) as well as assets rendered with Assetic.

For example, suppose you have the following:

  • Twig
    <img src="{{ asset('images/logo.png') }}" alt="Symfony!" />
    
  • PHP
    <img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" alt="Symfony!" />
    

By default, this will render a path to your image such as /images/logo.png. Now, activate the assets_version option:

  • YAML
    # app/config/config.yml
    framework:
        # ...
        templating: { engines: ['twig'], assets_version: v2 }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:templating assets-version="v2">
            <!-- ... -->
            <framework:engine>twig</framework:engine>
        </framework:templating>
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'templating'      => array(
            'engines'        => array('twig'),
            'assets_version' => 'v2',
        ),
    ));
    

Now, the same asset will be rendered as /images/logo.png?v2 If you use this feature, you must manually increment the assets_version value before each deployment so that the query parameters change.

You can also control how the query string works via the assets_version_format option.

assets_version_format

type: string default: %%s?%%s

This specifies a sprintf pattern that will be used with the assets_version option to construct an asset’s path. By default, the pattern adds the asset’s version as a query string. For example, if assets_version_format is set to %%s?version=%%s and assets_version is set to 5, the asset’s path would be /images/logo.png?version=5.

注解

All percentage signs (%) in the format string must be doubled to escape the character. Without escaping, values might inadvertently be interpreted as Service Parameters.

小技巧

Some CDN’s do not support cache-busting via query strings, so injecting the version into the actual file path is necessary. Thankfully, assets_version_format is not limited to producing versioned query strings.

The pattern receives the asset’s original path and version as its first and second parameters, respectively. Since the asset’s path is one parameter, you cannot modify it in-place (e.g. /images/logo-v5.png); however, you can prefix the asset’s path using a pattern of version-%%2$s/%%1$s, which would result in the path version-5/images/logo.png.

URL rewrite rules could then be used to disregard the version prefix before serving the asset. Alternatively, you could copy assets to the appropriate version path as part of your deployment process and forgot any URL rewriting. The latter option is useful if you would like older asset versions to remain accessible at their original URL.

profiler
enabled

2.2 新版功能: The enabled option was introduced in Symfony 2.2. Prior to Symfony 2.2, the profiler could only be disabled by omitting the framework.profiler configuration entirely.

type: boolean default: false

The profiler can be enabled by setting this key to true. When you are using the Symfony Standard Edition, the profiler is enabled in the dev and test environments.

collect

2.3 新版功能: The collect option was introduced in Symfony 2.3. Previously, when profiler.enabled was false, the profiler was actually enabled, but the collectors were disabled. Now, the profiler and the collectors can be controlled independently.

type: boolean default: true

This option configures the way the profiler behaves when it is enabled. If set to true, the profiler collects data for all requests. If you want to only collect information on-demand, you can set the collect flag to false and activate the data collectors by hand:

$profiler->enable();
translator
enabled

type: boolean default: false

Whether or not to enable the translator service in the service container.

fallback

type: string default: en

This option is used when the translation key for the current locale wasn’t found.

For more details, see Translations.

validation
cache

type: string

This value is used to determine the service that is used to persist class metadata in a cache. The actual service name is built by prefixing the configured value with validator.mapping.cache. (e.g. if the value is apc, the validator.mapping.cache.apc service will be injected). The service has to implement the CacheInterface.

enable_annotations

type: Boolean default: false

If this option is enabled, validation constraints can be defined using annotations.

translation_domain

type: string default: validators

The translation domain that is used when translating validation constraint error messages.

Full default Configuration
  • YAML
    framework:
        secret:               ~
        http_method_override: true
        trusted_proxies:      []
        ide:                  ~
        test:                 ~
        default_locale:       en
    
        # form configuration
        form:
            enabled:              false
        csrf_protection:
            enabled:              false
            field_name:           _token
    
        # esi configuration
        esi:
            enabled:              false
    
        # fragments configuration
        fragments:
            enabled:              false
            path:                 /_fragment
    
        # profiler configuration
        profiler:
            enabled:              false
            collect:              true
            only_exceptions:      false
            only_master_requests: false
            dsn:                  file:%kernel.cache_dir%/profiler
            username:
            password:
            lifetime:             86400
            matcher:
                ip:                   ~
    
                # use the urldecoded format
                path:                 ~ # Example: ^/path to resource/
                service:              ~
    
        # router configuration
        router:
            resource:             ~ # Required
            type:                 ~
            http_port:            80
            https_port:           443
    
            # set to true to throw an exception when a parameter does not match the requirements
            # set to false to disable exceptions when a parameter does not match the requirements (and return null instead)
            # set to null to disable parameter checks against requirements
            # 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production
            strict_requirements:  true
    
        # session configuration
        session:
            storage_id:           session.storage.native
            handler_id:           session.handler.native_file
            name:                 ~
            cookie_lifetime:      ~
            cookie_path:          ~
            cookie_domain:        ~
            cookie_secure:        ~
            cookie_httponly:      ~
            gc_divisor:           ~
            gc_probability:       ~
            gc_maxlifetime:       ~
            save_path:            "%kernel.cache_dir%/sessions"
    
        # serializer configuration
        serializer:
           enabled: false
    
        # templating configuration
        templating:
            assets_version:       ~
            assets_version_format:  "%%s?%%s"
            hinclude_default_template:  ~
            form:
                resources:
    
                    # Default:
                    - FrameworkBundle:Form
            assets_base_urls:
                http:                 []
                ssl:                  []
            cache:                ~
            engines:              # Required
    
                # Example:
                - twig
            loaders:              []
            packages:
    
                # Prototype
                name:
                    version:              ~
                    version_format:       "%%s?%%s"
                    base_urls:
                        http:                 []
                        ssl:                  []
    
        # translator configuration
        translator:
            enabled:              false
            fallback:             en
    
        # validation configuration
        validation:
            enabled:              false
            cache:                ~
            enable_annotations:   false
            translation_domain:   validators
    
        # annotation configuration
        annotations:
            cache:                file
            file_cache_dir:       "%kernel.cache_dir%/annotations"
            debug:                "%kernel.debug%"
    

DoctrineBundle Configuration (“doctrine”)

Full Default Configuration
  • YAML
    doctrine:
        dbal:
            default_connection:   default
            types:
                # A collection of custom types
                # Example
                some_custom_type:
                    class:                Acme\HelloBundle\MyCustomType
                    commented:            true
            # If enabled all tables not prefixed with sf2_ will be ignored by the schema
            # tool. This is for custom tables which should not be altered automatically.
            #schema_filter:        ^sf2_
    
            connections:
                # A collection of different named connections (e.g. default, conn2, etc)
                default:
                    dbname:               ~
                    host:                 localhost
                    port:                 ~
                    user:                 root
                    password:             ~
                    charset:              ~
                    path:                 ~
                    memory:               ~
    
                    # The unix socket to use for MySQL
                    unix_socket:          ~
    
                    # True to use as persistent connection for the ibm_db2 driver
                    persistent:           ~
    
                    # The protocol to use for the ibm_db2 driver (default to TCPIP if omitted)
                    protocol:             ~
    
                    # True to use dbname as service name instead of SID for Oracle
                    service:              ~
    
                    # The session mode to use for the oci8 driver
                    sessionMode:          ~
    
                    # True to use a pooled server with the oci8 driver
                    pooled:               ~
    
                    # Configuring MultipleActiveResultSets for the pdo_sqlsrv driver
                    MultipleActiveResultSets:  ~
                    driver:               pdo_mysql
                    platform_service:     ~
    
                    # when true, queries are logged to a "doctrine" monolog channel
                    logging:              "%kernel.debug%"
                    profiling:            "%kernel.debug%"
                    driver_class:         ~
                    wrapper_class:        ~
                    options:
                        # an array of options
                        key:                  []
                    mapping_types:
                        # an array of mapping types
                        name:                 []
                    slaves:
    
                        # a collection of named slave connections (e.g. slave1, slave2)
                        slave1:
                            dbname:               ~
                            host:                 localhost
                            port:                 ~
                            user:                 root
                            password:             ~
                            charset:              ~
                            path:                 ~
                            memory:               ~
    
                            # The unix socket to use for MySQL
                            unix_socket:          ~
    
                            # True to use as persistent connection for the ibm_db2 driver
                            persistent:           ~
    
                            # The protocol to use for the ibm_db2 driver (default to TCPIP if omitted)
                            protocol:             ~
    
                            # True to use dbname as service name instead of SID for Oracle
                            service:              ~
    
                            # The session mode to use for the oci8 driver
                            sessionMode:          ~
    
                            # True to use a pooled server with the oci8 driver
                            pooled:               ~
    
                            # Configuring MultipleActiveResultSets for the pdo_sqlsrv driver
                            MultipleActiveResultSets:  ~
    
        orm:
            default_entity_manager:  ~
            auto_generate_proxy_classes:  false
            proxy_dir:            "%kernel.cache_dir%/doctrine/orm/Proxies"
            proxy_namespace:      Proxies
            # search for the "ResolveTargetEntityListener" class for a cookbook about this
            resolve_target_entities: []
            entity_managers:
                # A collection of different named entity managers (e.g. some_em, another_em)
                some_em:
                    query_cache_driver:
                        type:                 array # Required
                        host:                 ~
                        port:                 ~
                        instance_class:       ~
                        class:                ~
                    metadata_cache_driver:
                        type:                 array # Required
                        host:                 ~
                        port:                 ~
                        instance_class:       ~
                        class:                ~
                    result_cache_driver:
                        type:                 array # Required
                        host:                 ~
                        port:                 ~
                        instance_class:       ~
                        class:                ~
                    connection:           ~
                    class_metadata_factory_name:  Doctrine\ORM\Mapping\ClassMetadataFactory
                    default_repository_class:  Doctrine\ORM\EntityRepository
                    auto_mapping:         false
                    hydrators:
    
                        # An array of hydrator names
                        hydrator_name:                 []
                    mappings:
                        # An array of mappings, which may be a bundle name or something else
                        mapping_name:
                            mapping:              true
                            type:                 ~
                            dir:                  ~
                            alias:                ~
                            prefix:               ~
                            is_bundle:            ~
                    dql:
                        # a collection of string functions
                        string_functions:
                            # example
                            # test_string: Acme\HelloBundle\DQL\StringFunction
    
                        # a collection of numeric functions
                        numeric_functions:
                            # example
                            # test_numeric: Acme\HelloBundle\DQL\NumericFunction
    
                        # a collection of datetime functions
                        datetime_functions:
                            # example
                            # test_datetime: Acme\HelloBundle\DQL\DatetimeFunction
    
                    # Register SQL Filters in the entity manager
                    filters:
                        # An array of filters
                        some_filter:
                            class:                ~ # Required
                            enabled:              false
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/doctrine
            http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
    
        <doctrine:config>
            <doctrine:dbal default-connection="default">
                <doctrine:connection
                    name="default"
                    dbname="database"
                    host="localhost"
                    port="1234"
                    user="user"
                    password="secret"
                    driver="pdo_mysql"
                    driver-class="MyNamespace\MyDriverImpl"
                    path="%kernel.data_dir%/data.sqlite"
                    memory="true"
                    unix-socket="/tmp/mysql.sock"
                    wrapper-class="MyDoctrineDbalConnectionWrapper"
                    charset="UTF8"
                    logging="%kernel.debug%"
                    platform-service="MyOwnDatabasePlatformService"
                >
                    <doctrine:option key="foo">bar</doctrine:option>
                    <doctrine:mapping-type name="enum">string</doctrine:mapping-type>
                </doctrine:connection>
                <doctrine:connection name="conn1" />
                <doctrine:type name="custom">Acme\HelloBundle\MyCustomType</doctrine:type>
            </doctrine:dbal>
    
            <doctrine:orm
                default-entity-manager="default"
                auto-generate-proxy-classes="false"
                proxy-namespace="Proxies"
                proxy-dir="%kernel.cache_dir%/doctrine/orm/Proxies"
            >
                <doctrine:entity-manager
                    name="default"
                    query-cache-driver="array"
                    result-cache-driver="array"
                    connection="conn1"
                    class-metadata-factory-name="Doctrine\ORM\Mapping\ClassMetadataFactory"
                >
                    <doctrine:metadata-cache-driver
                        type="memcache"
                        host="localhost"
                        port="11211"
                        instance-class="Memcache"
                        class="Doctrine\Common\Cache\MemcacheCache"
                    />
    
                    <doctrine:mapping name="AcmeHelloBundle" />
    
                    <doctrine:dql>
                        <doctrine:string-function name="test_string">
                            Acme\HelloBundle\DQL\StringFunction
                        </doctrine:string-function>
    
                        <doctrine:numeric-function name="test_numeric">
                            Acme\HelloBundle\DQL\NumericFunction
                        </doctrine:numeric-function>
    
                        <doctrine:datetime-function name="test_datetime">
                            Acme\HelloBundle\DQL\DatetimeFunction
                        </doctrine:datetime-function>
                    </doctrine:dql>
                </doctrine:entity-manager>
    
                <doctrine:entity-manager name="em2" connection="conn2" metadata-cache-driver="apc">
                    <doctrine:mapping
                        name="DoctrineExtensions"
                        type="xml"
                        dir="%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/DoctrineExtensions/Entity"
                        prefix="DoctrineExtensions\Entity"
                        alias="DExt"
                    />
                </doctrine:entity-manager>
            </doctrine:orm>
        </doctrine:config>
    </container>
    
Configuration Overview

This following configuration example shows all the configuration defaults that the ORM resolves to:

doctrine:
    orm:
        auto_mapping: true
        # the standard distribution overrides this to be true in debug, false otherwise
        auto_generate_proxy_classes: false
        proxy_namespace: Proxies
        proxy_dir: "%kernel.cache_dir%/doctrine/orm/Proxies"
        default_entity_manager: default
        metadata_cache_driver: array
        query_cache_driver: array
        result_cache_driver: array

There are lots of other configuration options that you can use to overwrite certain classes, but those are for very advanced use-cases only.

Caching Drivers

For the caching drivers you can specify the values “array”, “apc”, “memcache”, “memcached”, “xcache” or “service”.

The following example shows an overview of the caching configurations:

doctrine:
    orm:
        auto_mapping: true
        metadata_cache_driver: apc
        query_cache_driver:
            type: service
            id: my_doctrine_common_cache_service
        result_cache_driver:
            type: memcache
            host: localhost
            port: 11211
            instance_class: Memcache
Mapping Configuration

Explicit definition of all the mapped entities is the only necessary configuration for the ORM and there are several configuration options that you can control. The following configuration options exist for a mapping:

type

One of annotation, xml, yml, php or staticphp. This specifies which type of metadata type your mapping uses.

dir

Path to the mapping or entity files (depending on the driver). If this path is relative it is assumed to be relative to the bundle root. This only works if the name of your mapping is a bundle name. If you want to use this option to specify absolute paths you should prefix the path with the kernel parameters that exist in the DIC (for example %kernel.root_dir%).

prefix

A common namespace prefix that all entities of this mapping share. This prefix should never conflict with prefixes of other defined mappings otherwise some of your entities cannot be found by Doctrine. This option defaults to the bundle namespace + Entity, for example for an application bundle called AcmeHelloBundle prefix would be Acme\HelloBundle\Entity.

alias

Doctrine offers a way to alias entity namespaces to simpler, shorter names to be used in DQL queries or for Repository access. When using a bundle the alias defaults to the bundle name.

is_bundle

This option is a derived value from dir and by default is set to true if dir is relative proved by a file_exists() check that returns false. It is false if the existence check returns true. In this case an absolute path was specified and the metadata files are most likely in a directory outside of a bundle.

Doctrine DBAL Configuration

DoctrineBundle supports all parameters that default Doctrine drivers accept, converted to the XML or YAML naming standards that Symfony enforces. See the Doctrine DBAL documentation for more information. The following block shows all possible configuration keys:

  • YAML
    doctrine:
        dbal:
            dbname:               database
            host:                 localhost
            port:                 1234
            user:                 user
            password:             secret
            driver:               pdo_mysql
            # the DBAL driverClass option
            driver_class:         MyNamespace\MyDriverImpl
            # the DBAL driverOptions option
            options:
                foo: bar
            path:                 "%kernel.data_dir%/data.sqlite"
            memory:               true
            unix_socket:          /tmp/mysql.sock
            # the DBAL wrapperClass option
            wrapper_class:        MyDoctrineDbalConnectionWrapper
            charset:              UTF8
            logging:              "%kernel.debug%"
            platform_service:     MyOwnDatabasePlatformService
            mapping_types:
                enum: string
            types:
                custom: Acme\HelloBundle\MyCustomType
            # the DBAL keepSlave option
            keep_slave:           false
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/doctrine
            http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"
    >
    
        <doctrine:config>
            <doctrine:dbal
                name="default"
                dbname="database"
                host="localhost"
                port="1234"
                user="user"
                password="secret"
                driver="pdo_mysql"
                driver-class="MyNamespace\MyDriverImpl"
                path="%kernel.data_dir%/data.sqlite"
                memory="true"
                unix-socket="/tmp/mysql.sock"
                wrapper-class="MyDoctrineDbalConnectionWrapper"
                charset="UTF8"
                logging="%kernel.debug%"
                platform-service="MyOwnDatabasePlatformService">
    
                <doctrine:option key="foo">bar</doctrine:option>
                <doctrine:mapping-type name="enum">string</doctrine:mapping-type>
                <doctrine:type name="custom">Acme\HelloBundle\MyCustomType</doctrine:type>
            </doctrine:dbal>
        </doctrine:config>
    </container>
    

If you want to configure multiple connections in YAML, put them under the connections key and give them a unique name:

doctrine:
    dbal:
        default_connection:       default
        connections:
            default:
                dbname:           Symfony
                user:             root
                password:         null
                host:             localhost
            customer:
                dbname:           customer
                user:             root
                password:         null
                host:             localhost

The database_connection service always refers to the default connection, which is the first one defined or the one configured via the default_connection parameter.

Each connection is also accessible via the doctrine.dbal.[name]_connection service where [name] is the name of the connection.

Shortened Configuration Syntax

When you are only using one entity manager, all config options available can be placed directly under doctrine.orm config level.

doctrine:
    orm:
        # ...
        query_cache_driver:
           # ...
        metadata_cache_driver:
            # ...
        result_cache_driver:
            # ...
        connection: ~
        class_metadata_factory_name:  Doctrine\ORM\Mapping\ClassMetadataFactory
        default_repository_class:  Doctrine\ORM\EntityRepository
        auto_mapping: false
        hydrators:
            # ...
        mappings:
            # ...
        dql:
            # ...
        filters:
            # ...

This shortened version is commonly used in other documentation sections. Keep in mind that you can’t use both syntaxes at the same time.

SecurityBundle Configuration (“security”)

The security system is one of the most powerful parts of Symfony, and can largely be controlled via its configuration.

Full default Configuration

The following is the full default configuration for the security system. Each part will be explained in the next section.

  • YAML
    # app/config/security.yml
    security:
        access_denied_url:    ~ # Example: /foo/error403
    
        # strategy can be: none, migrate, invalidate
        session_fixation_strategy:  migrate
        hide_user_not_found:  true
        always_authenticate_before_granting:  false
        erase_credentials:    true
        access_decision_manager:
            strategy:             affirmative
            allow_if_all_abstain:  false
            allow_if_equal_granted_denied:  true
        acl:
    
            # any name configured in doctrine.dbal section
            connection:           ~
            cache:
                id:                   ~
                prefix:               sf2_acl_
            provider:             ~
            tables:
                class:                acl_classes
                entry:                acl_entries
                object_identity:      acl_object_identities
                object_identity_ancestors:  acl_object_identity_ancestors
                security_identity:    acl_security_identities
            voter:
                allow_if_object_identity_unavailable:  true
    
        encoders:
            # Examples:
            Acme\DemoBundle\Entity\User1: sha512
            Acme\DemoBundle\Entity\User2:
                algorithm:           sha512
                encode_as_base64:    true
                iterations:          5000
    
            # PBKDF2 encoder
            # see the note about PBKDF2 below for details on security and speed
            Acme\Your\Class\Name:
                algorithm:            pbkdf2
                hash_algorithm:       sha512
                encode_as_base64:     true
                iterations:           1000
                key_length:           40
    
            # Example options/values for what a custom encoder might look like
            Acme\DemoBundle\Entity\User3:
                id:                   my.encoder.id
    
            # BCrypt encoder
            # see the note about bcrypt below for details on specific dependencies
            Acme\DemoBundle\Entity\User4:
                algorithm:            bcrypt
                cost:                 13
    
            # Plaintext encoder
            # it does not do any encoding
            Acme\DemoBundle\Entity\User5:
                algorithm:            plaintext
                ignore_case:          false
    
        providers:            # Required
            # Examples:
            my_in_memory_provider:
                memory:
                    users:
                        foo:
                            password:           foo
                            roles:              ROLE_USER
                        bar:
                            password:           bar
                            roles:              [ROLE_USER, ROLE_ADMIN]
    
            my_entity_provider:
                entity:
                    class:              SecurityBundle:User
                    property:           username
                    manager_name:       ~
    
            # Example custom provider
            my_some_custom_provider:
                id:                   ~
    
            # Chain some providers
            my_chain_provider:
                chain:
                    providers:          [ my_in_memory_provider, my_entity_provider ]
    
        firewalls:            # Required
            # Examples:
            somename:
                pattern: .*
                request_matcher: some.service.id
                access_denied_url: /foo/error403
                access_denied_handler: some.service.id
                entry_point: some.service.id
                provider: some_key_from_above
                # manages where each firewall stores session information
                # See "Firewall Context" below for more details
                context: context_key
                stateless: false
                x509:
                    provider: some_key_from_above
                http_basic:
                    provider: some_key_from_above
                http_digest:
                    provider: some_key_from_above
                form_login:
                    # submit the login form here
                    check_path: /login_check
    
                    # the user is redirected here when they need to log in
                    login_path: /login
    
                    # if true, forward the user to the login form instead of redirecting
                    use_forward: false
    
                    # login success redirecting options (read further below)
                    always_use_default_target_path: false
                    default_target_path:            /
                    target_path_parameter:          _target_path
                    use_referer:                    false
    
                    # login failure redirecting options (read further below)
                    failure_path:    /foo
                    failure_forward: false
                    failure_path_parameter: _failure_path
                    failure_handler: some.service.id
                    success_handler: some.service.id
    
                    # field names for the username and password fields
                    username_parameter: _username
                    password_parameter: _password
    
                    # csrf token options
                    csrf_parameter: _csrf_token
                    intention:      authenticate
                    csrf_provider:  my.csrf_provider.id
    
                    # by default, the login form *must* be a POST, not a GET
                    post_only:      true
                    remember_me:    false
    
                    # by default, a session must exist before submitting an authentication request
                    # if false, then Request::hasPreviousSession is not called during authentication
                    # new in Symfony 2.3
                    require_previous_session: true
    
                remember_me:
                    token_provider: name
                    key: someS3cretKey
                    name: NameOfTheCookie
                    lifetime: 3600 # in seconds
                    path: /foo
                    domain: somedomain.foo
                    secure: false
                    httponly: true
                    always_remember_me: false
                    remember_me_parameter: _remember_me
                logout:
                    path:   /logout
                    target: /
                    invalidate_session: false
                    delete_cookies:
                        a: { path: null, domain: null }
                        b: { path: null, domain: null }
                    handlers: [some.service.id, another.service.id]
                    success_handler: some.service.id
                anonymous: ~
    
            # Default values and options for any firewall
            some_firewall_listener:
                pattern:              ~
                security:             true
                request_matcher:      ~
                access_denied_url:    ~
                access_denied_handler:  ~
                entry_point:          ~
                provider:             ~
                stateless:            false
                context:              ~
                logout:
                    csrf_parameter:       _csrf_token
                    csrf_provider:        ~
                    intention:            logout
                    path:                 /logout
                    target:               /
                    success_handler:      ~
                    invalidate_session:   true
                    delete_cookies:
    
                        # Prototype
                        name:
                            path:                 ~
                            domain:               ~
                    handlers:             []
                anonymous:
                    key:                  4f954a0667e01
                switch_user:
                    provider:             ~
                    parameter:            _switch_user
                    role:                 ROLE_ALLOWED_TO_SWITCH
    
        access_control:
            requires_channel:     ~
    
            # use the urldecoded format
            path:                 ~ # Example: ^/path to resource/
            host:                 ~
            ips:                  []
            methods:              []
            roles:                []
        role_hierarchy:
            ROLE_ADMIN:      [ROLE_ORGANIZER, ROLE_USER]
            ROLE_SUPERADMIN: [ROLE_ADMIN]
    
Form Login Configuration

When using the form_login authentication listener beneath a firewall, there are several common options for configuring the “form login” experience.

For even more details, see How to Customize your Form Login.

The Login Form and Process
login_path

type: string default: /login

This is the route or path that the user will be redirected to (unless use_forward is set to true) when they try to access a protected resource but isn’t fully authenticated.

This path must be accessible by a normal, un-authenticated user, else you may create a redirect loop. For details, see “Avoid Common Pitfalls”.

check_path

type: string default: /login_check

This is the route or path that your login form must submit to. The firewall will intercept any requests (POST requests only, by default) to this URL and process the submitted login credentials.

Be sure that this URL is covered by your main firewall (i.e. don’t create a separate firewall just for check_path URL).

use_forward

type: Boolean default: false

If you’d like the user to be forwarded to the login form instead of being redirected, set this option to true.

username_parameter

type: string default: _username

This is the field name that you should give to the username field of your login form. When you submit the form to check_path, the security system will look for a POST parameter with this name.

password_parameter

type: string default: _password

This is the field name that you should give to the password field of your login form. When you submit the form to check_path, the security system will look for a POST parameter with this name.

post_only

type: Boolean default: true

By default, you must submit your login form to the check_path URL as a POST request. By setting this option to false, you can send a GET request to the check_path URL.

Redirecting after Login
  • always_use_default_target_path (type: Boolean, default: false)
  • default_target_path (type: string, default: /)
  • target_path_parameter (type: string, default: _target_path)
  • use_referer (type: Boolean, default: false)
Using the PBKDF2 Encoder: Security and Speed

2.2 新版功能: The PBKDF2 password encoder was introduced in Symfony 2.2.

The PBKDF2 encoder provides a high level of Cryptographic security, as recommended by the National Institute of Standards and Technology (NIST).

You can see an example of the pbkdf2 encoder in the YAML block on this page.

But using PBKDF2 also warrants a warning: using it (with a high number of iterations) slows down the process. Thus, PBKDF2 should be used with caution and care.

A good configuration lies around at least 1000 iterations and sha512 for the hash algorithm.

Using the BCrypt Password Encoder

警告

To use this encoder, you either need to use PHP Version 5.5 or install the ircmaxell/password-compat library via Composer.

2.2 新版功能: The BCrypt password encoder was introduced in Symfony 2.2.

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Symfony\Component\Security\Core\User\User:
                algorithm: bcrypt
                cost:      15
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
        <encoder
            class="Symfony\Component\Security\Core\User\User"
            algorithm="bcrypt"
            cost="15"
        />
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => array(
                'algorithm' => 'bcrypt',
                'cost'      => 15,
            ),
        ),
    ));
    

The cost can be in the range of 4-31 and determines how long a password will be encoded. Each increment of cost doubles the time it takes to encode a password.

If you don’t provide the cost option, the default cost of 13 is used.

注解

You can change the cost at any time — even if you already have some passwords encoded using a different cost. New passwords will be encoded using the new cost, while the already encoded ones will be validated using a cost that was used back when they were encoded.

A salt for each new password is generated automatically and need not be persisted. Since an encoded password contains the salt used to encode it, persisting the encoded password alone is enough.

注解

All the encoded passwords are 60 characters long, so make sure to allocate enough space for them to be persisted.

Firewall Context

Most applications will only need one firewall. But if your application does use multiple firewalls, you’ll notice that if you’re authenticated in one firewall, you’re not automatically authenticated in another. In other words, the systems don’t share a common “context”: each firewall acts like a separate security system.

However, each firewall has an optional context key (which defaults to the name of the firewall), which is used when storing and retrieving security data to and from the session. If this key were set to the same value across multiple firewalls, the “context” could actually be shared:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            somename:
                # ...
                context: my_context
            othername:
                # ...
                context: my_context
    
  • XML
    <!-- app/config/security.xml -->
    <security:config>
        <firewall name="somename" context="my_context">
            <! ... ->
        </firewall>
        <firewall name="othername" context="my_context">
            <! ... ->
        </firewall>
    </security:config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'somename' => array(
                // ...
                'context' => 'my_context'
            ),
            'othername' => array(
                // ...
                'context' => 'my_context'
            ),
        ),
    ));
    
HTTP-Digest Authentication

To use HTTP-Digest authentication you need to provide a realm and a key:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            somename:
                http_digest:
                    key: "a_random_string"
                    realm: "secure-api"
    
  • XML
    <!-- app/config/security.xml -->
    <security:config>
        <firewall name="somename">
            <http-digest key="a_random_string" realm="secure-api" />
        </firewall>
    </security:config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'somename' => array(
                'http_digest' => array(
                    'key'   => 'a_random_string',
                    'realm' => 'secure-api',
                ),
            ),
        ),
    ));
    

AsseticBundle Configuration (“assetic”)

Full Default Configuration
  • YAML
    assetic:
        debug:                "%kernel.debug%"
        use_controller:
            enabled:              "%kernel.debug%"
            profiler:             false
        read_from:            "%kernel.root_dir%/../web"
        write_to:             "%assetic.read_from%"
        java:                 /usr/bin/java
        node:                 /usr/bin/node
        ruby:                 /usr/bin/ruby
        sass:                 /usr/bin/sass
        # An key-value pair of any number of named elements
        variables:
            some_name:                 []
        bundles:
    
            # Defaults (all currently registered bundles):
            - FrameworkBundle
            - SecurityBundle
            - TwigBundle
            - MonologBundle
            - SwiftmailerBundle
            - DoctrineBundle
            - AsseticBundle
            - ...
        assets:
            # An array of named assets (e.g. some_asset, some_other_asset)
            some_asset:
                inputs:               []
                filters:              []
                options:
                    # A key-value array of options and values
                    some_option_name: []
        filters:
    
            # An array of named filters (e.g. some_filter, some_other_filter)
            some_filter:                 []
        twig:
            functions:
                # An array of named functions (e.g. some_function, some_other_function)
                some_function:                 []
    
  • XML
    <assetic:config
        debug="%kernel.debug%"
        use-controller="%kernel.debug%"
        read-from="%kernel.root_dir%/../web"
        write-to="%assetic.read_from%"
        java="/usr/bin/java"
        node="/usr/bin/node"
        sass="/usr/bin/sass"
    >
        <!-- Defaults (all currently registered bundles) -->
        <assetic:bundle>FrameworkBundle</assetic:bundle>
        <assetic:bundle>SecurityBundle</assetic:bundle>
        <assetic:bundle>TwigBundle</assetic:bundle>
        <assetic:bundle>MonologBundle</assetic:bundle>
        <assetic:bundle>SwiftmailerBundle</assetic:bundle>
        <assetic:bundle>DoctrineBundle</assetic:bundle>
        <assetic:bundle>AsseticBundle</assetic:bundle>
        <assetic:bundle>...</assetic:bundle>
    
        <assetic:asset>
            <!-- prototype -->
            <assetic:name>
                <assetic:input />
    
                <assetic:filter />
    
                <assetic:option>
                    <!-- prototype -->
                    <assetic:name />
                </assetic:option>
            </assetic:name>
        </assetic:asset>
    
        <assetic:filter>
            <!-- prototype -->
            <assetic:name />
        </assetic:filter>
    
        <assetic:twig>
            <assetic:functions>
                <!-- prototype -->
                <assetic:name />
            </assetic:functions>
        </assetic:twig>
    
    </assetic:config>
    

SwiftmailerBundle Configuration (“swiftmailer”)

This reference document is a work in progress. It should be accurate, but all options are not yet fully covered. For a full list of the default configuration options, see Full Default Configuration

The swiftmailer key configures Symfony’s integration with Swift Mailer, which is responsible for creating and delivering email messages.

The following section lists all options that are available to configure a mailer. It is also possible to configure several mailers (see Using Multiple Mailers).

Configuration
transport

type: string default: smtp

The exact transport method to use to deliver emails. Valid values are:

username

type: string

The username when using smtp as the transport.

password

type: string

The password when using smtp as the transport.

host

type: string default: localhost

The host to connect to when using smtp as the transport.

port

type: string default: 25 or 465 (depending on encryption)

The port when using smtp as the transport. This defaults to 465 if encryption is ssl and 25 otherwise.

encryption

type: string

The encryption mode to use when using smtp as the transport. Valid values are tls, ssl, or null (indicating no encryption).

auth_mode

type: string

The authentication mode to use when using smtp as the transport. Valid values are plain, login, cram-md5, or null.

spool

For details on email spooling, see How to Spool Emails.

type

type: string default: file

The method used to store spooled messages. Valid values are memory and file. A custom spool should be possible by creating a service called swiftmailer.spool.myspool and setting this value to myspool.

path

type: string default: %kernel.cache_dir%/swiftmailer/spool

When using the file spool, this is the path where the spooled messages will be stored.

sender_address

type: string

If set, all messages will be delivered with this address as the “return path” address, which is where bounced messages should go. This is handled internally by Swift Mailer’s Swift_Plugins_ImpersonatePlugin class.

antiflood
threshold

type: integer default: 99

Used with Swift_Plugins_AntiFloodPlugin. This is the number of emails to send before restarting the transport.

sleep

type: integer default: 0

Used with Swift_Plugins_AntiFloodPlugin. This is the number of seconds to sleep for during a transport restart.

delivery_address

type: string

If set, all email messages will be sent to this address instead of being sent to their actual recipients. This is often useful when developing. For example, by setting this in the config_dev.yml file, you can guarantee that all emails sent during development go to a single account.

This uses Swift_Plugins_RedirectingPlugin. Original recipients are available on the X-Swift-To, X-Swift-Cc and X-Swift-Bcc headers.

disable_delivery

type: Boolean default: false

If true, the transport will automatically be set to null, and no emails will actually be delivered.

logging

type: Boolean default: %kernel.debug%

If true, Symfony’s data collector will be activated for Swift Mailer and the information will be available in the profiler.

Full default Configuration
  • YAML
    swiftmailer:
        transport:            smtp
        username:             ~
        password:             ~
        host:                 localhost
        port:                 false
        encryption:           ~
        auth_mode:            ~
        spool:
            type:                 file
            path:                 "%kernel.cache_dir%/swiftmailer/spool"
        sender_address:       ~
        antiflood:
            threshold:            99
            sleep:                0
        delivery_address:     ~
        disable_delivery:     ~
        logging:              "%kernel.debug%"
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd">
    
        <swiftmailer:config
            transport="smtp"
            username=""
            password=""
            host="localhost"
            port="false"
            encryption=""
            auth_mode=""
            sender_address=""
            delivery_address=""
            disable_delivery=""
            logging="%kernel.debug%"
            >
            <swiftmailer:spool
                path="%kernel.cache_dir%/swiftmailer/spool"
                type="file" />
    
            <swiftmailer:antiflood
                sleep="0"
                threshold="99" />
        </swiftmailer:config>
    </container>
    
Using multiple Mailers

You can configure multiple mailers by grouping them under the mailers key (the default mailer is identified by the default_mailer option):

  • YAML
    swiftmailer:
        default_mailer: second_mailer
        mailers:
            first_mailer:
                # ...
            second_mailer:
                # ...
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/swiftmailer
            http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"
    >
        <swiftmailer:config default-mailer="second_mailer">
            <swiftmailer:mailer name="first_mailer"/>
            <swiftmailer:mailer name="second_mailer"/>
        </swiftmailer:config>
    </container>
    
  • PHP
    $container->loadFromExtension('swiftmailer', array(
        'default_mailer' => 'second_mailer',
        'mailers' => array(
            'first_mailer' => array(
                // ...
            ),
            'second_mailer' => array(
                // ...
            ),
        ),
    ));
    

Each mailer is registered as a service:

// ...

// returns the first mailer
$container->get('swiftmailer.mailer.first_mailer');

// also returns the second mailer since it is the default mailer
$container->get('swiftmailer.mailer');

// returns the second mailer
$container->get('swiftmailer.mailer.second_mailer');

TwigBundle Configuration (“twig”)

  • YAML
    twig:
        exception_controller:  twig.controller.exception:showAction
        form:
            resources:
    
                # Default:
                - form_div_layout.html.twig
    
                # Example:
                - MyBundle::form.html.twig
        globals:
    
            # Examples:
            foo:                 "@bar"
            pi:                  3.14
    
            # Example options, but the easiest use is as seen above
            some_variable_name:
                # a service id that should be the value
                id:                   ~
                # set to service or leave blank
                type:                 ~
                value:                ~
        autoescape:                ~
    
        # The following were added in Symfony 2.3.
        # See http://twig.sensiolabs.org/doc/recipes.html#using-the-template-name-to-set-the-default-escaping-strategy
        autoescape_service:        ~ # Example: @my_service
        autoescape_service_method: ~ # use in combination with autoescape_service option
        base_template_class:       ~ # Example: Twig_Template
        cache:                     "%kernel.cache_dir%/twig"
        charset:                   "%kernel.charset%"
        debug:                     "%kernel.debug%"
        strict_variables:          ~
        auto_reload:               ~
        optimizations:             ~
    
  • XML
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:twig="http://symfony.com/schema/dic/twig"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/twig http://symfony.com/schema/dic/twig/twig-1.0.xsd">
    
        <twig:config auto-reload="%kernel.debug%" autoescape="true" base-template-class="Twig_Template" cache="%kernel.cache_dir%/twig" charset="%kernel.charset%" debug="%kernel.debug%" strict-variables="false">
            <twig:form>
                <twig:resource>MyBundle::form.html.twig</twig:resource>
            </twig:form>
            <twig:global key="foo" id="bar" type="service" />
            <twig:global key="pi">3.14</twig:global>
        </twig:config>
    </container>
    
  • PHP
    $container->loadFromExtension('twig', array(
        'form' => array(
            'resources' => array(
                'MyBundle::form.html.twig',
            )
         ),
         'globals' => array(
             'foo' => '@bar',
             'pi'  => 3.14,
         ),
         'auto_reload'         => '%kernel.debug%',
         'autoescape'          => true,
         'base_template_class' => 'Twig_Template',
         'cache'               => '%kernel.cache_dir%/twig',
         'charset'             => '%kernel.charset%',
         'debug'               => '%kernel.debug%',
         'strict_variables'    => false,
    ));
    
Configuration
exception_controller

type: string default: twig.controller.exception:showAction

This is the controller that is activated after an exception is thrown anywhere in your application. The default controller (ExceptionController) is what’s responsible for rendering specific templates under different error conditions (see How to Customize Error Pages). Modifying this option is advanced. If you need to customize an error page you should use the previous link. If you need to perform some behavior on an exception, you should add a listener to the kernel.exception event (see kernel.event_listener).

MonologBundle Configuration (“monolog”)

Full Default Configuration
  • YAML
    monolog:
        handlers:
    
            # Examples:
            syslog:
                type:                stream
                path:                /var/log/symfony.log
                level:               ERROR
                bubble:              false
                formatter:           my_formatter
            main:
                type:                fingers_crossed
                action_level:        WARNING
                buffer_size:         30
                handler:             custom
            custom:
                type:                service
                id:                  my_handler
    
            # Default options and values for some "my_custom_handler"
            # Note: many of these options are specific to the "type".
            # For example, the "service" type doesn't use any options
            # except id and channels
            my_custom_handler:
                type:                 ~ # Required
                id:                   ~
                priority:             0
                level:                DEBUG
                bubble:               true
                path:                 "%kernel.logs_dir%/%kernel.environment%.log"
                ident:                false
                facility:             user
                max_files:            0
                action_level:         WARNING
                activation_strategy:  ~
                stop_buffering:       true
                buffer_size:          0
                handler:              ~
                members:              []
                channels:
                    type:     ~
                    elements: ~
                from_email:           ~
                to_email:             ~
                subject:              ~
                mailer:               ~
                email_prototype:
                    id:                   ~ # Required (when the email_prototype is used)
                    method:               ~
                formatter:            ~
    
  • XML
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:monolog="http://symfony.com/schema/dic/monolog"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/monolog
            http://symfony.com/schema/dic/monolog/monolog-1.0.xsd"
    >
    
        <monolog:config>
            <monolog:handler
                name="syslog"
                type="stream"
                path="/var/log/symfony.log"
                level="error"
                bubble="false"
                formatter="my_formatter"
            />
            <monolog:handler
                name="main"
                type="fingers_crossed"
                action-level="warning"
                handler="custom"
            />
            <monolog:handler
                name="custom"
                type="service"
                id="my_handler"
            />
        </monolog:config>
    </container>
    

注解

When the profiler is enabled, a handler is added to store the logs’ messages in the profiler. The profiler uses the name “debug” so it is reserved and cannot be used in the configuration.

WebProfilerBundle Configuration (“web_profiler”)

Full default Configuration
  • YAML
    web_profiler:
    
        # DEPRECATED, it is not useful anymore and can be removed safely from your configuration
        verbose:              true
    
        # display the web debug toolbar at the bottom of pages with a summary of profiler info
        toolbar:              false
        position:             bottom
    
        # gives you the opportunity to look at the collected data before following the redirect
        intercept_redirects: false
    
  • XML
    <web-profiler:config
        toolbar="false"
        verbose="true"
        intercept_redirects="false"
    />
    

Configuring in the Kernel (e.g. AppKernel)

Some configuration can be done on the kernel class itself (usually called app/AppKernel.php). You can do this by overriding specific methods in the parent Kernel class.

Configuration
Charset

type: string default: UTF-8

This returns the charset that is used in the application. To change it, override the getCharset() method and return another charset, for instance:

// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function getCharset()
    {
        return 'ISO-8859-1';
    }
}
Kernel Name

type: string default: app (i.e. the directory name holding the kernel class)

To change this setting, override the getName() method. Alternatively, move your kernel into a different directory. For example, if you moved the kernel into a foo directory (instead of app), the kernel name will be foo.

The name of the kernel isn’t usually directly important - it’s used in the generation of cache files. If you have an application with multiple kernels, the easiest way to make each have a unique name is to duplicate the app directory and rename it to something else (e.g. foo).

Root Directory

type: string default: the directory of AppKernel

This returns the root directory of your kernel. If you use the Symfony Standard edition, the root directory refers to the app directory.

To change this setting, override the getRootDir() method:

// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    // ...

    public function getRootDir()
    {
        return realpath(parent::getRootDir().'/../');
    }
}
Cache Directory

type: string default: $this->rootDir/cache/$this->environment

This returns the path to the cache directory. To change it, override the getCacheDir() method. Read “Override the cache Directory” for more information.

Log Directory

type: string default: $this->rootDir/logs

This returns the path to the log directory. To change it, override the getLogDir() method. Read “Override the logs Directory” for more information.

Form Types Reference

text Field Type

The text field represents the most basic input text field.

Rendered as input text field
Inherited options
Parent type form
Class TextType
Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

max_length

type: integer default: null

If this option is not null, an attribute maxlength is added, which is used by some browsers to limit the amount of text in a field.

This is just a browser validation, so data must still be validated server-side.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

trim

type: Boolean default: true

If true, the whitespace of the submitted string value will be stripped via the trim() function when the data is bound. This guarantees that if a value is submitted with extra whitespace, it will be removed before the value is merged back onto the underlying object.

textarea Field Type

Renders a textarea HTML element.

Rendered as textarea tag
Inherited options
Parent type text
Class TextareaType
Inherited Options

These options inherit from the form type:

attr

type: array default: Empty array

If you want to add extra attributes to an HTML field representation you can use the attr option. It’s an associative array with HTML attributes as keys. This can be useful when you need to set a custom class for some widget:

$builder->add('body', 'textarea', array(
    'attr' => array('class' => 'tinymce'),
));
data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

max_length

type: integer default: null

If this option is not null, an attribute maxlength is added, which is used by some browsers to limit the amount of text in a field.

This is just a browser validation, so data must still be validated server-side.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

trim

type: Boolean default: true

If true, the whitespace of the submitted string value will be stripped via the trim() function when the data is bound. This guarantees that if a value is submitted with extra whitespace, it will be removed before the value is merged back onto the underlying object.

email Field Type

The email field is a text field that is rendered using the HTML5 <input type="email" /> tag.

Rendered as input email field (a text box)
Inherited options
Parent type text
Class EmailType
Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

max_length

type: integer default: null

If this option is not null, an attribute maxlength is added, which is used by some browsers to limit the amount of text in a field.

This is just a browser validation, so data must still be validated server-side.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

trim

type: Boolean default: true

If true, the whitespace of the submitted string value will be stripped via the trim() function when the data is bound. This guarantees that if a value is submitted with extra whitespace, it will be removed before the value is merged back onto the underlying object.

integer Field Type

Renders an input “number” field. Basically, this is a text field that’s good at handling data that’s in an integer form. The input number field looks like a text box, except that - if the user’s browser supports HTML5 - it will have some extra frontend functionality.

This field has different options on how to handle input values that aren’t integers. By default, all non-integer values (e.g. 6.78) will round down (e.g. 6).

Rendered as input number field
Options
Inherited options
Parent type form
Class IntegerType
Field Options
grouping

type: integer default: false

This value is used internally as the NumberFormatter::GROUPING_USED value when using PHP’s NumberFormatter class. Its documentation is non-existent, but it appears that if you set this to true, numbers will be grouped with a comma or period (depending on your locale): 12345.123 would display as 12,345.123.

precision

type: integer default: Locale-specific (usually around 3)

This specifies how many decimals will be allowed until the field rounds the submitted value (via rounding_mode). For example, if precision is set to 2, a submitted value of 20.123 will be rounded to, for example, 20.12 (depending on your rounding_mode).

rounding_mode

type: integer default: IntegerToLocalizedStringTransformer::ROUND_DOWN

By default, if the user enters a non-integer number, it will be rounded down. There are several other rounding methods, and each is a constant on the IntegerToLocalizedStringTransformer:

  • IntegerToLocalizedStringTransformer::ROUND_DOWN Rounding mode to round towards zero.
  • IntegerToLocalizedStringTransformer::ROUND_FLOOR Rounding mode to round towards negative infinity.
  • IntegerToLocalizedStringTransformer::ROUND_UP Rounding mode to round away from zero.
  • IntegerToLocalizedStringTransformer::ROUND_CEILING Rounding mode to round towards positive infinity.
Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

money Field Type

Renders an input text field and specializes in handling submitted “money” data.

This field type allows you to specify a currency, whose symbol is rendered next to the text field. There are also several other options for customizing how the input and output of the data is handled.

Rendered as input text field
Options
Inherited options
Parent type form
Class MoneyType
Field Options
currency

type: string default: EUR

Specifies the currency that the money is being specified in. This determines the currency symbol that should be shown by the text box. Depending on the currency - the currency symbol may be shown before or after the input text field.

This can be any 3 letter ISO 4217 code. You can also set this to false to hide the currency symbol.

divisor

type: integer default: 1

If, for some reason, you need to divide your starting value by a number before rendering it to the user, you can use the divisor option. For example:

$builder->add('price', 'money', array(
    'divisor' => 100,
));

In this case, if the price field is set to 9900, then the value 99 will actually be rendered to the user. When the user submits the value 99, it will be multiplied by 100 and 9900 will ultimately be set back on your object.

grouping

type: integer default: false

This value is used internally as the NumberFormatter::GROUPING_USED value when using PHP’s NumberFormatter class. Its documentation is non-existent, but it appears that if you set this to true, numbers will be grouped with a comma or period (depending on your locale): 12345.123 would display as 12,345.123.

precision

type: integer default: 2

For some reason, if you need some precision other than 2 decimal places, you can modify this value. You probably won’t need to do this unless, for example, you want to round to the nearest dollar (set the precision to 0).

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

Form Variables
Variable Type Usage
money_pattern string The format to use to display the money, including the currency.
number Field Type

Renders an input text field and specializes in handling number input. This type offers different options for the precision, rounding, and grouping that you want to use for your number.

Rendered as input text field
Options
Inherited options
Parent type form
Class NumberType
Field Options
grouping

type: integer default: false

This value is used internally as the NumberFormatter::GROUPING_USED value when using PHP’s NumberFormatter class. Its documentation is non-existent, but it appears that if you set this to true, numbers will be grouped with a comma or period (depending on your locale): 12345.123 would display as 12,345.123.

precision

type: integer default: Locale-specific (usually around 3)

This specifies how many decimals will be allowed until the field rounds the submitted value (via rounding_mode). For example, if precision is set to 2, a submitted value of 20.123 will be rounded to, for example, 20.12 (depending on your rounding_mode).

rounding_mode

type: integer default: IntegerToLocalizedStringTransformer::ROUND_HALFUP

If a submitted number needs to be rounded (based on the precision option), you have several configurable options for that rounding. Each option is a constant on the IntegerToLocalizedStringTransformer:

  • IntegerToLocalizedStringTransformer::ROUND_DOWN Rounding mode to round towards zero.
  • IntegerToLocalizedStringTransformer::ROUND_FLOOR Rounding mode to round towards negative infinity.
  • IntegerToLocalizedStringTransformer::ROUND_UP Rounding mode to round away from zero.
  • IntegerToLocalizedStringTransformer::ROUND_CEILING Rounding mode to round towards positive infinity.
  • IntegerToLocalizedStringTransformer::ROUND_HALFDOWN Rounding mode to round towards “nearest neighbor” unless both neighbors are equidistant, in which case round down.
  • IntegerToLocalizedStringTransformer::ROUND_HALFEVEN Rounding mode to round towards the “nearest neighbor” unless both neighbors are equidistant, in which case, round towards the even neighbor.
  • IntegerToLocalizedStringTransformer::ROUND_HALFUP Rounding mode to round towards “nearest neighbor” unless both neighbors are equidistant, in which case round up.
Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

password Field Type

The password field renders an input password text box.

Rendered as input password field
Options
Inherited options
Parent type text
Class PasswordType
Field Options
always_empty

type: Boolean default: true

If set to true, the field will always render blank, even if the corresponding field has a value. When set to false, the password field will be rendered with the value attribute set to its true value only upon submission.

Put simply, if for some reason you want to render your password field with the password value already entered into the box, set this to false and submit the form.

Inherited Options

These options inherit from the form type:

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

max_length

type: integer default: null

If this option is not null, an attribute maxlength is added, which is used by some browsers to limit the amount of text in a field.

This is just a browser validation, so data must still be validated server-side.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

trim

type: Boolean default: true

If true, the whitespace of the submitted string value will be stripped via the trim() function when the data is bound. This guarantees that if a value is submitted with extra whitespace, it will be removed before the value is merged back onto the underlying object.

percent Field Type

The percent type renders an input text field and specializes in handling percentage data. If your percentage data is stored as a decimal (e.g. .95), you can use this field out-of-the-box. If you store your data as a number (e.g. 95), you should set the type option to integer.

This field adds a percentage sign “%” after the input box.

Rendered as input text field
Options
Inherited options
Parent type form
Class PercentType
Field Options
precision

type: integer default: 0

By default, the input numbers are rounded. To allow for more decimal places, use this option.

type

type: string default: fractional

This controls how your data is stored on your object. For example, a percentage corresponding to “55%”, might be stored as .55 or 55 on your object. The two “types” handle these two cases:

  • fractional If your data is stored as a decimal (e.g. .55), use this type. The data will be multiplied by 100 before being shown to the user (e.g. 55). The submitted data will be divided by 100 on form submit so that the decimal value is stored (.55);
  • integer If your data is stored as an integer (e.g. 55), then use this option. The raw value (55) is shown to the user and stored on your object. Note that this only works for integer values.
Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

search Field Type

This renders an <input type="search" /> field, which is a text box with special functionality supported by some browsers.

Read about the input search field at DiveIntoHTML5.info

Rendered as input search field
Inherited options
Parent type text
Class SearchType
Inherited Options

These options inherit from the form type:

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

max_length

type: integer default: null

If this option is not null, an attribute maxlength is added, which is used by some browsers to limit the amount of text in a field.

This is just a browser validation, so data must still be validated server-side.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

trim

type: Boolean default: true

If true, the whitespace of the submitted string value will be stripped via the trim() function when the data is bound. This guarantees that if a value is submitted with extra whitespace, it will be removed before the value is merged back onto the underlying object.

url Field Type

The url field is a text field that prepends the submitted value with a given protocol (e.g. http://) if the submitted value doesn’t already have a protocol.

Rendered as input url field
Options
Inherited options
Parent type text
Class UrlType
Field Options
default_protocol

type: string default: http

If a value is submitted that doesn’t begin with some protocol (e.g. http://, ftp://, etc), this protocol will be prepended to the string when the data is submitted to the form.

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is '' (the empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

max_length

type: integer default: null

If this option is not null, an attribute maxlength is added, which is used by some browsers to limit the amount of text in a field.

This is just a browser validation, so data must still be validated server-side.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

trim

type: Boolean default: true

If true, the whitespace of the submitted string value will be stripped via the trim() function when the data is bound. This guarantees that if a value is submitted with extra whitespace, it will be removed before the value is merged back onto the underlying object.

choice Field Type

A multi-purpose field used to allow the user to “choose” one or more options. It can be rendered as a select tag, radio buttons, or checkboxes.

To use this field, you must specify either the choice_list or choices option.

Rendered as can be various tags (see below)
Options
Overridden options
Inherited options
Parent type form
Class ChoiceType
Example Usage

The easiest way to use this field is to specify the choices directly via the choices option. The key of the array becomes the value that’s actually set on your underlying object (e.g. m), while the value is what the user sees on the form (e.g. Male).

$builder->add('gender', 'choice', array(
    'choices'   => array('m' => 'Male', 'f' => 'Female'),
    'required'  => false,
));

By setting multiple to true, you can allow the user to choose multiple values. The widget will be rendered as a multiple select tag or a series of checkboxes depending on the expanded option:

$builder->add('availability', 'choice', array(
    'choices'   => array(
        'morning'   => 'Morning',
        'afternoon' => 'Afternoon',
        'evening'   => 'Evening',
    ),
    'multiple'  => true,
));

You can also use the choice_list option, which takes an object that can specify the choices for your widget.

Select Tag, Checkboxes or Radio Buttons

This field may be rendered as one of several different HTML fields, depending on the expanded and multiple options:

Element Type Expanded Multiple
select tag false false
select tag (with multiple attribute) false true
radio buttons true false
checkboxes true true
Field Options
choices

type: array default: array()

This is the most basic way to specify the choices that should be used by this field. The choices option is an array, where the array key is the item value and the array value is the item’s label:

$builder->add('gender', 'choice', array(
    'choices' => array('m' => 'Male', 'f' => 'Female')
));

小技巧

When the values to choose from are not integers or strings (but e.g. floats or booleans), you should use the choice_list option instead. With this option you are able to keep the original data format which is important to ensure that the user input is validated properly and useless database updates caused by a data type mismatch are avoided.

choice_list

type: ChoiceListInterface

This is one way of specifying the options to be used for this field. The choice_list option must be an instance of the ChoiceListInterface. For more advanced cases, a custom class that implements the interface can be created to supply the choices.

With this option you can also allow float values to be selected as data.

use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;

// ...
$builder->add('status', 'choice', array(
  'choice_list' => new ChoiceList(array(1, 0.5), array('Full', 'Half'))
));
empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
expanded

type: Boolean default: false

If set to true, radio buttons or checkboxes will be rendered (depending on the multiple value). If false, a select element will be rendered.

multiple

type: Boolean default: false

If true, the user will be able to select multiple options (as opposed to choosing just one option). Depending on the value of the expanded option, this will render either a select tag or checkboxes if true and a select tag or radio buttons if false. The returned value will be an array.

preferred_choices

type: array default: array()

If this option is specified, then a sub-set of all of the options will be moved to the top of the select menu. The following would move the “Baz” option to the top, with a visual separator between it and the rest of the options:

$builder->add('foo_choices', 'choice', array(
    'choices' => array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'),
    'preferred_choices' => array('baz'),
));

Note that preferred choices are only meaningful when rendering as a select element (i.e. expanded is false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. -------------------). This can be customized when rendering the field:

  • Twig
    {{ form_widget(form.foo_choices, { 'separator': '=====' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['foo_choices'], array('separator' => '=====')) ?>
    
Overridden Options
compound

type: boolean default: same value as expanded option

This option specifies if a form is compound. The value is by default overridden by the value of the expanded option.

empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If multiple is false and expanded is false, then '' (empty string);
  • Otherwise array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: boolean default: false

Set that error on this field must be attached to the field instead of the parent field (the form in most cases).

Inherited Options

These options inherit from the form type:

by_reference

type: Boolean default: true

In most cases, if you have a name field, then you expect setName() to be called on the underlying object. In some cases, however, setName() may not be called. Setting by_reference ensures that the setter is called in all cases.

To explain this further, here’s a simple example:

$builder = $this->createFormBuilder($article);
$builder
    ->add('title', 'text')
    ->add(
        $builder->create('author', 'form', array('by_reference' => ?))
            ->add('name', 'text')
            ->add('email', 'email')
    )

If by_reference is true, the following takes place behind the scenes when you call submit() (or handleRequest()) on the form:

$article->setTitle('...');
$article->getAuthor()->setName('...');
$article->getAuthor()->setEmail('...');

Notice that setAuthor() is not called. The author is modified by reference.

If you set by_reference to false, submitting looks like this:

$article->setTitle('...');
$author = $article->getAuthor();
$author->setName('...');
$author->setEmail('...');
$article->setAuthor($author);

So, all that by_reference=false really does is force the framework to call the setter on the parent object.

Similarly, if you’re using the collection form type where your underlying collection data is an object (like with Doctrine’s ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
inherit_data

2.3 新版功能: The inherit_data option was introduced in Symfony 2.3. Before, it was known as virtual.

type: boolean default: false

This option determines if the form will inherit data from its parent form. This can be useful if you have a set of fields that are duplicated across multiple forms. See How to Reduce Code Duplication with “inherit_data”.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

Field Variables
Variable Type Usage
multiple Boolean The value of the multiple option.
expanded Boolean The value of the expanded option.
preferred_choices array A nested array containing the ChoiceView objects of choices which should be presented to the user with priority.
choices array A nested array containing the ChoiceView objects of the remaining choices.
separator string The separator to use between choice groups.
empty_value mixed The empty value if not already in the list, otherwise null.
is_selected callable A callable which takes a ChoiceView and the selected value(s) and returns whether the choice is in the selected value(s).
empty_value_in_choices Boolean Whether the empty value is in the choice list.

小技巧

It’s significantly faster to use the selectedchoice(selected_value) test instead when using Twig.

entity Field Type

A special choice field that’s designed to load options from a Doctrine entity. For example, if you have a Category entity, you could use this field to display a select field of all, or some, of the Category objects from the database.

Rendered as can be various tags (see Select Tag, Checkboxes or Radio Buttons)
Options
Overridden Options
Inherited options

from the choice type:

from the form type:

Parent type choice
Class EntityType
Basic Usage

The entity type has just one required option: the entity which should be listed inside the choice field:

$builder->add('users', 'entity', array(
    'class' => 'AcmeHelloBundle:User',
    'property' => 'username',
));

In this case, all User objects will be loaded from the database and rendered as either a select tag, a set or radio buttons or a series of checkboxes (this depends on the multiple and expanded values). If the entity object does not have a __toString() method the property option is needed.

Using a Custom Query for the Entities

If you need to specify a custom query to use when fetching the entities (e.g. you only want to return some entities, or need to order them), use the query_builder option. The easiest way to use the option is as follows:

use Doctrine\ORM\EntityRepository;
// ...

$builder->add('users', 'entity', array(
    'class' => 'AcmeHelloBundle:User',
    'query_builder' => function(EntityRepository $er) {
        return $er->createQueryBuilder('u')
            ->orderBy('u.username', 'ASC');
    },
));
Using Choices

If you already have the exact collection of entities that you want included in the choice element, you can simply pass them via the choices key. For example, if you have a $group variable (passed into your form perhaps as a form option) and getUsers returns a collection of User entities, then you can supply the choices option directly:

$builder->add('users', 'entity', array(
    'class' => 'AcmeHelloBundle:User',
    'choices' => $group->getUsers(),
));
Select Tag, Checkboxes or Radio Buttons

This field may be rendered as one of several different HTML fields, depending on the expanded and multiple options:

Element Type Expanded Multiple
select tag false false
select tag (with multiple attribute) false true
radio buttons true false
checkboxes true true
Field Options
class

type: string required

The class of your entity (e.g. AcmeStoreBundle:Category). This can be a fully-qualified class name (e.g. Acme\StoreBundle\Entity\Category) or the short alias name (as shown prior).

data_class

type: string

This option is used to set the appropriate data mapper to be used by the form, so you can use it for any form field type which requires an object.

$builder->add('media', 'sonata_media_type', array(
    'data_class' => 'Acme\DemoBundle\Entity\Media',
));
em

type: string default: the default entity manager

If specified, the specified entity manager will be used to load the choices instead of the default entity manager.

group_by

type: string

This is a property path (e.g. author.name) used to organize the available choices in groups. It only works when rendered as a select tag and does so by adding optgroup elements around options. Choices that do not return a value for this property path are rendered directly under the select tag, without a surrounding optgroup.

property

type: string

This is the property that should be used for displaying the entities as text in the HTML element. If left blank, the entity object will be cast into a string and so must have a __toString() method.

注解

The property option is the property path used to display the option. So you can use anything supported by the PropertyAccessor component

For example, if the translations property is actually an associative array of objects, each with a name property, then you could do this:

$builder->add('gender', 'entity', array(
   'class' => 'MyBundle:Gender',
   'property' => 'translations[en].name',
));
query_builder

type: Doctrine\ORM\QueryBuilder or a Closure

If specified, this is used to query the subset of options (and their order) that should be used for the field. The value of this option can either be a QueryBuilder object or a Closure. If using a Closure, it should take a single argument, which is the EntityRepository of the entity.

Overridden Options
choice_list

default: EntityChoiceList

The purpose of the entity type is to create and configure this EntityChoiceList for you, by using all of the above options. If you need to override this option, you may just consider using the choice Field Type directly.

choices

type: array | \Traversable default: null

Instead of allowing the class and query_builder options to fetch the entities to include for you, you can pass the choices option directly. See Using Choices.

Inherited Options

These options inherit from the choice type:

empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
expanded

type: Boolean default: false

If set to true, radio buttons or checkboxes will be rendered (depending on the multiple value). If false, a select element will be rendered.

multiple

type: Boolean default: false

If true, the user will be able to select multiple options (as opposed to choosing just one option). Depending on the value of the expanded option, this will render either a select tag or checkboxes if true and a select tag or radio buttons if false. The returned value will be an array.

注解

If you are working with a collection of Doctrine entities, it will be helpful to read the documentation for the collection Field Type as well. In addition, there is a complete example in the cookbook article How to Embed a Collection of Forms.

preferred_choices

type: array default: array()

If this option is specified, then a sub-set of all of the options will be moved to the top of the select menu. The following would move the “Baz” option to the top, with a visual separator between it and the rest of the options:

$builder->add('foo_choices', 'choice', array(
    'choices' => array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'),
    'preferred_choices' => array('baz'),
));

Note that preferred choices are only meaningful when rendering as a select element (i.e. expanded is false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. -------------------). This can be customized when rendering the field:

  • Twig
    {{ form_widget(form.foo_choices, { 'separator': '=====' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['foo_choices'], array('separator' => '=====')) ?>
    

注解

This option expects an array of entity objects, unlike the choice field that requires an array of keys.

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If multiple is false and expanded is false, then '' (empty string);
  • Otherwise array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

country Field Type

The country type is a subset of the ChoiceType that displays countries of the world. As an added bonus, the country names are displayed in the language of the user.

The “value” for each country is the two-letter country code.

注解

The locale of your user is guessed using Locale::getDefault()

Unlike the choice type, you don’t need to specify a choices or choice_list option as the field type automatically uses all of the countries of the world. You can specify either of these options manually, but then you should just use the choice type directly.

Rendered as can be various tags (see Select Tag, Checkboxes or Radio Buttons)
Overridden Options
Inherited options

from the choice type

from the form type

Parent type choice
Class CountryType
Overridden Options
choices

default: Symfony\Component\Intl\Intl::getRegionBundle()->getCountryNames()

The country type defaults the choices option to the whole list of countries. The locale is used to translate the countries names.

Inherited Options

These options inherit from the choice type:

empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
expanded

type: Boolean default: false

If set to true, radio buttons or checkboxes will be rendered (depending on the multiple value). If false, a select element will be rendered.

multiple

type: Boolean default: false

If true, the user will be able to select multiple options (as opposed to choosing just one option). Depending on the value of the expanded option, this will render either a select tag or checkboxes if true and a select tag or radio buttons if false. The returned value will be an array.

preferred_choices

type: array default: array()

If this option is specified, then a sub-set of all of the options will be moved to the top of the select menu. The following would move the “Baz” option to the top, with a visual separator between it and the rest of the options:

$builder->add('foo_choices', 'choice', array(
    'choices' => array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'),
    'preferred_choices' => array('baz'),
));

Note that preferred choices are only meaningful when rendering as a select element (i.e. expanded is false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. -------------------). This can be customized when rendering the field:

  • Twig
    {{ form_widget(form.foo_choices, { 'separator': '=====' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['foo_choices'], array('separator' => '=====')) ?>
    

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If multiple is false and expanded is false, then '' (empty string);
  • Otherwise array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

language Field Type

The language type is a subset of the ChoiceType that allows the user to select from a large list of languages. As an added bonus, the language names are displayed in the language of the user.

The “value” for each language is the Unicode language identifier used in the International Components for Unicode (e.g. fr or zh_Hant).

注解

The locale of your user is guessed using Locale::getDefault()

Unlike the choice type, you don’t need to specify a choices or choice_list option as the field type automatically uses a large list of languages. You can specify either of these options manually, but then you should just use the choice type directly.

Rendered as can be various tags (see Select Tag, Checkboxes or Radio Buttons)
Overridden Options
Inherited options

from the choice type

from the form type

Parent type choice
Class LanguageType
Overridden Options
choices

default: Symfony\Component\Intl\Intl::getLanguageBundle()->getLanguageNames().

The choices option defaults to all languages. The default locale is used to translate the languages names.

Inherited Options

These options inherit from the choice type:

empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
expanded

type: Boolean default: false

If set to true, radio buttons or checkboxes will be rendered (depending on the multiple value). If false, a select element will be rendered.

multiple

type: Boolean default: false

If true, the user will be able to select multiple options (as opposed to choosing just one option). Depending on the value of the expanded option, this will render either a select tag or checkboxes if true and a select tag or radio buttons if false. The returned value will be an array.

preferred_choices

type: array default: array()

If this option is specified, then a sub-set of all of the options will be moved to the top of the select menu. The following would move the “Baz” option to the top, with a visual separator between it and the rest of the options:

$builder->add('foo_choices', 'choice', array(
    'choices' => array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'),
    'preferred_choices' => array('baz'),
));

Note that preferred choices are only meaningful when rendering as a select element (i.e. expanded is false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. -------------------). This can be customized when rendering the field:

  • Twig
    {{ form_widget(form.foo_choices, { 'separator': '=====' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['foo_choices'], array('separator' => '=====')) ?>
    

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If multiple is false and expanded is false, then '' (empty string);
  • Otherwise array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

locale Field Type

The locale type is a subset of the ChoiceType that allows the user to select from a large list of locales (language+country). As an added bonus, the locale names are displayed in the language of the user.

The “value” for each locale is either the two letter ISO 639-1 language code (e.g. fr), or the language code followed by an underscore (_), then the ISO 3166-1 alpha-2 country code (e.g. fr_FR for French/France).

注解

The locale of your user is guessed using Locale::getDefault()

Unlike the choice type, you don’t need to specify a choices or choice_list option as the field type automatically uses a large list of locales. You can specify either of these options manually, but then you should just use the choice type directly.

Rendered as can be various tags (see Select Tag, Checkboxes or Radio Buttons)
Overridden Options
Inherited options

from the choice type

from the form type

Parent type choice
Class LocaleType
Overridden Options
choices

default: Symfony\Component\Intl\Intl::getLocaleBundle()->getLocaleNames()

The choices option defaults to all locales. It uses the default locale to specify the language.

Inherited Options

These options inherit from the choice type:

empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
expanded

type: Boolean default: false

If set to true, radio buttons or checkboxes will be rendered (depending on the multiple value). If false, a select element will be rendered.

multiple

type: Boolean default: false

If true, the user will be able to select multiple options (as opposed to choosing just one option). Depending on the value of the expanded option, this will render either a select tag or checkboxes if true and a select tag or radio buttons if false. The returned value will be an array.

preferred_choices

type: array default: array()

If this option is specified, then a sub-set of all of the options will be moved to the top of the select menu. The following would move the “Baz” option to the top, with a visual separator between it and the rest of the options:

$builder->add('foo_choices', 'choice', array(
    'choices' => array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'),
    'preferred_choices' => array('baz'),
));

Note that preferred choices are only meaningful when rendering as a select element (i.e. expanded is false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. -------------------). This can be customized when rendering the field:

  • Twig
    {{ form_widget(form.foo_choices, { 'separator': '=====' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['foo_choices'], array('separator' => '=====')) ?>
    

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If multiple is false and expanded is false, then '' (empty string);
  • Otherwise array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

timezone Field Type

The timezone type is a subset of the ChoiceType that allows the user to select from all possible timezones.

The “value” for each timezone is the full timezone name, such as America/Chicago or Europe/Istanbul.

Unlike the choice type, you don’t need to specify a choices or choice_list option as the field type automatically uses a large list of timezones. You can specify either of these options manually, but then you should just use the choice type directly.

Rendered as can be various tags (see Select Tag, Checkboxes or Radio Buttons)
Overridden Options
Inherited options

from the choice type

from the form type

Parent type choice
Class TimezoneType
Overridden Options
choice_list

default: TimezoneChoiceList

The Timezone type defaults the choice_list to all timezones returned by DateTimeZone::listIdentifiers(), broken down by continent.

Inherited Options

These options inherit from the choice type:

empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
expanded

type: Boolean default: false

If set to true, radio buttons or checkboxes will be rendered (depending on the multiple value). If false, a select element will be rendered.

multiple

type: Boolean default: false

If true, the user will be able to select multiple options (as opposed to choosing just one option). Depending on the value of the expanded option, this will render either a select tag or checkboxes if true and a select tag or radio buttons if false. The returned value will be an array.

preferred_choices

type: array default: array()

If this option is specified, then a sub-set of all of the options will be moved to the top of the select menu. The following would move the “Baz” option to the top, with a visual separator between it and the rest of the options:

$builder->add('foo_choices', 'choice', array(
    'choices' => array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'),
    'preferred_choices' => array('baz'),
));

Note that preferred choices are only meaningful when rendering as a select element (i.e. expanded is false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. -------------------). This can be customized when rendering the field:

  • Twig
    {{ form_widget(form.foo_choices, { 'separator': '=====' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['foo_choices'], array('separator' => '=====')) ?>
    

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If multiple is false and expanded is false, then '' (empty string);
  • Otherwise array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

currency Field Type

The currency type is a subset of the choice type that allows the user to select from a large list of 3-letter ISO 4217 currencies.

Unlike the choice type, you don’t need to specify a choices or choice_list option as the field type automatically uses a large list of currencies. You can specify either of these options manually, but then you should just use the choice type directly.

Rendered as can be various tags (see Select Tag, Checkboxes or Radio Buttons)
Overridden Options
Inherited options

from the choice type

from the form type

Parent type choice
Class CurrencyType
Overridden Options
choices

default: Symfony\Component\Intl\Intl::getCurrencyBundle()->getCurrencyNames()

The choices option defaults to all currencies.

Inherited Options

These options inherit from the choice type:

empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

expanded

type: Boolean default: false

If set to true, radio buttons or checkboxes will be rendered (depending on the multiple value). If false, a select element will be rendered.

multiple

type: Boolean default: false

If true, the user will be able to select multiple options (as opposed to choosing just one option). Depending on the value of the expanded option, this will render either a select tag or checkboxes if true and a select tag or radio buttons if false. The returned value will be an array.

preferred_choices

type: array default: array()

If this option is specified, then a sub-set of all of the options will be moved to the top of the select menu. The following would move the “Baz” option to the top, with a visual separator between it and the rest of the options:

$builder->add('foo_choices', 'choice', array(
    'choices' => array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'),
    'preferred_choices' => array('baz'),
));

Note that preferred choices are only meaningful when rendering as a select element (i.e. expanded is false). The preferred choices and normal choices are separated visually by a set of dotted lines (i.e. -------------------). This can be customized when rendering the field:

  • Twig
    {{ form_widget(form.foo_choices, { 'separator': '=====' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['foo_choices'], array('separator' => '=====')) ?>
    

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If multiple is false and expanded is false, then '' (empty string);
  • Otherwise array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

date Field Type

A field that allows the user to modify date information via a variety of different HTML elements.

The underlying data used for this field type can be a DateTime object, a string, a timestamp or an array. As long as the input option is set correctly, the field will take care of all of the details.

The field can be rendered as a single text box, three text boxes (month, day, and year) or three select boxes (see the widget option).

Underlying Data Type can be DateTime, string, timestamp, or array (see the input option)
Rendered as single text box or three select fields
Options
Overridden Options
Inherited options
Parent type form
Class DateType
Basic Usage

This field type is highly configurable, but easy to use. The most important options are input and widget.

Suppose that you have a publishedAt field whose underlying date is a DateTime object. The following configures the date type for that field as three different choice fields:

$builder->add('publishedAt', 'date', array(
    'input'  => 'datetime',
    'widget' => 'choice',
));

The input option must be changed to match the type of the underlying date data. For example, if the publishedAt field’s data were a unix timestamp, you’d need to set input to timestamp:

$builder->add('publishedAt', 'date', array(
    'input'  => 'timestamp',
    'widget' => 'choice',
));

The field also supports an array and string as valid input option values.

Field Options
days

type: array default: 1 to 31

List of days available to the day field type. This option is only relevant when the widget option is set to choice:

'days' => range(1,31)
empty_value

type: string or array

If your widget option is set to choice, then this field will be represented as a series of select boxes. The empty_value option can be used to add a “blank” entry to the top of each select box:

$builder->add('dueDate', 'date', array(
    'empty_value' => '',
));

Alternatively, you can specify a string to be displayed for the “blank” value:

$builder->add('dueDate', 'date', array(
    'empty_value' => array('year' => 'Year', 'month' => 'Month', 'day' => 'Day')
));
format

type: integer or string default: IntlDateFormatter::MEDIUM (or yyyy-MM-dd if widget is single_text)

Option passed to the IntlDateFormatter class, used to transform user input into the proper format. This is critical when the widget option is set to single_text, and will define how the user will input the data. By default, the format is determined based on the current user locale: meaning that the expected format will be different for different users. You can override it by passing the format as a string.

For more information on valid formats, see Date/Time Format Syntax:

$builder->add('date_created', 'date', array(
    'widget' => 'single_text',
    // this is actually the default format for single_text
    'format' => 'yyyy-MM-dd',
));

注解

If you want your field to be rendered as an HTML5 “date” field, you have to use a single_text widget with the yyyy-MM-dd format (the RFC 3339 format) which is the default value if you use the single_text widget.

input

type: string default: datetime

The format of the input data - i.e. the format that the date is stored on your underlying object. Valid values are:

  • string (e.g. 2011-06-05)
  • datetime (a DateTime object)
  • array (e.g. array('year' => 2011, 'month' => 06, 'day' => 05))
  • timestamp (e.g. 1307232000)

The value that comes back from the form will also be normalized back into this format.

警告

If timestamp is used, DateType is limited to dates between Fri, 13 Dec 1901 20:45:54 GMT and Tue, 19 Jan 2038 03:14:07 GMT on 32bit systems. This is due to a limitation in PHP itself.

model_timezone

type: string default: system default timezone

Timezone that the input data is stored in. This must be one of the PHP supported timezones.

months

type: array default: 1 to 12

List of months available to the month field type. This option is only relevant when the widget option is set to choice.

view_timezone

type: string default: system default timezone

Timezone for how the data should be shown to the user (and therefore also the data that the user submits). This must be one of the PHP supported timezones.

widget

type: string default: choice

The basic way in which this field should be rendered. Can be one of the following:

  • choice: renders three select inputs. The order of the selects is defined in the format option.
  • text: renders a three field input of type text (month, day, year).
  • single_text: renders a single input of type date. User’s input is validated based on the format option.
years

type: array default: five years before to five years after the current year

List of years available to the year field type. This option is only relevant when the widget option is set to choice.

Overridden Options
by_reference

default: false

The DateTime classes are treated as immutable objects.

error_bubbling

default: false

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
inherit_data

2.3 新版功能: The inherit_data option was introduced in Symfony 2.3. Before, it was known as virtual.

type: boolean default: false

This option determines if the form will inherit data from its parent form. This can be useful if you have a set of fields that are duplicated across multiple forms. See How to Reduce Code Duplication with “inherit_data”.

invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

Field Variables
Variable Type Usage
widget mixed The value of the widget option.
type string Only present when widget is single_text and HTML5 is activated, contains the input type to use (datetime, date or time).
date_pattern string A string with the date format to use.
datetime Field Type

This field type allows the user to modify data that represents a specific date and time (e.g. 1984-06-05 12:15:30).

Can be rendered as a text input or select tags. The underlying format of the data can be a DateTime object, a string, a timestamp or an array.

Underlying Data Type can be DateTime, string, timestamp, or array (see the input option)
Rendered as single text box or three select fields
Options
Inherited options
Parent type form
Class DateTimeType
Field Options
date_format

type: integer or string default: IntlDateFormatter::MEDIUM

Defines the format option that will be passed down to the date field. See the date type’s format option for more details.

date_widget

type: string default: choice

Defines the widget option for the date type

days

type: array default: 1 to 31

List of days available to the day field type. This option is only relevant when the widget option is set to choice:

'days' => range(1,31)
empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
format

type: string default: Symfony\Component\Form\Extension\Core\Type\DateTimeType::HTML5_FORMAT

If the widget option is set to single_text, this option specifies the format of the input, i.e. how Symfony will interpret the given input as a datetime string. It defaults to the RFC 3339 format which is used by the HTML5 datetime field. Keeping the default value will cause the field to be rendered as an input field with type="datetime".

hours

type: array default: 0 to 23

List of hours available to the hours field type. This option is only relevant when the widget option is set to choice.

input

type: string default: datetime

The format of the input data - i.e. the format that the date is stored on your underlying object. Valid values are:

  • string (e.g. 2011-06-05 12:15:00)
  • datetime (a DateTime object)
  • array (e.g. array(2011, 06, 05, 12, 15, 0))
  • timestamp (e.g. 1307276100)

The value that comes back from the form will also be normalized back into this format.

警告

If timestamp is used, DateType is limited to dates between Fri, 13 Dec 1901 20:45:54 GMT and Tue, 19 Jan 2038 03:14:07 GMT on 32bit systems. This is due to a limitation in PHP itself.

minutes

type: array default: 0 to 59

List of minutes available to the minutes field type. This option is only relevant when the widget option is set to choice.

model_timezone

type: string default: system default timezone

Timezone that the input data is stored in. This must be one of the PHP supported timezones.

months

type: array default: 1 to 12

List of months available to the month field type. This option is only relevant when the widget option is set to choice.

seconds

type: array default: 0 to 59

List of seconds available to the seconds field type. This option is only relevant when the widget option is set to choice.

time_widget

type: string default: choice

Defines the widget option for the time type

view_timezone

type: string default: system default timezone

Timezone for how the data should be shown to the user (and therefore also the data that the user submits). This must be one of the PHP supported timezones.

widget

type: string default: null

Defines the widget option for both the date type and time type. This can be overridden with the date_widget and time_widget options.

with_minutes

2.2 新版功能: The with_minutes option was introduced in Symfony 2.2.

type: Boolean default: true

Whether or not to include minutes in the input. This will result in an additional input to capture minutes.

with_seconds

type: Boolean default: false

Whether or not to include seconds in the input. This will result in an additional input to capture seconds.

years

type: array default: five years before to five years after the current year

List of years available to the year field type. This option is only relevant when the widget option is set to choice.

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

inherit_data

2.3 新版功能: The inherit_data option was introduced in Symfony 2.3. Before, it was known as virtual.

type: boolean default: false

This option determines if the form will inherit data from its parent form. This can be useful if you have a set of fields that are duplicated across multiple forms. See How to Reduce Code Duplication with “inherit_data”.

invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

Field Variables
Variable Type Usage
widget mixed The value of the widget option.
type string Only present when widget is single_text and HTML5 is activated, contains the input type to use (datetime, date or time).
time Field Type

A field to capture time input.

This can be rendered as a text field, a series of text fields (e.g. hour, minute, second) or a series of select fields. The underlying data can be stored as a DateTime object, a string, a timestamp or an array.

Underlying Data Type can be DateTime, string, timestamp, or array (see the input option)
Rendered as can be various tags (see below)
Options
Overridden Options
Inherited Options
Parent type form
Class TimeType
Basic Usage

This field type is highly configurable, but easy to use. The most important options are input and widget.

Suppose that you have a startTime field whose underlying time data is a DateTime object. The following configures the time type for that field as two different choice fields:

$builder->add('startTime', 'time', array(
    'input'  => 'datetime',
    'widget' => 'choice',
));

The input option must be changed to match the type of the underlying date data. For example, if the startTime field’s data were a unix timestamp, you’d need to set input to timestamp:

$builder->add('startTime', 'time', array(
    'input'  => 'timestamp',
    'widget' => 'choice',
));

The field also supports an array and string as valid input option values.

Field Options
empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
hours

type: array default: 0 to 23

List of hours available to the hours field type. This option is only relevant when the widget option is set to choice.

input

type: string default: datetime

The format of the input data - i.e. the format that the date is stored on your underlying object. Valid values are:

  • string (e.g. 12:17:26)
  • datetime (a DateTime object)
  • array (e.g. array('hour' => 12, 'minute' => 17, 'second' => 26))
  • timestamp (e.g. 1307232000)

The value that comes back from the form will also be normalized back into this format.

minutes

type: array default: 0 to 59

List of minutes available to the minutes field type. This option is only relevant when the widget option is set to choice.

model_timezone

type: string default: system default timezone

Timezone that the input data is stored in. This must be one of the PHP supported timezones.

seconds

type: array default: 0 to 59

List of seconds available to the seconds field type. This option is only relevant when the widget option is set to choice.

view_timezone

type: string default: system default timezone

Timezone for how the data should be shown to the user (and therefore also the data that the user submits). This must be one of the PHP supported timezones.

widget

type: string default: choice

The basic way in which this field should be rendered. Can be one of the following:

  • choice: renders one, two (default) or three select inputs (hour, minute, second), depending on the with_minutes and with_seconds options.
  • text: renders one, two (default) or three text inputs (hour, minute, second), depending on the with_minutes and with_seconds options.
  • single_text: renders a single input of type time. User’s input will be validated against the form hh:mm (or hh:mm:ss if using seconds).

警告

Combining the widget type single_text and the with_minutes option set to false can cause unexpected behavior in the client as the input type time might not support selecting an hour only.

with_minutes

2.2 新版功能: The with_minutes option was introduced in Symfony 2.2.

type: Boolean default: true

Whether or not to include minutes in the input. This will result in an additional input to capture minutes.

with_seconds

type: Boolean default: false

Whether or not to include seconds in the input. This will result in an additional input to capture seconds.

Overridden Options
by_reference

default: false

The DateTime classes are treated as immutable objects.

error_bubbling

default: false

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
inherit_data

2.3 新版功能: The inherit_data option was introduced in Symfony 2.3. Before, it was known as virtual.

type: boolean default: false

This option determines if the form will inherit data from its parent form. This can be useful if you have a set of fields that are duplicated across multiple forms. See How to Reduce Code Duplication with “inherit_data”.

invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

Form Variables
Variable Type Usage
widget mixed The value of the widget option.
with_minutes Boolean The value of the with_minutes option.
with_seconds Boolean The value of the with_seconds option.
type string Only present when widget is single_text and HTML5 is activated, contains the input type to use (datetime, date or time).
birthday Field Type

A date field that specializes in handling birthdate data.

Can be rendered as a single text box, three text boxes (month, day, and year), or three select boxes.

This type is essentially the same as the date type, but with a more appropriate default for the years option. The years option defaults to 120 years ago to the current year.

Underlying Data Type can be DateTime, string, timestamp, or array (see the input option)
Rendered as can be three select boxes or 1 or 3 text boxes, based on the widget option
Overridden options
Inherited options

from the date type:

from the form type:

Parent type date
Class BirthdayType
Overridden Options
years

type: array default: 120 years ago to the current year

List of years available to the year field type. This option is only relevant when the widget option is set to choice.

Inherited Options

These options inherit from the date type:

days

type: array default: 1 to 31

List of days available to the day field type. This option is only relevant when the widget option is set to choice:

'days' => range(1,31)
empty_value

2.3 新版功能: Since Symfony 2.3, empty values are also supported if the expanded option is set to true.

type: string or Boolean

This option determines whether or not a special “empty” option (e.g. “Choose an option”) will appear at the top of a select widget. This option only applies if the multiple option is set to false.

  • Add an empty value with “Choose an option” as the text:

    $builder->add('states', 'choice', array(
        'empty_value' => 'Choose an option',
    ));
    
  • Guarantee that no “empty” value option is displayed:

    $builder->add('states', 'choice', array(
        'empty_value' => false,
    ));
    

If you leave the empty_value option unset, then a blank (with no text) option will automatically be added if and only if the required option is false:

// a blank (with no text) option will be added
$builder->add('states', 'choice', array(
    'required' => false,
));
format

type: integer or string default: IntlDateFormatter::MEDIUM (or yyyy-MM-dd if widget is single_text)

Option passed to the IntlDateFormatter class, used to transform user input into the proper format. This is critical when the widget option is set to single_text, and will define how the user will input the data. By default, the format is determined based on the current user locale: meaning that the expected format will be different for different users. You can override it by passing the format as a string.

For more information on valid formats, see Date/Time Format Syntax:

$builder->add('date_created', 'date', array(
    'widget' => 'single_text',
    // this is actually the default format for single_text
    'format' => 'yyyy-MM-dd',
));

注解

If you want your field to be rendered as an HTML5 “date” field, you have to use a single_text widget with the yyyy-MM-dd format (the RFC 3339 format) which is the default value if you use the single_text widget.

input

type: string default: datetime

The format of the input data - i.e. the format that the date is stored on your underlying object. Valid values are:

  • string (e.g. 2011-06-05)
  • datetime (a DateTime object)
  • array (e.g. array('year' => 2011, 'month' => 06, 'day' => 05))
  • timestamp (e.g. 1307232000)

The value that comes back from the form will also be normalized back into this format.

警告

If timestamp is used, DateType is limited to dates between Fri, 13 Dec 1901 20:45:54 GMT and Tue, 19 Jan 2038 03:14:07 GMT on 32bit systems. This is due to a limitation in PHP itself.

model_timezone

type: string default: system default timezone

Timezone that the input data is stored in. This must be one of the PHP supported timezones.

months

type: array default: 1 to 12

List of months available to the month field type. This option is only relevant when the widget option is set to choice.

view_timezone

type: string default: system default timezone

Timezone for how the data should be shown to the user (and therefore also the data that the user submits). This must be one of the PHP supported timezones.

widget

type: string default: choice

The basic way in which this field should be rendered. Can be one of the following:

  • choice: renders three select inputs. The order of the selects is defined in the format option.
  • text: renders a three field input of type text (month, day, year).
  • single_text: renders a single input of type date. User’s input is validated based on the format option.

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

inherit_data

2.3 新版功能: The inherit_data option was introduced in Symfony 2.3. Before, it was known as virtual.

type: boolean default: false

This option determines if the form will inherit data from its parent form. This can be useful if you have a set of fields that are duplicated across multiple forms. See How to Reduce Code Duplication with “inherit_data”.

invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

checkbox Field Type

Creates a single input checkbox. This should always be used for a field that has a Boolean value: if the box is checked, the field will be set to true, if the box is unchecked, the value will be set to false.

Rendered as input checkbox field
Options
Overridden options
Inherited options
Parent type form
Class CheckboxType
Example Usage
$builder->add('public', 'checkbox', array(
    'label'     => 'Show this entry publicly?',
    'required'  => false,
));
Field Options
value

type: mixed default: 1

The value that’s actually used as the value for the checkbox or radio button. This does not affect the value that’s set on your object.

警告

To make a checkbox or radio button checked by default, use the data option.

Overridden Options
compound

type: boolean default: false

This option specifies if a form is compound. As it’s not the case for checkbox, by default the value is overridden with the false value.

empty_data

type: string default: mixed

This option determines what value the field will return when the empty_value choice is selected. In the checkbox and the radio type, the value of empty_data is overriden by the value returned by the data transformer (see How to Use Data Transformers).

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

Form Variables
Variable Type Usage
checked Boolean Whether or not the current input is checked.
file Field Type

The file type represents a file input in your form.

Rendered as input file field
Inherited options
Parent type form
Class FileType
Basic Usage

Say you have this form definition:

$builder->add('attachment', 'file');

When the form is submitted, the attachment field will be an instance of UploadedFile. It can be used to move the attachment file to a permanent location:

use Symfony\Component\HttpFoundation\File\UploadedFile;

public function uploadAction()
{
    // ...

    if ($form->isValid()) {
        $someNewFilename = ...

        $form['attachment']->getData()->move($dir, $someNewFilename);

        // ...
    }

    // ...
}

The move() method takes a directory and a file name as its arguments. You might calculate the filename in one of the following ways:

// use the original file name
$file->move($dir, $file->getClientOriginalName());

// compute a random name and try to guess the extension (more secure)
$extension = $file->guessExtension();
if (!$extension) {
    // extension cannot be guessed
    $extension = 'bin';
}
$file->move($dir, rand(1, 99999).'.'.$extension);

Using the original name via getClientOriginalName() is not safe as it could have been manipulated by the end-user. Moreover, it can contain characters that are not allowed in file names. You should sanitize the name before using it directly.

Read the cookbook for an example of how to manage a file upload associated with a Doctrine entity.

Inherited Options

These options inherit from the form type:

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: mixed

The default value is null.

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

Form Variables
Variable Type Usage
type string The type variable is set to file, in order to render as a file input field.
radio Field Type

Creates a single radio button. If the radio button is selected, the field will be set to the specified value. Radio buttons cannot be unchecked - the value only changes when another radio button with the same name gets checked.

The radio type isn’t usually used directly. More commonly it’s used internally by other types such as choice. If you want to have a Boolean field, use checkbox.

Rendered as input radio field
Inherited options

from the checkbox type:

from the form type:

Parent type checkbox
Class RadioType
Inherited Options

These options inherit from the checkbox type:

value

type: mixed default: 1

The value that’s actually used as the value for the checkbox or radio button. This does not affect the value that’s set on your object.

警告

To make a checkbox or radio button checked by default, use the data option.

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

empty_data

type: string default: mixed

This option determines what value the field will return when the empty_value choice is selected. In the checkbox and the radio type, the value of empty_data is overriden by the value returned by the data transformer (see How to Use Data Transformers).

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

Form Variables
Variable Type Usage
checked Boolean Whether or not the current input is checked.
collection Field Type

This field type is used to render a “collection” of some field or form. In the easiest sense, it could be an array of text fields that populate an array emails field. In more complex examples, you can embed entire forms, which is useful when creating forms that expose one-to-many relationships (e.g. a product from where you can manage many related product photos).

Rendered as depends on the type option
Options
Inherited options
Parent type form
Class CollectionType

注解

If you are working with a collection of Doctrine entities, pay special attention to the allow_add, allow_delete and by_reference options. You can also see a complete example in the cookbook article How to Embed a Collection of Forms.

Basic Usage

This type is used when you want to manage a collection of similar items in a form. For example, suppose you have an emails field that corresponds to an array of email addresses. In the form, you want to expose each email address as its own input text box:

$builder->add('emails', 'collection', array(
    // each item in the array will be an "email" field
    'type'   => 'email',
    // these options are passed to each "email" type
    'options'  => array(
        'required'  => false,
        'attr'      => array('class' => 'email-box')
    ),
));

The simplest way to render this is all at once:

  • Twig
    {{ form_row(form.emails) }}
    
  • PHP
    <?php echo $view['form']->row($form['emails']) ?>
    

A much more flexible method would look like this:

  • Twig
    {{ form_label(form.emails) }}
    {{ form_errors(form.emails) }}
    
    <ul>
    {% for emailField in form.emails %}
        <li>
            {{ form_errors(emailField) }}
            {{ form_widget(emailField) }}
        </li>
    {% endfor %}
    </ul>
    
  • PHP
    <?php echo $view['form']->label($form['emails']) ?>
    <?php echo $view['form']->errors($form['emails']) ?>
    
    <ul>
    <?php foreach ($form['emails'] as $emailField): ?>
        <li>
            <?php echo $view['form']->errors($emailField) ?>
            <?php echo $view['form']->widget($emailField) ?>
        </li>
    <?php endforeach ?>
    </ul>
    

In both cases, no input fields would render unless your emails data array already contained some emails.

In this simple example, it’s still impossible to add new addresses or remove existing addresses. Adding new addresses is possible by using the allow_add option (and optionally the prototype option) (see example below). Removing emails from the emails array is possible with the allow_delete option.

Adding and Removing Items

If allow_add is set to true, then if any unrecognized items are submitted, they’ll be added seamlessly to the array of items. This is great in theory, but takes a little bit more effort in practice to get the client-side JavaScript correct.

Following along with the previous example, suppose you start with two emails in the emails data array. In that case, two input fields will be rendered that will look something like this (depending on the name of your form):

<input type="email" id="form_emails_0" name="form[emails][0]" value="foo@foo.com" />
<input type="email" id="form_emails_1" name="form[emails][1]" value="bar@bar.com" />

To allow your user to add another email, just set allow_add to true and - via JavaScript - render another field with the name form[emails][2] (and so on for more and more fields).

To help make this easier, setting the prototype option to true allows you to render a “template” field, which you can then use in your JavaScript to help you dynamically create these new fields. A rendered prototype field will look like this:

<input type="email" id="form_emails___name__" name="form[emails][__name__]" value="" />

By replacing __name__ with some unique value (e.g. 2), you can build and insert new HTML fields into your form.

Using jQuery, a simple example might look like this. If you’re rendering your collection fields all at once (e.g. form_row(form.emails)), then things are even easier because the data-prototype attribute is rendered automatically for you (with a slight difference - see note below) and all you need is the JavaScript:

  • Twig
    {{ form_start(form) }}
        {# ... #}
    
        {# store the prototype on the data-prototype attribute #}
        <ul id="email-fields-list" data-prototype="{{ form_widget(form.emails.vars.prototype)|e }}">
        {% for emailField in form.emails %}
            <li>
                {{ form_errors(emailField) }}
                {{ form_widget(emailField) }}
            </li>
        {% endfor %}
        </ul>
    
        <a href="#" id="add-another-email">Add another email</a>
    
        {# ... #}
    {{ form_end(form) }}
    
    <script type="text/javascript">
        // keep track of how many email fields have been rendered
        var emailCount = '{{ form.emails|length }}';
    
        jQuery(document).ready(function() {
            jQuery('#add-another-email').click(function(e) {
                e.preventDefault();
    
                var emailList = jQuery('#email-fields-list');
    
                // grab the prototype template
                var newWidget = emailList.attr('data-prototype');
                // replace the "__name__" used in the id and name of the prototype
                // with a number that's unique to your emails
                // end name attribute looks like name="contact[emails][2]"
                newWidget = newWidget.replace(/__name__/g, emailCount);
                emailCount++;
    
                // create a new list element and add it to the list
                var newLi = jQuery('<li></li>').html(newWidget);
                newLi.appendTo(emailList);
            });
        })
    </script>
    

小技巧

If you’re rendering the entire collection at once, then the prototype is automatically available on the data-prototype attribute of the element (e.g. div or table) that surrounds your collection. The only difference is that the entire “form row” is rendered for you, meaning you wouldn’t have to wrap it in any container element as it was done above.

Field Options
allow_add

type: Boolean default: false

If set to true, then if unrecognized items are submitted to the collection, they will be added as new items. The ending array will contain the existing items as well as the new item that was in the submitted data. See the above example for more details.

The prototype option can be used to help render a prototype item that can be used - with JavaScript - to create new form items dynamically on the client side. For more information, see the above example and Allowing “new” Tags with the “Prototype”.

警告

If you’re embedding entire other forms to reflect a one-to-many database relationship, you may need to manually ensure that the foreign key of these new objects is set correctly. If you’re using Doctrine, this won’t happen automatically. See the above link for more details.

allow_delete

type: Boolean default: false

If set to true, then if an existing item is not contained in the submitted data, it will be correctly absent from the final array of items. This means that you can implement a “delete” button via JavaScript which simply removes a form element from the DOM. When the user submits the form, its absence from the submitted data will mean that it’s removed from the final array.

For more information, see Allowing Tags to be Removed.

警告

Be careful when using this option when you’re embedding a collection of objects. In this case, if any embedded forms are removed, they will correctly be missing from the final array of objects. However, depending on your application logic, when one of those objects is removed, you may want to delete it or at least remove its foreign key reference to the main object. None of this is handled automatically. For more information, see Allowing Tags to be Removed.

options

type: array default: array()

This is the array that’s passed to the form type specified in the type option. For example, if you used the choice type as your type option (e.g. for a collection of drop-down menus), then you’d need to at least pass the choices option to the underlying type:

$builder->add('favorite_cities', 'collection', array(
    'type'   => 'choice',
    'options'  => array(
        'choices'  => array(
            'nashville' => 'Nashville',
            'paris'     => 'Paris',
            'berlin'    => 'Berlin',
            'london'    => 'London',
        ),
    ),
));
prototype

type: Boolean default: true

This option is useful when using the allow_add option. If true (and if allow_add is also true), a special “prototype” attribute will be available so that you can render a “template” example on your page of what a new element should look like. The name attribute given to this element is __name__. This allows you to add a “add another” button via JavaScript which reads the prototype, replaces __name__ with some unique name or number, and render it inside your form. When submitted, it will be added to your underlying array due to the allow_add option.

The prototype field can be rendered via the prototype variable in the collection field:

  • Twig
    {{ form_row(form.emails.vars.prototype) }}
    
  • PHP
    <?php echo $view['form']->row($form['emails']->vars['prototype']) ?>
    

Note that all you really need is the “widget”, but depending on how you’re rendering your form, having the entire “form row” may be easier for you.

小技巧

If you’re rendering the entire collection field at once, then the prototype form row is automatically available on the data-prototype attribute of the element (e.g. div or table) that surrounds your collection.

For details on how to actually use this option, see the above example as well as Allowing “new” Tags with the “Prototype”.

prototype_name

type: String default: __name__

If you have several collections in your form, or worse, nested collections you may want to change the placeholder so that unrelated placeholders are not replaced with the same value.

type

type: string or FormTypeInterface required

This is the field type for each item in this collection (e.g. text, choice, etc). For example, if you have an array of email addresses, you’d use the email type. If you want to embed a collection of some other form, create a new instance of your form type and pass it as this option.

Inherited Options

These options inherit from the form type. Not all options are listed here - only the most applicable to this type:

by_reference

type: Boolean default: true

In most cases, if you have a name field, then you expect setName() to be called on the underlying object. In some cases, however, setName() may not be called. Setting by_reference ensures that the setter is called in all cases.

To explain this further, here’s a simple example:

$builder = $this->createFormBuilder($article);
$builder
    ->add('title', 'text')
    ->add(
        $builder->create('author', 'form', array('by_reference' => ?))
            ->add('name', 'text')
            ->add('email', 'email')
    )

If by_reference is true, the following takes place behind the scenes when you call submit() (or handleRequest()) on the form:

$article->setTitle('...');
$article->getAuthor()->setName('...');
$article->getAuthor()->setEmail('...');

Notice that setAuthor() is not called. The author is modified by reference.

If you set by_reference to false, submitting looks like this:

$article->setTitle('...');
$author = $article->getAuthor();
$author->setName('...');
$author->setEmail('...');
$article->setAuthor($author);

So, all that by_reference=false really does is force the framework to call the setter on the parent object.

Similarly, if you’re using the collection form type where your underlying collection data is an object (like with Doctrine’s ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.

cascade_validation

type: Boolean default: false

Set this option to true to force validation on embedded form types. For example, if you have a ProductType with an embedded CategoryType, setting cascade_validation to true on ProductType will cause the data from CategoryType to also be validated.

小技巧

Instead of using this option, it is recommended that you use the Valid constraint in your model to force validation on a child object stored on a property. This cascades only the validation but not the use of the validation_group option on child forms. You can read more about this in the section about Embedding a Single Object.

小技巧

By default the error_bubbling option is enabled for the collection Field Type, which passes the errors to the parent form. If you want to attach the errors to the locations where they actually occur you have to set error_bubbling to false.

empty_data

type: mixed

The default value is array() (empty array).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: true

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

Field Variables
Variable Type Usage
allow_add Boolean The value of the allow_add option.
allow_delete Boolean The value of the allow_delete option.
repeated Field Type

This is a special field “group”, that creates two identical fields whose values must match (or a validation error is thrown). The most common use is when you need the user to repeat their password or email to verify accuracy.

Rendered as input text field by default, but see type option
Options
Overridden Options
Inherited options
Parent type form
Class RepeatedType
Example Usage
$builder->add('password', 'repeated', array(
    'type' => 'password',
    'invalid_message' => 'The password fields must match.',
    'options' => array('attr' => array('class' => 'password-field')),
    'required' => true,
    'first_options'  => array('label' => 'Password'),
    'second_options' => array('label' => 'Repeat Password'),
));

Upon a successful form submit, the value entered into both of the “password” fields becomes the data of the password key. In other words, even though two fields are actually rendered, the end data from the form is just the single value (usually a string) that you need.

The most important option is type, which can be any field type and determines the actual type of the two underlying fields. The options option is passed to each of those individual fields, meaning - in this example - any option supported by the password type can be passed in this array.

Rendering

The repeated field type is actually two underlying fields, which you can render all at once, or individually. To render all at once, use something like:

  • Twig
    {{ form_row(form.password) }}
    
  • PHP
    <?php echo $view['form']->row($form['password']) ?>
    

To render each field individually, use something like this:

  • Twig
    {# .first and .second may vary in your use - see the note below #}
    {{ form_row(form.password.first) }}
    {{ form_row(form.password.second) }}
    
  • PHP
    <?php echo $view['form']->row($form['password']['first']) ?>
    <?php echo $view['form']->row($form['password']['second']) ?>
    

注解

The names first and second are the default names for the two sub-fields. However, these names can be controlled via the first_name and second_name options. If you’ve set these options, then use those values instead of first and second when rendering.

Validation

One of the key features of the repeated field is internal validation (you don’t need to do anything to set this up) that forces the two fields to have a matching value. If the two fields don’t match, an error will be shown to the user.

The invalid_message is used to customize the error that will be displayed when the two fields do not match each other.

Field Options
first_name

type: string default: first

This is the actual field name to be used for the first field. This is mostly meaningless, however, as the actual data entered into both of the fields will be available under the key assigned to the repeated field itself (e.g. password). However, if you don’t specify a label, this field name is used to “guess” the label for you.

first_options

type: array default: array()

Additional options (will be merged into options above) that should be passed only to the first field. This is especially useful for customizing the label:

$builder->add('password', 'repeated', array(
    'first_options'  => array('label' => 'Password'),
    'second_options' => array('label' => 'Repeat Password'),
));
options

type: array default: array()

This options array will be passed to each of the two underlying fields. In other words, these are the options that customize the individual field types. For example, if the type option is set to password, this array might contain the options always_empty or required - both options that are supported by the password field type.

second_name

type: string default: second

The same as first_name, but for the second field.

second_options

type: array default: array()

Additional options (will be merged into options above) that should be passed only to the second field. This is especially useful for customizing the label (see first_options).

type

type: string default: text

The two underlying fields will be of this field type. For example, passing a type of password will render two password fields.

Overridden Options
error_bubbling

default: false

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

hidden Field Type

The hidden type represents a hidden input field.

Rendered as input hidden field
Overriden options
Inherited options
Parent type form
Class HiddenType
Overridden Options
error_bubbling

default: true

Pass errors to the root form, otherwise they will not be visible.

required

default: false

Hidden fields cannot have a required attribute.

Inherited Options

These options inherit from the form type:

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

property_path

type: any default: the field's name

Fields display a property value of the form’s domain object by default. When the form is submitted, the submitted value is written back into the object.

If you want to override the property that a field reads from and writes to, you can set the property_path option. Its default value is the field’s name.

If you wish the field to be ignored when reading or writing to the object you can set the property_path option to false, but using property_path for this purpose is deprecated, you should use the mapped option.

2.1 新版功能: The mapped option was introduced in Symfony 2.1 for this use-case.

button Field Type

2.3 新版功能: The button type was introduced in Symfony 2.3

A simple, non-responsive button.

Rendered as button tag
Inherited options
Parent type none
Class ButtonType
Inherited Options

The following options are defined in the BaseType class. The BaseType class is the parent class for both the button type and the form type, but it is not part of the form type tree (i.e. it can not be used as a form type on its own).

attr

type: array default: Empty array

If you want to add extra attributes to the HTML representation of the button, you can use attr option. It’s an associative array with HTML attribute as a key. This can be useful when you need to set a custom class for the button:

$builder->add('save', 'button', array(
    'attr' => array('class' => 'save'),
));
disabled

type: boolean default: false

If you don’t want a user to be able to click a button, you can set the disabled option to true. It will not be possible to submit the form with this button, not even when bypassing the browser and sending a request manually, for example with cURL.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be displayed on the button. The label can also be directly set inside the template:

  • Twig
    {{ form_widget(form.save, { 'label': 'Click me' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['save'], array('label' => 'Click me')) ?>
    
translation_domain

type: string default: messages

This is the translation domain that will be used for any labels or options that are rendered for this button.

reset Field Type

2.3 新版功能: The reset type was introduced in Symfony 2.3

A button that resets all fields to their original values.

Rendered as input reset tag
Inherited options
Parent type button
Class ResetType
Inherited Options
attr

type: array default: Empty array

If you want to add extra attributes to the HTML representation of the button, you can use attr option. It’s an associative array with HTML attribute as a key. This can be useful when you need to set a custom class for the button:

$builder->add('save', 'button', array(
    'attr' => array('class' => 'save'),
));
disabled

type: boolean default: false

If you don’t want a user to be able to click a button, you can set the disabled option to true. It will not be possible to submit the form with this button, not even when bypassing the browser and sending a request manually, for example with cURL.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be displayed on the button. The label can also be directly set inside the template:

  • Twig
    {{ form_widget(form.save, { 'label': 'Click me' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['save'], array('label' => 'Click me')) ?>
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
translation_domain

type: string default: messages

This is the translation domain that will be used for any labels or options that are rendered for this button.

submit Field Type

2.3 新版功能: The submit type was introduced in Symfony 2.3

A submit button.

Rendered as button submit tag
Inherited options
Parent type button
Class SubmitType

The Submit button has an additional method isClicked() that lets you check whether this button was used to submit the form. This is especially useful when a form has multiple submit buttons:

if ($form->get('save')->isClicked()) {
    // ...
}
Inherited Options
attr

type: array default: Empty array

If you want to add extra attributes to the HTML representation of the button, you can use attr option. It’s an associative array with HTML attribute as a key. This can be useful when you need to set a custom class for the button:

$builder->add('save', 'button', array(
    'attr' => array('class' => 'save'),
));
disabled

type: boolean default: false

If you don’t want a user to be able to click a button, you can set the disabled option to true. It will not be possible to submit the form with this button, not even when bypassing the browser and sending a request manually, for example with cURL.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be displayed on the button. The label can also be directly set inside the template:

  • Twig
    {{ form_widget(form.save, { 'label': 'Click me' }) }}
    
  • PHP
    <?php echo $view['form']->widget($form['save'], array('label' => 'Click me')) ?>
    
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
translation_domain

type: string default: messages

This is the translation domain that will be used for any labels or options that are rendered for this button.

validation_groups

type: array default: null

When your form contains multiple submit buttons, you can change the validation group based on the button which was used to submit the form. Imagine a registration form wizard with buttons to go to the previous or the next step:

$form = $this->createFormBuilder($user)
    ->add('previousStep', 'submit', array(
        'validation_groups' => false,
    ))
    ->add('nextStep', 'submit', array(
        'validation_groups' => array('Registration'),
    ))
    ->getForm();

The special false ensures that no validation is performed when the previous step button is clicked. When the second button is clicked, all constraints from the “Registration” are validated.

参见

You can read more about this in the Form chapter of the book.

Form Variables
Variable Type Usage
clicked Boolean Whether the button is clicked or not.
form Field Type

The form type predefines a couple of options that are then available on all types for which form is the parent type.

Options
Inherited options
Parent none
Class FormType
Field Options
action

2.3 新版功能: The action option was introduced in Symfony 2.3.

type: string default: empty string

This option specifies where to send the form’s data on submission (usually a URI). Its value is rendered as the action attribute of the form element. An empty value is considered a same-document reference, i.e. the form will be submitted to the same URI that rendered the form.

by_reference

type: Boolean default: true

In most cases, if you have a name field, then you expect setName() to be called on the underlying object. In some cases, however, setName() may not be called. Setting by_reference ensures that the setter is called in all cases.

To explain this further, here’s a simple example:

$builder = $this->createFormBuilder($article);
$builder
    ->add('title', 'text')
    ->add(
        $builder->create('author', 'form', array('by_reference' => ?))
            ->add('name', 'text')
            ->add('email', 'email')
    )

If by_reference is true, the following takes place behind the scenes when you call submit() (or handleRequest()) on the form:

$article->setTitle('...');
$article->getAuthor()->setName('...');
$article->getAuthor()->setEmail('...');

Notice that setAuthor() is not called. The author is modified by reference.

If you set by_reference to false, submitting looks like this:

$article->setTitle('...');
$author = $article->getAuthor();
$author->setName('...');
$author->setEmail('...');
$article->setAuthor($author);

So, all that by_reference=false really does is force the framework to call the setter on the parent object.

Similarly, if you’re using the collection form type where your underlying collection data is an object (like with Doctrine’s ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.

cascade_validation

type: Boolean default: false

Set this option to true to force validation on embedded form types. For example, if you have a ProductType with an embedded CategoryType, setting cascade_validation to true on ProductType will cause the data from CategoryType to also be validated.

小技巧

Instead of using this option, it is recommended that you use the Valid constraint in your model to force validation on a child object stored on a property. This cascades only the validation but not the use of the validation_group option on child forms. You can read more about this in the section about Embedding a Single Object.

小技巧

By default the error_bubbling option is enabled for the collection Field Type, which passes the errors to the parent form. If you want to attach the errors to the locations where they actually occur you have to set error_bubbling to false.

compound

type: boolean default: true

This option specifies if a form is compound. This is independent of whether the form actually has children. A form can be compound but not have any children at all (e.g. an empty collection form).

constraints

type: array or Constraint default: null

Allows you to attach one or more validation constraints to a specific field. For more information, see Adding Validation. This option is added in the FormTypeValidatorExtension form extension.

data

type: mixed default: Defaults to field of the underlying object (if there is one)

When you create a form, each field initially displays the value of the corresponding property of the form’s domain object (if an object is bound to the form). If you want to override the initial value for the form or just an individual field, you can set it in the data option:

$builder->add('token', 'hidden', array(
    'data' => 'abcdef',
));

注解

The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.

data_class

type: string

This option is used to set the appropriate data mapper to be used by the form, so you can use it for any form field type which requires an object.

$builder->add('media', 'sonata_media_type', array(
    'data_class' => 'Acme\DemoBundle\Entity\Media',
));
empty_data

type: mixed

The actual default value of this option depends on other field options:

  • If data_class is set and required is true, then new $data_class();
  • If data_class is set and required is false, then null;
  • If data_class is not set and compound is true, then array() (empty array);
  • If data_class is not set and compound is false, then '' (empty string).

This option determines what value the field will return when the submitted value is empty.

But you can customize this to your needs. For example, if you want the gender choice field to be explicitly set to null when no value is selected, you can do it like this:

$builder->add('gender', 'choice', array(
    'choices' => array(
        'm' => 'Male',
        'f' => 'Female'
    ),
    'required'    => false,
    'empty_value' => 'Choose your gender',
    'empty_data'  => null
));

注解

If you want to set the empty_data option for your entire form class, see the cookbook article How to Configure empty Data for a Form Class.

error_bubbling

type: Boolean default: false unless the form is compound

If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.

error_mapping

2.1 新版功能: The error_mapping option was introduced in Symfony 2.1.

type: array default: empty

This option allows you to modify the target of a validation error.

Imagine you have a custom method named matchingCityAndZipCode that validates whether the city and zip code match. Unfortunately, there is no “matchingCityAndZipCode” field in your form, so all that Symfony can do is display the error on top of the form.

With customized error mapping, you can do better: map the error to the city field so that it displays above it:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'error_mapping' => array(
            'matchingCityAndZipCode' => 'city',
        ),
    ));
}

Here are the rules for the left and the right side of the mapping:

  • The left side contains property paths;
  • If the violation is generated on a property or method of a class, its path is simply propertyName;
  • If the violation is generated on an entry of an array or ArrayAccess object, the property path is [indexName];
  • You can construct nested property paths by concatenating them, separating properties by dots. For example: addresses[work].matchingCityAndZipCode;
  • The left side of the error mapping also accepts a dot ., which refers to the field itself. That means that any error added to the field is added to the given nested field instead;
  • The right side contains simply the names of fields in the form.
extra_fields_message

type: string default: This form should not contain extra fields.

This is the validation error message that’s used if the submitted form data contains one or more fields that are not part of the form definition. The placeholder {{ extra_fields }} can be used to display a comma separated list of the submitted extra field names.

inherit_data

2.3 新版功能: The inherit_data option was introduced in Symfony 2.3. Before, it was known as virtual.

type: boolean default: false

This option determines if the form will inherit data from its parent form. This can be useful if you have a set of fields that are duplicated across multiple forms. See How to Reduce Code Duplication with “inherit_data”.

invalid_message

type: string default: This value is not valid

This is the validation error message that’s used if the data entered into this field doesn’t make sense (i.e. fails validation).

This might happen, for example, if the user enters a nonsense string into a time field that cannot be converted into a real time or if the user enters a string (e.g. apple) into a number field.

Normal (business logic) validation (such as when setting a minimum length for a field) should be set using validation messages with your validation rules (reference).

invalid_message_parameters

type: array default: array()

When setting the invalid_message option, you may need to include some variables in the string. This can be done by adding placeholders to that option and including the variables in this option:

$builder->add('some_field', 'some_type', array(
    // ...
    'invalid_message'            => 'You entered an invalid value - it should include %num% letters',
    'invalid_message_parameters' => array('%num%' => 6),
));
label_attr

type: array default: array()

Sets the HTML attributes for the <label> element, which will be used when rendering the label for the field. It’s an associative array with HTML attribute as a key. This attributes can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name', {'label_attr': {'class': 'CUSTOM_LABEL_CLASS'}}) }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name',
        array('label_attr' => array('class' => 'CUSTOM_LABEL_CLASS'))
    );
    
mapped

type: boolean default: true

If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.

max_length

type: integer default: null

If this option is not null, an attribute maxlength is added, which is used by some browsers to limit the amount of text in a field.

This is just a browser validation, so data must still be validated server-side.

method

2.3 新版功能: The method option was introduced in Symfony 2.3.

type: string default: POST

This option specifies the HTTP method used to submit the form’s data. Its value is rendered as the method attribute of the form element and is used to decide whether to process the form submission in the handleRequest() method after submission. Possible values are:

  • POST
  • GET
  • PUT
  • DELETE
  • PATCH

注解

When the method is PUT, PATCH, or DELETE, Symfony will automatically render a _method hidden field in your form. This is used to “fake” these HTTP methods, as they’re not supported on standard browsers. For more information, see How to Use HTTP Methods beyond GET and POST in Routes.

注解

The PATCH method allows submitting partial data. In other words, if the submitted form data is missing certain fields, those will be ignored and the default values (if any) will be used. With all other HTTP methods, if the submitted form data is missing some fields, those fields are set to null.

pattern

type: string default: null

This adds an HTML5 pattern attribute to restrict the field input by a given regular expression.

警告

The pattern attribute provides client-side validation for convenience purposes only and must not be used as a replacement for reliable server-side validation.

注解

When using validation constraints, this option is set automatically for some constraints to match the server-side validation.

post_max_size_message

type: string default: The uploaded file was too large. Please try to upload a smaller file.

This is the validation error message that’s used if submitted POST form data exceeds php.ini‘s post_max_size directive. The {{ max }} placeholder can be used to display the allowed size.

注解

Validating the post_max_size only happens on the root form.

property_path

type: any default: the field's name

Fields display a property value of the form’s domain object by default. When the form is submitted, the submitted value is written back into the object.

If you want to override the property that a field reads from and writes to, you can set the property_path option. Its default value is the field’s name.

If you wish the field to be ignored when reading or writing to the object you can set the property_path option to false, but using property_path for this purpose is deprecated, you should use the mapped option.

2.1 新版功能: The mapped option was introduced in Symfony 2.1 for this use-case.

read_only

type: Boolean default: false

If this option is true, the field will be rendered with the readonly attribute so that the field is not editable.

required

type: Boolean default: true

If true, an HTML5 required attribute will be rendered. The corresponding label will also render with a required class.

This is superficial and independent from validation. At best, if you let Symfony guess your field type, then the value of this option will be guessed from your validation information.

注解

The required option also affects how empty data for each field is handled. For more details, see the empty_data option.

trim

type: Boolean default: true

If true, the whitespace of the submitted string value will be stripped via the trim() function when the data is bound. This guarantees that if a value is submitted with extra whitespace, it will be removed before the value is merged back onto the underlying object.

Inherited Options

The following options are defined in the BaseType class. The BaseType class is the parent class for both the form type and the button type, but it is not part of the form type tree (i.e. it can not be used as a form type on its own).

attr

type: array default: Empty array

If you want to add extra attributes to an HTML field representation you can use the attr option. It’s an associative array with HTML attributes as keys. This can be useful when you need to set a custom class for some widget:

$builder->add('body', 'textarea', array(
    'attr' => array('class' => 'tinymce'),
));
auto_initialize

type: boolean default: true

An internal option: sets whether the form should be initialized automatically. For all fields, this option should only be true for root forms. You won’t need to change this option and probably won’t need to worry about it.

block_name

type: string default: the form’s name (see Knowing which block to customize)

Allows you to override the block name used to render the form type. Useful for example if you have multiple instances of the same form and you need to personalize the rendering of the forms individually.

disabled

2.1 新版功能: The disabled option was introduced in Symfony 2.1.

type: boolean default: false

If you don’t want a user to modify the value of a field, you can set the disabled option to true. Any submitted value will be ignored.

label

type: string default: The label is “guessed” from the field name

Sets the label that will be used when rendering the field. Setting to false will suppress the label. The label can also be directly set inside the template:

  • Twig
    {{ form_label(form.name, 'Your name') }}
    
  • PHP
    echo $view['form']->label(
        $form['name'],
        'Your name'
    );
    
translation_domain

type: string default: messages

This is the translation domain that will be used for any labels or options that are rendered for this field.

A form is composed of fields, each of which are built with the help of a field type (e.g. a text type, choice type, etc). Symfony comes standard with a large list of field types that can be used in your application.

Supported Field Types

The following field types are natively available in Symfony:

Date and Time Fields
Other Fields
Field Groups
Hidden Fields
Base Fields

Validation Constraints Reference

NotBlank

Validates that a value is not blank, defined as not equal to a blank string and also not equal to null. To force that a value is simply not equal to null, see the NotNull constraint.

Applies to property or method
Options
Class NotBlank
Validator NotBlankValidator
Basic Usage

If you wanted to ensure that the firstName property of an Author class were not blank, you could do the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            firstName:
                - NotBlank: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\NotBlank()
         */
        protected $firstName;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="firstName">
                <constraint name="NotBlank" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', new Assert\NotBlank());
        }
    }
    
Options
message

type: string default: This value should not be blank.

This is the message that will be shown if the value is blank.

Blank

Validates that a value is blank, defined as equal to a blank string or equal to null. To force that a value strictly be equal to null, see the Null constraint. To force that a value is not blank, see NotBlank.

Applies to property or method
Options
Class Blank
Validator BlankValidator
Basic Usage

If, for some reason, you wanted to ensure that the firstName property of an Author class were blank, you could do the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            firstName:
                - Blank: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Blank()
         */
        protected $firstName;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="firstName">
                <constraint name="Blank" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', new Assert\Blank());
        }
    }
    
Options
message

type: string default: This value should be blank.

This is the message that will be shown if the value is not blank.

NotNull

Validates that a value is not strictly equal to null. To ensure that a value is simply not blank (not a blank string), see the NotBlank constraint.

Applies to property or method
Options
Class NotNull
Validator NotNullValidator
Basic Usage

If you wanted to ensure that the firstName property of an Author class were not strictly equal to null, you would:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            firstName:
                - NotNull: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\NotNull()
         */
        protected $firstName;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="firstName">
                <constraint name="NotNull" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', new Assert\NotNull());
        }
    }
    
Options
message

type: string default: This value should not be null.

This is the message that will be shown if the value is null.

Null

Validates that a value is exactly equal to null. To force that a property is simply blank (blank string or null), see the Blank constraint. To ensure that a property is not null, see NotNull.

Applies to property or method
Options
Class Null
Validator NullValidator
Basic Usage

If, for some reason, you wanted to ensure that the firstName property of an Author class exactly equal to null, you could do the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            firstName:
                - 'Null': ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Null()
         */
        protected $firstName;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="firstName">
                <constraint name="Null" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', Assert\Null());
        }
    }
    

警告

When using YAML, be sure to surround Null with quotes ('Null') or else YAML will convert this into a null value.

Options
message

type: string default: This value should be null.

This is the message that will be shown if the value is not null.

True

Validates that a value is true. Specifically, this checks to see if the value is exactly true, exactly the integer 1, or exactly the string “1”.

Also see False.

Applies to property or method
Options
Class True
Validator TrueValidator
Basic Usage

This constraint can be applied to properties (e.g. a termsAccepted property on a registration model) or to a “getter” method. It’s most powerful in the latter case, where you can assert that a method returns a true value. For example, suppose you have the following method:

// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;

class Author
{
    protected $token;

    public function isTokenValid()
    {
        return $this->token == $this->generateToken();
    }
}

Then you can constrain this method with True.

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        getters:
            tokenValid:
                - 'True':
                    message: The token is invalid.
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        protected $token;
    
        /**
         * @Assert\True(message = "The token is invalid")
         */
        public function isTokenValid()
        {
            return $this->token == $this->generateToken();
        }
    }
    
  • XML
    <!-- src/Acme/Blogbundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <getter property="tokenValid">
                <constraint name="True">
                    <option name="message">The token is invalid.</option>
                </constraint>
            </getter>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\True;
    
    class Author
    {
        protected $token;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addGetterConstraint('tokenValid', new True(array(
                'message' => 'The token is invalid.',
            )));
        }
    
        public function isTokenValid()
        {
            return $this->token == $this->generateToken();
        }
    }
    

If the isTokenValid() returns false, the validation will fail.

警告

When using YAML, be sure to surround True with quotes ('True') or else YAML will convert this into a true Boolean value.

Options
message

type: string default: This value should be true.

This message is shown if the underlying data is not true.

False

Validates that a value is false. Specifically, this checks to see if the value is exactly false, exactly the integer 0, or exactly the string “0”.

Also see True.

Applies to property or method
Options
Class False
Validator FalseValidator
Basic Usage

The False constraint can be applied to a property or a “getter” method, but is most commonly useful in the latter case. For example, suppose that you want to guarantee that some state property is not in a dynamic invalidStates array. First, you’d create a “getter” method:

protected $state;

protected $invalidStates = array();

public function isStateInvalid()
{
    return in_array($this->state, $this->invalidStates);
}

In this case, the underlying object is only valid if the isStateInvalid method returns false:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author
        getters:
            stateInvalid:
                - 'False':
                    message: You've entered an invalid state.
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\False(
         *     message = "You've entered an invalid state."
         * )
         */
         public function isStateInvalid()
         {
            // ...
         }
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <getter property="stateInvalid">
                <constraint name="False">
                    <option name="message">You've entered an invalid state.</option>
                </constraint>
            </getter>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addGetterConstraint('stateInvalid', new Assert\False());
        }
    }
    

警告

When using YAML, be sure to surround False with quotes ('False') or else YAML will convert this into a false Boolean value.

Options
message

type: string default: This value should be false.

This message is shown if the underlying data is not false.

Type

Validates that a value is of a specific data type. For example, if a variable should be an array, you can use this constraint with the array type option to validate this.

Applies to property or method
Options
Class Type
Validator TypeValidator
Basic Usage
  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            age:
                - Type:
                    type: integer
                    message: The value {{ value }} is not a valid {{ type }}.
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Type(type="integer", message="The value {{ value }} is not a valid {{ type }}.")
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="age">
                <constraint name="Type">
                    <option name="type">integer</option>
                    <option name="message">The value {{ value }} is not a valid {{ type }}.</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\Type(array(
                'type'    => 'integer',
                'message' => 'The value {{ value }} is not a valid {{ type }}.',
            )));
        }
    }
    
Options
type

type: string [default option]

This required option is the fully qualified class name or one of the PHP datatypes as determined by PHP’s is_ functions.

Also, you can use ctype_ functions from corresponding built-in PHP extension. Consider a list of ctype functions:

Make sure that the proper locale is set before using one of these.

message

type: string default: This value should be of type {{ type }}.

The message if the underlying data is not of the given type.

Email

Validates that a value is a valid email address. The underlying value is cast to a string before being validated.

Applies to property or method
Options
Class Email
Validator EmailValidator
Basic Usage
  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            email:
                - Email:
                    message: The email "{{ value }}" is not a valid email.
                    checkMX: true
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Email(
         *     message = "The email '{{ value }}' is not a valid email.",
         *     checkMX = true
         * )
         */
         protected $email;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="email">
                <constraint name="Email">
                    <option name="message">The email "{{ value }}" is not a valid email.</option>
                    <option name="checkMX">true</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('email', new Assert\Email(array(
                'message' => 'The email "{{ value }}" is not a valid email.',
                'checkMX' => true,
            )));
        }
    }
    
Options
message

type: string default: This value is not a valid email address.

This message is shown if the underlying data is not a valid email address.

checkMX

type: Boolean default: false

If true, then the checkdnsrr PHP function will be used to check the validity of the MX record of the host of the given email.

checkHost

type: Boolean default: false

If true, then the checkdnsrr PHP function will be used to check the validity of the MX or the A or the AAAA record of the host of the given email.

Length

Validates that a given string length is between some minimum and maximum value.

Applies to property or method
Options
Class Length
Validator LengthValidator
Basic Usage

To verify that the firstName field length of a class is between “2” and “50”, you might add the following:

  • YAML
    # src/Acme/EventBundle/Resources/config/validation.yml
    Acme\EventBundle\Entity\Participant:
        properties:
            firstName:
                - Length:
                    min: 2
                    max: 50
                    minMessage: "Your first name must be at least {{ limit }} characters long"
                    maxMessage: "Your first name cannot be longer than {{ limit }} characters long"
    
  • Annotations
    // src/Acme/EventBundle/Entity/Participant.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Participant
    {
        /**
         * @Assert\Length(
         *      min = 2,
         *      max = 50,
         *      minMessage = "Your first name must be at least {{ limit }} characters long",
         *      maxMessage = "Your first name cannot be longer than {{ limit }} characters long"
         * )
         */
         protected $firstName;
    }
    
  • XML
    <!-- src/Acme/EventBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\EventBundle\Entity\Participant">
            <property name="firstName">
                <constraint name="Length">
                    <option name="min">2</option>
                    <option name="max">50</option>
                    <option name="minMessage">Your first name must be at least {{ limit }} characters long</option>
                    <option name="maxMessage">Your first name cannot be longer than {{ limit }} characters long</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/EventBundle/Entity/Participant.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Participant
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', new Assert\Length(array(
                'min'        => 2,
                'max'        => 50,
                'minMessage' => 'Your first name must be at least {{ limit }} characters long',
                'maxMessage' => 'Your first name cannot be longer than {{ limit }} characters long',
            )));
        }
    }
    
Options
min

type: integer

This required option is the “min” length value. Validation will fail if the given value’s length is less than this min value.

max

type: integer

This required option is the “max” length value. Validation will fail if the given value’s length is greater than this max value.

charset

type: string default: UTF-8

The charset to be used when computing value’s length. The grapheme_strlen PHP function is used if available. If not, the mb_strlen PHP function is used if available. If neither are available, the strlen PHP function is used.

minMessage

type: string default: This value is too short. It should have {{ limit }} characters or more.

The message that will be shown if the underlying value’s length is less than the min option.

maxMessage

type: string default: This value is too long. It should have {{ limit }} characters or less.

The message that will be shown if the underlying value’s length is more than the max option.

exactMessage

type: string default: This value should have exactly {{ limit }} characters.

The message that will be shown if min and max values are equal and the underlying value’s length is not exactly this value.

Url

Validates that a value is a valid URL string.

Applies to property or method
Options
Class Url
Validator UrlValidator
Basic Usage
  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            bioUrl:
                - Url: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Url()
         */
         protected $bioUrl;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="bioUrl">
                <constraint name="Url" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('bioUrl', new Assert\Url());
        }
    }
    
Options
message

type: string default: This value is not a valid URL.

This message is shown if the URL is invalid.

protocols

type: array default: array('http', 'https')

The protocols that will be considered to be valid. For example, if you also needed ftp:// type URLs to be valid, you’d redefine the protocols array, listing http, https, and also ftp.

Regex

Validates that a value matches a regular expression.

Applies to property or method
Options
Class Regex
Validator RegexValidator
Basic Usage

Suppose you have a description field and you want to verify that it begins with a valid word character. The regular expression to test for this would be /^\w+/, indicating that you’re looking for at least one or more word characters at the beginning of your string:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            description:
                - Regex: '/^\w+/'
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Regex("/^\w+/")
         */
        protected $description;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="description">
                <constraint name="Regex">
                    <option name="pattern">/^\w+/</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('description', new Assert\Regex(array(
                'pattern' => '/^\w+/',
            )));
        }
    }
    

Alternatively, you can set the match option to false in order to assert that a given string does not match. In the following example, you’ll assert that the firstName field does not contain any numbers and give it a custom message:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            firstName:
                - Regex:
                    pattern: '/\d/'
                    match:   false
                    message: Your name cannot contain a number
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Regex(
         *     pattern="/\d/",
         *     match=false,
         *     message="Your name cannot contain a number"
         * )
         */
        protected $firstName;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="firstName">
                <constraint name="Regex">
                    <option name="pattern">/\d/</option>
                    <option name="match">false</option>
                    <option name="message">Your name cannot contain a number</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', new Assert\Regex(array(
                'pattern' => '/\d/',
                'match'   => false,
                'message' => 'Your name cannot contain a number',
            )));
        }
    }
    
Options
pattern

type: string [default option]

This required option is the regular expression pattern that the input will be matched against. By default, this validator will fail if the input string does not match this regular expression (via the preg_match PHP function). However, if match is set to false, then validation will fail if the input string does match this pattern.

htmlPattern

2.1 新版功能: The htmlPattern option was introduced in Symfony 2.1

type: string|Boolean default: null

This option specifies the pattern to use in the HTML5 pattern attribute. You usually don’t need to specify this option because by default, the constraint will convert the pattern given in the pattern option into an HTML5 compatible pattern. This means that the delimiters are removed (e.g. /[a-z]+/ becomes [a-z]+).

However, there are some other incompatibilities between both patterns which cannot be fixed by the constraint. For instance, the HTML5 pattern attribute does not support flags. If you have a pattern like /[a-z]+/i, you need to specify the HTML5 compatible pattern in the htmlPattern option:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            name:
                - Regex:
                    pattern: "/^[a-z]+$/i"
                    htmlPattern: "^[a-zA-Z]+$"
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Regex(
         *     pattern     = "/^[a-z]+$/i",
         *     htmlPattern = "^[a-zA-Z]+$"
         * )
         */
        protected $name;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="name">
                <constraint name="Regex">
                    <option name="pattern">/^[a-z]+$/i</option>
                    <option name="htmlPattern">^[a-zA-Z]+$</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('name', new Assert\Regex(array(
                'pattern'     => '/^[a-z]+$/i',
                'htmlPattern' => '^[a-zA-Z]+$',
            )));
        }
    }
    

Setting htmlPattern to false will disable client side validation.

match

type: Boolean default: true

If true (or not set), this validator will pass if the given string matches the given pattern regular expression. However, when this option is set to false, the opposite will occur: validation will pass only if the given string does not match the pattern regular expression.

message

type: string default: This value is not valid.

This is the message that will be shown if this validator fails.

Ip

Validates that a value is a valid IP address. By default, this will validate the value as IPv4, but a number of different options exist to validate as IPv6 and many other combinations.

Applies to property or method
Options
Class Ip
Validator IpValidator
Basic Usage
  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            ipAddress:
                - Ip: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Ip
         */
         protected $ipAddress;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="ipAddress">
                <constraint name="Ip" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('ipAddress', new Assert\Ip());
        }
    }
    
Options
version

type: string default: 4

This determines exactly how the IP address is validated and can take one of a variety of different values:

All ranges

  • 4 - Validates for IPv4 addresses
  • 6 - Validates for IPv6 addresses
  • all - Validates all IP formats

No private ranges

  • 4_no_priv - Validates for IPv4 but without private IP ranges
  • 6_no_priv - Validates for IPv6 but without private IP ranges
  • all_no_priv - Validates for all IP formats but without private IP ranges

No reserved ranges

  • 4_no_res - Validates for IPv4 but without reserved IP ranges
  • 6_no_res - Validates for IPv6 but without reserved IP ranges
  • all_no_res - Validates for all IP formats but without reserved IP ranges

Only public ranges

  • 4_public - Validates for IPv4 but without private and reserved ranges
  • 6_public - Validates for IPv6 but without private and reserved ranges
  • all_public - Validates for all IP formats but without private and reserved ranges
message

type: string default: This is not a valid IP address.

This message is shown if the string is not a valid IP address.

Range

Validates that a given number is between some minimum and maximum number.

Applies to property or method
Options
Class Range
Validator RangeValidator
Basic Usage

To verify that the “height” field of a class is between “120” and “180”, you might add the following:

  • YAML
    # src/Acme/EventBundle/Resources/config/validation.yml
    Acme\EventBundle\Entity\Participant:
        properties:
            height:
                - Range:
                    min: 120
                    max: 180
                    minMessage: You must be at least {{ limit }}cm tall to enter
                    maxMessage: You cannot be taller than {{ limit }}cm to enter
    
  • Annotations
    // src/Acme/EventBundle/Entity/Participant.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Participant
    {
        /**
         * @Assert\Range(
         *      min = 120,
         *      max = 180,
         *      minMessage = "You must be at least {{ limit }}cm tall to enter",
         *      maxMessage = "You cannot be taller than {{ limit }}cm to enter"
         * )
         */
         protected $height;
    }
    
  • XML
    <!-- src/Acme/EventBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\EventBundle\Entity\Participant">
            <property name="height">
                <constraint name="Range">
                    <option name="min">120</option>
                    <option name="max">180</option>
                    <option name="minMessage">You must be at least {{ limit }}cm tall to enter</option>
                    <option name="maxMessage">You cannot be taller than {{ limit }}cm to enter</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/EventBundle/Entity/Participant.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Participant
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('height', new Assert\Range(array(
                'min'        => 120,
                'max'        => 180,
                'minMessage' => 'You must be at least {{ limit }}cm tall to enter',
                'maxMessage' => 'You cannot be taller than {{ limit }}cm to enter',
            )));
        }
    }
    
Options
min

type: integer

This required option is the “min” value. Validation will fail if the given value is less than this min value.

max

type: integer

This required option is the “max” value. Validation will fail if the given value is greater than this max value.

minMessage

type: string default: This value should be {{ limit }} or more.

The message that will be shown if the underlying value is less than the min option.

maxMessage

type: string default: This value should be {{ limit }} or less.

The message that will be shown if the underlying value is more than the max option.

invalidMessage

type: string default: This value should be a valid number.

The message that will be shown if the underlying value is not a number (per the is_numeric PHP function).

EqualTo

2.3 新版功能: The EqualTo constraint was introduced in Symfony 2.3.

Validates that a value is equal to another value, defined in the options. To force that a value is not equal, see NotEqualTo.

警告

This constraint compares using ==, so 3 and "3" are considered equal. Use IdenticalTo to compare with ===.

Applies to property or method
Options
Class EqualTo
Validator EqualToValidator
Basic Usage

If you want to ensure that the age of a Person class is equal to 20, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - EqualTo:
                    value: 20
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\EqualTo(
         *     value = 20
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="EqualTo">
                    <option name="value">20</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\EqualTo(array(
                'value' => 20,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should be equal to {{ compared_value }}.

This is the message that will be shown if the value is not equal.

NotEqualTo

2.3 新版功能: The NotEqualTo constraint was introduced in Symfony 2.3.

Validates that a value is not equal to another value, defined in the options. To force that a value is equal, see EqualTo.

警告

This constraint compares using !=, so 3 and "3" are considered equal. Use NotIdenticalTo to compare with !==.

Applies to property or method
Options
Class NotEqualTo
Validator NotEqualToValidator
Basic Usage

If you want to ensure that the age of a Person class is not equal to 15, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - NotEqualTo:
                    value: 15
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\NotEqualTo(
         *     value = 15
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="NotEqualTo">
                    <option name="value">15</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\NotEqualTo(array(
                'value' => 15,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should not be equal to {{ compared_value }}.

This is the message that will be shown if the value is equal.

IdenticalTo

2.3 新版功能: The IdenticalTo constraint was introduced in Symfony 2.3.

Validates that a value is identical to another value, defined in the options. To force that a value is not identical, see NotIdenticalTo.

警告

This constraint compares using ===, so 3 and "3" are not considered equal. Use EqualTo to compare with ==.

Applies to property or method
Options
Class IdenticalTo
Validator IdenticalToValidator
Basic Usage

If you want to ensure that the age of a Person class is equal to 20 and an integer, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - IdenticalTo:
                    value: 20
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\IdenticalTo(
         *     value = 20
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="IdenticalTo">
                    <option name="value">20</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\IdenticalTo(array(
                'value' => 20,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should be identical to {{ compared_value_type }} {{ compared_value }}.

This is the message that will be shown if the value is not identical.

NotIdenticalTo

2.3 新版功能: The NotIdenticalTo constraint was introduced in Symfony 2.3.

Validates that a value is not identical to another value, defined in the options. To force that a value is identical, see IdenticalTo.

警告

This constraint compares using !==, so 3 and "3" are considered not equal. Use NotEqualTo to compare with !=.

Applies to property or method
Options
Class NotIdenticalTo
Validator NotIdenticalToValidator
Basic Usage

If you want to ensure that the age of a Person class is not equal to 15 and not an integer, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - NotIdenticalTo:
                    value: 15
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\NotIdenticalTo(
         *     value = 15
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="NotIdenticalTo">
                    <option name="value">15</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo(array(
                'value' => 15,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should not be identical to {{ compared_value_type }} {{ compared_value }}.

This is the message that will be shown if the value is not equal.

LessThan

2.3 新版功能: The LessThan constraint was introduced in Symfony 2.3.

Validates that a value is less than another value, defined in the options. To force that a value is less than or equal to another value, see LessThanOrEqual. To force a value is greater than another value, see GreaterThan.

Applies to property or method
Options
Class LessThan
Validator LessThanValidator
Basic Usage

If you want to ensure that the age of a Person class is less than 80, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - LessThan:
                    value: 80
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\LessThan(
         *     value = 80
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="LessThan">
                    <option name="value">80</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\LessThan(array(
                'value' => 80,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should be less than {{ compared_value }}.

This is the message that will be shown if the value is not less than the comparison value.

LessThanOrEqual

2.3 新版功能: The LessThanOrEqual constraint was introduced in Symfony 2.3.

Validates that a value is less than or equal to another value, defined in the options. To force that a value is less than another value, see LessThan.

Applies to property or method
Options
Class LessThanOrEqual
Validator LessThanOrEqualValidator
Basic Usage

If you want to ensure that the age of a Person class is less than or equal to 80, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - LessThanOrEqual:
                    value: 80
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\LessThanOrEqual(
         *     value = 80
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="LessThanOrEqual">
                    <option name="value">80</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\LessThanOrEqual(array(
                'value' => 80,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should be less than or equal to {{ compared_value }}.

This is the message that will be shown if the value is not less than or equal to the comparison value.

GreaterThan

2.3 新版功能: The GreaterThan constraint was introduced in Symfony 2.3.

Validates that a value is greater than another value, defined in the options. To force that a value is greater than or equal to another value, see GreaterThanOrEqual. To force a value is less than another value, see LessThan.

Applies to property or method
Options
Class GreaterThan
Validator GreaterThanValidator
Basic Usage

If you want to ensure that the age of a Person class is greater than 18, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - GreaterThan:
                    value: 18
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\GreaterThan(
         *     value = 18
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="GreaterThan">
                    <option name="value">18</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\GreaterThan(array(
                'value' => 18,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should be greater than {{ compared_value }}.

This is the message that will be shown if the value is not greater than the comparison value.

GreaterThanOrEqual

2.3 新版功能: The GreaterThanOrEqual constraint was introduced in Symfony 2.3.

Validates that a value is greater than or equal to another value, defined in the options. To force that a value is greater than another value, see GreaterThan.

Applies to property or method
Options
Class GreaterThanOrEqual
Validator GreaterThanOrEqualValidator
Basic Usage

If you want to ensure that the age of a Person class is greater than or equal to 18, you could do the following:

  • YAML
    # src/Acme/SocialBundle/Resources/config/validation.yml
    Acme\SocialBundle\Entity\Person:
        properties:
            age:
                - GreaterThanOrEqual:
                    value: 18
    
  • Annotations
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        /**
         * @Assert\GreaterThanOrEqual(
         *     value = 18
         * )
         */
        protected $age;
    }
    
  • XML
    <!-- src/Acme/SocialBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SocialBundle\Entity\Person">
            <property name="age">
                <constraint name="GreaterThanOrEqual">
                    <option name="value">18</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SocialBundle/Entity/Person.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Person
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('age', new Assert\GreaterThanOrEqual(array(
                'value' => 18,
            )));
        }
    }
    
Options
value

type: mixed [default option]

This option is required. It defines the value to compare to. It can be a string, number or object.

message

type: string default: This value should be greater than or equal to {{ compared_value }}.

This is the message that will be shown if the value is not greater than or equal to the comparison value.

Date

Validates that a value is a valid date, meaning either a DateTime object or a string (or an object that can be cast into a string) that follows a valid YYYY-MM-DD format.

Applies to property or method
Options
Class Date
Validator DateValidator
Basic Usage
  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            birthday:
                - Date: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Date()
         */
         protected $birthday;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="birthday">
                <constraint name="Date" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('birthday', new Assert\Date());
        }
    }
    
Options
message

type: string default: This value is not a valid date.

This message is shown if the underlying data is not a valid date.

DateTime

Validates that a value is a valid “datetime”, meaning either a DateTime object or a string (or an object that can be cast into a string) that follows a valid YYYY-MM-DD HH:MM:SS format.

Applies to property or method
Options
Class DateTime
Validator DateTimeValidator
Basic Usage
  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            createdAt:
                - DateTime: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\DateTime()
         */
         protected $createdAt;
    }
    
  • XML
    <!-- src/Acme/UserBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="createdAt">
                <constraint name="DateTime" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('createdAt', new Assert\DateTime());
        }
    }
    
Options
message

type: string default: This value is not a valid datetime.

This message is shown if the underlying data is not a valid datetime.

Time

Validates that a value is a valid time, meaning either a DateTime object or a string (or an object that can be cast into a string) that follows a valid “HH:MM:SS” format.

Applies to property or method
Options
Class Time
Validator TimeValidator
Basic Usage

Suppose you have an Event class, with a startAt field that is the time of the day when the event starts:

  • YAML
    # src/Acme/EventBundle/Resources/config/validation.yml
    Acme\EventBundle\Entity\Event:
        properties:
            startsAt:
                - Time: ~
    
  • Annotations
    // src/Acme/EventBundle/Entity/Event.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Event
    {
        /**
         * @Assert\Time()
         */
         protected $startsAt;
    }
    
  • XML
    <!-- src/Acme/EventBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\EventBundle\Entity\Event">
            <property name="startsAt">
                <constraint name="Time" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/EventBundle/Entity/Event.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Event
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('startsAt', new Assert\Time());
        }
    }
    
Options
message

type: string default: This value is not a valid time.

This message is shown if the underlying data is not a valid time.

Choice

This constraint is used to ensure that the given value is one of a given set of valid choices. It can also be used to validate that each item in an array of items is one of those valid choices.

Applies to property or method
Options
Class Choice
Validator ChoiceValidator
Basic Usage

The basic idea of this constraint is that you supply it with an array of valid values (this can be done in several ways) and it validates that the value of the given property exists in that array.

If your valid choice list is simple, you can pass them in directly via the choices option:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            gender:
                - Choice:
                    choices:  [male, female]
                    message:  Choose a valid gender.
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Choice(choices = {"male", "female"}, message = "Choose a valid gender.")
         */
        protected $gender;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="gender">
                <constraint name="Choice">
                    <option name="choices">
                        <value>male</value>
                        <value>female</value>
                    </option>
                    <option name="message">Choose a valid gender.</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/EntityAuthor.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        protected $gender;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('gender', new Assert\Choice(array(
                'choices' => array('male', 'female'),
                'message' => 'Choose a valid gender.',
            )));
        }
    }
    
Supplying the Choices with a Callback Function

You can also use a callback function to specify your options. This is useful if you want to keep your choices in some central location so that, for example, you can easily access those choices for validation or for building a select form element.

// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;

class Author
{
    public static function getGenders()
    {
        return array('male', 'female');
    }
}

You can pass the name of this method to the callback option of the Choice constraint.

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            gender:
                - Choice: { callback: getGenders }
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Choice(callback = "getGenders")
         */
        protected $gender;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="gender">
                <constraint name="Choice">
                    <option name="callback">getGenders</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/EntityAuthor.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        protected $gender;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('gender', new Assert\Choice(array(
                'callback' => 'getGenders',
            )));
        }
    }
    

If the static callback is stored in a different class, for example Util, you can pass the class name and the method as an array.

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            gender:
                - Choice: { callback: [Util, getGenders] }
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Choice(callback = {"Util", "getGenders"})
         */
        protected $gender;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="gender">
                <constraint name="Choice">
                    <option name="callback">
                        <value>Util</value>
                        <value>getGenders</value>
                    </option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/EntityAuthor.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        protected $gender;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('gender', new Assert\Choice(array(
                'callback' => array('Util', 'getGenders'),
            )));
        }
    }
    
Available Options
choices

type: array [default option]

A required option (unless callback is specified) - this is the array of options that should be considered in the valid set. The input value will be matched against this array.

callback

type: string|array|Closure

This is a callback method that can be used instead of the choices option to return the choices array. See Supplying the Choices with a Callback Function for details on its usage.

multiple

type: Boolean default: false

If this option is true, the input value is expected to be an array instead of a single, scalar value. The constraint will check that each value of the input array can be found in the array of valid choices. If even one of the input values cannot be found, the validation will fail.

min

type: integer

If the multiple option is true, then you can use the min option to force at least XX number of values to be selected. For example, if min is 3, but the input array only contains 2 valid items, the validation will fail.

max

type: integer

If the multiple option is true, then you can use the max option to force no more than XX number of values to be selected. For example, if max is 3, but the input array contains 4 valid items, the validation will fail.

message

type: string default: The value you selected is not a valid choice.

This is the message that you will receive if the multiple option is set to false, and the underlying value is not in the valid array of choices.

multipleMessage

type: string default: One or more of the given values is invalid.

This is the message that you will receive if the multiple option is set to true, and one of the values on the underlying array being checked is not in the array of valid choices.

minMessage

type: string default: You must select at least {{ limit }} choices.

This is the validation error message that’s displayed when the user chooses too few choices per the min option.

maxMessage

type: string default: You must select at most {{ limit }} choices.

This is the validation error message that’s displayed when the user chooses too many options per the max option.

strict

type: Boolean default: false

If true, the validator will also check the type of the input value. Specifically, this value is passed to as the third argument to the PHP in_array method when checking to see if a value is in the valid choices array.

Collection

This constraint is used when the underlying data is a collection (i.e. an array or an object that implements Traversable and ArrayAccess), but you’d like to validate different keys of that collection in different ways. For example, you might validate the email key using the Email constraint and the inventory key of the collection with the Range constraint.

This constraint can also make sure that certain collection keys are present and that extra keys are not present.

Applies to property or method
Options
Class Collection
Validator CollectionValidator
Basic Usage

The Collection constraint allows you to validate the different keys of a collection individually. Take the following example:

// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;

class Author
{
    protected $profileData = array(
        'personal_email',
        'short_bio',
    );

    public function setProfileData($key, $value)
    {
        $this->profileData[$key] = $value;
    }
}

To validate that the personal_email element of the profileData array property is a valid email address and that the short_bio element is not blank but is no longer than 100 characters in length, you would do the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            profileData:
                - Collection:
                    fields:
                        personal_email: Email
                        short_bio:
                            - NotBlank
                            - Length:
                                max:   100
                                maxMessage: Your short bio is too long!
                    allowMissingFields: true
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Collection(
         *     fields = {
         *         "personal_email" = @Assert\Email,
         *         "short_bio" = {
         *             @Assert\NotBlank(),
         *             @Assert\Length(
         *                 max = 100,
         *                 maxMessage = "Your short bio is too long!"
         *             )
         *         }
         *     },
         *     allowMissingFields = true
         * )
         */
         protected $profileData = array(
             'personal_email',
             'short_bio',
         );
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="profileData">
                <constraint name="Collection">
                    <option name="fields">
                        <value key="personal_email">
                            <constraint name="Email" />
                        </value>
                        <value key="short_bio">
                            <constraint name="NotBlank" />
                            <constraint name="Length">
                                <option name="max">100</option>
                                <option name="maxMessage">Your short bio is too long!</option>
                            </constraint>
                        </value>
                    </option>
                    <option name="allowMissingFields">true</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        private $options = array();
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('profileData', new Assert\Collection(array(
                'fields' => array(
                    'personal_email' => new Assert\Email(),
                    'short_bio' => array(
                        new Assert\NotBlank(),
                        new Assert\Length(array(
                            'max' => 100,
                            'maxMessage' => 'Your short bio is too long!',
                        )),
                    ),
                ),
                'allowMissingFields' => true,
            )));
        }
    }
    
Presence and Absence of Fields

By default, this constraint validates more than simply whether or not the individual fields in the collection pass their assigned constraints. In fact, if any keys of a collection are missing or if there are any unrecognized keys in the collection, validation errors will be thrown.

If you would like to allow for keys to be absent from the collection or if you would like “extra” keys to be allowed in the collection, you can modify the allowMissingFields and allowExtraFields options respectively. In the above example, the allowMissingFields option was set to true, meaning that if either of the personal_email or short_bio elements were missing from the $personalData property, no validation error would occur.

Required and optional Field Constraints

2.3 新版功能: The Required and Optional constraints were moved to the namespace Symfony\Component\Validator\Constraints\ in Symfony 2.3.

Constraints for fields within a collection can be wrapped in the Required or Optional constraint to control whether they should always be applied (Required) or only applied when the field is present (Optional).

For instance, if you want to require that the personal_email field of the profileData array is not blank and is a valid email but the alternate_email field is optional but must be a valid email if supplied, you can do the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            profile_data:
                - Collection:
                    fields:
                        personal_email:
                            - Required
                                - NotBlank: ~
                                - Email: ~
                        alternate_email:
                            - Optional:
                                - Email: ~
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Collection(
         *     fields={
         *         "personal_email"  = @Assert\Required({@Assert\NotBlank, @Assert\Email}),
         *         "alternate_email" = @Assert\Optional(@Assert\Email)
         *     }
         * )
         */
         protected $profileData = array('personal_email');
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="profile_data">
                <constraint name="Collection">
                    <option name="fields">
                        <value key="personal_email">
                            <constraint name="Required">
                                <constraint name="NotBlank" />
                                <constraint name="Email" />
                            </constraint>
                        </value>
                        <value key="alternate_email">
                            <constraint name="Optional">
                                <constraint name="Email" />
                            </constraint>
                        </value>
                    </option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        protected $profileData = array('personal_email');
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('profileData', new Assert\Collection(array(
                'fields' => array(
                    'personal_email'  => new Assert\Required(array(new Assert\NotBlank(), new Assert\Email())),
                    'alternate_email' => new Assert\Optional(new Assert\Email()),
                ),
            )));
        }
    }
    

Even without allowMissingFields set to true, you can now omit the alternate_email property completely from the profileData array, since it is Optional. However, if the personal_email field does not exist in the array, the NotBlank constraint will still be applied (since it is wrapped in Required) and you will receive a constraint violation.

Options
fields

type: array [default option]

This option is required, and is an associative array defining all of the keys in the collection and, for each key, exactly which validator(s) should be executed against that element of the collection.

allowExtraFields

type: Boolean default: false

If this option is set to false and the underlying collection contains one or more elements that are not included in the fields option, a validation error will be returned. If set to true, extra fields are ok.

extraFieldsMessage

type: Boolean default: The fields {{ fields }} were not expected.

The message shown if allowExtraFields is false and an extra field is detected.

allowMissingFields

type: Boolean default: false

If this option is set to false and one or more fields from the fields option are not present in the underlying collection, a validation error will be returned. If set to true, it’s ok if some fields in the fields option are not present in the underlying collection.

missingFieldsMessage

type: Boolean default: The fields {{ fields }} are missing.

The message shown if allowMissingFields is false and one or more fields are missing from the underlying collection.

Count

Validates that a given collection’s (i.e. an array or an object that implements Countable) element count is between some minimum and maximum value.

Applies to property or method
Options
Class Count
Validator CountValidator
Basic Usage

To verify that the emails array field contains between 1 and 5 elements you might add the following:

  • YAML
    # src/Acme/EventBundle/Resources/config/validation.yml
    Acme\EventBundle\Entity\Participant:
        properties:
            emails:
                - Count:
                    min: 1
                    max: 5
                    minMessage: "You must specify at least one email"
                    maxMessage: "You cannot specify more than {{ limit }} emails"
    
  • Annotations
    // src/Acme/EventBundle/Entity/Participant.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Participant
    {
        /**
         * @Assert\Count(
         *      min = "1",
         *      max = "5",
         *      minMessage = "You must specify at least one email",
         *      maxMessage = "You cannot specify more than {{ limit }} emails"
         * )
         */
         protected $emails = array();
    }
    
  • XML
    <!-- src/Acme/EventBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\EventBundle\Entity\Participant">
            <property name="emails">
                <constraint name="Count">
                    <option name="min">1</option>
                    <option name="max">5</option>
                    <option name="minMessage">You must specify at least one email</option>
                    <option name="maxMessage">You cannot specify more than {{ limit }} emails</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/EventBundle/Entity/Participant.php
    namespace Acme\EventBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Participant
    {
        public static function loadValidatorMetadata(ClassMetadata $data)
        {
            $metadata->addPropertyConstraint('emails', new Assert\Count(array(
                'min'        => 1,
                'max'        => 5,
                'minMessage' => 'You must specify at least one email',
                'maxMessage' => 'You cannot specify more than {{ limit }} emails',
            )));
        }
    }
    
Options
min

type: integer

This required option is the “min” count value. Validation will fail if the given collection elements count is less than this min value.

max

type: integer

This required option is the “max” count value. Validation will fail if the given collection elements count is greater than this max value.

minMessage

type: string default: This collection should contain {{ limit }} elements or more.

The message that will be shown if the underlying collection elements count is less than the min option.

maxMessage

type: string default: This collection should contain {{ limit }} elements or less.

The message that will be shown if the underlying collection elements count is more than the max option.

exactMessage

type: string default: This collection should contain exactly {{ limit }} elements.

The message that will be shown if min and max values are equal and the underlying collection elements count is not exactly this value.

UniqueEntity

Validates that a particular field (or fields) in a Doctrine entity is (are) unique. This is commonly used, for example, to prevent a new user to register using an email address that already exists in the system.

Applies to class
Options
Class UniqueEntity
Validator UniqueEntityValidator
Basic Usage

Suppose you have an AcmeUserBundle bundle with a User entity that has an email field. You can use the UniqueEntity constraint to guarantee that the email field remains unique between all of the constraints in your user table:

  • YAML
    # src/Acme/UserBundle/Resources/config/validation.yml
    Acme\UserBundle\Entity\Author:
        constraints:
            - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: email
        properties:
            email:
                - Email: ~
    
  • Annotations
    // Acme/UserBundle/Entity/Author.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    use Doctrine\ORM\Mapping as ORM;
    
    // DON'T forget this use statement!!!
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    
    /**
     * @ORM\Entity
     * @UniqueEntity("email")
     */
    class Author
    {
        /**
         * @var string $email
         *
         * @ORM\Column(name="email", type="string", length=255, unique=true)
         * @Assert\Email()
         */
        protected $email;
    
        // ...
    }
    
  • XML
    <!-- src/Acme/AdministrationBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\UserBundle\Entity\Author">
            <constraint name="Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity">
                <option name="fields">email</option>
            </constraint>
            <property name="email">
                <constraint name="Email" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    // DON'T forget this use statement!!!
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addConstraint(new UniqueEntity(array(
                'fields'  => 'email',
            )));
    
            $metadata->addPropertyConstraint('email', new Assert\Email());
        }
    }
    
Options
fields

type: array | string [default option]

This required option is the field (or list of fields) on which this entity should be unique. For example, if you specified both the email and name field in a single UniqueEntity constraint, then it would enforce that the combination value where unique (e.g. two users could have the same email, as long as they don’t have the same name also).

If you need to require two fields to be individually unique (e.g. a unique email and a unique username), you use two UniqueEntity entries, each with a single field.

message

type: string default: This value is already used.

The message that’s displayed when this constraint fails.

em

type: string

The name of the entity manager to use for making the query to determine the uniqueness. If it’s left blank, the correct entity manager will be determined for this class. For that reason, this option should probably not need to be used.

repositoryMethod

type: string default: findBy

The name of the repository method to use for making the query to determine the uniqueness. If it’s left blank, the findBy method will be used. This method should return a countable result.

errorPath

type: string default: The name of the first field in fields

2.1 新版功能: The errorPath option was introduced in Symfony 2.1.

If the entity violates the constraint the error message is bound to the first field in fields. If there is more than one field, you may want to map the error message to another field.

Consider this example:

  • YAML
    # src/Acme/AdministrationBundle/Resources/config/validation.yml
    Acme\AdministrationBundle\Entity\Service:
        constraints:
            - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
                fields: [host, port]
                errorPath: port
                message: 'This port is already in use on that host.'
    
  • Annotations
    // src/Acme/AdministrationBundle/Entity/Service.php
    namespace Acme\AdministrationBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    
    /**
     * @ORM\Entity
     * @UniqueEntity(
     *     fields={"host", "port"},
     *     errorPath="port",
     *     message="This port is already in use on that host."
     * )
     */
    class Service
    {
        /**
         * @ORM\ManyToOne(targetEntity="Host")
         */
        public $host;
    
        /**
         * @ORM\Column(type="integer")
         */
        public $port;
    }
    
  • XML
    <!-- src/Acme/AdministrationBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\AdministrationBundle\Entity\Service">
            <constraint name="Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity">
                <option name="fields">
                    <value>host</value>
                    <value>port</value>
                </option>
                <option name="errorPath">port</option>
                <option name="message">This port is already in use on that host.</option>
            </constraint>
        </class>
    
    </constraint-mapping>
    
  • PHP
    // src/Acme/AdministrationBundle/Entity/Service.php
    namespace Acme\AdministrationBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    
    class Service
    {
        public $host;
        public $port;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addConstraint(new UniqueEntity(array(
                'fields'    => array('host', 'port'),
                'errorPath' => 'port',
                'message'   => 'This port is already in use on that host.',
            )));
        }
    }
    

Now, the message would be bound to the port field with this configuration.

ignoreNull

type: Boolean default: true

2.1 新版功能: The ignoreNull option was introduced in Symfony 2.1.

If this option is set to true, then the constraint will allow multiple entities to have a null value for a field without failing validation. If set to false, only one null value is allowed - if a second entity also has a null value, validation would fail.

Language

Validates that a value is a valid language Unicode language identifier (e.g. fr or zh-Hant).

Applies to property or method
Options
Class Language
Validator LanguageValidator
Basic Usage
  • YAML
    # src/Acme/UserBundle/Resources/config/validation.yml
    Acme\UserBundle\Entity\User:
        properties:
            preferredLanguage:
                - Language: ~
    
  • Annotations
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        /**
         * @Assert\Language()
         */
         protected $preferredLanguage;
    }
    
  • XML
    <!-- src/Acme/UserBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\UserBundle\Entity\User">
            <property name="preferredLanguage">
                <constraint name="Language" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('preferredLanguage', new Assert\Language());
        }
    }
    
Options
message

type: string default: This value is not a valid language.

This message is shown if the string is not a valid language code.

Locale

Validates that a value is a valid locale.

The “value” for each locale is either the two letter ISO 639-1 language code (e.g. fr), or the language code followed by an underscore (_), then the ISO 3166-1 alpha-2 country code (e.g. fr_FR for French/France).

Applies to property or method
Options
Class Locale
Validator LocaleValidator
Basic Usage
  • YAML
    # src/Acme/UserBundle/Resources/config/validation.yml
    Acme\UserBundle\Entity\User:
        properties:
            locale:
                - Locale: ~
    
  • Annotations
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        /**
         * @Assert\Locale()
         */
         protected $locale;
    }
    
  • XML
    <!-- src/Acme/UserBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\UserBundle\Entity\User">
            <property name="locale">
                <constraint name="Locale" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('locale', new Assert\Locale());
        }
    }
    
Options
message

type: string default: This value is not a valid locale.

This message is shown if the string is not a valid locale.

Country

Validates that a value is a valid ISO 3166-1 alpha-2 country code.

Applies to property or method
Options
Class Country
Validator CountryValidator
Basic Usage
  • YAML
    # src/Acme/UserBundle/Resources/config/validation.yml
    Acme\UserBundle\Entity\User:
        properties:
            country:
                - Country: ~
    
  • Annotations
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        /**
         * @Assert\Country()
         */
         protected $country;
    }
    
  • XML
    <!-- src/Acme/UserBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\UserBundle\Entity\User">
            <property name="country">
                <constraint name="Country" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        public static function loadValidationMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('country', new Assert\Country());
        }
    }
    
Options
message

type: string default: This value is not a valid country.

This message is shown if the string is not a valid country code.

File

Validates that a value is a valid “file”, which can be one of the following:

  • A string (or object with a __toString() method) path to an existing file;
  • A valid File object (including objects of class UploadedFile).

This constraint is commonly used in forms with the file form type.

小技巧

If the file you’re validating is an image, try the Image constraint.

Applies to property or method
Options
Class File
Validator FileValidator
Basic Usage

This constraint is most commonly used on a property that will be rendered in a form as a file form type. For example, suppose you’re creating an author form where you can upload a “bio” PDF for the author. In your form, the bioFile property would be a file type. The Author class might look as follows:

// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;

use Symfony\Component\HttpFoundation\File\File;

class Author
{
    protected $bioFile;

    public function setBioFile(File $file = null)
    {
        $this->bioFile = $file;
    }

    public function getBioFile()
    {
        return $this->bioFile;
    }
}

To guarantee that the bioFile File object is valid, and that it is below a certain file size and a valid PDF, add the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        properties:
            bioFile:
                - File:
                    maxSize: 1024k
                    mimeTypes: [application/pdf, application/x-pdf]
                    mimeTypesMessage: Please upload a valid PDF
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\File(
         *     maxSize = "1024k",
         *     mimeTypes = {"application/pdf", "application/x-pdf"},
         *     mimeTypesMessage = "Please upload a valid PDF"
         * )
         */
        protected $bioFile;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="bioFile">
                <constraint name="File">
                    <option name="maxSize">1024k</option>
                    <option name="mimeTypes">
                        <value>application/pdf</value>
                        <value>application/x-pdf</value>
                    </option>
                    <option name="mimeTypesMessage">Please upload a valid PDF</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('bioFile', new Assert\File(array(
                'maxSize' => '1024k',
                'mimeTypes' => array(
                    'application/pdf',
                    'application/x-pdf',
                ),
                'mimeTypesMessage' => 'Please upload a valid PDF',
            )));
        }
    }
    

The bioFile property is validated to guarantee that it is a real file. Its size and mime type are also validated because the appropriate options have been specified.

Options
maxSize

type: mixed

If set, the size of the underlying file must be below this file size in order to be valid. The size of the file can be given in one of the following formats:

  • bytes: To specify the maxSize in bytes, pass a value that is entirely numeric (e.g. 4096);
  • kilobytes: To specify the maxSize in kilobytes, pass a number and suffix it with a lowercase “k” (e.g. 200k);
  • megabytes: To specify the maxSize in megabytes, pass a number and suffix it with a capital “M” (e.g. 4M).
mimeTypes

type: array or string

If set, the validator will check that the mime type of the underlying file is equal to the given mime type (if a string) or exists in the collection of given mime types (if an array).

You can find a list of existing mime types on the IANA website.

maxSizeMessage

type: string default: The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.

The message displayed if the file is larger than the maxSize option.

mimeTypesMessage

type: string default: The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.

The message displayed if the mime type of the file is not a valid mime type per the mimeTypes option.

notFoundMessage

type: string default: The file could not be found.

The message displayed if no file can be found at the given path. This error is only likely if the underlying value is a string path, as a File object cannot be constructed with an invalid file path.

notReadableMessage

type: string default: The file is not readable.

The message displayed if the file exists, but the PHP is_readable function fails when passed the path to the file.

uploadIniSizeErrorMessage

type: string default: The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.

The message that is displayed if the uploaded file is larger than the upload_max_filesize php.ini setting.

uploadFormSizeErrorMessage

type: string default: The file is too large.

The message that is displayed if the uploaded file is larger than allowed by the HTML file input field.

uploadErrorMessage

type: string default: The file could not be uploaded.

The message that is displayed if the uploaded file could not be uploaded for some unknown reason, such as the file upload failed or it couldn’t be written to disk.

Image

The Image constraint works exactly like the File constraint, except that its mimeTypes and mimeTypesMessage options are automatically setup to work for image files specifically.

Additionally, as of Symfony 2.1, it has options so you can validate against the width and height of the image.

See the File constraint for the bulk of the documentation on this constraint.

Applies to property or method
Options
Class Image
Validator ImageValidator
Basic Usage

This constraint is most commonly used on a property that will be rendered in a form as a file form type. For example, suppose you’re creating an author form where you can upload a “headshot” image for the author. In your form, the headshot property would be a file type. The Author class might look as follows:

// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;

use Symfony\Component\HttpFoundation\File\File;

class Author
{
    protected $headshot;

    public function setHeadshot(File $file = null)
    {
        $this->headshot = $file;
    }

    public function getHeadshot()
    {
        return $this->headshot;
    }
}

To guarantee that the headshot File object is a valid image and that it is between a certain size, add the following:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author
        properties:
            headshot:
                - Image:
                    minWidth: 200
                    maxWidth: 400
                    minHeight: 200
                    maxHeight: 400
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Image(
         *     minWidth = 200,
         *     maxWidth = 400,
         *     minHeight = 200,
         *     maxHeight = 400
         * )
         */
        protected $headshot;
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <property name="headshot">
                <constraint name="Image">
                    <option name="minWidth">200</option>
                    <option name="maxWidth">400</option>
                    <option name="minHeight">200</option>
                    <option name="maxHeight">400</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\Image;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('headshot', new Image(array(
                'minWidth' => 200,
                'maxWidth' => 400,
                'minHeight' => 200,
                'maxHeight' => 400,
            )));
        }
    }
    

The headshot property is validated to guarantee that it is a real image and that it is between a certain width and height.

Options

This constraint shares all of its options with the File constraint. It does, however, modify two of the default option values and add several other options.

mimeTypes

type: array or string default: image/*

You can find a list of existing image mime types on the IANA website.

mimeTypesMessage

type: string default: This file is not a valid image.

minWidth

type: integer

If set, the width of the image file must be greater than or equal to this value in pixels.

maxWidth

type: integer

If set, the width of the image file must be less than or equal to this value in pixels.

minHeight

type: integer

If set, the height of the image file must be greater than or equal to this value in pixels.

maxHeight

type: integer

If set, the height of the image file must be less than or equal to this value in pixels.

sizeNotDetectedMessage

type: string default: The size of the image could not be detected.

If the system is unable to determine the size of the image, this error will be displayed. This will only occur when at least one of the four size constraint options has been set.

maxWidthMessage

type: string default: The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.

The error message if the width of the image exceeds maxWidth.

minWidthMessage

type: string default: The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.

The error message if the width of the image is less than minWidth.

maxHeightMessage

type: string default: The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.

The error message if the height of the image exceeds maxHeight.

minHeightMessage

type: string default: The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.

The error message if the height of the image is less than minHeight.

CardScheme

2.2 新版功能: The CardScheme constraint was introduced in Symfony 2.2.

This constraint ensures that a credit card number is valid for a given credit card company. It can be used to validate the number before trying to initiate a payment through a payment gateway.

Applies to property or method
Options
Class CardScheme
Validator CardSchemeValidator
Basic Usage

To use the CardScheme validator, simply apply it to a property or method on an object that will contain a credit card number.

  • YAML
    # src/Acme/SubscriptionBundle/Resources/config/validation.yml
    Acme\SubscriptionBundle\Entity\Transaction:
        properties:
            cardNumber:
                - CardScheme:
                    schemes: [VISA]
                    message: Your credit card number is invalid.
    
  • XML
    <!-- src/Acme/SubscriptionBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SubscriptionBundle\Entity\Transaction">
            <property name="cardNumber">
                <constraint name="CardScheme">
                    <option name="schemes">
                        <value>VISA</value>
                    </option>
                    <option name="message">Your credit card number is invalid.</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • Annotations
    // src/Acme/SubscriptionBundle/Entity/Transaction.php
    namespace Acme\SubscriptionBundle\Entity\Transaction;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Transaction
    {
        /**
         * @Assert\CardScheme(schemes = {"VISA"}, message = "Your credit card number is invalid.")
         */
        protected $cardNumber;
    }
    
  • PHP
    // src/Acme/SubscriptionBundle/Entity/Transaction.php
    namespace Acme\SubscriptionBundle\Entity\Transaction;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Transaction
    {
        protected $cardNumber;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('cardNumber', new Assert\CardScheme(array(
                'schemes' => array(
                    'VISA'
                ),
                'message' => 'Your credit card number is invalid.',
            )));
        }
    }
    
Available Options
schemes

type: mixed [default option]

This option is required and represents the name of the number scheme used to validate the credit card number, it can either be a string or an array. Valid values are:

  • AMEX
  • CHINA_UNIONPAY
  • DINERS
  • DISCOVER
  • INSTAPAYMENT
  • JCB
  • LASER
  • MAESTRO
  • MASTERCARD
  • VISA

For more information about the used schemes, see Wikipedia: Issuer identification number (IIN).

message

type: string default: Unsupported card type or invalid card number.

The message shown when the value does not pass the CardScheme check.

Currency

2.3 新版功能: The Currency constraint was introduced in Symfony 2.3.

Validates that a value is a valid 3-letter ISO 4217 currency name.

Applies to property or method
Options
Class Currency
Validator CurrencyValidator
Basic Usage

If you want to ensure that the currency property of an Order is a valid currency, you could do the following:

  • YAML
    # src/Acme/EcommerceBundle/Resources/config/validation.yml
    Acme\EcommerceBundle\Entity\Order:
        properties:
            currency:
                - Currency: ~
    
  • Annotations
    // src/Acme/EcommerceBundle/Entity/Order.php
    namespace Acme\EcommerceBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Order
    {
        /**
         * @Assert\Currency
         */
        protected $currency;
    }
    
  • XML
    <!-- src/Acme/EcommerceBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\EcommerceBundle\Entity\Order">
            <property name="currency">
                <constraint name="Currency" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/EcommerceBundle/Entity/Order.php
    namespace Acme\SocialBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Order
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('currency', new Assert\Currency());
        }
    }
    
Options
message

type: string default: This value is not a valid currency.

This is the message that will be shown if the value is not a valid currency.

Luhn

2.2 新版功能: The Luhn constraint was introduced in Symfony 2.2.

This constraint is used to ensure that a credit card number passes the Luhn algorithm. It is useful as a first step to validating a credit card: before communicating with a payment gateway.

Applies to property or method
Options
Class Luhn
Validator LuhnValidator
Basic Usage

To use the Luhn validator, simply apply it to a property on an object that will contain a credit card number.

  • YAML
    # src/Acme/SubscriptionBundle/Resources/config/validation.yml
    Acme\SubscriptionBundle\Entity\Transaction:
        properties:
            cardNumber:
                - Luhn:
                    message: Please check your credit card number.
    
  • Annotations
    // src/Acme/SubscriptionBundle/Entity/Transaction.php
    namespace Acme\SubscriptionBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Transaction
    {
        /**
         * @Assert\Luhn(message = "Please check your credit card number.")
         */
        protected $cardNumber;
    }
    
  • XML
    <!-- src/Acme/SubscriptionBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SubscriptionBundle\Entity\Transaction">
            <property name="cardNumber">
                <constraint name="Luhn">
                    <option name="message">Please check your credit card number.</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SubscriptionBundle/Entity/Transaction.php
    namespace Acme\SubscriptionBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Transaction
    {
        protected $cardNumber;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('cardNumber', new Assert\Luhn(array(
                'message' => 'Please check your credit card number',
            )));
        }
    }
    
Available Options
message

type: string default: Invalid card number.

The default message supplied when the value does not pass the Luhn check.

Iban

2.3 新版功能: The Iban constraint was introduced in Symfony 2.3.

This constraint is used to ensure that a bank account number has the proper format of an International Bank Account Number (IBAN). IBAN is an internationally agreed means of identifying bank accounts across national borders with a reduced risk of propagating transcription errors.

Applies to property or method
Options
Class Iban
Validator IbanValidator
Basic Usage

To use the Iban validator, simply apply it to a property on an object that will contain an International Bank Account Number.

  • YAML
    # src/Acme/SubscriptionBundle/Resources/config/validation.yml
    Acme\SubscriptionBundle\Entity\Transaction:
        properties:
            bankAccountNumber:
                - Iban:
                    message: This is not a valid International Bank Account Number (IBAN).
    
  • Annotations
    // src/Acme/SubscriptionBundle/Entity/Transaction.php
    namespace Acme\SubscriptionBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Transaction
    {
        /**
         * @Assert\Iban(message = "This is not a valid International Bank Account Number (IBAN).")
         */
        protected $bankAccountNumber;
    }
    
  • XML
    <!-- src/Acme/SubscriptionBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\SubscriptionBundle\Entity\Transaction">
            <property name="bankAccountNumber">
                <constraint name="Iban">
                    <option name="message">This is not a valid International Bank Account Number (IBAN).</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/SubscriptionBundle/Entity/Transaction.php
    namespace Acme\SubscriptionBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Transaction
    {
        protected $bankAccountNumber;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban(array(
                'message' => 'This is not a valid International Bank Account Number (IBAN).',
            )));
        }
    }
    
Available Options
message

type: string default: This is not a valid International Bank Account Number (IBAN).

The default message supplied when the value does not pass the Iban check.

Isbn

2.3 新版功能: The Isbn constraint was introduced in Symfony 2.3.

This constraint validates that an International Standard Book Number (ISBN) is either a valid ISBN-10, a valid ISBN-13 or both.

Applies to property or method
Options
Class Isbn
Validator IsbnValidator
Basic Usage

To use the Isbn validator, simply apply it to a property or method on an object that will contain a ISBN number.

  • YAML
    # src/Acme/BookcaseBundle/Resources/config/validation.yml
    Acme\BookcaseBundle\Entity\Book:
        properties:
            isbn:
                - Isbn:
                    isbn10: true
                    isbn13: true
                    bothIsbnMessage: This value is neither a valid ISBN-10 nor a valid ISBN-13.
    
  • Annotations
    // src/Acme/BookcaseBundle/Entity/Book.php
    namespace Acme\BookcaseBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Book
    {
        /**
         * @Assert\Isbn(
         *     isbn10 = true,
         *     isbn13 = true,
         *     bothIsbnMessage = "This value is neither a valid ISBN-10 nor a valid ISBN-13."
         * )
         */
        protected $isbn;
    }
    
  • XML
    <!-- src/Acme/BookcaseBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BookcaseBundle\Entity\Book">
            <property name="isbn">
                <constraint name="Isbn">
                    <option name="isbn10">true</option>
                    <option name="isbn13">true</option>
                    <option name="bothIsbnMessage">This value is neither a valid ISBN-10 nor a valid ISBN-13.</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BookcaseBundle/Entity/Book.php
    namespace Acme\BookcaseBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Book
    {
        protected $isbn;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('isbn', new Assert\Isbn(array(
                'isbn10'          => true,
                'isbn13'          => true,
                'bothIsbnMessage' => 'This value is neither a valid ISBN-10 nor a valid ISBN-13.'
            )));
        }
    }
    
Available Options
isbn10

type: boolean

If this required option is set to true the constraint will check if the code is a valid ISBN-10 code.

isbn13

type: boolean

If this required option is set to true the constraint will check if the code is a valid ISBN-13 code.

isbn10Message

type: string default: This value is not a valid ISBN-10.

The message that will be shown if the isbn10 option is true and the given value does not pass the ISBN-10 check.

isbn13Message

type: string default: This value is not a valid ISBN-13.

The message that will be shown if the isbn13 option is true and the given value does not pass the ISBN-13 check.

bothIsbnMessage

type: string default: This value is neither a valid ISBN-10 nor a valid ISBN-13.

The message that will be shown if both the isbn10 and isbn13 options are true and the given value does not pass the ISBN-13 nor the ISBN-13 check.

Issn

2.3 新版功能: The Issn constraint was introduced in Symfony 2.3.

Validates that a value is a valid International Standard Serial Number (ISSN).

Applies to property or method
Options
Class Issn
Validator IssnValidator
Basic Usage
  • YAML
    # src/Acme/JournalBundle/Resources/config/validation.yml
    Acme\JournalBundle\Entity\Journal:
        properties:
            issn:
                - Issn: ~
    
  • Annotations
    // src/Acme/JournalBundle/Entity/Journal.php
    namespace Acme\JournalBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Journal
    {
        /**
         * @Assert\Issn
         */
         protected $issn;
    }
    
  • XML
    <!-- src/Acme/JournalBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\JournalBundle\Entity\Journal">
            <property name="issn">
                <constraint name="Issn" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/JournalBundle/Entity/Journal.php
    namespace Acme\JournalBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Journal
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('issn', new Assert\Issn());
        }
    }
    
Options
message

type: String default: This value is not a valid ISSN.

The message shown if the given value is not a valid ISSN.

caseSensitive

type: Boolean default: false

The validator will allow ISSN values to end with a lower case ‘x’ by default. When switching this to true, the validator requires an upper case ‘X’.

requireHyphen

type: Boolean default: false

The validator will allow non hyphenated ISSN values by default. When switching this to true, the validator requires a hyphenated ISSN value.

Callback

The purpose of the Callback assertion is to let you create completely custom validation rules and to assign any validation errors to specific fields on your object. If you’re using validation with forms, this means that you can make these custom errors display next to a specific field, instead of simply at the top of your form.

This process works by specifying one or more callback methods, each of which will be called during the validation process. Each of those methods can do anything, including creating and assigning validation errors.

注解

A callback method itself doesn’t fail or return any value. Instead, as you’ll see in the example, a callback method has the ability to directly add validator “violations”.

Applies to class
Options
Class Callback
Validator CallbackValidator
Setup
  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\BlogBundle\Entity\Author:
        constraints:
            - Callback:
                methods:   [isAuthorValid]
    
  • Annotations
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @Assert\Callback(methods={"isAuthorValid"})
     */
    class Author
    {
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\BlogBundle\Entity\Author">
            <constraint name="Callback">
                <option name="methods">
                    <value>isAuthorValid</value>
                </option>
            </constraint>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/BlogBundle/Entity/Author.php
    namespace Acme\BlogBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addConstraint(new Assert\Callback(array(
                'methods' => array('isAuthorValid'),
            )));
        }
    }
    
The Callback Method

The callback method is passed a special ExecutionContextInterface object. You can set “violations” directly on this object and determine to which field those errors should be attributed:

// ...
use Symfony\Component\Validator\ExecutionContextInterface;

class Author
{
    // ...
    private $firstName;

    public function isAuthorValid(ExecutionContextInterface $context)
    {
        // somehow you have an array of "fake names"
        $fakeNames = array();

        // check if the name is actually a fake name
        if (in_array($this->getFirstName(), $fakeNames)) {
            $context->addViolationAt('firstname', 'This name sounds totally fake!', array(), null);
        }
    }
}
Options
methods

type: array default: array() [default option]

This is an array of the methods that should be executed during the validation process. Each method can be one of the following formats:

  1. String method name

    If the name of a method is a simple string (e.g. isAuthorValid), that method will be called on the same object that’s being validated and the ExecutionContextInterface will be the only argument (see the above example).

  2. Static array callback

    Each method can also be specified as a standard array callback:

    • YAML
      # src/Acme/BlogBundle/Resources/config/validation.yml
      Acme\BlogBundle\Entity\Author:
          constraints:
              - Callback:
                  methods:
                      -    [Acme\BlogBundle\MyStaticValidatorClass, isAuthorValid]
      
    • Annotations
      // src/Acme/BlogBundle/Entity/Author.php
      use Symfony\Component\Validator\Constraints as Assert;
      
      /**
       * @Assert\Callback(methods={
       *     { "Acme\BlogBundle\MyStaticValidatorClass", "isAuthorValid" }
       * })
       */
      class Author
      {
      }
      
    • XML
      <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
      <?xml version="1.0" encoding="UTF-8" ?>
      <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
      
          <class name="Acme\BlogBundle\Entity\Author">
              <constraint name="Callback">
                  <option name="methods">
                      <value>
                          <value>Acme\BlogBundle\MyStaticValidatorClass</value>
                          <value>isAuthorValid</value>
                      </value>
                  </option>
              </constraint>
          </class>
      </constraint-mapping>
      
    • PHP
      // src/Acme/BlogBundle/Entity/Author.php
      
      use Symfony\Component\Validator\Mapping\ClassMetadata;
      use Symfony\Component\Validator\Constraints\Callback;
      
      class Author
      {
          public $name;
      
          public static function loadValidatorMetadata(ClassMetadata $metadata)
          {
              $metadata->addConstraint(new Callback(array(
                  'methods' => array(
                      array('Acme\BlogBundle\MyStaticValidatorClass', 'isAuthorValid'),
                  ),
              )));
          }
      }
      

    In this case, the static method isAuthorValid will be called on the Acme\BlogBundle\MyStaticValidatorClass class. It’s passed both the original object being validated (e.g. Author) as well as the ExecutionContextInterface:

    namespace Acme\BlogBundle;
    
    use Symfony\Component\Validator\ExecutionContextInterface;
    use Acme\BlogBundle\Entity\Author;
    
    class MyStaticValidatorClass
    {
        public static function isAuthorValid(Author $author, ExecutionContextInterface $context)
        {
            // ...
        }
    }
    

    小技巧

    If you specify your Callback constraint via PHP, then you also have the option to make your callback either a PHP closure or a non-static callback. It is not currently possible, however, to specify a service as a constraint. To validate using a service, you should create a custom validation constraint and add that new constraint to your class.

All

When applied to an array (or Traversable object), this constraint allows you to apply a collection of constraints to each element of the array.

Applies to property or method
Options
Class All
Validator AllValidator
Basic Usage

Suppose that you have an array of strings, and you want to validate each entry in that array:

  • YAML
    # src/Acme/UserBundle/Resources/config/validation.yml
    Acme\UserBundle\Entity\User:
        properties:
            favoriteColors:
                - All:
                    - NotBlank:  ~
                    - Length:
                        min: 5
    
  • Annotations
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        /**
         * @Assert\All({
         *     @Assert\NotBlank,
         *     @Assert\Length(min = 5)
         * })
         */
         protected $favoriteColors = array();
    }
    
  • XML
    <!-- src/Acme/UserBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\UserBundle\Entity\User">
            <property name="favoriteColors">
                <constraint name="All">
                    <option name="constraints">
                        <constraint name="NotBlank" />
                        <constraint name="Length">
                            <option name="min">5</option>
                        </constraint>
                    </option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/UserBundle/Entity/User.php
    namespace Acme\UserBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('favoriteColors', new Assert\All(array(
                'constraints' => array(
                    new Assert\NotBlank(),
                    new Assert\Length(array('min' => 5)),
                ),
            )));
        }
    }
    

Now, each entry in the favoriteColors array will be validated to not be blank and to be at least 5 characters long.

Options
constraints

type: array [default option]

This required option is the array of validation constraints that you want to apply to each element of the underlying array.

UserPassword

注解

Since Symfony 2.2, the UserPassword* classes in the Symfony\Component\Security\Core\Validator\Constraint namespace are deprecated and will be removed in Symfony 2.3. Please use the UserPassword* classes in the Symfony\Component\Security\Core\Validator\Constraints namespace instead.

This validates that an input value is equal to the current authenticated user’s password. This is useful in a form where a user can change their password, but needs to enter their old password for security.

注解

This should not be used to validate a login form, since this is done automatically by the security system.

Applies to property or method
Options
Class UserPassword
Validator UserPasswordValidator
Basic Usage

Suppose you have a PasswordChange class, that’s used in a form where the user can change their password by entering their old password and a new password. This constraint will validate that the old password matches the user’s current password:

  • YAML
    # src/Acme/UserBundle/Resources/config/validation.yml
    Acme\UserBundle\Form\Model\ChangePassword:
        properties:
            oldPassword:
                - Symfony\Component\Security\Core\Validator\Constraints\UserPassword:
                    message: "Wrong value for your current password"
    
  • Annotations
    // src/Acme/UserBundle/Form/Model/ChangePassword.php
    namespace Acme\UserBundle\Form\Model;
    
    use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
    
    class ChangePassword
    {
        /**
         * @SecurityAssert\UserPassword(
         *     message = "Wrong value for your current password"
         * )
         */
         protected $oldPassword;
    }
    
  • XML
    <!-- src/Acme/UserBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\UserBundle\Form\Model\ChangePassword">
            <property name="oldPassword">
                <constraint name="Symfony\Component\Security\Core\Validator\Constraints\UserPassword">
                    <option name="message">Wrong value for your current password</option>
                </constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/UserBundle/Form/Model/ChangePassword.php
    namespace Acme\UserBundle\Form\Model;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
    
    class ChangePassword
    {
        public static function loadValidatorData(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('oldPassword', new SecurityAssert\UserPassword(array(
                'message' => 'Wrong value for your current password',
            )));
        }
    }
    
Options
message

type: message default: This value should be the user current password.

This is the message that’s displayed when the underlying string does not match the current user’s password.

Valid

This constraint is used to enable validation on objects that are embedded as properties on an object being validated. This allows you to validate an object and all sub-objects associated with it.

Applies to property or method
Options
Class Valid

小技巧

By default the error_bubbling option is enabled for the collection Field Type, which passes the errors to the parent form. If you want to attach the errors to the locations where they actually occur you have to set error_bubbling to false.

Basic Usage

In the following example, create two classes Author and Address that both have constraints on their properties. Furthermore, Author stores an Address instance in the $address property.

// src/Acme/HelloBundle/Entity/Address.php
namespace Acme\HelloBundle\Entity;

class Address
{
    protected $street;
    protected $zipCode;
}
// src/Acme/HelloBundle/Entity/Author.php
namespace Acme\HelloBundle\Entity;

class Author
{
    protected $firstName;
    protected $lastName;
    protected $address;
}
  • YAML
    # src/Acme/HelloBundle/Resources/config/validation.yml
    Acme\HelloBundle\Entity\Address:
        properties:
            street:
                - NotBlank: ~
            zipCode:
                - NotBlank: ~
                - Length:
                    max: 5
    
    Acme\HelloBundle\Entity\Author:
        properties:
            firstName:
                - NotBlank: ~
                - Length:
                    min: 4
            lastName:
                - NotBlank: ~
    
  • Annotations
    // src/Acme/HelloBundle/Entity/Address.php
    namespace Acme\HelloBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Address
    {
        /**
         * @Assert\NotBlank()
         */
        protected $street;
    
        /**
         * @Assert\NotBlank
         * @Assert\Length(max = 5)
         */
        protected $zipCode;
    }
    
    // src/Acme/HelloBundle/Entity/Author.php
    namespace Acme\HelloBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\NotBlank
         * @Assert\Length(min = 4)
         */
        protected $firstName;
    
        /**
         * @Assert\NotBlank
         */
        protected $lastName;
    
        protected $address;
    }
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\HelloBundle\Entity\Address">
            <property name="street">
                <constraint name="NotBlank" />
            </property>
            <property name="zipCode">
                <constraint name="NotBlank" />
                <constraint name="Length">
                    <option name="max">5</option>
                </constraint>
            </property>
        </class>
    
        <class name="Acme\HelloBundle\Entity\Author">
            <property name="firstName">
                <constraint name="NotBlank" />
                <constraint name="Length">
                    <option name="min">4</option>
                </constraint>
            </property>
            <property name="lastName">
                <constraint name="NotBlank" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/HelloBundle/Entity/Address.php
    namespace Acme\HelloBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Address
    {
        protected $street;
        protected $zipCode;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('street', new Assert\NotBlank());
            $metadata->addPropertyConstraint('zipCode', new Assert\NotBlank());
            $metadata->addPropertyConstraint('zipCode', new Assert\Length(array("max" => 5)));
        }
    }
    
    // src/Acme/HelloBundle/Entity/Author.php
    namespace Acme\HelloBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        protected $firstName;
        protected $lastName;
        protected $address;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('firstName', new Assert\NotBlank());
            $metadata->addPropertyConstraint('firstName', new Assert\Length(array("min" => 4)));
            $metadata->addPropertyConstraint('lastName', new Assert\NotBlank());
        }
    }
    

With this mapping, it is possible to successfully validate an author with an invalid address. To prevent that, add the Valid constraint to the $address property.

  • YAML
    # src/Acme/HelloBundle/Resources/config/validation.yml
    Acme\HelloBundle\Entity\Author:
        properties:
            address:
                - Valid: ~
    
  • Annotations
    // src/Acme/HelloBundle/Entity/Author.php
    namespace Acme\HelloBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        /**
         * @Assert\Valid
         */
        protected $address;
    }
    
  • XML
    <!-- src/Acme/HelloBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\HelloBundle\Entity\Author">
            <property name="address">
                <constraint name="Valid" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/HelloBundle/Entity/Author.php
    namespace Acme\HelloBundle\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Author
    {
        protected $address;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('address', new Assert\Valid());
        }
    }
    

If you validate an author with an invalid address now, you can see that the validation of the Address fields failed.

Acme\\HelloBundle\\Author.address.zipCode:
    This value is too long. It should have 5 characters or less.
Options
traverse

type: boolean default: true

If this constraint is applied to a property that holds an array of objects, then each object in that array will be validated only if this option is set to true.

deep

type: boolean default: false

If this constraint is applied to a property that holds an array of objects, then each object in that array will be validated recursively if this option is set to true.

The Validator is designed to validate objects against constraints. In real life, a constraint could be: “The cake must not be burned”. In Symfony, constraints are similar: They are assertions that a condition is true.

Supported Constraints

The following constraints are natively available in Symfony:

Basic Constraints

These are the basic constraints: use them to assert very basic things about the value of properties or the return value of methods on your object.

String Constraints
Number Constraints
Date Constraints
File Constraints
Financial and other Number Constraints
Other Constraints

Twig Template Form Function and Variable Reference

When working with forms in a template, there are two powerful things at your disposal:

  • Functions for rendering each part of a form
  • Variables for getting any information about any field

You’ll use functions often to render your fields. Variables, on the other hand, are less commonly-used, but infinitely powerful since you can access a fields label, id attribute, errors, and anything else about the field.

Form Rendering Functions

This reference manual covers all the possible Twig functions available for rendering forms. There are several different functions available, and each is responsible for rendering a different part of a form (e.g. labels, errors, widgets, etc).

form(view, variables)

Renders the HTML of a complete form.

{# render the form and change the submission method #}
{{ form(form, {'method': 'GET'}) }}

You will mostly use this helper for prototyping or if you use custom form themes. If you need more flexibility in rendering the form, you should use the other helpers to render individual parts of the form instead:

{{ form_start(form) }}
    {{ form_errors(form) }}

    {{ form_row(form.name) }}
    {{ form_row(form.dueDate) }}

    {{ form_row(form.submit, { 'label': 'Submit me' }) }}
{{ form_end(form) }}
form_start(view, variables)

Renders the start tag of a form. This helper takes care of printing the configured method and target action of the form. It will also include the correct enctype property if the form contains upload fields.

{# render the start tag and change the submission method #}
{{ form_start(form, {'method': 'GET'}) }}
form_end(view, variables)

Renders the end tag of a form.

{{ form_end(form) }}

This helper also outputs form_rest() unless you set render_rest to false:

{# don't render unrendered fields #}
{{ form_end(form, {'render_rest': false}) }}
form_label(view, label, variables)

Renders the label for the given field. You can optionally pass the specific label you want to display as the second argument.

{{ form_label(form.name) }}

{# The two following syntaxes are equivalent #}
{{ form_label(form.name, 'Your Name', {'label_attr': {'class': 'foo'}}) }}
{{ form_label(form.name, null, {'label': 'Your name', 'label_attr': {'class': 'foo'}}) }}

See “More about Form Variables” to learn about the variables argument.

form_errors(view)

Renders any errors for the given field.

{{ form_errors(form.name) }}

{# render any "global" errors #}
{{ form_errors(form) }}
form_widget(view, variables)

Renders the HTML widget of a given field. If you apply this to an entire form or collection of fields, each underlying form row will be rendered.

{# render a widget, but add a "foo" class to it #}
{{ form_widget(form.name, {'attr': {'class': 'foo'}}) }}

The second argument to form_widget is an array of variables. The most common variable is attr, which is an array of HTML attributes to apply to the HTML widget. In some cases, certain types also have other template-related options that can be passed. These are discussed on a type-by-type basis. The attributes are not applied recursively to child fields if you’re rendering many fields at once (e.g. form_widget(form)).

See “More about Form Variables” to learn more about the variables argument.

form_row(view, variables)

Renders the “row” of a given field, which is the combination of the field’s label, errors and widget.

{# render a field row, but display a label with text "foo" #}
{{ form_row(form.name, {'label': 'foo'}) }}

The second argument to form_row is an array of variables. The templates provided in Symfony only allow to override the label as shown in the example above.

See “More about Form Variables” to learn about the variables argument.

form_rest(view, variables)

This renders all fields that have not yet been rendered for the given form. It’s a good idea to always have this somewhere inside your form as it’ll render hidden fields for you and make any fields you forgot to render more obvious (since it’ll render the field for you).

{{ form_rest(form) }}
form_enctype(view)

注解

This helper was deprecated in Symfony 2.3 and will be removed in Symfony 3.0. You should use form_start() instead.

If the form contains at least one file upload field, this will render the required enctype="multipart/form-data" form attribute. It’s always a good idea to include this in your form tag:

<form action="{{ path('form_submit') }}" method="post" {{ form_enctype(form) }}>
Form Tests Reference

Tests can be executed by using the is operator in Twig to create a condition. Read the Twig documentation for more information.

selectedchoice(selected_value)

This test will check if the current choice is equal to the selected_value or if the current choice is in the array (when selected_value is an array).

<option {% if choice is selectedchoice(value) %} selected="selected"{% endif %} ...>
More about Form Variables

小技巧

For a full list of variables, see: Form Variables Reference.

In almost every Twig function above, the final argument is an array of “variables” that are used when rendering that one part of the form. For example, the following would render the “widget” for a field, and modify its attributes to include a special class:

{# render a widget, but add a "foo" class to it #}
{{ form_widget(form.name, { 'attr': {'class': 'foo'} }) }}

The purpose of these variables - what they do & where they come from - may not be immediately clear, but they’re incredibly powerful. Whenever you render any part of a form, the block that renders it makes use of a number of variables. By default, these blocks live inside form_div_layout.html.twig.

Look at the form_label as an example:

{% block form_label %}
    {% if not compound %}
        {% set label_attr = label_attr|merge({'for': id}) %}
    {% endif %}
    {% if required %}
        {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
    {% endif %}
    {% if label is empty %}
        {% set label = name|humanize %}
    {% endif %}
    <label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ label|trans({}, translation_domain) }}</label>
{% endblock form_label %}

This block makes use of several variables: compound, label_attr, required, label, name and translation_domain. These variables are made available by the form rendering system. But more importantly, these are the variables that you can override when calling form_label (since in this example, you’re rendering the label).

The exact variables available to override depends on which part of the form you’re rendering (e.g. label versus widget) and which field you’re rendering (e.g. a choice widget has an extra expanded option). If you get comfortable with looking through form_div_layout.html.twig, you’ll always be able to see what options you have available.

小技巧

Behind the scenes, these variables are made available to the FormView object of your form when the Form component calls buildView and finishView on each “node” of your form tree. To see what “view” variables a particular field has, find the source code for the form field (and its parent fields) and look at the above two functions.

注解

If you’re rendering an entire form at once (or an entire embedded form), the variables argument will only be applied to the form itself and not its children. In other words, the following will not pass a “foo” class attribute to all of the child fields in the form:

{# does **not** work - the variables are not recursive #}
{{ form_widget(form, { 'attr': {'class': 'foo'} }) }}
Form Variables Reference

The following variables are common to every field type. Certain field types may have even more variables and some variables here only really apply to certain types.

Assuming you have a form variable in your template, and you want to reference the variables on the name field, accessing the variables is done by using a public vars property on the FormView object:

  • Twig
    <label for="{{ form.name.vars.id }}"
        class="{{ form.name.vars.required ? 'required' : '' }}">
        {{ form.name.vars.label }}
    </label>
    
  • PHP
    <label for="<?php echo $view['form']->get('name')->vars['id'] ?>"
        class="<?php echo $view['form']->get('name')->vars['required'] ? 'required' : '' ?>">
        <?php echo $view['form']->get('name')->vars['label'] ?>
    </label>
    

2.3 新版功能: The method and action variables were introduced in Symfony 2.3.

Variable Usage
form The current FormView instance.
id The id HTML attribute to be rendered.
name The name of the field (e.g. title) - but not the name HTML attribute, which is full_name.
full_name The name HTML attribute to be rendered.
errors An array of any errors attached to this specific field (e.g. form.title.errors). Note that you can’t use form.errors to determine if a form is valid, since this only returns “global” errors: some individual fields may have errors. Instead, use the valid option.
valid Returns true or false depending on whether the whole form is valid.
value The value that will be used when rendering (commonly the value HTML attribute).
read_only If true, readonly="readonly" is added to the field.
disabled If true, disabled="disabled" is added to the field.
required If true, a required attribute is added to the field to activate HTML5 validation. Additionally, a required class is added to the label.
max_length Adds a maxlength HTML attribute to the element.
pattern Adds a pattern HTML attribute to the element.
label The string label that will be rendered.
multipart If true, form_enctype will render enctype="multipart/form-data". This only applies to the root form element.
attr A key-value array that will be rendered as HTML attributes on the field.
label_attr A key-value array that will be rendered as HTML attributes on the label.
compound Whether or not a field is actually a holder for a group of children fields (for example, a choice field, which is actually a group of checkboxes.
block_prefixes An array of all the names of the parent types.
translation_domain The domain of the translations for this form.
cache_key A unique key which is used for caching.
data The normalized data of the type.
method The method of the current form (POST, GET, etc.).
action The action of the current form.

Symfony Twig Extensions

Twig is the default template engine for Symfony. By itself, it already contains a lot of built-in functions, filters, tags and tests (learn more about them from the Twig Reference).

Symfony adds custom extensions on top of Twig to integrate some components into the Twig templates. The following sections describe the custom functions, filters, tags and tests that are available when using the Symfony Core Framework.

There may also be tags in bundles you use that aren’t listed here.

Functions
render

2.2 新版功能: The render() function was introduced in Symfony 2.2. Prior, the {% render %} tag was used and had a different signature.

{{ render(uri, options) }}
uri
type: string | ControllerReference
options
type: array default: []

Renders the fragment for the given controller (using the controller function) or URI. For more information, see Embedding Controllers.

The render strategy can be specified in the strategy key of the options.

小技巧

The URI can be generated by other functions, like path and url.

render_esi
{{ render_esi(uri, options) }}
uri
type: string | ControllerReference
options
type: array default: []

Generates an ESI tag when possible or falls back to the behavior of render function instead. For more information, see Embedding Controllers.

小技巧

The URI can be generated by other functions, like path and url.

小技巧

The render_esi() function is an example of the shortcut functions of render. It automatically sets the strategy based on what’s given in the function name, e.g. render_hinclude() will use the hinclude.js strategy. This works for all render_*() functions.

controller

2.2 新版功能: The controller() function was introduced in Symfony 2.2.

{{ controller(controller, attributes, query) }}
controller
type: string
attributes
type: array default: []
query
type: array default: []

Returns an instance of ControllerReference to be used with functions like render() and render_esi().

asset
{{ asset(path, packageName) }}
path
type: string
packageName
type: string | null default: null

Returns a public path to path, which takes into account the base path set for the package and the URL path. More information in Linking to Assets.

asset_version
{{ asset_version(packageName) }}
packageName
type: string | null default: null

Returns the current version of the package, more information in Linking to Assets.

form
{{ form(view, variables) }}
view
type: FormView
variables
type: array default: []

Renders the HTML of a complete form, more information in the Twig Form reference.

form_start
{{ form_start(view, variables) }}
view
type: FormView
variables
type: array default: []

Renders the HTML start tag of a form, more information in the Twig Form reference.

form_end
{{ form_end(view, variables) }}
view
type: FormView
variables
type: array default: []

Renders the HTML end tag of a form together with all fields that have not been rendered yet, more information in the Twig Form reference.

form_enctype
{{ form_enctype(view) }}
view
type: FormView

Renders the required enctype="multipart/form-data" attribute if the form contains at least one file upload field, more information in the Twig Form reference.

form_widget
{{ form_widget(view, variables) }}
view
type: FormView
variables
type: array default: []

Renders a complete form or a specific HTML widget of a field, more information in the Twig Form reference.

form_errors
{{ form_errors(view) }}
view
type: FormView

Renders any errors for the given field or the global errors, more information in the Twig Form reference.

form_label
{{ form_label(view, label, variables) }}
view
type: FormView
label
type: string default: null
variables
type: array default: []

Renders the label for the given field, more information in the Twig Form reference.

form_row
{{ form_row(view, variables) }}
view
type: FormView
variables
type: array default: []

Renders the row (the field’s label, errors and widget) of the given field, more information in the Twig Form reference.

form_rest
{{ form_rest(view, variables) }}
view
type: FormView
variables
type: array default: []

Renders all fields that have not yet been rendered, more information in the Twig Form reference.

csrf_token
{{ csrf_token(intention) }}
intention
type: string

Renders a CSRF token. Use this function if you want CSRF protection without creating a form.

is_granted
{{ is_granted(role, object, field) }}
role
type: string
object
type: object
field
type: string

Returns true if the current user has the required role. Optionally, an object can be pasted to be used by the voter. More information can be found in Access Control in Templates.

注解

You can also pass in the field to use ACE for a specific field. Read more about this in Scope of Access Control Entries.

logout_path
{{ logout_path(key) }}
key
type: string

Generates a relative logout URL for the given firewall.

logout_url
{{ logout_url(key) }}
key
type: string

Equal to the logout_path function, but it’ll generate an absolute URL instead of a relative one.

path
{{ path(name, parameters, relative) }}
name
type: string
parameters
type: array default: []
relative
type: boolean default: false

Returns the relative URL (without the scheme and host) for the given route. If relative is enabled, it’ll create a path relative to the current path. More information in Linking to Pages.

url
{{ url(name, parameters, schemeRelative) }}
name
type: string
parameters
type: array default: []
schemeRelative
type: boolean default: false

Returns the absolute URL (with scheme and host) for the given route. If schemeRelative is enabled, it’ll create a scheme-relative URL. More information in Linking to Pages.

Filters
humanize

2.1 新版功能: The humanize filter was introduced in Symfony 2.1

{{ text|humanize }}
text
type: string

Makes a technical name human readable (i.e. replaces underscores by spaces and capitalizes the string).

trans
{{ message|trans(arguments, domain, locale) }}
message
type: string
arguments
type: array default: []
domain
type: string default: null
locale
type: string default: null

Translates the text into the current language. More information in Translation Filters.

transchoice
{{ message|transchoice(count, arguments, domain, locale) }}
message
type: string
count
type: integer
arguments
type: array default: []
domain
type: string default: null
locale
type: string default: null

Translates the text with pluralization support. More information in Translation Filters.

yaml_encode
{{ input|yaml_encode(inline, dumpObjects) }}
input
type: mixed
inline
type: integer default: 0
dumpObjects
type: boolean default: false

Transforms the input into YAML syntax. See Writing YAML Files for more information.

yaml_dump
{{ value|yaml_dump(inline, dumpObjects) }}
value
type: mixed
inline
type: integer default: 0
dumpObjects
type: boolean default: false

Does the same as yaml_encode(), but includes the type in the output.

abbr_class
{{ class|abbr_class }}
class
type: string

Generates an <abbr> element with the short name of a PHP class (the FQCN will be shown in a tooltip when a user hovers over the element).

abbr_method
{{ method|abbr_method }}
method
type: string

Generates an <abbr> element using the FQCN::method() syntax. If method is Closure, Closure will be used instead and if method doesn’t have a class name, it’s shown as a function (method()).

format_args
{{ args|format_args }}
args
type: array

Generates a string with the arguments and their types (within <em> elements).

format_args_as_text
{{ args|format_args_as_text }}
args
type: array

Equal to the format_args filter, but without using HTML tags.

file_excerpt
{{ file|file_excerpt(line) }}
file
type: string
line
type: integer

Generates an excerpt of seven lines around the given line.

format_file
{{ file|format_file(line, text) }}
file
type: string
line
type: integer
text
type: string default: null

Generates the file path inside an <a> element. If the path is inside the kernel root directory, the kernel root directory path is replaced by kernel.root_dir (showing the full path in a tooltip on hover).

format_file_from_text
{{ text|format_file_from_text }}
text
type: string

Uses format_file to improve the output of default PHP errors.

Tags
form_theme
{% form_theme form resources %}
form
type: FormView
resources
type: array | string

Sets the resources to override the form theme for the given form view instance. You can use _self as resources to set it to the current resource. More information in How to Customize Form Rendering.

trans
{% trans with vars from domain into locale %}{% endtrans %}
vars
type: array default: []
domain
type: string default: string
locale
type: string default: string

Renders the translation of the content. More information in Twig Templates.

transchoice
{% transchoice count with vars from domain into locale %}{% endtranschoice %}
count
type: integer
vars
type: array default: []
domain
type: string default: null
locale
type: string default: null

Renders the translation of the content with pluralization support, more information in Twig Templates.

trans_default_domain
{% trans_default_domain domain %}
domain
type: string

This will set the default domain in the current template.

Tests
selectedchoice
{% if choice is selectedchoice(selectedValue) %}
choice
type: ChoiceView
selectedValue
type: string

Checks if selectedValue was checked for the provided choice field. Using this test is the most effective way.

Global Variables
app

The app variable is available everywhere and gives access to many commonly needed objects and values. It is an instance of GlobalVariables.

The available attributes are:

  • app.user
  • app.request
  • app.session
  • app.environment
  • app.debug
  • app.security
Symfony Standard Edition Extensions

The Symfony Standard Edition adds some bundles to the Symfony Core Framework. Those bundles can have other Twig extensions:

The Dependency Injection Tags

Dependency Injection Tags are little strings that can be applied to a service to “flag” it to be used in some special way. For example, if you have a service that you would like to register as a listener to one of Symfony’s core events, you can flag it with the kernel.event_listener tag.

You can learn a little bit more about “tags” by reading the “Tags” section of the Service Container chapter.

Below is information about all of the tags available inside Symfony. There may also be tags in other bundles you use that aren’t listed here.

Tag Name Usage
assetic.asset Register an asset to the current asset manager
assetic.factory_worker Add a factory worker
assetic.filter Register a filter
assetic.formula_loader Add a formula loader to the current asset manager
assetic.formula_resource Adds a resource to the current asset manager
assetic.templating.php Remove this service if PHP templating is disabled
assetic.templating.twig Remove this service if Twig templating is disabled
data_collector Create a class that collects custom data for the profiler
doctrine.event_listener Add a Doctrine event listener
doctrine.event_subscriber Add a Doctrine event subscriber
form.type Create a custom form field type
form.type_extension Create a custom “form extension”
form.type_guesser Add your own logic for “form type guessing”
kernel.cache_clearer Register your service to be called during the cache clearing process
kernel.cache_warmer Register your service to be called during the cache warming process
kernel.event_listener Listen to different events/hooks in Symfony
kernel.event_subscriber To subscribe to a set of different events/hooks in Symfony
kernel.fragment_renderer Add new HTTP content rendering strategies
monolog.logger Logging with a custom logging channel
monolog.processor Add a custom processor for logging
routing.loader Register a custom service that loads routes
security.voter Add a custom voter to Symfony’s authorization logic
security.remember_me_aware To allow remember me authentication
serializer.encoder Register a new encoder in the serializer service
serializer.normalizer Register a new normalizer in the serializer service
swiftmailer.default.plugin Register a custom SwiftMailer Plugin
templating.helper Make your service available in PHP templates
translation.loader Register a custom service that loads translations
translation.extractor Register a custom service that extracts translation messages from a file
translation.dumper Register a custom service that dumps translation messages
twig.extension Register a custom Twig Extension
twig.loader Register a custom service that loads Twig templates
validator.constraint_validator Create your own custom validation constraint
validator.initializer Register a service that initializes objects before validation
assetic.asset

Purpose: Register an asset with the current asset manager

assetic.factory_worker

Purpose: Add a factory worker

A Factory worker is a class implementing Assetic\Factory\Worker\WorkerInterface. Its process($asset) method is called for each asset after asset creation. You can modify an asset or even return a new one.

In order to add a new worker, first create a class:

use Assetic\Asset\AssetInterface;
use Assetic\Factory\Worker\WorkerInterface;

class MyWorker implements WorkerInterface
{
    public function process(AssetInterface $asset)
    {
        // ... change $asset or return a new one
    }

}

And then register it as a tagged service:

  • YAML
    services:
        acme.my_worker:
            class: MyWorker
            tags:
                - { name: assetic.factory_worker }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="acme.my_worker" class="MyWorker>
                <tag name="assetic.factory_worker" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('acme.my_worker', 'MyWorker')
        ->addTag('assetic.factory_worker')
    ;
    
assetic.filter

Purpose: Register a filter

AsseticBundle uses this tag to register common filters. You can also use this tag to register your own filters.

First, you need to create a filter:

use Assetic\Asset\AssetInterface;
use Assetic\Filter\FilterInterface;

class MyFilter implements FilterInterface
{
    public function filterLoad(AssetInterface $asset)
    {
        $asset->setContent('alert("yo");' . $asset->getContent());
    }

    public function filterDump(AssetInterface $asset)
    {
        // ...
    }
}

Second, define a service:

  • YAML
    services:
        acme.my_filter:
            class: MyFilter
            tags:
                - { name: assetic.filter, alias: my_filter }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="acme.my_filter" class="MyFilter">
                <tag name="assetic.filter" alias="my_filter" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('acme.my_filter', 'MyFilter')
        ->addTag('assetic.filter', array('alias' => 'my_filter'))
    ;
    

Finally, apply the filter:

{% javascripts
    '@AcmeBaseBundle/Resources/public/js/global.js'
    filter='my_filter'
%}
    <script src="{{ asset_url }}"></script>
{% endjavascripts %}

You can also apply your filter via the assetic.filters.my_filter.apply_to config option as it’s described here: How to Apply an Assetic Filter to a specific File Extension. In order to do that, you must define your filter service in a separate xml config file and point to this file’s path via the assetic.filters.my_filter.resource configuration key.

assetic.formula_loader

Purpose: Add a formula loader to the current asset manager

A Formula loader is a class implementing Assetic\\Factory\Loader\\FormulaLoaderInterface interface. This class is responsible for loading assets from a particular kind of resources (for instance, twig template). Assetic ships loaders for PHP and Twig templates.

An alias attribute defines the name of the loader.

assetic.formula_resource

Purpose: Adds a resource to the current asset manager

A resource is something formulae can be loaded from. For instance, Twig templates are resources.

assetic.templating.php

Purpose: Remove this service if PHP templating is disabled

The tagged service will be removed from the container if the framework.templating.engines config section does not contain php.

assetic.templating.twig

Purpose: Remove this service if Twig templating is disabled

The tagged service will be removed from the container if framework.templating.engines config section does not contain twig.

data_collector

Purpose: Create a class that collects custom data for the profiler

For details on creating your own custom data collection, read the cookbook article: How to Create a custom Data Collector.

doctrine.event_listener

Purpose: Add a Doctrine event listener

For details on creating Doctrine event listeners, read the cookbook article: How to Register Event Listeners and Subscribers.

doctrine.event_subscriber

Purpose: Add a Doctrine event subscriber

For details on creating Doctrine event subscribers, read the cookbook article: How to Register Event Listeners and Subscribers.

form.type

Purpose: Create a custom form field type

For details on creating your own custom form type, read the cookbook article: How to Create a Custom Form Field Type.

form.type_extension

Purpose: Create a custom “form extension”

Form type extensions are a way for you took “hook into” the creation of any field in your form. For example, the addition of the CSRF token is done via a form type extension (FormTypeCsrfExtension).

A form type extension can modify any part of any field in your form. To create a form type extension, first create a class that implements the FormTypeExtensionInterface interface. For simplicity, you’ll often extend an AbstractTypeExtension class instead of the interface directly:

// src/Acme/MainBundle/Form/Type/MyFormTypeExtension.php
namespace Acme\MainBundle\Form\Type;

use Symfony\Component\Form\AbstractTypeExtension;

class MyFormTypeExtension extends AbstractTypeExtension
{
    // ... fill in whatever methods you want to override
    // like buildForm(), buildView(), finishView(), setDefaultOptions()
}

In order for Symfony to know about your form extension and use it, give it the form.type_extension tag:

  • YAML
    services:
        main.form.type.my_form_type_extension:
            class: Acme\MainBundle\Form\Type\MyFormTypeExtension
            tags:
                - { name: form.type_extension, alias: field }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="main.form.type.my_form_type_extension"
                class="Acme\MainBundle\Form\Type\MyFormTypeExtension">
    
                <tag name="form.type_extension" alias="field" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('main.form.type.my_form_type_extension', 'Acme\MainBundle\Form\Type\MyFormTypeExtension')
        ->addTag('form.type_extension', array('alias' => 'field'))
    ;
    

The alias key of the tag is the type of field that this extension should be applied to. For example, to apply the extension to any form/field, use the “form” value.

form.type_guesser

Purpose: Add your own logic for “form type guessing”

This tag allows you to add your own logic to the Form Guessing process. By default, form guessing is done by “guessers” based on the validation metadata and Doctrine metadata (if you’re using Doctrine) or Propel metadata (if you’re using Propel).

参见

For information on how to create your own type guesser, see Creating a custom Type Guesser.

kernel.cache_clearer

Purpose: Register your service to be called during the cache clearing process

Cache clearing occurs whenever you call cache:clear command. If your bundle caches files, you should add custom cache clearer for clearing those files during the cache clearing process.

In order to register your custom cache clearer, first you must create a service class:

// src/Acme/MainBundle/Cache/MyClearer.php
namespace Acme\MainBundle\Cache;

use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;

class MyClearer implements CacheClearerInterface
{
    public function clear($cacheDir)
    {
        // clear your cache
    }

}

Then register this class and tag it with kernel.cache_clearer:

  • YAML
    services:
        my_cache_clearer:
            class: Acme\MainBundle\Cache\MyClearer
            tags:
                - { name: kernel.cache_clearer }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_cache_clearer" class="Acme\MainBundle\Cache\MyClearer">
                <tag name="kernel.cache_clearer" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('my_cache_clearer', 'Acme\MainBundle\Cache\MyClearer')
        ->addTag('kernel.cache_clearer')
    ;
    
kernel.cache_warmer

Purpose: Register your service to be called during the cache warming process

Cache warming occurs whenever you run the cache:warmup or cache:clear task (unless you pass --no-warmup to cache:clear). It is also run when handling the request, if it wasn’t done by one of the commands yet. The purpose is to initialize any cache that will be needed by the application and prevent the first user from any significant “cache hit” where the cache is generated dynamically.

To register your own cache warmer, first create a service that implements the CacheWarmerInterface interface:

// src/Acme/MainBundle/Cache/MyCustomWarmer.php
namespace Acme\MainBundle\Cache;

use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

class MyCustomWarmer implements CacheWarmerInterface
{
    public function warmUp($cacheDir)
    {
        // ... do some sort of operations to "warm" your cache
    }

    public function isOptional()
    {
        return true;
    }
}

The isOptional method should return true if it’s possible to use the application without calling this cache warmer. In Symfony, optional warmers are always executed by default (you can change this by using the --no-optional-warmers option when executing the command).

To register your warmer with Symfony, give it the kernel.cache_warmer tag:

  • YAML
    services:
        main.warmer.my_custom_warmer:
            class: Acme\MainBundle\Cache\MyCustomWarmer
            tags:
                - { name: kernel.cache_warmer, priority: 0 }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="main.warmer.my_custom_warmer" class="Acme\MainBundle\Cache\MyCustomWarmer">
                <tag name="kernel.cache_warmer" priority="0" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('main.warmer.my_custom_warmer', 'Acme\MainBundle\Cache\MyCustomWarmer')
        ->addTag('kernel.cache_warmer', array('priority' => 0))
    ;
    

注解

The priority value is optional, and defaults to 0. The higher the priority, the sooner it gets executed.

Core Cache Warmers
Cache Warmer Class Name Priority
TemplatePathsCacheWarmer 20
RouterCacheWarmer 0
TemplateCacheCacheWarmer 0
kernel.event_listener

Purpose: To listen to different events/hooks in Symfony

This tag allows you to hook your own classes into Symfony’s process at different points.

For a full example of this listener, read the How to Create an Event Listener cookbook entry.

For another practical example of a kernel listener, see the cookbook article: How to Register a new Request Format and Mime Type.

Core Event Listener Reference

When adding your own listeners, it might be useful to know about the other core Symfony listeners and their priorities.

注解

All listeners listed here may not be listening depending on your environment, settings and bundles. Additionally, third-party bundles will bring in additional listeners not listed here.

kernel.request
Listener Class Name Priority
ProfilerListener 1024
TestSessionListener 192
SessionListener 128
RouterListener 32
LocaleListener 16
Firewall 8
kernel.controller
Listener Class Name Priority
RequestDataCollector 0
kernel.exception
Listener Class Name Priority
ProfilerListener 0
ExceptionListener -128
kernel.terminate
Listener Class Name Priority
EmailSenderListener 0
kernel.event_subscriber

Purpose: To subscribe to a set of different events/hooks in Symfony

To enable a custom subscriber, add it as a regular service in one of your configuration, and tag it with kernel.event_subscriber:

  • YAML
    services:
        kernel.subscriber.your_subscriber_name:
            class: Fully\Qualified\Subscriber\Class\Name
            tags:
                - { name: kernel.event_subscriber }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="kernel.subscriber.your_subscriber_name"
                class="Fully\Qualified\Subscriber\Class\Name">
    
                <tag name="kernel.event_subscriber" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('kernel.subscriber.your_subscriber_name', 'Fully\Qualified\Subscriber\Class\Name')
        ->addTag('kernel.event_subscriber')
    ;
    

注解

Your service must implement the EventSubscriberInterface interface.

注解

If your service is created by a factory, you MUST correctly set the class parameter for this tag to work correctly.

kernel.fragment_renderer

Purpose: Add a new HTTP content rendering strategy

To add a new rendering strategy - in addition to the core strategies like EsiFragmentRenderer - create a class that implements FragmentRendererInterface, register it as a service, then tag it with kernel.fragment_renderer.

monolog.logger

Purpose: To use a custom logging channel with Monolog

Monolog allows you to share its handlers between several logging channels. The logger service uses the channel app but you can change the channel when injecting the logger in a service.

  • YAML
    services:
        my_service:
            class: Fully\Qualified\Loader\Class\Name
            arguments: ["@logger"]
            tags:
                - { name: monolog.logger, channel: acme }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_service" class="Fully\Qualified\Loader\Class\Name">
                <argument type="service" id="logger" />
                <tag name="monolog.logger" channel="acme" />
            </service>
        </services>
    </container>
    
  • PHP
    $definition = new Definition('Fully\Qualified\Loader\Class\Name', array(new Reference('logger'));
    $definition->addTag('monolog.logger', array('channel' => 'acme'));
    $container->setDefinition('my_service', $definition);
    

小技巧

If you use MonologBundle 2.4 or higher, you can configure custom channels in the configuration and retrieve the corresponding logger service from the service container directly (see Configure Additional Channels without Tagged Services).

monolog.processor

Purpose: Add a custom processor for logging

Monolog allows you to add processors in the logger or in the handlers to add extra data in the records. A processor receives the record as an argument and must return it after adding some extra data in the extra attribute of the record.

The built-in IntrospectionProcessor can be used to add the file, the line, the class and the method where the logger was triggered.

You can add a processor globally:

  • YAML
    services:
        my_service:
            class: Monolog\Processor\IntrospectionProcessor
            tags:
                - { name: monolog.processor }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_service" class="Monolog\Processor\IntrospectionProcessor">
                <tag name="monolog.processor" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('my_service', 'Monolog\Processor\IntrospectionProcessor')
        ->addTag('monolog.processor')
    ;
    

小技巧

If your service is not a callable (using __invoke) you can add the method attribute in the tag to use a specific method.

You can add also a processor for a specific handler by using the handler attribute:

  • YAML
    services:
        my_service:
            class: Monolog\Processor\IntrospectionProcessor
            tags:
                - { name: monolog.processor, handler: firephp }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_service" class="Monolog\Processor\IntrospectionProcessor">
                <tag name="monolog.processor" handler="firephp" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('my_service', 'Monolog\Processor\IntrospectionProcessor')
        ->addTag('monolog.processor', array('handler' => 'firephp'))
    ;
    

You can also add a processor for a specific logging channel by using the channel attribute. This will register the processor only for the security logging channel used in the Security component:

  • YAML
    services:
        my_service:
            class: Monolog\Processor\IntrospectionProcessor
            tags:
                - { name: monolog.processor, channel: security }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="my_service" class="Monolog\Processor\IntrospectionProcessor">
                <tag name="monolog.processor" channel="security" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('my_service', 'Monolog\Processor\IntrospectionProcessor')
        ->addTag('monolog.processor', array('channel' => 'security'))
    ;
    

注解

You cannot use both the handler and channel attributes for the same tag as handlers are shared between all channels.

routing.loader

Purpose: Register a custom service that loads routes

To enable a custom routing loader, add it as a regular service in one of your configuration, and tag it with routing.loader:

  • YAML
    services:
        routing.loader.your_loader_name:
            class: Fully\Qualified\Loader\Class\Name
            tags:
                - { name: routing.loader }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="routing.loader.your_loader_name"
                class="Fully\Qualified\Loader\Class\Name">
    
                <tag name="routing.loader" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('routing.loader.your_loader_name', 'Fully\Qualified\Loader\Class\Name')
        ->addTag('routing.loader')
    ;
    

For more information, see How to Create a custom Route Loader.

security.remember_me_aware

Purpose: To allow remember me authentication

This tag is used internally to allow remember-me authentication to work. If you have a custom authentication method where a user can be remember-me authenticated, then you may need to use this tag.

If your custom authentication factory extends AbstractFactory and your custom authentication listener extends AbstractAuthenticationListener, then your custom authentication listener will automatically have this tagged applied and it will function automatically.

security.voter

Purpose: To add a custom voter to Symfony’s authorization logic

When you call isGranted on Symfony’s security context, a system of “voters” is used behind the scenes to determine if the user should have access. The security.voter tag allows you to add your own custom voter to that system.

For more information, read the cookbook article: How to Implement your own Voter to Blacklist IP Addresses.

serializer.encoder

Purpose: Register a new encoder in the serializer service

The class that’s tagged should implement the EncoderInterface and DecoderInterface.

For more details, see How to Use the Serializer.

serializer.normalizer

Purpose: Register a new normalizer in the Serializer service

The class that’s tagged should implement the NormalizerInterface and DenormalizerInterface.

For more details, see How to Use the Serializer.

swiftmailer.default.plugin

Purpose: Register a custom SwiftMailer Plugin

If you’re using a custom SwiftMailer plugin (or want to create one), you can register it with SwiftMailer by creating a service for your plugin and tagging it with swiftmailer.default.plugin (it has no options).

注解

default in this tag is the name of the mailer. If you have multiple mailers configured or have changed the default mailer name for some reason, you should change it to the name of your mailer in order to use this tag.

A SwiftMailer plugin must implement the Swift_Events_EventListener interface. For more information on plugins, see SwiftMailer’s Plugin Documentation.

Several SwiftMailer plugins are core to Symfony and can be activated via different configuration. For details, see SwiftmailerBundle Configuration (“swiftmailer”).

templating.helper

Purpose: Make your service available in PHP templates

To enable a custom template helper, add it as a regular service in one of your configuration, tag it with templating.helper and define an alias attribute (the helper will be accessible via this alias in the templates):

  • YAML
    services:
        templating.helper.your_helper_name:
            class: Fully\Qualified\Helper\Class\Name
            tags:
                - { name: templating.helper, alias: alias_name }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="templating.helper.your_helper_name"
                class="Fully\Qualified\Helper\Class\Name">
    
                <tag name="templating.helper" alias="alias_name" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('templating.helper.your_helper_name', 'Fully\Qualified\Helper\Class\Name')
        ->addTag('templating.helper', array('alias' => 'alias_name'))
    ;
    
translation.loader

Purpose: To register a custom service that loads translations

By default, translations are loaded from the filesystem in a variety of different formats (YAML, XLIFF, PHP, etc).

参见

Learn how to load custom formats in the components section.

Now, register your loader as a service and tag it with translation.loader:

  • YAML
    services:
        main.translation.my_custom_loader:
            class: Acme\MainBundle\Translation\MyCustomLoader
            tags:
                - { name: translation.loader, alias: bin }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="main.translation.my_custom_loader"
                class="Acme\MainBundle\Translation\MyCustomLoader">
    
                <tag name="translation.loader" alias="bin" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('main.translation.my_custom_loader', 'Acme\MainBundle\Translation\MyCustomLoader')
        ->addTag('translation.loader', array('alias' => 'bin'))
    ;
    

The alias option is required and very important: it defines the file “suffix” that will be used for the resource files that use this loader. For example, suppose you have some custom bin format that you need to load. If you have a bin file that contains French translations for the messages domain, then you might have a file app/Resources/translations/messages.fr.bin.

When Symfony tries to load the bin file, it passes the path to your custom loader as the $resource argument. You can then perform any logic you need on that file in order to load your translations.

If you’re loading translations from a database, you’ll still need a resource file, but it might either be blank or contain a little bit of information about loading those resources from the database. The file is key to trigger the load method on your custom loader.

translation.extractor

Purpose: To register a custom service that extracts messages from a file

2.1 新版功能: The ability to add message extractors was introduced in Symfony 2.1.

When executing the translation:update command, it uses extractors to extract translation messages from a file. By default, the Symfony framework has a TwigExtractor and a PhpExtractor, which help to find and extract translation keys from Twig templates and PHP files.

You can create your own extractor by creating a class that implements ExtractorInterface and tagging the service with translation.extractor. The tag has one required option: alias, which defines the name of the extractor:

// src/Acme/DemoBundle/Translation/FooExtractor.php
namespace Acme\DemoBundle\Translation;

use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\MessageCatalogue;

class FooExtractor implements ExtractorInterface
{
    protected $prefix;

    /**
     * Extracts translation messages from a template directory to the catalogue.
     */
    public function extract($directory, MessageCatalogue $catalogue)
    {
        // ...
    }

    /**
     * Sets the prefix that should be used for new found messages.
     */
    public function setPrefix($prefix)
    {
        $this->prefix = $prefix;
    }
}
  • YAML
    services:
        acme_demo.translation.extractor.foo:
            class: Acme\DemoBundle\Translation\FooExtractor
            tags:
                - { name: translation.extractor, alias: foo }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="acme_demo.translation.extractor.foo"
                class="Acme\DemoBundle\Translation\FooExtractor">
    
                <tag name="translation.extractor" alias="foo" />
            </service>
        </services>
    </container>
    
  • PHP
    $container->register(
        'acme_demo.translation.extractor.foo',
        'Acme\DemoBundle\Translation\FooExtractor'
    )
        ->addTag('translation.extractor', array('alias' => 'foo'));
    
translation.dumper

Purpose: To register a custom service that dumps messages to a file

2.1 新版功能: The ability to add message dumpers was introduced in Symfony 2.1.

After an Extractor has extracted all messages from the templates, the dumpers are executed to dump the messages to a translation file in a specific format.

Symfony already comes with many dumpers:

You can create your own dumper by extending FileDumper or implementing DumperInterface and tagging the service with translation.dumper. The tag has one option: alias This is the name that’s used to determine which dumper should be used.

  • YAML
    services:
        acme_demo.translation.dumper.json:
            class: Acme\DemoBundle\Translation\JsonFileDumper
            tags:
                - { name: translation.dumper, alias: json }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="acme_demo.translation.dumper.json"
                class="Acme\DemoBundle\Translation\JsonFileDumper">
    
                <tag name="translation.dumper" alias="json" />
            </service>
        </services>
    </container>
    
  • PHP
    $container->register(
        'acme_demo.translation.dumper.json',
        'Acme\DemoBundle\Translation\JsonFileDumper'
    )
        ->addTag('translation.dumper', array('alias' => 'json'));
    

参见

Learn how to dump to custom formats in the components section.

twig.extension

Purpose: To register a custom Twig Extension

To enable a Twig extension, add it as a regular service in one of your configuration, and tag it with twig.extension:

  • YAML
    services:
        twig.extension.your_extension_name:
            class: Fully\Qualified\Extension\Class\Name
            tags:
                - { name: twig.extension }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="twig.extension.your_extension_name"
                class="Fully\Qualified\Extension\Class\Name">
    
                <tag name="twig.extension" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('twig.extension.your_extension_name', 'Fully\Qualified\Extension\Class\Name')
        ->addTag('twig.extension')
    ;
    

For information on how to create the actual Twig Extension class, see Twig’s documentation on the topic or read the cookbook article: How to Write a custom Twig Extension.

Before writing your own extensions, have a look at the Twig official extension repository which already includes several useful extensions. For example Intl and its localizeddate filter that formats a date according to user’s locale. These official Twig extensions also have to be added as regular services:

  • YAML
    services:
        twig.extension.intl:
            class: Twig_Extensions_Extension_Intl
            tags:
                - { name: twig.extension }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="twig.extension.intl" class="Twig_Extensions_Extension_Intl">
                <tag name="twig.extension" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('twig.extension.intl', 'Twig_Extensions_Extension_Intl')
        ->addTag('twig.extension')
    ;
    
twig.loader

Purpose: Register a custom service that loads Twig templates

By default, Symfony uses only one Twig Loader - FilesystemLoader. If you need to load Twig templates from another resource, you can create a service for the new loader and tag it with twig.loader:

  • YAML
    services:
        acme.demo_bundle.loader.some_twig_loader:
            class: Acme\DemoBundle\Loader\SomeTwigLoader
            tags:
                - { name: twig.loader }
    
  • XML
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service
                id="acme.demo_bundle.loader.some_twig_loader"
                class="Acme\DemoBundle\Loader\SomeTwigLoader">
    
                <tag name="twig.loader" />
            </service>
        </services>
    </container>
    
  • PHP
    $container
        ->register('acme.demo_bundle.loader.some_twig_loader', 'Acme\DemoBundle\Loader\SomeTwigLoader')
        ->addTag('twig.loader')
    ;
    
validator.constraint_validator

Purpose: Create your own custom validation constraint

This tag allows you to create and register your own custom validation constraint. For more information, read the cookbook article: How to Create a custom Validation Constraint.

validator.initializer

Purpose: Register a service that initializes objects before validation

This tag provides a very uncommon piece of functionality that allows you to perform some sort of action on an object right before it’s validated. For example, it’s used by Doctrine to query for all of the lazily-loaded data on an object before it’s validated. Without this, some data on a Doctrine entity would appear to be “missing” when validated, even though this is not really the case.

If you do need to use this tag, just make a new class that implements the ObjectInitializerInterface interface. Then, tag it with the validator.initializer tag (it has no options).

For an example, see the EntityInitializer class inside the Doctrine Bridge.

Requirements for Running Symfony

To run Symfony, your system needs to adhere to a list of requirements. You can easily see if your system passes all requirements by running the web/config.php in your Symfony distribution. Since the CLI often uses a different php.ini configuration file, it’s also a good idea to check your requirements from the command line via:

$ php app/check.php

Below is the list of required and optional requirements.

Required
  • PHP needs to be a minimum version of PHP 5.3.3
  • JSON needs to be enabled
  • ctype needs to be enabled
  • Your php.ini needs to have the date.timezone setting

警告

Be aware that Symfony has some known limitations when using a PHP version less than 5.3.8 or equal to 5.3.16. For more information see the Requirements section of the README.

Optional
  • You need to have the PHP-XML module installed
  • You need to have at least version 2.6.21 of libxml
  • PHP tokenizer needs to be enabled
  • mbstring functions need to be enabled
  • iconv needs to be enabled
  • POSIX needs to be enabled (only on *nix)
  • Intl needs to be installed with ICU 4+
  • APC 3.0.17+ (or another opcode cache needs to be installed)
  • php.ini recommended settings
    • short_open_tag = Off
    • magic_quotes_gpc = Off
    • register_globals = Off
    • session.auto_start = Off
Doctrine

If you want to use Doctrine, you will need to have PDO installed. Additionally, you need to have the PDO driver installed for the database server you want to use.

Contributing

Contribute to Symfony:

Contributing

Contributing Code

Reporting a Bug

Whenever you find a bug in Symfony, we kindly ask you to report it. It helps us make a better Symfony.

警告

If you think you’ve found a security issue, please use the special procedure instead.

Before submitting a bug:

If your problem definitely looks like a bug, report it using the official bug tracker and follow some basic rules:

  • Use the title field to clearly describe the issue;
  • Describe the steps needed to reproduce the bug with short code examples (providing a unit test that illustrates the bug is best);
  • If the bug you experienced affects more than one layer, providing a simple failing unit test may not be sufficient. In this case, please fork the Symfony Standard Edition and reproduce your issue on a new branch;
  • Give as much detail as possible about your environment (OS, PHP version, Symfony version, enabled extensions, ...);
  • (optional) Attach a patch.
Submitting a Patch

Patches are the best way to provide a bug fix or to propose enhancements to Symfony.

Step 1: Setup your Environment
Install the Software Stack

Before working on Symfony, setup a friendly environment with the following software:

  • Git;
  • PHP version 5.3.3 or above;
  • PHPUnit 4.2 or above.
Configure Git

Set up your user information with your real name and a working email address:

$ git config --global user.name "Your Name"
$ git config --global user.email you@example.com

小技巧

If you are new to Git, you are highly recommended to read the excellent and free ProGit book.

小技巧

If your IDE creates configuration files inside the project’s directory, you can use global .gitignore file (for all projects) or .git/info/exclude file (per project) to ignore them. See GitHub’s documentation.

小技巧

Windows users: when installing Git, the installer will ask what to do with line endings, and suggests replacing all LF with CRLF. This is the wrong setting if you wish to contribute to Symfony! Selecting the as-is method is your best choice, as Git will convert your line feeds to the ones in the repository. If you have already installed Git, you can check the value of this setting by typing:

$ git config core.autocrlf

This will return either “false”, “input” or “true”; “true” and “false” being the wrong values. Change it to “input” by typing:

$ git config --global core.autocrlf input

Replace –global by –local if you want to set it only for the active repository

Get the Symfony Source Code

Get the Symfony source code:

  • Create a GitHub account and sign in;
  • Fork the Symfony repository (click on the “Fork” button);
  • After the “forking action” has completed, clone your fork locally (this will create a symfony directory):
$ git clone git@github.com:USERNAME/symfony.git
  • Add the upstream repository as a remote:
$ cd symfony
$ git remote add upstream git://github.com/symfony/symfony.git
Check that the current Tests Pass

Now that Symfony is installed, check that all unit tests pass for your environment as explained in the dedicated document.

Step 2: Work on your Patch
The License

Before you start, you must know that all the patches you are going to submit must be released under the MIT license, unless explicitly specified in your commits.

Choose the right Branch

Before working on a patch, you must determine on which branch you need to work:

  • 2.3, if you are fixing a bug for an existing feature (you may have to choose a higher branch if the feature you are fixing was introduced in a later version);
  • 2.7, if you are adding a new feature which is backward compatible;
  • master, if you are adding a new and backward incompatible feature.

注解

All bug fixes merged into maintenance branches are also merged into more recent branches on a regular basis. For instance, if you submit a patch for the 2.3 branch, the patch will also be applied by the core team on the master branch.

Create a Topic Branch

Each time you want to work on a patch for a bug or on an enhancement, create a topic branch:

$ git checkout -b BRANCH_NAME master

Or, if you want to provide a bugfix for the 2.3 branch, first track the remote 2.3 branch locally:

$ git checkout -t origin/2.3

Then create a new branch off the 2.3 branch to work on the bugfix:

$ git checkout -b BRANCH_NAME 2.3

小技巧

Use a descriptive name for your branch (ticket_XXX where XXX is the ticket number is a good convention for bug fixes).

The above checkout commands automatically switch the code to the newly created branch (check the branch you are working on with git branch).

Work on your Patch

Work on the code as much as you want and commit as much as you want; but keep in mind the following:

  • Read about the Symfony conventions and follow the coding standards (use git diff --check to check for trailing spaces – also read the tip below);
  • Add unit tests to prove that the bug is fixed or that the new feature actually works;
  • Try hard to not break backward compatibility (if you must do so, try to provide a compatibility layer to support the old way) – patches that break backward compatibility have less chance to be merged;
  • Do atomic and logically separate commits (use the power of git rebase to have a clean and logical history);
  • Squash irrelevant commits that are just about fixing coding standards or fixing typos in your own code;
  • Never fix coding standards in some existing code as it makes the code review more difficult;
  • Write good commit messages (see the tip below).

小技巧

When submitting pull requests, fabbot checks your code for common typos and verifies that you are using the PHP coding standards as defined in PSR-1 and PSR-2.

A status is posted below the pull request description with a summary of any problems it detects or any Travis CI build failures.

小技巧

A good commit message is composed of a summary (the first line), optionally followed by a blank line and a more detailed description. The summary should start with the Component you are working on in square brackets ([DependencyInjection], [FrameworkBundle], ...). Use a verb (fixed ..., added ..., ...) to start the summary and don’t add a period at the end.

Prepare your Patch for Submission

When your patch is not about a bug fix (when you add a new feature or change an existing one for instance), it must also include the following:

  • An explanation of the changes in the relevant CHANGELOG file(s) (the [BC BREAK] or the [DEPRECATION] prefix must be used when relevant);
  • An explanation on how to upgrade an existing application in the relevant UPGRADE file(s) if the changes break backward compatibility or if you deprecate something that will ultimately break backward compatibility.
Step 3: Submit your Patch

Whenever you feel that your patch is ready for submission, follow the following steps.

Rebase your Patch

Before submitting your patch, update your branch (needed if it takes you a while to finish your changes):

$ git checkout master
$ git fetch upstream
$ git merge upstream/master
$ git checkout BRANCH_NAME
$ git rebase master

小技巧

Replace master with the branch you selected previously (e.g. 2.3) if you are working on a bugfix

When doing the rebase command, you might have to fix merge conflicts. git status will show you the unmerged files. Resolve all the conflicts, then continue the rebase:

$ git add ... # add resolved files
$ git rebase --continue

Check that all tests still pass and push your branch remotely:

$ git push --force origin BRANCH_NAME
Make a Pull Request

You can now make a pull request on the symfony/symfony GitHub repository.

小技巧

Take care to point your pull request towards symfony:2.3 if you want the core team to pull a bugfix based on the 2.3 branch.

To ease the core team work, always include the modified components in your pull request message, like in:

[Yaml] fixed something
[Form] [Validator] [FrameworkBundle] added something

The pull request description must include the following checklist at the top to ensure that contributions may be reviewed without needless feedback loops and that your contributions can be included into Symfony as quickly as possible:

| Q             | A
| ------------- | ---
| Bug fix?      | [yes|no]
| New feature?  | [yes|no]
| BC breaks?    | [yes|no]
| Deprecations? | [yes|no]
| Tests pass?   | [yes|no]
| Fixed tickets | [comma separated list of tickets fixed by the PR]
| License       | MIT
| Doc PR        | [The reference to the documentation PR if any]

An example submission could now look as follows:

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #12, #43
| License       | MIT
| Doc PR        | symfony/symfony-docs#123

The whole table must be included (do not remove lines that you think are not relevant). For simple typos, minor changes in the PHPDocs, or changes in translation files, use the shorter version of the check-list:

| Q             | A
| ------------- | ---
| Fixed tickets | [comma separated list of tickets fixed by the PR]
| License       | MIT

Some answers to the questions trigger some more requirements:

  • If you answer yes to “Bug fix?”, check if the bug is already listed in the Symfony issues and reference it/them in “Fixed tickets”;
  • If you answer yes to “New feature?”, you must submit a pull request to the documentation and reference it under the “Doc PR” section;
  • If you answer yes to “BC breaks?”, the patch must contain updates to the relevant CHANGELOG and UPGRADE files;
  • If you answer yes to “Deprecations?”, the patch must contain updates to the relevant CHANGELOG and UPGRADE files;
  • If you answer no to “Tests pass”, you must add an item to a todo-list with the actions that must be done to fix the tests;
  • If the “license” is not MIT, just don’t submit the pull request as it won’t be accepted anyway.

If some of the previous requirements are not met, create a todo-list and add relevant items:

- [ ] fix the tests as they have not been updated yet
- [ ] submit changes to the documentation
- [ ] document the BC breaks

If the code is not finished yet because you don’t have time to finish it or because you want early feedback on your work, add an item to todo-list:

- [ ] finish the code
- [ ] gather feedback for my changes

As long as you have items in the todo-list, please prefix the pull request title with “[WIP]”.

In the pull request description, give as much details as possible about your changes (don’t hesitate to give code examples to illustrate your points). If your pull request is about adding a new feature or modifying an existing one, explain the rationale for the changes. The pull request description helps the code review and it serves as a reference when the code is merged (the pull request description and all its associated comments are part of the merge commit message).

In addition to this “code” pull request, you must also send a pull request to the documentation repository to update the documentation when appropriate.

Rework your Patch

Based on the feedback on the pull request, you might need to rework your patch. Before re-submitting the patch, rebase with upstream/master or upstream/2.3, don’t merge; and force the push to the origin:

$ git rebase -f upstream/master
$ git push --force origin BRANCH_NAME

注解

When doing a push --force, always specify the branch name explicitly to avoid messing other branches in the repo (--force tells Git that you really want to mess with things so do it carefully).

Often, moderators will ask you to “squash” your commits. This means you will convert many commits to one commit. To do this, use the rebase command:

$ git rebase -i upstream/master
$ git push --force origin BRANCH_NAME

After you type this command, an editor will popup showing a list of commits:

pick 1a31be6 first commit
pick 7fc64b4 second commit
pick 7d33018 third commit

To squash all commits into the first one, remove the word pick before the second and the last commits, and replace it by the word squash or just s. When you save, Git will start rebasing, and if successful, will ask you to edit the commit message, which by default is a listing of the commit messages of all the commits. When you are finished, execute the push command.

Symfony Core Team

This document states the rules that govern the Symfony Core group. These rules are effective upon publication of this document and all Symfony Core members must adhere to said rules and protocol.

Core Organization

Symfony Core members are divided into three groups. Each member can only belong to one group at a time. The privileges granted to a group are automatically granted to all higher priority groups.

The Symfony Core groups, in descending order of priority, are as follows:

  1. Project Leader
  • Elects members in any other group;
  • Merges pull requests in all Symfony repositories.
  1. Mergers
  • Merge pull requests for the component or components on which they have been granted privileges.
  1. Deciders
  • Decide to merge or reject a pull request.
Active Core Members
Core Membership Application

At present, new Symfony Core membership applications are not accepted.

Core Membership Revocation

A Symfony Core membership can be revoked for any of the following reasons:

  • Refusal to follow the rules and policies stated in this document;
  • Lack of activity for the past six months;
  • Willful negligence or intent to harm the Symfony project;
  • Upon decision of the Project Leader.

Should new Symfony Core memberships be accepted in the future, revoked members must wait at least 12 months before re-applying.

Code Development Rules

Symfony project development is based on pull requests proposed by any member of the Symfony community. Pull request acceptance or rejection is decided based on the votes cast by the Symfony Core members.

Pull Request Voting Policy
  • -1 votes must always be justified by technical and objective reasons;
  • +1 votes do not require justification, unless there is at least one -1 vote;
  • Core members can change their votes as many times as they desire during the course of a pull request discussion;
  • Core members are not allowed to vote on their own pull requests.
Pull Request Merging Policy

A pull request can be merged if:

  • Enough time was given for peer reviews (a few minutes for typos or minor changes, at least 2 days for “regular” pull requests, and 4 days for pull requests with “a significant impact”);
  • It is a minor change [1], regardless of the number of votes;
  • At least the component’s Merger or two other Core members voted +1 and no Core member voted -1.
Pull Request Merging Process

All code must be committed to the repository through pull requests, except for minor changes [1] which can be committed directly to the repository.

Mergers must always use the command-line gh tool provided by the Project Leader to merge the pull requests.

Release Policy

The Project Leader is also the release manager for every Symfony version.

Symfony Core Rules and Protocol Amendments

The rules described in this document may be amended at anytime at the discretion of the Project Leader.

[1](1, 2) Minor changes comprise typos, DocBlock fixes, code standards violations, and minor CSS, JavaScript and HTML modifications.
Security Issues

This document explains how Symfony security issues are handled by the Symfony core team (Symfony being the code hosted on the main symfony/symfony Git repository).

Reporting a Security Issue

If you think that you have found a security issue in Symfony, don’t use the mailing-list or the bug tracker and don’t publish it publicly. Instead, all security issues must be sent to security [at] symfony.com. Emails sent to this address are forwarded to the Symfony core-team private mailing-list.

Resolving Process

For each report, we first try to confirm the vulnerability. When it is confirmed, the core-team works on a solution following these steps:

  1. Send an acknowledgement to the reporter;
  2. Work on a patch;
  3. Get a CVE identifier from mitre.org;
  4. Write a security announcement for the official Symfony blog about the vulnerability. This post should contain the following information:
    • a title that always include the “Security release” string;
    • a description of the vulnerability;
    • the affected versions;
    • the possible exploits;
    • how to patch/upgrade/workaround affected applications;
    • the CVE identifier;
    • credits.
  5. Send the patch and the announcement to the reporter for review;
  6. Apply the patch to all maintained versions of Symfony;
  7. Package new versions for all affected versions;
  8. Publish the post on the official Symfony blog (it must also be added to the “Security Advisories” category);
  9. Update the security advisory list (see below).

注解

Releases that include security issues should not be done on Saturday or Sunday, except if the vulnerability has been publicly posted.

注解

While we are working on a patch, please do not reveal the issue publicly.

注解

The resolution takes anywhere between a couple of days to a month depending on its complexity and the coordination with the downstream projects (see next paragraph).

Collaborating with Downstream Open-Source Projects

As Symfony is used by many large Open-Source projects, we standardized the way the Symfony security team collaborates on security issues with downstream projects. The process works as follows:

  1. After the Symfony security team has acknowledged a security issue, it immediately sends an email to the downstream project security teams to inform them of the issue;
  2. The Symfony security team creates a private Git repository to ease the collaboration on the issue and access to this repository is given to the Symfony security team, to the Symfony contributors that are impacted by the issue, and to one representative of each downstream projects;
  3. All people with access to the private repository work on a solution to solve the issue via pull requests, code reviews, and comments;
  4. Once the fix is found, all involved projects collaborate to find the best date for a joint release (there is no guarantee that all releases will be at the same time but we will try hard to make them at about the same time). When the issue is not known to be exploited in the wild, a period of two weeks seems like a reasonable amount of time.

The list of downstream projects participating in this process is kept as small as possible in order to better manage the flow of confidential information prior to disclosure. As such, projects are included at the sole discretion of the Symfony security team.

As of today, the following projects have validated this process and are part of the downstream projects included in this process:

  • Drupal (releases typically happen on Wednesdays)
  • eZPublish
Security Advisories

This section indexes security vulnerabilities that were fixed in Symfony releases, starting from Symfony 1.0.0:

Running Symfony Tests

Before submitting a patch for inclusion, you need to run the Symfony test suite to check that you have not broken anything.

PHPUnit

To run the Symfony test suite, install PHPUnit 4.2 (or later) first.

Dependencies (optional)

To run the entire test suite, including tests that depend on external dependencies, Symfony needs to be able to autoload them. By default, they are autoloaded from vendor/ under the main root directory (see autoload.php.dist).

The test suite needs the following third-party libraries:

  • Doctrine
  • Swift Mailer
  • Twig
  • Monolog

To install them all, use Composer:

Step 1: Install Composer globally

Step 2: Install vendors.

$ composer install

注解

Note that the script takes some time to finish.

After installation, you can update the vendors to their latest version with the follow command:

$ composer --dev update
Running

First, update the vendors (see above).

Then, run the test suite from the Symfony root directory with the following command:

$ phpunit

The output should display OK. If not, you need to figure out what’s going on and if the tests are broken because of your modifications.

小技巧

If you want to test a single component type its path after the phpunit command, e.g.:

$ phpunit src/Symfony/Component/Finder/

小技巧

Run the test suite before applying your modifications to check that they run fine on your configuration.

Code Coverage

If you add a new feature, you also need to check the code coverage by using the coverage-html option:

$ phpunit --coverage-html=cov/

Check the code coverage by opening the generated cov/index.html page in a browser.

小技巧

The code coverage only works if you have Xdebug enabled and all dependencies installed.

Our backwards Compatibility Promise

Ensuring smooth upgrades of your projects is our first priority. That’s why we promise you backwards compatibility (BC) for all minor Symfony releases. You probably recognize this strategy as Semantic Versioning. In short, Semantic Versioning means that only major releases (such as 2.0, 3.0 etc.) are allowed to break backwards compatibility. Minor releases (such as 2.5, 2.6 etc.) may introduce new features, but must do so without breaking the existing API of that release branch (2.x in the previous example).

警告

This promise was introduced with Symfony 2.3 and does not apply to previous versions of Symfony.

However, backwards compatibility comes in many different flavors. In fact, almost every change that we make to the framework can potentially break an application. For example, if we add a new method to a class, this will break an application which extended this class and added the same method, but with a different method signature.

Also, not every BC break has the same impact on application code. While some BC breaks require you to make significant changes to your classes or your architecture, others are fixed as easily as changing the name of a method.

That’s why we created this page for you. The section “Using Symfony Code” will tell you how you can ensure that your application won’t break completely when upgrading to a newer version of the same major release branch.

The second section, “Working on Symfony Code”, is targeted at Symfony contributors. This section lists detailed rules that every contributor needs to follow to ensure smooth upgrades for our users.

Using Symfony Code

If you are using Symfony in your projects, the following guidelines will help you to ensure smooth upgrades to all future minor releases of your Symfony version.

Using our Interfaces

All interfaces shipped with Symfony can be used in type hints. You can also call any of the methods that they declare. We guarantee that we won’t break code that sticks to these rules.

警告

The exception to this rule are interfaces tagged with @internal. Such interfaces should not be used or implemented.

If you want to implement an interface, you should first make sure that the interface is an API interface. You can recognize API interfaces by the @api tag in their source code:

/**
 * HttpKernelInterface handles a Request to convert it to a Response.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @api
 */
interface HttpKernelInterface
{
    // ...
}

If you implement an API interface, we promise that we won’t ever break your code. Regular interfaces, by contrast, may be extended between minor releases, for example by adding a new method. Be prepared to upgrade your code manually if you implement a regular interface.

注解

Even if we do changes that require manual upgrades, we limit ourselves to changes that can be upgraded easily. We will always document the precise upgrade instructions in the UPGRADE file in Symfony’s root directory.

The following table explains in detail which use cases are covered by our backwards compatibility promise:

Use Case Regular API
If you... Then we guarantee BC...
Type hint against the interface Yes Yes
Call a method Yes Yes
If you implement the interface and... Then we guarantee BC...
Implement a method No [1] Yes
Add an argument to an implemented method No [1] Yes
Add a default value to an argument Yes Yes

注解

If you think that one of our regular classes should have an @api tag, put your request into a new ticket on GitHub. We will then evaluate whether we can add the tag or not.

Using our Classes

All classes provided by Symfony may be instantiated and accessed through their public methods and properties.

警告

Classes, properties and methods that bear the tag @internal as well as the classes located in the various *\\Tests\\ namespaces are an exception to this rule. They are meant for internal use only and should not be accessed by your own code.

Just like with interfaces, we also distinguish between regular and API classes. Like API interfaces, API classes are marked with an @api tag:

/**
 * Request represents an HTTP request.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @api
 */
class Request
{
    // ...
}

The difference between regular and API classes is that we guarantee full backwards compatibility if you extend an API class and override its methods. We can’t give the same promise for regular classes, because there we may, for example, add an optional argument to a method. Consequently, the signature of your overridden method wouldn’t match anymore and generate a fatal error.

注解

As with interfaces, we limit ourselves to changes that can be upgraded easily. We will document the precise upgrade instructions in the UPGRADE file in Symfony’s root directory.

In some cases, only specific properties and methods are tagged with the @api tag, even though their class is not. In these cases, we guarantee full backwards compatibility for the tagged properties and methods (as indicated in the column “API” below), but not for the rest of the class.

To be on the safe side, check the following table to know which use cases are covered by our backwards compatibility promise:

Use Case Regular API
If you... Then we guarantee BC...
Type hint against the class Yes Yes
Create a new instance Yes Yes
Extend the class Yes Yes
Access a public property Yes Yes
Call a public method Yes Yes
If you extend the class and... Then we guarantee BC...
Access a protected property No [1] Yes
Call a protected method No [1] Yes
Override a public property Yes Yes
Override a protected property No [1] Yes
Override a public method No [1] Yes
Override a protected method No [1] Yes
Add a new property No No
Add a new method No No
Add an argument to an overridden method No [1] Yes
Add a default value to an argument Yes Yes
Call a private method (via Reflection) No No
Access a private property (via Reflection) No No

注解

If you think that one of our regular classes should have an @api tag, put your request into a new ticket on GitHub. We will then evaluate whether we can add the tag or not.

Working on Symfony Code

Do you want to help us improve Symfony? That’s great! However, please stick to the rules listed below in order to ensure smooth upgrades for our users.

Changing Interfaces

This table tells you which changes you are allowed to do when working on Symfony’s interfaces:

Type of Change Regular API
Remove entirely No No
Change name or namespace No No
Add parent interface Yes [2] Yes [3]
Remove parent interface No No
Methods    
Add method Yes [2] No
Remove method No No
Change name No No
Move to parent interface Yes Yes
Add argument without a default value No No
Add argument with a default value Yes [2] No
Remove argument Yes [4] Yes [4]
Add default value to an argument Yes [2] No
Remove default value of an argument No No
Add type hint to an argument No No
Remove type hint of an argument Yes [2] No
Change argument type Yes [2] [5] No
Change return type Yes [2] [6] No
Changing Classes

This table tells you which changes you are allowed to do when working on Symfony’s classes:

Type of Change Regular API
Remove entirely No No
Make final No No
Make abstract No No
Change name or namespace No No
Change parent class Yes [7] Yes [7]
Add interface Yes Yes
Remove interface No No
Public Properties    
Add public property Yes Yes
Remove public property No No
Reduce visibility No No
Move to parent class Yes Yes
Protected Properties    
Add protected property Yes Yes
Remove protected property Yes [2] No
Reduce visibility Yes [2] No
Move to parent class Yes Yes
Private Properties    
Add private property Yes Yes
Remove private property Yes Yes
Constructors    
Add constructor without mandatory arguments Yes [2] Yes [2]
Remove constructor Yes [2] No
Reduce visibility of a public constructor No No
Reduce visibility of a protected constructor Yes [2] No
Move to parent class Yes Yes
Public Methods    
Add public method Yes Yes
Remove public method No No
Change name No No
Reduce visibility No No
Move to parent class Yes Yes
Add argument without a default value No No
Add argument with a default value Yes [2] No
Remove argument Yes [4] Yes [4]
Add default value to an argument Yes [2] No
Remove default value of an argument No No
Add type hint to an argument Yes [8] No
Remove type hint of an argument Yes [2] No
Change argument type Yes [2] [5] No
Change return type Yes [2] [6] No
Protected Methods    
Add protected method Yes Yes
Remove protected method Yes [2] No
Change name No No
Reduce visibility Yes [2] No
Move to parent class Yes Yes
Add argument without a default value Yes [2] No
Add argument with a default value Yes [2] No
Remove argument Yes [4] Yes [4]
Add default value to an argument Yes [2] No
Remove default value of an argument Yes [2] No
Add type hint to an argument Yes [2] No
Remove type hint of an argument Yes [2] No
Change argument type Yes [2] [5] No
Change return type Yes [2] [6] No
Private Methods    
Add private method Yes Yes
Remove private method Yes Yes
Change name Yes Yes
Reduce visibility Yes Yes
Add argument without a default value Yes Yes
Add argument with a default value Yes Yes
Remove argument Yes Yes
Add default value to an argument Yes Yes
Remove default value of an argument Yes Yes
Add type hint to an argument Yes Yes
Remove type hint of an argument Yes Yes
Change argument type Yes Yes
Change return type Yes Yes
Static Methods    
Turn non static into static No No
Turn static into non static No No
[1](1, 2, 3, 4, 5, 6, 7, 8) Your code may be broken by changes in the Symfony code. Such changes will however be documented in the UPGRADE file.
[2](1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28) Should be avoided. When done, this change must be documented in the UPGRADE file.
[3]The added parent interface must not introduce any new methods that don’t exist in the interface already.
[4](1, 2, 3, 4, 5, 6) Only the last argument(s) of a method may be removed, as PHP does not care about additional arguments that you pass to a method.
[5](1, 2, 3)

The argument type may only be changed to a compatible or less specific type. The following type changes are allowed:

Original Type New Type
boolean any scalar type with equivalent boolean values
string any scalar type or object with equivalent string values
integer any scalar type with equivalent integer values
float any scalar type with equivalent float values
class <C> any superclass or interface of <C>
interface <I> any superinterface of <I>
[6](1, 2, 3)

The return type may only be changed to a compatible or more specific type. The following type changes are allowed:

Original Type New Type
boolean any scalar type with equivalent boolean values
string any scalar type or object with equivalent string values
integer any scalar type with equivalent integer values
float any scalar type with equivalent float values
array instance of ArrayAccess, Traversable and Countable
ArrayAccess array
Traversable array
Countable array
class <C> any subclass of <C>
interface <I> any subinterface or implementing class of <I>
[7](1, 2) When changing the parent class, the original parent class must remain an ancestor of the class.
[8]A type hint may only be added if passing a value with a different type previously generated a fatal error.
Coding Standards

When contributing code to Symfony, you must follow its coding standards. To make a long story short, here is the golden rule: Imitate the existing Symfony code. Most open-source Bundles and libraries used by Symfony also follow the same guidelines, and you should too.

Remember that the main advantage of standards is that every piece of code looks and feels familiar, it’s not about this or that being more readable.

Symfony follows the standards defined in the PSR-0, PSR-1 and PSR-2 documents.

Since a picture - or some code - is worth a thousand words, here’s a short example containing most features described below:

<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Acme;

/**
 * Coding standards demonstration.
 */
class FooBar
{
    const SOME_CONST = 42;

    private $fooBar;

    /**
     * @param string $dummy Some argument description
     */
    public function __construct($dummy)
    {
        $this->fooBar = $this->transformText($dummy);
    }

    /**
     * @param string $dummy Some argument description
     * @param array  $options
     *
     * @return string|null Transformed input
     *
     * @throws \RuntimeException
     */
    private function transformText($dummy, array $options = array())
    {
        $mergedOptions = array_merge(
            array(
                'some_default' => 'values',
                'another_default' => 'more values',
            ),
            $options
        );

        if (true === $dummy) {
            return;
        }

        if ('string' === $dummy) {
            if ('values' === $mergedOptions['some_default']) {
                return substr($dummy, 0, 5);
            }

            return ucwords($dummy);
        }

        throw new \RuntimeException(sprintf('Unrecognized dummy option "%s"', $dummy));
    }

    private function reverseBoolean($value = null, $theSwitch = false)
    {
        if (!$theSwitch) {
            return;
        }

        return !$value;
    }
}
Structure
  • Add a single space after each comma delimiter;
  • Add a single space around binary operators (==, &&, ...), with the exception of the concatenation (.) operator;
  • Place unary operators (!, --, ...) adjacent to the affected variable;
  • Add a comma after each array item in a multi-line array, even after the last one;
  • Add a blank line before return statements, unless the return is alone inside a statement-group (like an if statement);
  • Use braces to indicate control structure body regardless of the number of statements it contains;
  • Define one class per file - this does not apply to private helper classes that are not intended to be instantiated from the outside and thus are not concerned by the PSR-0 standard;
  • Declare class properties before methods;
  • Declare public methods first, then protected ones and finally private ones. The exceptions to this rule are the class constructor and the setUp and tearDown methods of PHPUnit tests, which should always be the first methods to increase readability;
  • Use parentheses when instantiating classes regardless of the number of arguments the constructor has;
  • Exception message strings should be concatenated using sprintf.
Naming Conventions
  • Use camelCase, not underscores, for variable, function and method names, arguments;
  • Use underscores for option names and parameter names;
  • Use namespaces for all classes;
  • Prefix abstract classes with Abstract. Please note some early Symfony classes do not follow this convention and have not been renamed for backward compatibility reasons. However all new abstract classes must follow this naming convention;
  • Suffix interfaces with Interface;
  • Suffix traits with Trait;
  • Suffix exceptions with Exception;
  • Use alphanumeric characters and underscores for file names;
  • For type-hinting in PHPDocs and casting, use bool (instead of boolean or Boolean), int (instead of integer), float (instead of double or real);
  • Don’t forget to look at the more verbose Conventions document for more subjective naming considerations.
Service Naming Conventions
  • A service name contains groups, separated by dots;
  • The DI alias of the bundle is the first group (e.g. fos_user);
  • Use lowercase letters for service and parameter names;
  • A group name uses the underscore notation;
  • Each service has a corresponding parameter containing the class name, following the SERVICE NAME.class convention.
Documentation
  • Add PHPDoc blocks for all classes, methods, and functions;
  • Omit the @return tag if the method does not return anything;
  • The @package and @subpackage annotations are not used.
License
  • Symfony is released under the MIT license, and the license block has to be present at the top of every PHP file, before the namespace.
Conventions

The Coding Standards document describes the coding standards for the Symfony projects and the internal and third-party bundles. This document describes coding standards and conventions used in the core framework to make it more consistent and predictable. You are encouraged to follow them in your own code, but you don’t need to.

Method Names

When an object has a “main” many relation with related “things” (objects, parameters, ...), the method names are normalized:

  • get()
  • set()
  • has()
  • all()
  • replace()
  • remove()
  • clear()
  • isEmpty()
  • add()
  • register()
  • count()
  • keys()

The usage of these methods are only allowed when it is clear that there is a main relation:

  • a CookieJar has many Cookie objects;
  • a Service Container has many services and many parameters (as services is the main relation, the naming convention is used for this relation);
  • a Console Input has many arguments and many options. There is no “main” relation, and so the naming convention does not apply.

For many relations where the convention does not apply, the following methods must be used instead (where XXX is the name of the related thing):

Main Relation Other Relations
get() getXXX()
set() setXXX()
n/a replaceXXX()
has() hasXXX()
all() getXXXs()
replace() setXXXs()
remove() removeXXX()
clear() clearXXX()
isEmpty() isEmptyXXX()
add() addXXX()
register() registerXXX()
count() countXXX()
keys() n/a

注解

While “setXXX” and “replaceXXX” are very similar, there is one notable difference: “setXXX” may replace, or add new elements to the relation. “replaceXXX”, on the other hand, cannot add new elements. If an unrecognized key is passed to “replaceXXX” it must throw an exception.

Deprecations

From time to time, some classes and/or methods are deprecated in the framework; that happens when a feature implementation cannot be changed because of backward compatibility issues, but we still want to propose a “better” alternative. In that case, the old implementation can simply be deprecated.

A feature is marked as deprecated by adding a @deprecated phpdoc to relevant classes, methods, properties, ...:

/**
 * @deprecated Deprecated since version 2.X, to be removed in 2.Y. Use XXX instead.
 */

The deprecation message should indicate the version when the class/method was deprecated, the version when it will be removed, and whenever possible, how the feature was replaced.

A PHP E_USER_DEPRECATED error must also be triggered to help people with the migration starting one or two minor versions before the version where the feature will be removed (depending on the criticality of the removal):

trigger_error('XXX() is deprecated since version 2.X and will be removed in 2.Y. Use XXX instead.', E_USER_DEPRECATED);
Git

This document explains some conventions and specificities in the way we manage the Symfony code with Git.

Pull Requests

Whenever a pull request is merged, all the information contained in the pull request (including comments) is saved in the repository.

You can easily spot pull request merges as the commit message always follows this pattern:

merged branch USER_NAME/BRANCH_NAME (PR #1111)

The PR reference allows you to have a look at the original pull request on GitHub: https://github.com/symfony/symfony/pull/1111. But all the information you can get on GitHub is also available from the repository itself.

The merge commit message contains the original message from the author of the changes. Often, this can help understand what the changes were about and the reasoning behind the changes.

Moreover, the full discussion that might have occurred back then is also stored as a Git note (before March 22 2013, the discussion was part of the main merge commit message). To get access to these notes, add this line to your .git/config file:

fetch = +refs/notes/*:refs/notes/*

After a fetch, getting the GitHub discussion for a commit is then a matter of adding --show-notes=github-comments to the git show command:

$ git show HEAD --show-notes=github-comments
Symfony License

Symfony is released under the MIT license.

According to Wikipedia:

“It is a permissive license, meaning that it permits reuse within proprietary software on the condition that the license is distributed with that software. The license is also GPL-compatible, meaning that the GPL permits combination and redistribution with software that uses the MIT License.”
The License

Copyright (c) 2004-2015 Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Contributing Documentation

Contributing to the Documentation

One of the essential principles of the Symfony project is that documentation is as important as code. That’s why a great amount of resources are dedicated to documenting new features and to keeping the rest of the documentation up-to-date.

More than 700 developers all around the world have contributed to Symfony’s documentation and we are glad that you are considering joining this big family. This guide will explain everything you need to contribute to the Symfony documentation.

Before Your First Contribution

Before contributing, you should consider the following:

  • Symfony documentation is written using reStructuredText markup language. If you are not familiar with this format, read this article for a quick overview of its basic features.
  • Symfony documentation is hosted on GitHub. You’ll need a GitHub user account to contribute to the documentation.
  • Symfony documentation is published under a Creative Commons BY-SA 3.0 License and all your contributions will implicitly adhere to that license.
Your First Documentation Contribution

In this section, you’ll learn how to contribute to the Symfony documentation for the first time. The next section will explain the shorter process you’ll follow in the future for every contribution after your first one.

Let’s imagine that you want to improve the installation chapter of the Symfony book. In order to make your changes, follow these steps:

Step 1. Go to the official Symfony documentation repository located at github.com/symfony/symfony-docs and fork the repository to your personal account. This is only needed the first time you contribute to Symfony.

Step 2. Clone the forked repository to your local machine (this example uses the projects/symfony-docs/ directory to store the documentation; change this value accordingly):

$ cd projects/
$ git clone git://github.com/<YOUR GITHUB USERNAME>/symfony-docs.git

Step 3. Switch to the oldest maintained branch before making any change. Nowadays this is the 2.3 branch:

$ cd symfony-docs/
$ git checkout 2.3

If you are instead documenting a new feature, switch to the first Symfony version which included it: 2.5, 2.6, etc.

Step 4. Create a dedicated new branch for your changes. This greatly simplifies the work of reviewing and merging your changes. Use a short and memorable name for the new branch:

$ git checkout -b improve_install_chapter

Step 5. Now make your changes in the documentation. Add, tweak, reword and even remove any content, but make sure that you comply with the Documentation Standards.

Step 6. Push the changes to your forked repository:

$ git commit book/installation.rst
$ git push origin improve_install_chapter

Step 7. Everything is now ready to initiate a pull request. Go to your forked repository at https//github.com/<YOUR GITHUB USERNAME>/symfony-docs and click on the Pull Requests link located in the sidebar.

Then, click on the big New pull request button. As GitHub cannot guess the exact changes that you want to propose, select the appropriate branches where changes should be applied:º

_images/docs-pull-request-change-base.png

In this example, the base repository should be symfony/symfony-docs and the base branch should be the 2.3, which is the branch that you selected to base your changes on. The compare repository should be your forked copy of symfony-docs and the compare branch should be improve_install_chapter, which is the name of the branch you created and where you made your changes.

Step 8. The last step is to prepare the description of the pull request. To ensure that your work is reviewed quickly, please add the following table at the beginning of your pull request description:

| Q             | A
| ------------- | ---
| Doc fix?      | [yes|no]
| New docs?     | [yes|no] (PR # on symfony/symfony if applicable)
| Applies to    | [Symfony version numbers this applies to]
| Fixed tickets | [comma separated list of tickets fixed by the PR]

In this example, this table would look as follows:

| Q             | A
| ------------- | ---
| Doc fix?      | yes
| New docs?     | no
| Applies to    | all
| Fixed tickets | #10575

Step 9. Now that you’ve successfully submitted your first contribution to the Symfony documentation, go and celebrate! The documentation managers will carefully review your work in short time and they will let you know about any required change.

In case you need to add or modify anything, there is no need to create a new pull request. Just make sure that you are on the correct branch, make your changes and push them:

$ cd projects/symfony-docs/
$ git checkout improve_install_chapter

# ... do your changes

$ git push

Step 10. After your pull request is eventually accepted and merged in the Symfony documentation, you will be included in the Symfony Documentation Contributors list. Moreover, if you happen to have a SensioLabsConnect profile, you will get a cool Symfony Documentation Badge.

Your Second Documentation Contribution

The first contribution took some time because you had to fork the repository, learn how to write documentation, comply with the pull requests standards, etc. The second contribution will be much easier, except for one detail: given the furious update activity of the Symfony documentation repository, odds are that your fork is now out of date with the official repository.

Solving this problem requires you to sync your fork with the original repository. To do this, execute this command first to tell git about the original repository:

$ cd projects/symfony-docs/
$ git remote add upstream https://github.com/symfony/symfony-docs.git

Now you can sync your fork by executing the following command:

$ cd projects/symfony-docs/
$ git fetch upstream
$ git checkout 2.3
$ git merge upstream/2.3

This command will update the 2.3 branch, which is the one you used to create the new branch for your changes. If you have used another base branch, e.g. master, replace the 2.3 with the appropriate branch name.

Great! Now you can proceed by following the same steps explained in the previous section:

# create a new branch to store your changes based on the 2.3 branch
$ cd projects/symfony-docs/
$ git checkout 2.3
$ git checkout -b my_changes

# ... do your changes

# submit the changes to your forked repository
$ git add xxx.rst     # (optional) only if this is a new content
$ git commit xxx.rst
$ git push

# go to GitHub and create the Pull Request
#
# Include this table in the description:
# | Q             | A
# | ------------- | ---
# | Doc fix?      | [yes|no]
# | New docs?     | [yes|no] (PR # on symfony/symfony if applicable)
# | Applies to    | [Symfony version numbers this applies to]
# | Fixed tickets | [comma separated list of tickets fixed by the PR]

Your second contribution is now complete, so go and celebrate again! You can also see how your ranking improves in the list of Symfony Documentation Contributors.

Your Next Documentation Contributions

Now that you’ve made two contributions to the Symfony documentation, you are probably comfortable with all the Git-magic involved in the process. That’s why your next contributions would be much faster. Here you can find the complete steps to contribute to the Symfony documentation, which you can use as a checklist:

# sync your fork with the official Symfony repository
$ cd projects/symfony-docs/
$ git fetch upstream
$ git checkout 2.3
$ git merge upstream/2.3

# create a new branch from the oldest maintained version
$ git checkout 2.3
$ git checkout -b my_changes

# ... do your changes

# add and commit your changes
$ git add xxx.rst     # (optional) only if this is a new content
$ git commit xxx.rst
$ git push

# go to GitHub and create the Pull Request
#
# Include this table in the description:
# | Q             | A
# | ------------- | ---
# | Doc fix?      | [yes|no]
# | New docs?     | [yes|no] (PR # on symfony/symfony if applicable)
# | Applies to    | [Symfony version numbers this applies to]
# | Fixed tickets | [comma separated list of tickets fixed by the PR]

# (optional) make the changes requested by reviewers and commit them
$ git commit xxx.rst
$ git push

You guessed right: after all this hard work, it’s time to celebrate again!

Frequently Asked Questions
Why Do my Changes Take so Long to Be Reviewed and/or Merged?

Please be patient. It can take up to several days before your pull request can be fully reviewed. After merging the changes, it could take again several hours before your changes appear on the symfony.com website.

What If I Want to Translate Some Documentation into my Language?

Read the dedicated document.

Why Should I Use the Oldest Maintained Branch Instead of the Master Branch?

Consistent with Symfony’s source code, the documentation repository is split into multiple branches, corresponding to the different versions of Symfony itself. The master branch holds the documentation for the development branch of the code.

Unless you’re documenting a feature that was introduced after Symfony 2.3, your changes should always be based on the 2.3 branch. Documentation managers will use the necessary Git-magic to also apply your changes to all the active branches of the documentation.

What If I Want to Submit my Work without Fully Finishing It?

You can do it. But please use one of these two prefixes to let reviewers know about the state of your work:

  • [WIP] (Work in Progress) is used when you are not yet finished with your pull request, but you would like it to be reviewed. The pull request won’t be merged until you say it is ready.
  • [WCM] (Waiting Code Merge) is used when you’re documenting a new feature or change that hasn’t been accepted yet into the core code. The pull request will not be merged until it is merged in the core code (or closed if the change is rejected).
Would You Accept a Huge Pull Request with Lots of Changes?

First, make sure that the changes are somewhat related. Otherwise, please create separate pull requests. Anyway, before submitting a huge change, it’s probably a good idea to open an issue in the Symfony Documentation repository to ask the managers if they agree with your proposed changes. Otherwise, they could refuse your proposal after you put all that hard work into making the changes. We definitely don’t want you to waste your time!

Documentation Format

The Symfony documentation uses reStructuredText as its markup language and Sphinx for generating the documentation in the formats read by the end users, such as HTML and PDF.

reStructuredText

reStructuredText is a plaintext markup syntax similar to Markdown, but much stricter with its syntax. If you are new to reStructuredText, take some time to familiarize with this format by reading the existing Symfony documentation

If you want to learn more about this format, check out the reStructuredText Primer tutorial and the reStructuredText Reference.

警告

If you are familiar with Markdown, be careful as things are sometimes very similar but different:

  • Lists starts at the beginning of a line (no indentation is allowed);
  • Inline code blocks use double-ticks (``like this``).
Sphinx

Sphinx is a build system that provides tools to create documentation from reStructuredText documents. As such, it adds new directives and interpreted text roles to the standard reST markup. Read more about the Sphinx Markup Constructs.

Syntax Highlighting

PHP is the default syntax highlighter applied to all code blocks. You can change it with the code-block directive:

.. code-block:: yaml

    { foo: bar, bar: { foo: bar, bar: baz } }

注解

Besides all of the major programming languages, the syntax highlighter supports all kinds of markup and configuration languages. Check out the list of supported languages on the syntax highlighter website.

Configuration Blocks

Whenever you include a configuration sample, use the configuration-block directive to show the configuration in all supported configuration formats (PHP, YAML and XML). Example:

.. configuration-block::

    .. code-block:: yaml

        # Configuration in YAML

    .. code-block:: xml

        <!-- Configuration in XML -->

    .. code-block:: php

        // Configuration in PHP

The previous reST snippet renders as follow:

  • YAML
    # Configuration in YAML
    
  • XML
    <!-- Configuration in XML -->
    
  • PHP
    // Configuration in PHP
    

The current list of supported formats are the following:

Markup Format Use It to Display
html HTML
xml XML
php PHP
yaml YAML
jinja Pure Twig markup
html+jinja Twig markup blended with HTML
html+php PHP code blended with HTML
ini INI
php-annotations PHP Annotations
New Features or Behavior Changes

If you’re documenting a brand new feature or a change that’s been made in Symfony, you should precede your description of the change with a .. versionadded:: 2.X directive and a short description:

.. versionadded:: 2.3
    The ``askHiddenResponse`` method was introduced in Symfony 2.3.

You can also ask a question and hide the response. This is particularly [...]

If you’re documenting a behavior change, it may be helpful to briefly describe how the behavior has changed.

.. versionadded:: 2.3
    The ``include()`` function is a new Twig feature that's available in
    Symfony 2.3. Prior, the ``{% include %}`` tag was used.

Whenever a new minor version of Symfony is released (e.g. 2.4, 2.5, etc), a new branch of the documentation is created from the master branch. At this point, all the versionadded tags for Symfony versions that have reached end-of-life will be removed. For example, if Symfony 2.5 were released today, and 2.2 had recently reached its end-of-life, the 2.2 versionadded tags would be removed from the new 2.5 branch.

Testing Documentation

When submitting a new content to the documentation repository or when changing any existing resource, an automatic process will check if your documentation is free of syntax errors and is ready to be reviewed.

Nevertheless, if you prefer to do this check locally on your own machine before submitting your documentation, follow these steps:

  • Install Sphinx;
  • Install the Sphinx extensions using git submodules: $ git submodule update --init;
  • Run make html and view the generated HTML in the build/ directory.
Documentation Standards

In order to help the reader as much as possible and to create code examples that look and feel familiar, you should follow these standards.

Sphinx
  • The following characters are chosen for different heading levels: level 1 is =, level 2 -, level 3 ~, level 4 . and level 5 ";
  • Each line should break approximately after the first word that crosses the 72nd character (so most lines end up being 72-78 characters);
  • The :: shorthand is preferred over .. code-block:: php to begin a PHP code block (read the Sphinx documentation to see when you should use the shorthand);
  • Inline hyperlinks are not used. Separate the link and their target definition, which you add on the bottom of the page;
  • Inline markup should be closed on the same line as the open-string;
Example
Example
=======

When you are working on the docs, you should follow the
`Symfony Documentation`_ standards.

Level 2
-------

A PHP example would be::

    echo 'Hello World';

Level 3
~~~~~~~

.. code-block:: php

    echo 'You cannot use the :: shortcut here';

.. _`Symfony Documentation`: http://symfony.com/doc
Code Examples
  • The code follows the Symfony Coding Standards as well as the Twig Coding Standards;
  • To avoid horizontal scrolling on code blocks, we prefer to break a line correctly if it crosses the 85th character;
  • When you fold one or more lines of code, place ... in a comment at the point of the fold. These comments are: // ... (php), # ... (yaml/bash), {# ... #} (twig), <!-- ... --> (xml/html), ; ... (ini), ... (text);
  • When you fold a part of a line, e.g. a variable value, put ... (without comment) at the place of the fold;
  • Description of the folded code: (optional) If you fold several lines: the description of the fold can be placed after the ... If you fold only part of a line: the description can be placed before the line;
  • If useful to the reader, a PHP code example should start with the namespace declaration;
  • When referencing classes, be sure to show the use statements at the top of your code block. You don’t need to show all use statements in every example, just show what is actually being used in the code block;
  • If useful, a codeblock should begin with a comment containing the filename of the file in the code block. Don’t place a blank line after this comment, unless the next line is also a comment;
  • You should put a $ in front of every bash line.
Formats

Configuration examples should show all supported formats using configuration blocks. The supported formats (and their orders) are:

  • Configuration (including services and routing): YAML, XML, PHP
  • Validation: YAML, Annotations, XML, PHP
  • Doctrine Mapping: Annotations, YAML, XML, PHP
  • Translation: XML, YAML, PHP
Example
// src/Foo/Bar.php
namespace Foo;

use Acme\Demo\Cat;
// ...

class Bar
{
    // ...

    public function foo($bar)
    {
        // set foo with a value of bar
        $foo = ...;

        $cat = new Cat($foo);

        // ... check if $bar has the correct value

        return $cat->baz($bar, ...);
    }
}

警告

In YAML you should put a space after { and before } (e.g. { _controller: ... }), but this should not be done in Twig (e.g. {'hello' : 'value'}).

Files and Directories
  • When referencing directories, always add a trailing slash to avoid confusions with regular files (e.g. “execute the console script located at the app/ directory”).

  • When referencing file extensions explicitly, you should include a leading dot for every extension (e.g. “XML files use the .xml extension”).

  • When you list a Symfony file/directory hierarchy, use your-project/ as the top level directory. E.g.

    your-project/
    ├─ app/
    ├─ src/
    ├─ vendor/
    └─ ...
    
English Language Standards
  • English Dialect: use the United States English dialect, commonly called American English.

  • Section titles: use a variant of the title case, where the first word is always capitalized and all other words are capitalized, except for the closed-class words (read Wikipedia article about headings and titles).

    E.g.: The Vitamins are in my Fresh California Raisins

  • Punctuation: avoid the use of Serial (Oxford) Commas;

  • Pronouns: avoid the use of nosism and always use you instead of we. (i.e. avoid the first person point of view: use the second instead);

  • Gender-neutral language: when referencing a hypothetical person, such as “a user with a session cookie”, use gender-neutral pronouns (they/their/them). For example, instead of: * he or she, use they * him or her, use them * his or her, use their * his or hers, use theirs * himself or herself, use themselves

Translations

The Symfony documentation is written in English and many people are involved in the translation process.

注解

Symfony Project officially discourages starting new translations for the documentation. As a matter of fact, there is an ongoing discussion in the community about the benefits and drawbacks of community driven translations.

Contributing

First, become familiar with the markup language used by the documentation.

Then, subscribe to the Symfony docs mailing-list, as collaboration happens there.

Finally, find the master repository for the language you want to contribute for. Here is the list of the official master repositories:

注解

If you want to contribute translations for a new language, read the dedicated section.

Joining the Translation Team

If you want to help translating some documents for your language or fix some bugs, consider joining us; it’s a very easy process:

  • Introduce yourself on the Symfony docs mailing-list;
  • (optional) Ask which documents you can work on;
  • Fork the master repository for your language (click the “Fork” button on the GitHub page);
  • Translate some documents;
  • Ask for a pull request (click on the “Pull Request” from your page on GitHub);
  • The team manager accepts your modifications and merges them into the master repository;
  • The documentation website is updated every other night from the master repository.
Adding a new Language

This section gives some guidelines for starting the translation of the Symfony documentation for a new language.

As starting a translation is a lot of work, talk about your plan on the Symfony docs mailing-list and try to find motivated people willing to help.

When the team is ready, nominate a team manager; they will be responsible for the master repository.

Create the repository and copy the English documents.

The team can now start the translation process.

When the team is confident that the repository is in a consistent and stable state (everything is translated, or non-translated documents have been removed from the toctrees – files named index.rst and map.rst.inc), the team manager can ask that the repository is added to the list of official master repositories by sending an email to Fabien (fabien at symfony.com).

Maintenance

Translation does not end when everything is translated. The documentation is a moving target (new documents are added, bugs are fixed, paragraphs are reorganized, ...). The translation team need to closely follow the English repository and apply changes to the translated documents as soon as possible.

警告

Non maintained languages are removed from the official list of repositories as obsolete documentation is dangerous.

Symfony Documentation License

The Symfony documentation is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License (CC BY-SA 3.0).

You are free:

  • to Share — to copy, distribute and transmit the work;
  • to Remix — to adapt the work.

Under the following conditions:

  • Attribution — You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work);
  • Share Alike — If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.

With the understanding that:

  • Waiver — Any of the above conditions can be waived if you get permission from the copyright holder;
  • Public Domain — Where the work or any of its elements is in the public domain under applicable law, that status is in no way affected by the license;
  • Other Rights — In no way are any of the following rights affected by the license:
    • Your fair dealing or fair use rights, or other applicable copyright exceptions and limitations;
    • The author’s moral rights;
    • Rights other persons may have either in the work itself or in how the work is used, such as publicity or privacy rights.
  • Notice — For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to this web page.

This is a human-readable summary of the Legal Code (the full license).

Community

The Release Process

This document explains the Symfony release process (Symfony being the code hosted on the main symfony/symfony Git repository).

Symfony manages its releases through a time-based model; a new Symfony minor version comes out every six months: one in May and one in November.

小技巧

The meaning of “minor” comes from the Semantic Versioning strategy.

Each minor version sticks to the same very well-defined process where we start with a development period, followed by a maintenance period.

注解

This release process has been adopted as of Symfony 2.2, and all the “rules” explained in this document must be strictly followed as of Symfony 2.4.

Development

The full development period lasts six months and is divided into two phases:

  • Development: Four months to add new features and to enhance existing ones;
  • Stabilisation: Two months to fix bugs, prepare the release, and wait for the whole Symfony ecosystem (third-party libraries, bundles, and projects using Symfony) to catch up.

During the development phase, any new feature can be reverted if it won’t be finished in time or if it won’t be stable enough to be included in the current final release.

Maintenance

Each Symfony minor version is maintained for a fixed period of time, depending on the type of the release. We have two maintenance periods:

  • Bug fixes and security fixes: During this period, all issues can be fixed. The end of this period is referenced as being the end of maintenance of a release.
  • Security fixes only: During this period, only security related issues can be fixed. The end of this period is referenced as being the end of life of a release.
Standard Versions

A standard minor version is maintained for an eight month period for bug fixes, and for a fourteen month period for security issue fixes.

Long Term Support Versions

Every two years, a new Long Term Support Version (aka LTS version) is published. Each LTS version is supported for a three year period for bug fixes, and for a four year period for security issue fixes.

注解

Paid support after the three year support provided by the community can also be bought from SensioLabs.

Schedule

Below is the schedule for the first few versions that use this release model:

_images/release-process.jpg
  • Yellow represents the Development phase
  • Blue represents the Stabilisation phase
  • Green represents the Maintenance period

This results in very predictable dates and maintenance periods:

Version Feature Freeze Release End of Maintenance End of Life
2.0 05/2011 07/2011 03/2013 (20 months) 09/2013
2.1 07/2012 09/2012 05/2013 (9 months) 11/2013
2.2 01/2013 03/2013 11/2013 (8 months) 05/2014
2.3 03/2013 05/2013 05/2016 (36 months) 05/2017
2.4 09/2013 11/2013 09/2014 (10 months [1]) 01/2015
2.5 03/2014 05/2014 01/2015 (8 months) 07/2015
2.6 09/2014 11/2014 07/2015 (8 months) 01/2016
2.7 03/2015 05/2015 05/2018 (36 months [2]) 05/2019
3.0 09/2015 11/2015 07/2016 (8 months) 01/2017
3.1 03/2016 05/2016 01/2017 (8 months) 07/2017
3.2 09/2016 11/2016 07/2017 (8 months) 01/2018
3.3 03/2017 05/2017 05/2020 (36 months) 05/2021
... ... ... ... ...
[1]Symfony 2.4 maintenance has been extended to September 2014.
[2]Symfony 2.7 is the last version of the Symfony 2.x branch.

小技巧

If you want to learn more about the timeline of any given Symfony version, use the online timeline calculator. You can also get all data as a JSON string via a URL like http://symfony.com/roadmap.json?version=2.x.

小技巧

Whenever an important event related to Symfony versions happens (a version reaches end of maintenance or a new patch version is released for instance), you can automatically receive an email notification if you subscribed on the roadmap notification page.

Backwards Compatibility

Our Backwards Compatibility Promise is very strict and allows developers to upgrade with confidence from one minor version of Symfony to the next one.

Whenever keeping backward compatibility is not possible, the feature, the enhancement or the bug fix will be scheduled for the next major version.

注解

The work on a new major version of Symfony starts whenever enough major features breaking backward compatibility are waiting on the todo-list.

Deprecations

When a feature implementation cannot be replaced with a better one without breaking backward compatibility, there is still the possibility to deprecate the old implementation and add a new preferred one along side. Read the conventions document to learn more about how deprecations are handled in Symfony.

Rationale

This release process was adopted to give more predictability and transparency. It was discussed based on the following goals:

  • Shorten the release cycle (allow developers to benefit from the new features faster);
  • Give more visibility to the developers using the framework and Open-Source projects using Symfony;
  • Improve the experience of Symfony core contributors: everyone knows when a feature might be available in Symfony;
  • Coordinate the Symfony timeline with popular PHP projects that work well with Symfony and with projects using Symfony;
  • Give time to the Symfony ecosystem to catch up with the new versions (bundle authors, documentation writers, translators, ...).

The six month period was chosen as two releases fit in a year. It also allows for plenty of time to work on new features and it allows for non-ready features to be postponed to the next version without having to wait too long for the next cycle.

The dual maintenance mode was adopted to make every Symfony user happy. Fast movers, who want to work with the latest and the greatest, use the standard version: a new version is published every six months, and there is a two months period to upgrade. Companies wanting more stability use the LTS versions: a new version is published every two years and there is a year to upgrade.

Other Resources

In order to follow what is happening in the community you might find helpful these additional resources: