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¶
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¶
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¶
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:

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!

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:

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,
));
}
// ...
}
Creating Links between Pages¶
Creating links between pages is a must for web applications. Instead of hardcoding URLs in templates, the path function knows how to generate URLs based on the routing configuration. That way, all your URLs can be easily updated by just changing the configuration:
<a href="{{ path('homepage') }}">Return to homepage</a>
The path function takes the route name as the first argument and you can optionally pass an array of route parameters as the second argument.
小技巧
The url function is very similar to the path function, but generates absolute URLs, which is very handy when rendering emails and RSS files: <a href="{{ url('homepage') }}">Visit our website</a>.
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.
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.
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.
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:

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:

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:

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:

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:
- Provides a selection of components (i.e. the Symfony Components) and third-party libraries (e.g. Swift Mailer for sending emails);
- 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.php 和 show.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 提供了 Request 和 Response , 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 中的独立 Routing 和 Templating 组件,因为它们已经帮你解决了很多问题。
为了不用重新发明轮子,你可以让 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:

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:
- For Apache HTTP Server, refer to Apache’s DirectoryIndex documentation
- For Nginx, refer to Nginx HttpCoreModule location documentation
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 项目处理的请求都经过这个简单的生命周期。框架将接管所有重复的活动,这意味着你只需要把你自己的特有的代码写入控制器的函数即可:
- 每个请求都被交给一个单一的前端控制器(Front Controller)处理并引导整个程序(如 app.php 或 app_dev.php);
- Router(路由器) 从请求中读取信息(比如 URI),并 寻找一条符合这个信息的路由,然后读取路由信息中的 _controller 参数;
- 被命中的路由信息中给出的控制器将被执行,控制器中的代码将 创建并返回一个 Response 对象;
- 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`` 方法。 别被 *控制器类 这个名字搞糊涂了,这只是一种将几个 控制器(方法)组合在一起的简便方法而已。一般情况下,控制器类 里面会有一些控制器(比如 updateAction、deleteAction 等等)。
这个控制器相当明了:
- 第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。
也有一些可以简单快速地创建其他类型的响应的类。
- JSON对应的类是 JsonResponse。 参阅 Creating a JSON Response。
- 文件对应的类是 BinaryFileResponse。 参阅 Serving Files。
- 流式响应对应的类在 StreamedResponse。 参阅 Streaming a Response。
参见
别担心!在足见的文档里还有很多关于响应对象的 信息。参阅 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。
重定向到另一个控制器¶
虽然不是很常用,但你还是可以使用 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:
- The request is handled by the Symfony front controller (e.g. app.php);
- The Symfony core (i.e. Kernel) asks the router to inspect the request;
- The router matches the incoming URL to a specific route and returns information about the route, including the controller that should be executed;
- The Symfony Kernel executes the controller, which ultimately returns a Response object.

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 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.
Learn more from the Cookbook¶
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:
- app/Resources/AcmeBlogBundle/views/Blog/index.html.twig
- 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:
- app/Resources/AcmeBlogBundle/views/layout.html.twig
- 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 <script>alert('helloe')</script>
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.
Learn more from the Cookbook¶
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:

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')
);
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:
- fetching the object from Doctrine;
- modifying the object;
- 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.

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.
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:
- fetching the object from Propel (line 6 - 13);
- modifying the object (line 15);
- 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');
});
Links¶
To select links, you can use the traversing methods above or the convenient selectLink() shortcut:
$crawler->selectLink('Click here');
This selects all links that contain the given text, or clickable images for which the alt attribute contains the given text. Like the other filtering methods, this returns another Crawler object.
Once you’ve selected a link, you have access to a special Link object, which has helpful methods specific to links (such as getMethod() and getUri()). To click on the link, use the Client’s click() method and pass it a Link object:
$link = $crawler->selectLink('Click here')->link();
$client->click($link);
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.
Comparison Constraints¶
Collection 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) ?>

注解
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:
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.
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.
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:
Field Groups¶
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'))

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.
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.
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:
- Pass an object when creating the form (as the first argument to createFormBuilder or the second argument to createForm);
- 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.
Learn more from the Cookbook¶
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:
- Initial security.yml setup (authentication);
- Denying access to your app (authorization);
- 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:

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:

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:

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)).
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:
- Configure a different way for your users to login, like a login form or something completely custom;
- Load users from a different source, like the database or some other source;
- Learn how to deny access, load the User object and deal with roles in the Authorization section.
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:
- The user receives a specific set of roles when logging in (e.g. ROLE_ADMIN).
- 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:
- access_control in security.yml allows you to protect URL patterns (e.g. /admin/*). This is easy, but less flexible;
- in your code via the security.context service.
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.
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).
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') %}
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:
- 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.
- 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.
- 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).
- 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:
- Enable and configure Symfony’s translation service;
- Abstract strings (i.e. “messages”) by wrapping them in calls to the Translator (“Basic Translation”);
- Create translation resources/files for each supported locale that translate each message in the application;
- 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:
- First, Symfony looks for the translation in a fr_FR translation resource (e.g. messages.fr_FR.xliff);
- If it wasn’t found, Symfony looks for the translation in a fr translation resource (e.g. messages.fr.xliff);
- 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
Learn more¶
- Introduction to Parameters
- Compiling the Container
- Working with Container Service Definitions
- Using a Factory to Create Services
- Managing common Dependencies with parent Services
- Working with Tagged Services
- How to Define Controllers as Services
- How to Work with Scopes
- How to Work with Compiler Passes in Bundles
- Advanced Container Configuration
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):
- Before doing anything else, the kernel.request event is notified – if one of the listeners returns a Response, it jumps to step 8 directly;
- The Resolver is called to determine the Controller to execute;
- Listeners of the kernel.controller event can now manipulate the Controller callable the way they want (change it, wrap it, ...);
- The Kernel checks that the Controller is actually a valid PHP callable;
- The Resolver is called to determine the arguments to pass to the Controller;
- The Kernel calls the Controller;
- If the Controller does not return a Response, listeners of the kernel.view event can convert the Controller return value to a Response;
- Listeners of the kernel.response event can manipulate the Response (content and headers);
- The Response is returned;
- 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.
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.
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.
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.
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.
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.
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.
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¶
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).
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.
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);
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”.
- Symfony and HTTP Fundamentals
- 使用 Symfony 与不使用框架的对比
- Installing and Configuring Symfony
- Creating Pages in Symfony
- 控制器(Controller)
- Routing
- Creating and Using Templates
- Databases and Doctrine
- Databases and Propel
- Testing
- Validation
- Forms
- Security
- HTTP Cache
- Translations
- Service Container
- Performance
- Internals
- Symfony and HTTP Fundamentals
- 使用 Symfony 与不使用框架的对比
- Installing and Configuring Symfony
- Creating Pages in Symfony
- 控制器(Controller)
- Routing
- Creating and Using Templates
- Databases and Doctrine
- Databases and Propel
- Testing
- Validation
- Forms
- Security
- HTTP Cache
- Translations
- Service Container
- Performance
- Internals
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.
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.
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.
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.
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.
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 ?>
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.
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.
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.
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 ?>
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', ), ), ));
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:
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.
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.
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.
Learn more from the Cookbook¶
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.
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.
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();
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
}
}
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.
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.
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.
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:
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';
}
}
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.
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.
Going further¶
Read the article on How to Set external Parameters in the Service Container.
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:
- registerBundles(), which must return an array of all bundles needed to run the application;
- registerContainerConfiguration(), which loads the application configuration.
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¶
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;
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)
);
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
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>
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.
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.
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:
- Use the default ExceptionController and create a few templates that allow you to customize how your different error pages look (easy);
- Replace the default exception controller with your own (intermediate).
- 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.
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”.
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:
- Upload your modified code to the live server;
- Update your vendor dependencies (typically done via Composer, and may be done before uploading);
- Running database migrations or similar tasks to update any changed data structures;
- 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.
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)
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).
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:
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).
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.
Make sure you clear (and warm-up) your Symfony cache:
$ php app/console cache:clear --env=prod --no-debug
If you’re using Assetic, you’ll also want to dump your assets:
$ php app/console assetic:dump --env=prod --no-debug
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¶
This tool provides a specialized set of tools on top of Capistrano, tailored specifically to symfony and Symfony projects.
This tool helps you build a native Debian package for your Symfony project.
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:

Here, you will be prompted to fill in some basic information.

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.
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.

Agree to the terms and conditions and click on the right arrow to continue.
Now, on the third step, select a Local Git repository item and click on the right arrow to configure your Azure Website credentials.

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.

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:

The Microsoft Azure portal also provides a complete control panel for the Azure Website.

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.
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.

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.

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.
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.

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:

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:

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:

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.
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:

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:

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.
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.

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.

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.

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.
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.
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!
By default, Heroku will launch an Apache web server together with PHP to serve applications. However, two special circumstances apply to Symfony applications:
- The document root is in the web/ directory and not in the root directory of the application;
- 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(+)
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
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.
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.
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.
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 }
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:
- Use the StofDoctrineExtensionsBundle, which integrates the above library.
- 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', ));
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()
{
// ...
}
}
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!');
}
}
}
}
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.
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.
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.
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.
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.
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 %}
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.
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.
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.
注解
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 ?>
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.
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.
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);
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.

In any form, the three different types of data are:
- 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.
- 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.
- 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:
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.
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.
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.
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.
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.
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.
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.
Our form is now ready to use and there are two possible ways to use it inside of a controller:
- create it manually and remember to pass the security context to it;
or
- define it as a service.
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)
);
// ...
}
}
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="<div><label class=" required">__name__</label><div id="task_tags___name__"><div><label for="task_tags___name___name" class=" required">Name</label><input type="text" id="task_tags___name___name" name="task[tags][__name__][name]" required="required" maxlength="255" /></div></div></div>">
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);
}
}
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:
- You want to add a generic feature to several types (such as adding a “help” text to every field type);
- 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:
- Override the setDefaultOptions method in order to add an image_path option;
- 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(),
));
}
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.
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.
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.
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%%
</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.
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.
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.
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.
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:
- The user tries to access a resource that is protected;
- The firewall initiates the authentication process by redirecting the user to the login form (/login);
- The /login page renders login form via the route and controller created in this example;
- The user submits the login form to /login_check;
- 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:
- isAccountNonExpired() checks whether the user’s account has expired;
- isAccountNonLocked() checks whether the user is locked;
- isCredentialsNonExpired() checks whether the user’s credentials (password) has expired;
- isEnabled() checks whether the user is enabled.
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.
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.
// 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.
// 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.
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.
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.
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 |
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.
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.
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.
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, ), ), ), ));
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, ), ), ), ));
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, ), ), ), ));
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:
- Username / Password encryption
- Safe guarding against replay attacks
- 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?
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:
- should the incoming request match this access control entry
- 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.
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 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');
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' %}
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.
Speaking of web applications, creating links between pages is a must. Instead of hardcoding URLs in templates, the router helper knows how to generate URLs based on the routing configuration. That way, all your URLs can be easily updated by changing the configuration:
<a href="<?php echo $view['router']->generate('hello', array('name' => 'Thomas')) ?>">
Greet Thomas!
</a>
The generate() method takes the route name and an array of parameters as arguments. The route name is the main key under which routes are referenced and the parameters are the values of the placeholders defined in the route pattern:
# src/AppBundle/Resources/config/routing.yml
hello: # The route name
path: /hello/{name}
defaults: { _controller: AppBundle:Hello:index }
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.
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.
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.
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.
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:
Initialize your Git repository:
$ git init
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.
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¶
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:
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
Copy the Symfony project files in the Subversion folder:
$ mv Symfony/* myproject/
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)"
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¶
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.
- Assetic
- Bundles
- How to Install 3rd Party Bundles
- Best Practices for Reusable Bundles
- How to Use Bundle Inheritance to Override Parts of a Bundle
- How to Override any Part of a Bundle
- How to Remove the AcmeDemoBundle
- How to Load Service Configuration inside a Bundle
- How to Create Friendly Configuration for a Bundle
- How to Simplify Configuration of multiple Bundles
- Cache
- Composer
- Configuration
- How to Master and Create new Environments
- How to Override Symfony’s default Directory Structure
- Using Parameters within a Dependency Injection Class
- Understanding how the Front Controller, Kernel and Environments Work together
- How to Set external Parameters in the Service Container
- How to Use PdoSessionHandler to Store Sessions in the Database
- How to Use the Apache Router
- Configuring a Web Server
- How to Organize Configuration Files
- Console
- Controller
- Debugging
- Deployment
- Doctrine
- How to Handle File Uploads with Doctrine
- How to use Doctrine Extensions: Timestampable, Sluggable, Translatable, etc.
- How to Register Event Listeners and Subscribers
- How to Use Doctrine DBAL
- How to Generate Entities from an Existing Database
- How to Work with multiple Entity Managers and Connections
- How to Register custom DQL Functions
- How to Define Relationships with Abstract Classes and Interfaces
- How to Provide Model Classes for several Doctrine Implementations
- How to Implement a simple Registration Form
- Console Commands
- (configuration) How to Use PdoSessionHandler to Store Sessions in the Database
- Event Dispatcher
- Form
- How to Customize Form Rendering
- How to Use Data Transformers
- How to Dynamically Modify Forms Using Form Events
- How to Embed a Collection of Forms
- How to Create a Custom Form Field Type
- How to Create a Form Type Extension
- How to Reduce Code Duplication with “inherit_data”
- How to Unit Test your Forms
- How to Configure empty Data for a Form Class
- How to Use the submit() Function to Handle Form Submissions
- (validation) How to Create a custom Validation Constraint
- (doctrine) How to Handle File Uploads with Doctrine
- Logging
- Profiler
- Request
- Routing
- How to Force Routes to always Use HTTPS or HTTP
- How to Allow a “/” Character in a Route Parameter
- How to Configure a Redirect without a custom Controller
- How to Use HTTP Methods beyond GET and POST in Routes
- How to Use Service Container Parameters in your Routes
- How to Create a custom Route Loader
- Redirect URLs with a Trailing Slash
- How to Pass Extra Information from a Route to a Controller
- Security
- How to Build a Traditional Login Form
- How to Load Security Users from the Database (the Entity Provider)
- How to Add “Remember Me” Login Functionality
- How to Impersonate a User
- How to Implement your own Voter to Blacklist IP Addresses
- How to Use Voters to Check User Permissions
- How to Use Access Control Lists (ACLs)
- How to Use advanced ACL Concepts
- How to Force HTTPS or HTTP for different URLs
- How to Customize your Form Login
- How to Secure any Service or Method in your Application
- How to Create a custom User Provider
- How to Create a custom Authentication Provider
- Using pre Authenticated Security Firewalls
- How to Change the default Target Path Behavior
- Using CSRF Protection in the Login Form
- How Does the Security access_control Work?
- How to Use multiple User Providers
- Serializer
- Service Container
- Sessions
- Session Proxy Examples
- Making the Locale “Sticky” during a User’s Session
- Configuring the Directory where Session Files are Saved
- Bridge a legacy Application with Symfony Sessions
- (configuration) How to Use PdoSessionHandler to Store Sessions in the Database
- Avoid Starting Sessions for Anonymous Users
- symfony1
- Templating
- Testing
- How to Simulate HTTP Authentication in a Functional Test
- How to Simulate Authentication with a Token in a Functional Test
- How to Test the Interaction of several Clients
- How to Use the Profiler in a Functional Test
- How to Test Code that Interacts with the Database
- How to Test Doctrine Repositories
- How to Customize the Bootstrap Process before Running Tests
- (email) How to Test that an Email is Sent in a functional Test
- (form) How to Unit Test your Forms
- Upgrading
- Validation
- Web Server
- How to Use PHP’s built-in Web Server
- (configuration) Configuring a Web Server
- Web Services
- Workflow
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.
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:
- The PSR-0 Class Loader: loads classes that follow the PSR-0 class naming standard;
- MapClassLoader: loads classes using a static map from class name to file path.
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:
- Install it via Composer (symfony/class-loader on Packagist);
- Use the official Git repository (https://github.com/symfony/ClassLoader).
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:
- Install it via Composer (symfony/config on Packagist);
- Use the official Git repository (https://github.com/symfony/Config).
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¶
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.
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.
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()
;
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.
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.).
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:
- Install it via Composer (symfony/console on Packagist);
- Use the official Git repository (https://github.com/symfony/Console).
注解
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
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>');
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:
- Dialog Helper: interactively ask the user for information
- Formatter Helper: customize the output colorization
- Progress Helper: shows a progress bar
- Table Helper: displays tabular data as a table
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.
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.
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.
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
);
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.
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.
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.
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.
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.
Symfony offers a defined style when printing a message that belongs to some “section”. It prints the section in color and with brackets around it and the actual message to the right of this. Minus the color, it looks like this:
[SomeSection] Here is some message related to that section
To reproduce this style, you can use the formatSection() method:
$formattedLine = $formatter->formatSection(
'SomeSection',
'Here is some message related to that section'
);
$output->writeln($formattedLine);
Sometimes you want to be able to print a whole block of text with a background color. Symfony uses this when printing error messages.
If you print your error message on more than one line manually, you will notice that the background is only as long as each individual line. Use the formatBlock() to generate a block output:
$errorMessages = array('Error!', 'Something went wrong');
$formattedBlock = $formatter->formatBlock($errorMessages, 'error');
$output->writeln($formattedBlock);
As you can see, passing an array of messages to the formatBlock() method creates the desired output. If you pass true as third parameter, the block will be formatted with more padding (one blank line above and below the messages and 2 spaces on the left and right).
The exact “style” you use in the block is up to you. In this case, you’re using the pre-defined error style, but there are other styles, or you can create your own. See Coloring the Output.
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:

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:

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.
The Table helper ships with two preconfigured table layouts:
- TableHelper::LAYOUT_DEFAULT
- TableHelper::LAYOUT_BORDERLESS
Layout can be set using setLayout() method.
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:
- Install it via Composer (symfony/css-selector on Packagist);
- Use the official Git repository (https://github.com/symfony/CssSelector).
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:
- Install it via Composer (symfony/debug on Packagist);
- Use the official Git repository (https://github.com/symfony/Debug).
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:
- Install it via Composer (symfony/dependency-injection on Packagist);
- Use the official Git repository (https://github.com/symfony/DependencyInjection).
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¶
If you need to create a new definition rather than manipulate one retrieved from the container then the definition class is Definition.
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
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);
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.
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:
- Install it via Composer (symfony/dom-crawler on Packagist);
- Use the official Git repository (https://github.com/symfony/DomCrawler).
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);
Links¶
To find a link by name (or a clickable image by its alt attribute), use the selectLink method on an existing crawler. This returns a Crawler instance with just the selected link(s). Calling link() gives you a special Link object:
$linksCrawler = $crawler->selectLink('Go elsewhere...');
$link = $linksCrawler->link();
// or do this all at once
$link = $crawler->selectLink('Go elsewhere...')->link();
The Link object has several useful methods to get more information about the selected link itself:
// return the proper URI that can be used to make another request
$uri = $link->getUri();
注解
The getUri() is especially useful as it cleans the href value and transforms it into how it should really be processed. For example, for a link with href="#foo", this would return the full URI of the current page suffixed with #foo. The return from getUri() is always a full URI that you can act on.
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:
- Install it via Composer (symfony/event-dispatcher on Packagist);
- Use the official Git repository (https://github.com/symfony/EventDispatcher).
Usage¶
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.
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
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 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();
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();
// ...
}
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.
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.
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.
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
}
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.
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()) {
// ...
}
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.
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...
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.
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'));
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
- __construct(): Constructor takes the event subject and any arguments;
- getSubject(): Get the subject;
- setArgument(): Sets an argument by key;
- setArguments(): Sets arguments array;
- getArgument(): Gets an argument by key;
- getArguments(): Getter for all arguments;
- hasArgument(): Returns true if the argument key exists;
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:
- Install it via Composer (symfony/filesystem on Packagist);
- Use the official Git repository (https://github.com/symfony/Filesystem).
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');
symlink¶
symlink() creates a symbolic link from the target to the destination. If the filesystem does not support symbolic links, a third boolean argument is available:
// create a symbolic link
$fs->symlink('/path/to/source', '/path/to/destination');
// duplicate the source directory if the filesystem
// does not support symbolic links
$fs->symlink('/path/to/source', '/path/to/destination', true);
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')
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:
- Install it via Composer (symfony/finder on Packagist);
- Use the official Git repository (https://github.com/symfony/Finder).
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:
- Install it via Composer (symfony/form on Packagist);
- Use the official Git repository (https://github.com/symfony/Form).
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
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.
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);
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.
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.
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.
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.
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.
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>

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.
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', )); // ... }
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:
- 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:
- if the form is invalid, re-render the form (which will now contain errors);
- 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.
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.
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:
- guessType() - tries to guess the type of a field;
- guessRequired() - tries to guess the value of the required option;
- guessMaxLength() - tries to guess the value of the max_length option;
- guessPattern() - tries to guess the value of the pattern option.
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)
{
}
}
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!
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¶


Two events are dispatched during pre-population of a form, when Form::setData() is called: FormEvents::PRE_SET_DATA and FormEvents::POST_SET_DATA.
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).
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.
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.
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 |

Three events are dispatched when Form::handleRequest() or Form::submit() are called: FormEvents::PRE_SUBMIT, FormEvents::SUBMIT, FormEvents::POST_SUBMIT.
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.
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 |
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.
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.
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.
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.
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 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:
- Install it via Composer (symfony/http-foundation on Packagist);
- Use the official Git repository (https://github.com/symfony/HttpFoundation).
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
);
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:
- request: ParameterBag;
- query: ParameterBag;
- cookies: ParameterBag;
- attributes: ParameterBag;
- files: FileBag;
- server: ServerBag;
- headers: HeaderBag.
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.
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();
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().
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.
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();
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.
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();
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.
The Response class has a rich set of methods to manipulate the HTTP headers related to the cache:
- setPublic();
- setPrivate();
- expire();
- setExpires();
- setMaxAge();
- setSharedMaxAge();
- setTtl();
- setClientTtl();
- setLastModified();
- setEtag();
- setVary();
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.
To redirect the client to another URL, you can use the RedirectResponse class:
use Symfony\Component\HttpFoundation\RedirectResponse;
$response = new RedirectResponse('http://example.com/');
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.
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'
);
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.
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.
- 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().
- 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.
- 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
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 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.
- PdoSessionHandler
- MemcacheSessionHandler
- MemcachedSessionHandler
- MongoDbSessionHandler
- NullSessionHandler
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.
Session Cookie Lifetime¶
For security, session tokens are generally recommended to be sent as session cookies. You can configure the lifetime of session cookies by specifying the lifetime (in seconds) using the cookie_lifetime key in the constructor’s $options argument in NativeSessionStorage.
Setting a cookie_lifetime to 0 will cause the cookie to live only as long as the browser remains open. Generally, cookie_lifetime would be set to a relatively large number of days, weeks or months. It is not uncommon to set cookies for a year or more depending on the application.
Since session cookies are just a client-side token, they are less important in controlling the fine details of your security settings which ultimately can only be securely controlled from the server side.
注解
The cookie_lifetime setting is the number of seconds the cookie should live for, it is not a Unix timestamp. The resulting session cookie will be stamped with an expiry time of time() + cookie_lifetime where the time is taken from the server.
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:
- getId(): Gets the session ID.
- setId(): Sets the session ID.
- getName(): Gets the session name.
- setName(): Sets the session name.
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:
- X-Forwarded-For Used in getClientIp();
- X-Forwarded-Host Used in getHost();
- X-Forwarded-Port Used in getPort();
- X-Forwarded-Proto Used in getScheme() and isSecure();
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:
- Install it via Composer (symfony/http-kernel on Packagist);
- Use the official Git repository (https://github.com/symfony/HttpKernel).
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).

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.

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.
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.
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.

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.

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.
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.

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.
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.

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.
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.

At this point the kernel has a PHP callable (the controller) and an array of arguments that should be passed when executing that callable.
The next step is simple! HttpKernel::handle executes the controller.

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.

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.
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.

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.
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.
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.
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.

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

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:
- Install it via Composer (symfony/intl on Packagist);
- Using the official Git repository (https://github.com/symfony/Intl).
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:
- Collator
- IntlDateFormatter
- Locale
- NumberFormatter
- intl_error_name
- intl_is_failure
- intl_get_error_code
- intl_get_error_message
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:
- TextBundleWriter
- PhpBundleWriter
- BinaryBundleReader
- PhpBundleReader
- BufferedBundleReader
- StructuredBundleReader
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:
- Install it via Composer (symfony/options-resolver on Packagist);
- Use the official Git repository (https://github.com/symfony/OptionsResolver).
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:
- Install it via Composer (symfony/process on Packagist);
- Use the official Git repository (https://github.com/symfony/Process).
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:
- Install it via Composer (symfony/property-access on Packagist);
- Use the official Git repository (https://github.com/symfony/PropertyAccess).
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.
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.
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'
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
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(...)
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
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:
- Install it via Composer (symfony/routing on Packagist);
- Use the official Git repository (https://github.com/symfony/Routing).
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.
A full route definition can contain up to seven parts:
- 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.
- An array of default values. This contains an array of arbitrary values that will be returned when the request matches the route.
- An array of requirements. These define constraints for the values of the placeholders as regular expressions.
- An array of options. These contain internal settings for the route and are the least commonly needed.
- A host. This is matched against the host of the request. See How to Match a Route Based on the Host for more details.
- An array of schemes. These enforce a certain HTTP scheme (http, https).
- 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' => '.*')
);
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);
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());
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.
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;
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);
Last but not least there are AnnotationDirectoryLoader and AnnotationFileLoader to load route definitions from class annotations. The specific details are left out here.
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:
- Install it via Composer (symfony/security on Packagist);
- Use the official Git repository (https://github.com/symfony/Security).
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.
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).
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.
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:
- The Firewall is registered as a listener on the kernel.request event;
- At the beginning of the request, the Firewall checks the firewall map to see if any firewall should be active for this URL;
- If a firewall is found in the map for this URL, its listeners are notified;
- 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);
- 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).
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 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.
There are many built-in password encoders. But if you need to create your own, it just needs to follow these rules:
The class must implement PasswordEncoderInterface;
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; } // ... }
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:
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');
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'));
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 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
);
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.

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:
- Install it via Composer (symfony/serializer on Packagist);
- Use the official Git repository (https://github.com/symfony/Serializer).
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:
- The information to be decoded
- The name of the class this information will be decoded to
- 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:
- Install it via Composer (symfony/stopwatch on Packagist);
- Use the official Git repository (https://github.com/symfony/Stopwatch).
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:
- Install it via Composer (symfony/templating on Packagist);
- Use the official Git repository (https://github.com/symfony/Templating).
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());
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:
- render($name, array $parameters = array()) - Renders a template
- exists($name) - Checks if the template exists
- supports($name) - Checks if the given template can be handled by this engine.
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.
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());
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).
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">
-->
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.
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.
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">
-->
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:
- Install it via Composer (symfony/translation on Packagist);
- Use the official Git repository (https://github.com/symfony/Translation).
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).
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.
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:
- ArrayLoader - to load catalogs from PHP arrays.
- CsvFileLoader - to load catalogs from CSV files.
- IcuDatFileLoader - to load catalogs from resource bundles.
- IcuResFileLoader - to load catalogs from resource bundles.
- IniFileLoader - to load catalogs from ini files.
- MoFileLoader - to load catalogs from gettext files.
- PhpFileLoader - to load catalogs from PHP files.
- PoFileLoader - to load catalogs from gettext files.
- QtFileLoader - to load catalogs from QT XML files.
- XliffFileLoader - to load catalogs from Xliff files.
- YamlFileLoader - to load catalogs from Yaml files (requires the Yaml component).
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 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');
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).
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:
- First, the translator looks for the translation in the fr_FR locale;
- If it wasn’t found, the translator looks for the translation in the fr locale;
- 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:
- Abstract the message that needs to be translated by processing it through the Translator.
- 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).
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:
- Install it via Composer (symfony/yaml on Packagist);
- Use the official Git repository (https://github.com/symfony/Yaml).
Why?¶
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.
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.
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.
It is also able to dump PHP arrays to YAML with object support, and inline level configuration for pretty outputs.
It supports most of the YAML built-in types like dates, integers, octals, booleans, and much more...
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.
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.
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 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.
# an integer
12
# an octal
014
# an hexadecimal
0xC
# a float
13.4
# an exponential number
1.2e+34
# infinity
.inf
Nulls in YAML can be expressed with null or ~.
Booleans in YAML are expressed with true and false.
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.
- How to Install and Use the Symfony Components
- ClassLoader
- Config
- Console
- CssSelector
- Debug
- DependencyInjection
- The DependencyInjection Component
- Types of Injection
- Introduction to Parameters
- Working with Container Service Definitions
- Compiling the Container
- Working with Tagged Services
- Using a Factory to Create Services
- Configuring Services with a Service Configurator
- Managing common Dependencies with parent Services
- Advanced Container Configuration
- Lazy Services
- Container Building Workflow
- DomCrawler
- EventDispatcher
- Filesystem
- Finder
- Form
- HttpFoundation
- HttpKernel
- Intl
- OptionsResolver
- Process
- PropertyAccess
- Routing
- Security
- Serializer
- Stopwatch
- Templating
- Translation
- Yaml
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¶
- form
- enabled
- csrf_protection
- enabled
- field_name
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¶
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.
type: integer default: null
This determines the lifetime of the session - in seconds. It will use null by default, which means session.cookie_lifetime value from php.ini will be used. Setting this value to 0 means the cookie is valid for the length of the browser session.
type: string default: /
This determines the path to set in the session cookie. By default it will use /.
type: string default: ''
This determines the domain to set in the session cookie. By default it’s blank, meaning the host name of the server which generated the cookie according to the cookie specification.
type: Boolean default: false
This determines whether cookies should only be sent over secure connections.
type: Boolean default: false
This determines whether cookies should only be accessible through the HTTP protocol. This means that the cookie won’t be accessible by scripting languages, such as JavaScript. This setting can effectively help to reduce identity theft through XSS attacks.
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.
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.
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¶
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¶
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.
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.
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¶
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.
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¶
type: boolean default: false
Whether or not to enable the translator service in the service container.
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¶
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.
type: Boolean default: false
If this option is enabled, validation constraints can be defined using annotations.
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:
One of annotation, xml, yml, php or staticphp. This specifies which type of metadata type your mapping uses.
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%).
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.
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.
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¶
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”.
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).
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.
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.
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.
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:
- smtp
- gmail (see How to Use Gmail to Send Emails)
- sendmail
- null (same as setting disable_delivery to true)
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: 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.
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¶
type: integer default: 99
Used with Swift_Plugins_AntiFloodPlugin. This is the number of emails to send before restarting the transport.
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:
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.
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.
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.
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.
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.
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' );
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')) );
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.
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.
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.
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.
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:
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'),
));
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.
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.
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.
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.
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.
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' );
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')) );
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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' );
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')) );
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.
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.
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.
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.
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¶
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.
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).
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:
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.
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.
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.
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.
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.
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).
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),
));
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' );
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')) );
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.
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.
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¶
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.
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.
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.
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:
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.
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.
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.
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.
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.
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).
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),
));
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' );
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')) );
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.
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.
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¶
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.
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).
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:
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.
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.
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.
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.
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.
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).
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),
));
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' );
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')) );
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.
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.
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¶
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:
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.
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.
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.
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.
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' );
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')) );
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.
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.
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.
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.
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¶
type: integer default: 0
By default, the input numbers are rounded. To allow for more decimal places, use this option.
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:
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.
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.
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.
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.
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.
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).
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),
));
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' );
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')) );
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.
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.
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:
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.
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.
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.
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.
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' );
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')) );
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.
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.
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.
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.
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¶
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:
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.
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.
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.
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.
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.
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' );
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')) );
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.
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.
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.
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.
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¶
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.
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'))
));
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,
));
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.
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.
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¶
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.
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.
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:
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.
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.
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.
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.
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”.
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' );
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')) );
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.
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.
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.
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');
},
));
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¶
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).
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',
));
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.
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.
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',
));
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¶
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.
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:
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,
));
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.
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.
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:
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.
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.
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.
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.
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.
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' );
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')) );
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.
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.
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¶
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:
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,
));
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.
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.
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.
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.
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:
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.
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.
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.
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' );
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')) );
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.
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.
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¶
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:
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,
));
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.
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.
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.
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.
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:
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.
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.
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.
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' );
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')) );
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.
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.
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¶
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:
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,
));
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.
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.
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.
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.
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:
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.
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.
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.
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' );
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')) );
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.
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.
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¶
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:
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,
));
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.
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.
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:
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.
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.
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.
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.
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.
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' );
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')) );
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.
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.
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¶
default: Symfony\Component\Intl\Intl::getCurrencyBundle()->getCurrencyNames()
The choices option defaults to all currencies.
Inherited Options¶
These options inherit from the choice type:
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,
));
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.
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.
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.
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:
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.
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.
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.
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' );
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')) );
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.
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.
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¶
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)
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')
));
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.
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.
type: string default: system default timezone
Timezone that the input data is stored in. This must be one of the PHP supported timezones.
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.
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.
type: string default: choice
The basic way in which this field should be rendered. Can be one of the following:
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:
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.
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.
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.
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”.
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).
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),
));
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.
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.
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¶
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.
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)
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,
));
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".
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.
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.
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.
type: string default: system default timezone
Timezone that the input data is stored in. This must be one of the PHP supported timezones.
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.
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.
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.
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.
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.
type: Boolean default: false
Whether or not to include seconds in the input. This will result in an additional input to capture seconds.
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:
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.
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.
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”.
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).
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),
));
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.
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.
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¶
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,
));
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.
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.
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.
type: string default: system default timezone
Timezone that the input data is stored in. This must be one of the PHP supported timezones.
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.
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.
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.
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.
type: Boolean default: false
Whether or not to include seconds in the input. This will result in an additional input to capture seconds.
Inherited Options¶
These options inherit from the form type:
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.
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.
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.
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”.
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).
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),
));
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.
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¶
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:
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)
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,
));
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.
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.
type: string default: system default timezone
Timezone that the input data is stored in. This must be one of the PHP supported timezones.
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.
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.
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:
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.
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.
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”.
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).
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),
));
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.
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¶
Overridden Options¶
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.
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:
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.
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.
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.
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.
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' );
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')) );
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.
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.
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:
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.
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.
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.
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.
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' );
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')) );
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.
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.
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:
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:
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.
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.
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).
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.
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.
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' );
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')) );
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.
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.
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.
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¶
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.
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.
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',
),
),
));
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”.
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: 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:
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.
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.
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.
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.
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.
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' );
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')) );
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.
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.
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.
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¶
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.
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'),
));
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.
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: 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.
Inherited Options¶
These options inherit from the form type:
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.
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.
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).
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),
));
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.
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).
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'),
));
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.
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')) ?>
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¶
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'),
));
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.
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')) ?>
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')) );
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¶
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'),
));
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.
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')) ?>
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')) );
type: string default: messages
This is the translation domain that will be used for any labels or options that are rendered for this button.
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¶
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.
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.
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.
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).
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.
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.
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',
));
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.
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.
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.
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.
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”.
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).
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),
));
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')) );
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.
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.
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.
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.
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.
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.
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.
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.
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).
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'),
));
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.
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.
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.
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' );
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.
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()); } }
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()); } }
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()); } }
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.
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.
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.
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: 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.
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 | |
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¶
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.
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.
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¶
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.
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.
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.
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.
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.
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¶
type: string default: This value is not a valid URL.
This message is shown if the URL is invalid.
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¶
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.
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.
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.
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¶
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
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¶
type: integer
This required option is the “min” value. Validation will fail if the given value is less than this min value.
type: integer
This required option is the “max” value. Validation will fail if the given value is greater than this max value.
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.
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.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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¶
type: mixed [default option]
This option is required. It defines the value to compare to. It can be a string, number or object.
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()); } }
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()); } }
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()); } }
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¶
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.
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.
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.
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.
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.
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.
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.
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.
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, ))); } }
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.
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¶
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.
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.
type: Boolean default: The fields {{ fields }} were not expected.
The message shown if allowExtraFields is false and an extra field is detected.
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.
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¶
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.
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.
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.
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.
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¶
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.
type: string default: This value is already used.
The message that’s displayed when this constraint fails.
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.
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.
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.
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()); } }
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()); } }
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()); } }
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¶
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).
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.
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.
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.
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.
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.
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.
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.
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.
type: array or string default: image/*
You can find a list of existing image mime types on the IANA website.
type: string default: This file is not a valid image.
type: integer
If set, the width of the image file must be greater than or equal to this value in pixels.
type: integer
If set, the width of the image file must be less than or equal to this value in pixels.
type: integer
If set, the height of the image file must be greater than or equal to this value in pixels.
type: integer
If set, the height of the image file must be less than or equal to this value in pixels.
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.
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.
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.
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¶
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).
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()); } }
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', ))); } }
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).', ))); } }
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¶
type: boolean
If this required option is set to true the constraint will check if the code is a valid ISBN-10 code.
type: boolean
If this required option is set to true the constraint will check if the code is a valid ISBN-13 code.
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.
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¶
type: String default: This value is not a valid ISSN.
The message shown if the given value is not a valid ISSN.
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’.
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¶
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:
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).
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.
- YAML
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¶
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', ))); } }
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¶
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.
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.
Comparison Constraints¶
Collection 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:
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.
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 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.
file_link¶
{{ file|file_link(line) }}
- line
- type: integer
Generates a link to the provided file (and optionally line number) using a preconfigured scheme.
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:
- Twig Extensions includes some interesting extensions that do not belong to the Twig core. You can read more in the official Twig Extensions documentation;
- Assetic adds the {% stylesheets %}, {% javascripts %} and {% image %} tags. You can read more about them in the Assetic Documentation.
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.
Listener Class Name | Priority |
---|---|
ProfilerListener | 1024 |
TestSessionListener | 192 |
SessionListener | 128 |
RouterListener | 32 |
LocaleListener | 16 |
Firewall | 8 |
Listener Class Name | Priority |
---|---|
RequestDataCollector | 0 |
Listener Class Name | Priority |
---|---|
EsiListener | 0 |
ResponseListener | 0 |
ResponseListener | 0 |
ProfilerListener | -100 |
TestSessionListener | -128 |
WebDebugToolbarListener | -128 |
StreamedResponseListener | -1024 |
Listener Class Name | Priority |
---|---|
ProfilerListener | 0 |
ExceptionListener | -128 |
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:
- CsvFileDumper
- IcuResFileDumper
- IniFileDumper
- MoFileDumper
- PoFileDumper
- QtFileDumper
- XliffFileDumper
- YamlFileDumper
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.
Configuration Options
Ever wondered what configuration options you have available to you in files such as app/config/config.yml? In this section, all the available configuration is broken down by the key (e.g. framework) that defines each possible section of your Symfony configuration.
Forms and Validation
Other Areas
Configuration Options
Ever wondered what configuration options you have available to you in files such as app/config/config.yml? In this section, all the available configuration is broken down by the key (e.g. framework) that defines each possible section of your Symfony configuration.
Forms and Validation
Other Areas
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:
- Double-check the official documentation to see if you’re not misusing the framework;
- Ask for assistance on the users mailing-list, the forum, or on the #symfony IRC channel if you’re not sure if your issue is really 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¶
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.
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:
- 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
Step 2: Work on your Patch¶
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.
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.
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 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.
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.
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
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.
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:
- Project Leader
- Elects members in any other group;
- Merges pull requests in all Symfony repositories.
- Mergers
- Merge pull requests for the component or components on which they have been granted privileges.
- Deciders
- Decide to merge or reject a pull request.
- Project Leader:
- Fabien Potencier (fabpot).
- Mergers (@symfony/mergers on GitHub):
- Bernhard Schussek (webmozart) can merge into the Form, Validator, Icu, Intl, Locale, OptionsResolver and PropertyAccess components;
- Tobias Schultze (Tobion) can merge into the Routing component;
- Romain Neutron (romainneutron) can merge into the Process component;
- Nicolas Grekas (nicolas-grekas) can merge into the Debug component;
- Christophe Coevoet (stof) can merge into the BrowserKit, Config, Console, DependencyInjection, DomCrawler, EventDispatcher, HttpFoundation, HttpKernel, Serializer, Stopwatch, DoctrineBridge, MonologBridge, and TwigBridge components;
- Kévin Dunglas (dunglas) can merge into the Serializer component.
- Deciders (@symfony/deciders on GitHub):
- Jakub Zalas (jakzal);
- Jordi Boggiano (seldaek);
- Lukas Kahwe Smith (lsmith77);
- Ryan Weaver (weaverryan).
At present, new Symfony Core membership applications are not accepted.
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.
- -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.
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.
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.
The Project Leader is also the release manager for every Symfony version.
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:
- Send an acknowledgement to the reporter;
- Work on a patch;
- Get a CVE identifier from mitre.org;
- 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.
- Send the patch and the announcement to the reporter for review;
- Apply the patch to all maintained versions of Symfony;
- Package new versions for all affected versions;
- Publish the post on the official Symfony blog (it must also be added to the “Security Advisories” category);
- 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:
- 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;
- 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;
- All people with access to the private repository work on a solution to solve the issue via pull requests, code reviews, and comments;
- 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:
- July 15, 2014: Security releases: Symfony 2.3.18, 2.4.8, and 2.5.2 released (CVE-2014-4931)
- October 10, 2013: Security releases: Symfony 2.0.25, 2.1.13, 2.2.9, and 2.3.6 released (CVE-2013-5958)
- August 7, 2013: Security releases: Symfony 2.0.24, 2.1.12, 2.2.5, and 2.3.3 released (CVE-2013-4751 and CVE-2013-4752)
- January 17, 2013: Security release: Symfony 2.0.22 and 2.1.7 released (CVE-2013-1348 and CVE-2013-1397)
- December 20, 2012: Security release: Symfony 2.0.20 and 2.1.5 (CVE-2012-6431 and CVE-2012-6432)
- November 29, 2012: Security release: Symfony 2.0.19 and 2.1.4
- November 25, 2012: Security release: symfony 1.4.20 released (CVE-2012-5574)
- August 28, 2012: Security Release: Symfony 2.0.17 released
- May 30, 2012: Security Release: symfony 1.4.18 released (CVE-2012-2667)
- February 24, 2012: Security Release: Symfony 2.0.11 released
- November 16, 2011: Security Release: Symfony 2.0.6
- March 21, 2011: symfony 1.3.10 and 1.4.10: security releases
- June 29, 2010: Security Release: symfony 1.3.6 and 1.4.6
- May 31, 2010: symfony 1.3.5 and 1.4.5
- February 25, 2010: Security Release: 1.2.12, 1.3.3 and 1.4.3
- February 13, 2010: symfony 1.3.2 and 1.4.2
- April 27, 2009: symfony 1.2.6: Security fix
- October 03, 2008: symfony 1.1.4 released: Security fix
- May 14, 2008: symfony 1.0.16 is out
- April 01, 2008: symfony 1.0.13 is out
- March 21, 2008: symfony 1.0.12 is (finally) out !
- June 25, 2007: symfony 1.0.5 released (security fix)
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.
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.
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.
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 |
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:
|
[6] | (1, 2, 3) The return type may only be changed to a compatible or more specific type. The following type changes are allowed:
|
[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.
- 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:º

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¶
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.
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.
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).
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.
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.
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 |
The most common type of links are internal links to other documentation pages, which use the following syntax:
:doc:`/absolute/path/to/page`
The page name should not include the file extension (.rst). For example:
:doc:`/book/controller`
:doc:`/components/event_dispatcher/introduction`
:doc:`/cookbook/configuration/environments`
The title of the linked page will be automatically used as the text of the link. If you want to modify that title, use this alternative syntax:
:doc:`Spooling Email </cookbook/email/spool>`
注解
Although they are technically correct, avoid the use of relative internal links such as the following, because they break the references in the generated PDF documentation:
:doc:`controller`
:doc:`event_dispatcher/introduction`
:doc:`environments`
Links to the API follow a different syntax, where you must specify the type of the linked resource (namespace, class or method):
:namespace:`Symfony\\Component\\BrowserKit`
:class:`Symfony\\Component\\Routing\\Matcher\\ApacheUrlMatcher`
:method:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle::build`
Links to the PHP documentation follow a pretty similar syntax:
:phpclass:`SimpleXMLElement`
:phpmethod:`DateTime::createFromFormat`
:phpfunction:`iterator_to_array`
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.
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
=======
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.
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
// 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:
- English: https://github.com/symfony/symfony-docs
- French: https://github.com/symfony-fr/symfony-docs-fr
- Italian: https://github.com/garak/symfony-docs-it
- Japanese: https://github.com/symfony-japan/symfony-docs-ja
- Portuguese (Brazilian): https://github.com/andreia/symfony-docs-pt-BR
注解
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.
A standard minor version is maintained for an eight month period for bug fixes, and for a fourteen month period for security issue fixes.
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:

- 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:
- List of open pull requests
- List of recent commits
- List of open bugs and enhancements
- List of open source bundles